Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CPF-101 Update createUser() mutation resolver #112

Merged
merged 13 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
Warnings:

- You are about to drop the column `certifiedById` on the `Subrecipient` table. All the data in the column will be lost.
- You are about to drop the column `invalidatedById` on the `UploadValidation` table. All the data in the column will be lost.
- You are about to drop the column `validatedById` on the `UploadValidation` table. All the data in the column will be lost.
- You are about to drop the column `organizationId` on the `User` table. All the data in the column will be lost.
- Made the column `name` on table `User` required. This step will fail if there are existing NULL values in that column.
- Made the column `agencyId` on table `User` required. This step will fail if there are existing NULL values in that column.
- Made the column `role` on table `User` required. This step will fail if there are existing NULL values in that column.

*/
-- DropForeignKey
ALTER TABLE "Subrecipient" DROP CONSTRAINT "Subrecipient_certifiedById_fkey";

-- DropForeignKey
ALTER TABLE "UploadValidation" DROP CONSTRAINT "UploadValidation_invalidatedById_fkey";

-- DropForeignKey
ALTER TABLE "UploadValidation" DROP CONSTRAINT "UploadValidation_validatedById_fkey";

-- DropForeignKey
ALTER TABLE "User" DROP CONSTRAINT "User_agencyId_fkey";

-- DropForeignKey
ALTER TABLE "User" DROP CONSTRAINT "User_organizationId_fkey";

-- AlterTable
ALTER TABLE "Subrecipient" DROP COLUMN "certifiedById";

-- AlterTable
ALTER TABLE "UploadValidation" DROP COLUMN "invalidatedById",
DROP COLUMN "validatedById";

-- AlterTable
ALTER TABLE "User" DROP COLUMN "organizationId",
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
ALTER COLUMN "name" SET NOT NULL,
ALTER COLUMN "agencyId" SET NOT NULL,
ALTER COLUMN "role" SET NOT NULL;

-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_agencyId_fkey" FOREIGN KEY ("agencyId") REFERENCES "Agency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:

- A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail.

