Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
 into 14-add-users-page
  • Loading branch information
ifranch committed Dec 18, 2023
2 parents 54b7930 + 4d209fb commit 6714231
Show file tree
Hide file tree
Showing 49 changed files with 49,185 additions and 429 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,5 @@ dist
# Terraform cache
terraform/.terraform
.tool-versions

localstack/volume
4 changes: 4 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@aws-sdk/client-s3": "^3.472.0",
"@aws-sdk/client-ses": "^3.470.0",
"@aws-sdk/client-sqs": "^3.470.0",
"@aws-sdk/client-ssm": "^3.462.0",
"@aws-sdk/rds-signer": "^3.462.0",
"@aws-sdk/s3-request-presigner": "^3.472.0",
"@opentelemetry/instrumentation": "^0.45.1",
"@prisma/instrumentation": "^5.7.0",
"@redwoodjs/api": "6.4.2",
Expand Down
6 changes: 3 additions & 3 deletions api/src/directives/requireAuth/requireAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ describe('requireAuth directive', () => {
})

it('requireAuth has stub implementation. Should not throw when current user', () => {
// If you want to set values in context, pass it through e.g.
// mockRedwoodDirective(requireAuth, { context: { currentUser: { id: 1, name: 'Lebron McGretzky' } }})
const mockExecution = mockRedwoodDirective(requireAuth, { context: {} })
const mockExecution = mockRedwoodDirective(requireAuth, {
context: { currentUser: { id: 1, name: 'Some User' } },
})

expect(mockExecution).not.toThrowError()
})
Expand Down
2 changes: 2 additions & 0 deletions api/src/functions/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'

import { getCurrentUser } from 'src/lib/auth'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'