*/
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
23 changes: 6 additions & 17 deletions api/db/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ model Agency {
model Organization {
id Int @id @default(autoincrement())
agencies Agency[]
users User[]
name String
reportingPeriods ReportingPeriod[]
uploads Upload[]
Expand All @@ -36,20 +35,16 @@ model Organization {

model User {
id Int @id @default(autoincrement())
email String
name String?
agencyId Int?
organizationId Int?
email String @unique
name String
agencyId Int
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @default(now()) @db.Timestamptz(6)
role Role?
agency Agency? @relation(fields: [agencyId], references: [id])
organization Organization? @relation(fields: [organizationId], references: [id])
role Role
isActive Boolean @default(true)
agency Agency @relation(fields: [agencyId], references: [id])
certified ReportingPeriod[]
uploaded Upload[]
validated UploadValidation[] @relation("ValidatedUploads")
invalidated UploadValidation[] @relation("InvalidatedUploads")
subrecipients Subrecipient[]
}

enum Role {
Expand Down Expand Up @@ -143,12 +138,8 @@ model UploadValidation {
inputTemplate InputTemplate @relation(fields: [inputTemplateId], references: [id])
validationResults Json? @db.JsonB
validatedAt DateTime? @db.Timestamptz(6)
validatedById Int?
validatedBy User? @relation("ValidatedUploads", fields: [validatedById], references: [id])
invalidationResults Json? @db.JsonB
invalidatedAt DateTime? @db.Timestamptz(6)
invalidatedById Int?
invalidatedBy User? @relation("InvalidatedUploads", fields: [invalidatedById], references: [id])
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @default(now()) @db.Timestamptz(6)
}
Expand All @@ -161,8 +152,6 @@ model Subrecipient {
startDate DateTime @db.Date
endDate DateTime @db.Date
certifiedAt DateTime? @db.Timestamptz(6)
certifiedById Int?
certifiedBy User? @relation(fields: [certifiedById], references: [id])
originationUploadId Int
originationUpload Upload @relation(fields: [originationUploadId], references: [id])
createdAt DateTime @default(now()) @db.Timestamptz(6)
Expand Down
21 changes: 8 additions & 13 deletions api/src/graphql/users.sdl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,14 @@ export const schema = gql`
type User {
id: Int!
email: String!
name: String
agencyId: Int
organizationId: Int!
name: String!
agencyId: Int!
createdAt: DateTime!
updatedAt: DateTime!
agency: Agency
organization: Organization!
role: RoleEnum
agency: Agency!
role: RoleEnum!
certified: [ReportingPeriod]!
uploaded: [Upload]!
validated: [UploadValidation]!
invalidated: [UploadValidation]!
}

type Query {
Expand All @@ -30,17 +26,16 @@ export const schema = gql`

input CreateUserInput {
email: String!
name: String
agencyId: Int
organizationId: Int
role: RoleEnum
name: String!
agencyId: Int!
role: RoleEnum!
isActive: Boolean
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the ticket's requirement isActive field on CreateUserInput should be required. However, this means that we either need to pass isActive: true from the frontend (currently the form for creating users doesn't have this field), or make this field optional on CreateUserInput, allowing schema.prisma to set it to true by default.
If we would rather set it on the frontend, I can make this change.

}

input UpdateUserInput {
email: String
name: String
agencyId: Int
organizationId: Int
role: RoleEnum
}

Expand Down
3 changes: 2 additions & 1 deletion api/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export const getCurrentUser = async (
console.log(decoded)
return {
id: 1,
organizationId: 1,
organizationId: 1, // TODO: Organization id should be determined via the agency relationship
agencyId: 1,
email: '[email protected]',
roles: ['USDR_ADMIN'],
}
Expand Down
12 changes: 8 additions & 4 deletions api/src/services/subrecipients/subrecipients.scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ export const standard = defineScenario<Prisma.SubrecipientCreateArgs>({
updatedAt: '2023-12-09T14:50:18.317Z',
uploadedBy: {
create: {
email: 'String',
name: 'String',
email: '[email protected]',
updatedAt: '2023-12-09T14:50:18.317Z',
organization: { create: { name: 'String' } },
agency: { create: { name: 'String', code: 'String' } },
role: 'USDR_ADMIN',
},
},
agency: { create: { name: 'String', code: 'String' } },
Expand Down Expand Up @@ -73,9 +75,11 @@ export const standard = defineScenario<Prisma.SubrecipientCreateArgs>({
updatedAt: '2023-12-09T14:50:18.317Z',
uploadedBy: {
create: {
email: 'String',
name: 'String',
email: '[email protected]',
updatedAt: '2023-12-09T14:50:18.317Z',
organization: { create: { name: 'String' } },
agency: { create: { name: 'String', code: 'String' } },
role: 'USDR_ADMIN',
},
},
agency: { create: { name: 'String', code: 'String' } },
Expand Down
3 changes: 0 additions & 3 deletions api/src/services/subrecipients/subrecipients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ export const Subrecipient: SubrecipientRelationResolvers = {
.findUnique({ where: { id: root?.id } })
.organization()
},
certifiedBy: (_obj, { root }) => {
return db.subrecipient.findUnique({ where: { id: root?.id } }).certifiedBy()
},
originationUpload: (_obj, { root }) => {
return db.subrecipient
.findUnique({ where: { id: root?.id } })
Expand Down
18 changes: 16 additions & 2 deletions api/src/services/uploads/uploads.scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ export const standard = defineScenario<Prisma.UploadCreateArgs>({
one: {
data: {
filename: 'String',
uploadedBy: { create: { email: 'String' } },
uploadedBy: {
create: {
email: '[email protected]',
name: 'String',
role: 'USDR_ADMIN',
agency: { create: { name: 'String', code: 'String' } },
},
},
agency: { create: { name: 'String', code: 'String' } },
organization: { create: { name: 'String' } },
reportingPeriod: {
Expand Down Expand Up @@ -37,7 +44,14 @@ export const standard = defineScenario<Prisma.UploadCreateArgs>({
two: {
data: {
filename: 'String',
uploadedBy: { create: { email: 'String' } },
uploadedBy: {
create: {
email: '[email protected]',
name: 'String',
role: 'USDR_ADMIN',
agency: { create: { name: 'String', code: 'String' } },
},
},
agency: { create: { name: 'String', code: 'String' } },
organization: { create: { name: 'String' } },
reportingPeriod: {
Expand Down
3 changes: 1 addition & 2 deletions api/src/services/users/users.scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ export const standard = defineScenario<
user: {
one: {
data: {
email: 'String',
email: '[email protected]',
name: 'String',
organization: { create: { name: 'String' } },
agency: { create: { name: 'String', code: 'String' } },
role: 'ORGANIZATION_ADMIN',
},
Expand Down
12 changes: 5 additions & 7 deletions api/src/services/users/users.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,20 @@ describe('users', () => {
scenario('creates a user', async (scenario: StandardScenario) => {
mockCurrentUser({
id: scenario.user.one.id,
organizationId: scenario.user.one.organizationId,
email: '[email protected]',
email: scenario.user.one.email,
roles: ['USDR_ADMIN'],
})

const result = await createUser({
input: {
name: scenario.user.one.name,
email: scenario.user.one.email,
email: '[email protected]',
name: 'String',
agencyId: scenario.agency.one.id,
role: 'ORGANIZATION_STAFF',
role: 'USDR_ADMIN',
},
})

expect(result.email).toEqual(scenario.user.one.email)
expect(result.organizationId).toEqual(scenario.organization.one.id)
expect(result.email).toEqual('[email protected]')
})

scenario('updates a user', async (scenario: StandardScenario) => {
Expand Down
81 changes: 32 additions & 49 deletions api/src/services/users/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
UserRelationResolvers,
} from 'types/graphql'

import { validate, validateWith, validateUniqueness } from '@redwoodjs/api'
import { AuthenticationError } from '@redwoodjs/graphql-server'

import { ROLES } from 'src/lib/constants'
Expand All @@ -19,55 +20,46 @@ export const user: QueryResolvers['user'] = ({ id }) => {
})
}

/**
* Determines if the current user can create a new user with the specified role.
* - USDR_ADMIN can create users with any role.
* - ORGANIZATION_ADMIN can create users with roles ORGANIZATION_STAFF or ORGANIZATION_ADMIN.
* - ORGANIZATION_STAFF can't create users.
*
* @param {Object} currentUser - The current user object.
* @param {string} userRoleToCreate - The role of the user to be created.
* @returns {boolean} True if the user can create the new user, false otherwise.
*/
const canCreateUser = (currentUser, userRoleToCreate: string) => {
const { USDR_ADMIN, ORGANIZATION_ADMIN, ORGANIZATION_STAFF } = ROLES

if (currentUser.roles?.includes(USDR_ADMIN)) {
return true
}

if (currentUser.roles?.includes(ORGANIZATION_ADMIN)) {
return (
userRoleToCreate === ORGANIZATION_STAFF ||
userRoleToCreate === ORGANIZATION_ADMIN
)
}

return false
}

export const createUser: MutationResolvers['createUser'] = async ({
input,
}) => {
if (!canCreateUser(context.currentUser, input.role)) {
throw new AuthenticationError("You don't have permission to do that.")
}
const { email, name, agencyId } = input
const { currentUser } = context
const { USDR_ADMIN } = ROLES

const { agencyId } = input
validate(email, {
email: { message: 'Please provide a valid email address' },
})

try {
const agency = await db.agency.findUnique({ where: { id: agencyId } })
validate(name, {
presence: { allowEmptyString: false, message: 'Please provide a name' },
})

if (!agency) {
throw new Error('Agency not found.')
validateWith(async () => {
if (currentUser.roles?.includes(USDR_ADMIN)) {
return true
}

return db.user.create({
data: { ...input, organizationId: agency.organizationId },
const newUserAgency = await db.agency.findUniqueOrThrow({
where: { id: agencyId },
select: { organizationId: true },
})
} catch (err) {
throw new Error(err)
}
const loggedInUserAgency = await db.agency.findUniqueOrThrow({
where: { id: currentUser.agencyId as number },
select: { organizationId: true },
})

if (newUserAgency.organizationId !== loggedInUserAgency.organizationId) {
throw new AuthenticationError("You don't have permission to do that")
}
})

return validateUniqueness(
'user',
{ email },
{ message: 'This email is already in use' },
(db) => db.user.create({ data: input })
)
}

export const updateUser: MutationResolvers['updateUser'] = ({ id, input }) => {
Expand Down Expand Up @@ -101,19 +93,10 @@ export const User: UserRelationResolvers = {
agency: (_obj, { root }) => {
return db.user.findUnique({ where: { id: root?.id } }).agency()
},
organization: (_obj, { root }) => {
return db.user.findUnique({ where: { id: root?.id } }).organization()
},
certified: (_obj, { root }) => {
return db.user.findUnique({ where: { id: root?.id } }).certified()
},
uploaded: (_obj, { root }) => {
return db.user.findUnique({ where: { id: root?.id } }).uploaded()
},
validated: (_obj, { root }) => {
return db.user.findUnique({ where: { id: root?.id } }).validated()
},
invalidated: (_obj, { root }) => {
return db.user.findUnique({ where: { id: root?.id } }).invalidated()
},
}
Loading
Loading