export const handler = createGraphQLHandler({
getCurrentUser,
loggerConfig: { logger, options: { requestId: true, operationName: true } },
directives,
sdls,
Expand Down
133 changes: 117 additions & 16 deletions api/src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,126 @@
import { parseJWT, Decoded } from '@redwoodjs/api'
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'

/**
* Represents the user attributes returned by the decoding the
* Authentication provider's JWT together with an optional list of roles.
*/
type RedwoodUser = Record<string, unknown> & { roles?: string[] }

/**
* Once you are ready to add authentication to your application
* you'll build out requireAuth() with real functionality. For
* now we just return `true` so that the calls in services
* have something to check against, simulating a logged
* in user that is allowed to access that service.
* getCurrentUser returns the user information together with
* an optional collection of roles used by requireAuth() to check
* if the user is authenticated or has role-based access
*
* See https://redwoodjs.com/docs/authentication for more info.
* !! BEWARE !! Anything returned from this function will be available to the
* client--it becomes the content of `currentUser` on the web side (as well as
* `context.currentUser` on the api side). You should carefully add additional
* fields to the return object only once you've decided they are safe to be seen
* if someone were to open the Web Inspector in their browser.
*
* @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
*
* @param decoded - The decoded access token containing user info and JWT
* claims like `sub`. Note, this could be null.
* @param { token, SupportedAuthTypes type } - The access token itself as well
* as the auth provider type
* @param { APIGatewayEvent event, Context context } - An optional object which
* contains information from the invoker such as headers and cookies, and the
* context information about the invocation such as IP Address
* @returns RedwoodUser
*/
export const isAuthenticated = () => {
return true
export const getCurrentUser = async (
decoded: Decoded
): Promise<RedwoodUser | null> => {
if (!decoded) {
return null
}

const { roles } = parseJWT({ decoded })

if (roles) {
return { ...decoded, roles }
}

return { ...decoded }
}

export const hasRole = ({ roles }) => {
return roles !== undefined
/**
* The user is authenticated if there is a currentUser in the context
*
* @returns {boolean} - If the currentUser is authenticated
*/
export const isAuthenticated = (): boolean => {
return !!context.currentUser
}

// This is used by the redwood directive
// in ./api/src/directives/requireAuth
/**
* When checking role membership, roles can be a single value, a list, or none.
* You can use Prisma enums too (if you're using them for roles), just import your enum type from `@prisma/client`
*/
type AllowedRoles = string | string[] | undefined

/**
* Checks if the currentUser is authenticated (and assigned one of the given roles)
*
* @param roles: {@link AllowedRoles} - Checks if the currentUser is assigned one of these roles
*
* @returns {boolean} - Returns true if the currentUser is logged in and assigned one of the given roles,
* or when no roles are provided to check against. Otherwise returns false.
*/
export const hasRole = (roles: AllowedRoles): boolean => {
if (!isAuthenticated()) {
return false
}

const currentUserRoles = context.currentUser?.roles

if (typeof roles === 'string') {
if (typeof currentUserRoles === 'string') {
// roles to check is a string, currentUser.roles is a string
return currentUserRoles === roles
} else if (Array.isArray(currentUserRoles)) {
// roles to check is a string, currentUser.roles is an array
return currentUserRoles?.some((allowedRole) => roles === allowedRole)
}
}

if (Array.isArray(roles)) {
if (Array.isArray(currentUserRoles)) {
// roles to check is an array, currentUser.roles is an array
return currentUserRoles?.some((allowedRole) =>
roles.includes(allowedRole)
)
} else if (typeof currentUserRoles === 'string') {
// roles to check is an array, currentUser.roles is a string
return roles.some((allowedRole) => currentUserRoles === allowedRole)
}
}

// roles not found
return false
}

/**
* Use requireAuth in your services to check that a user is logged in,
* whether or not they are assigned a role, and optionally raise an
* error if they're not.
*
* @param roles?: {@link AllowedRoles} - When checking role membership, these roles grant access.
*
* @returns - If the currentUser is authenticated (and assigned one of the given roles)
*
* @throws {@link AuthenticationError} - If the currentUser is not authenticated
* @throws {@link ForbiddenError} - If the currentUser is not allowed due to role permissions
*
* @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
*/
export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => {
if (!isAuthenticated()) {
throw new AuthenticationError("You don't have permission to do that.")
}

// Roles are passed in by the requireAuth directive if you have auth setup
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
export const requireAuth = ({ roles }) => {
return isAuthenticated()
if (roles && !hasRole(roles)) {
throw new ForbiddenError("You don't have access to do that.")
}
}
106 changes: 106 additions & 0 deletions api/src/lib/aws.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {
GetObjectCommand,
PutObjectCommand,
PutObjectCommandInput,
S3Client,
} from '@aws-sdk/client-s3'
import {ReceiveMessageCommand, SendMessageCommand, SQSClient} from '@aws-sdk/client-sqs'

Check warning on line 7 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Replace `ReceiveMessageCommand,·SendMessageCommand,·SQSClient` with `⏎··ReceiveMessageCommand,⏎··SendMessageCommand,⏎··SQSClient,⏎`
import {getSignedUrl as awsGetSignedUrl} from '@aws-sdk/s3-request-presigner'

Check warning on line 8 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Replace `getSignedUrl·as·awsGetSignedUrl` with `·getSignedUrl·as·awsGetSignedUrl·`

function getS3Client() {
let s3: S3Client;

Check warning on line 11 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Delete `;`
if (process.env.LOCALSTACK_HOSTNAME) {
/*
1. Make sure the local environment has awslocal installed.
2. Use the commands to create a bucket to test with.
- awslocal s3api create-bucket --bucket arpa-audit-reports --region us-west-2 --create-bucket-configuration '{"LocationConstraint": "us-west-2"}'
3. Access bucket resource metadata through the following URL.
- awslocal s3api list-buckets
- awslocal s3api list-objects --bucket arpa-audit-reports
*/
console.log('------------ USING LOCALSTACK ------------');

Check warning on line 21 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Delete `;`
const endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:${process.env.EDGE_PORT || 4566}`;

Check warning on line 22 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Replace `process.env.EDGE_PORT·||·4566}`;` with `⏎······process.env.EDGE_PORT·||·4566⏎····}``
console.log(`endpoint: ${endpoint}`);

Check warning on line 23 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Delete `;`
s3 = new S3Client({
endpoint,
forcePathStyle: true,
region: process.env.AWS_DEFAULT_REGION,
});

Check warning on line 28 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Delete `;`
} else {
s3 = new S3Client();

Check warning on line 30 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Delete `;`
}
return s3;

Check warning on line 32 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Delete `;`
}

async function sendPutObjectToS3Bucket(bucketName: string, key: string, body: any) {

Check warning on line 35 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Replace `bucketName:·string,·key:·string,·body:·any` with `⏎··bucketName:·string,⏎··key:·string,⏎··body:·any⏎`
const s3 = getS3Client();
const uploadParams : PutObjectCommandInput = {
Bucket: bucketName,
Key: key,
Body: body,
ServerSideEncryption: 'AES256',
};
await s3.send(new PutObjectCommand(uploadParams));
}

async function sendHeadObjectToS3Bucket(bucketName: string, key: string) {
const s3 = getS3Client();
const uploadParams : PutObjectCommandInput = {
Bucket: bucketName,
Key: key,
};
await s3.send(new PutObjectCommand(uploadParams));
}

/**
* This function is a wrapper around the getSignedUrl function from the @aws-sdk/s3-request-presigner package.
* Exists to organize the imports and to make it easier to mock in tests.
*/
async function getSignedUrl(bucketName: string, key: string) {
const s3 = getS3Client();
const baseParams = { Bucket: bucketName, Key: key };
return awsGetSignedUrl(s3, new GetObjectCommand(baseParams), { expiresIn: 60 });
}

function getSQSClient() {
let sqs: SQSClient;
if (process.env.LOCALSTACK_HOSTNAME) {
console.log('------------ USING LOCALSTACK FOR SQS ------------');
const endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:${process.env.EDGE_PORT || 4566}`;
sqs = new SQSClient({ endpoint, region: process.env.AWS_DEFAULT_REGION });
} else {
sqs = new SQSClient();
}
return sqs;
}

async function sendSqsMessage(queueUrl: string, messageBody: any) {
const sqs = getSQSClient();
await sqs.send(new SendMessageCommand({
QueueUrl: queueUrl,
MessageBody: JSON.stringify(messageBody),
}));
}

async function receiveSqsMessage(queueUrl: string) {
const sqs = getSQSClient();
// const receiveResp = await sqs.send(new ReceiveMessageCommand({
// QueueUrl: process.env.TASK_QUEUE_URL, WaitTimeSeconds: 20, MaxNumberOfMessages: 1,
// }));

// const receiveResp = await sqs.send(new ReceiveMessageCommand({
// QueueUrl: process.env.TASK_QUEUE_URL, WaitTimeSeconds: 20, MaxNumberOfMessages: 1,
// }));

await sqs.send(new ReceiveMessageCommand({
QueueUrl: queueUrl, WaitTimeSeconds: 20, MaxNumberOfMessages: 1,
}));
}

export default {
sendPutObjectToS3Bucket,
sendHeadObjectToS3Bucket,
getSignedUrl,
sendSqsMessage,
receiveSqsMessage,
};
1 change: 1 addition & 0 deletions api/src/services/agencies/agencies.scenarios.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Prisma, agency } from '@prisma/client'

import type { ScenarioData } from '@redwoodjs/testing/api'

export const standard = defineScenario<Prisma.agencyCreateArgs>({
Expand Down
25 changes: 12 additions & 13 deletions api/src/services/agencies/agencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,16 @@ export const deleteAgency: MutationResolvers['deleteAgency'] = ({ id }) => {
})
}

export const agenciesByOrganization: QueryResolvers['agenciesByOrganization'] = async ({
organizationId,
}) => {
try {
const agencies = await db.agency.findMany({
where: { organizationId },
})
return agencies || [] // Return an empty array if null is received
} catch (error) {
console.error(error)
// Handle the error appropriately; maybe log it and return an empty array
return []
export const agenciesByOrganization: QueryResolvers['agenciesByOrganization'] =
async ({ organizationId }) => {
try {
const agencies = await db.agency.findMany({
where: { organizationId },
})
return agencies || [] // Return an empty array if null is received
} catch (error) {
console.error(error)
// Handle the error appropriately; maybe log it and return an empty array
return []
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Prisma, ExpenditureCategory } from '@prisma/client'

import type { ScenarioData } from '@redwoodjs/testing/api'

export const standard = defineScenario<Prisma.ExpenditureCategoryCreateArgs>({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Prisma, InputTemplate } from '@prisma/client'

import type { ScenarioData } from '@redwoodjs/testing/api'

export const standard = defineScenario<Prisma.InputTemplateCreateArgs>({
Expand Down
1 change: 1 addition & 0 deletions api/src/services/organizations/organizations.scenarios.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Prisma, Organization } from '@prisma/client'

import type { ScenarioData } from '@redwoodjs/testing/api'

export const standard = defineScenario<Prisma.OrganizationCreateArgs>({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Prisma, OutputTemplate } from '@prisma/client'

import type { ScenarioData } from '@redwoodjs/testing/api'

export const standard = defineScenario<Prisma.OutputTemplateCreateArgs>({
Expand Down
1 change: 1 addition & 0 deletions api/src/services/projects/projects.scenarios.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Prisma, Project } from '@prisma/client'

import type { ScenarioData } from '@redwoodjs/testing/api'

export const standard = defineScenario<Prisma.ProjectCreateArgs>({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Prisma, ReportingPeriod } from '@prisma/client'

import type { ScenarioData } from '@redwoodjs/testing/api'

export const standard = defineScenario<Prisma.ReportingPeriodCreateArgs>({
Expand Down
1 change: 1 addition & 0 deletions api/src/services/roles/roles.scenarios.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Prisma, Role } from '@prisma/client'

import type { ScenarioData } from '@redwoodjs/testing/api'

export const standard = defineScenario<Prisma.RoleCreateArgs>({
Expand Down
Loading

0 comments on commit 6714231

Please sign in to comment.