From 0985d8fccf261912737bf4f799e482aca5038a21 Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Wed, 14 Feb 2024 14:41:38 -0500 Subject: [PATCH 01/10] CPF-103 remove validated, invalidated and subrecipients relations from User model, add isActive, make fields required --- .../migration.sql | 43 +++++++++++++++++++ api/db/schema.prisma | 21 +++------ 2 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 api/db/migrations/20240214193745_remove_user_validation_subrecipients_relations/migration.sql diff --git a/api/db/migrations/20240214193745_remove_user_validation_subrecipients_relations/migration.sql b/api/db/migrations/20240214193745_remove_user_validation_subrecipients_relations/migration.sql new file mode 100644 index 00000000..569e28aa --- /dev/null +++ b/api/db/migrations/20240214193745_remove_user_validation_subrecipients_relations/migration.sql @@ -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; diff --git a/api/db/schema.prisma b/api/db/schema.prisma index 0f00b4e0..11bf84aa 100644 --- a/api/db/schema.prisma +++ b/api/db/schema.prisma @@ -25,7 +25,6 @@ model Agency { model Organization { id Int @id @default(autoincrement()) agencies Agency[] - users User[] name String reportingPeriods ReportingPeriod[] uploads Upload[] @@ -37,19 +36,15 @@ model Organization { model User { id Int @id @default(autoincrement()) email String - name String? - agencyId Int? - organizationId Int? + 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 { @@ -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) } @@ -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) From 76f6d578bbc636ef6bb6faecd96e88720d26469b Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Fri, 16 Feb 2024 13:27:11 -0500 Subject: [PATCH 02/10] CPF-103 update seed script to include 1 test organization, 1 agency and 3 users with different roles --- api/src/graphql/users.sdl.ts | 8 +--- api/types/graphql.d.ts | 14 ------- scripts/seed.ts | 40 +++++++++++++++++++ .../User/EditUserCell/EditUserCell.tsx | 4 +- web/src/components/User/UserCell/UserCell.tsx | 2 +- .../components/User/UsersCell/UsersCell.tsx | 3 +- web/types/graphql.d.ts | 14 ++----- 7 files changed, 51 insertions(+), 34 deletions(-) diff --git a/api/src/graphql/users.sdl.ts b/api/src/graphql/users.sdl.ts index 13b2c791..ac2bc87c 100644 --- a/api/src/graphql/users.sdl.ts +++ b/api/src/graphql/users.sdl.ts @@ -10,16 +10,12 @@ export const schema = gql` email: String! name: String agencyId: Int - organizationId: Int! createdAt: DateTime! updatedAt: DateTime! agency: Agency - organization: Organization! role: RoleEnum certified: [ReportingPeriod]! uploaded: [Upload]! - validated: [UploadValidation]! - invalidated: [UploadValidation]! } type Query { @@ -32,7 +28,7 @@ export const schema = gql` email: String! name: String agencyId: Int - organizationId: Int + # organizationId: Int role: RoleEnum } @@ -40,7 +36,7 @@ export const schema = gql` email: String name: String agencyId: Int - organizationId: Int + # organizationId: Int role: RoleEnum } diff --git a/api/types/graphql.d.ts b/api/types/graphql.d.ts index 97cee366..17f99d74 100644 --- a/api/types/graphql.d.ts +++ b/api/types/graphql.d.ts @@ -152,7 +152,6 @@ export type CreateUserInput = { agencyId?: InputMaybe; email: Scalars['String']; name?: InputMaybe; - organizationId?: InputMaybe; role?: InputMaybe; }; @@ -696,7 +695,6 @@ export type UpdateUserInput = { agencyId?: InputMaybe; email?: InputMaybe; name?: InputMaybe; - organizationId?: InputMaybe; role?: InputMaybe; }; @@ -753,14 +751,10 @@ export type User = { createdAt: Scalars['DateTime']; email: Scalars['String']; id: Scalars['Int']; - invalidated: Array>; name?: Maybe; - organization: Organization; - organizationId: Scalars['Int']; role?: Maybe; updatedAt: Scalars['DateTime']; uploaded: Array>; - validated: Array>; }; type MaybeOrArrayOfMaybe = T | Maybe | Maybe[]; @@ -1439,14 +1433,10 @@ export type UserResolvers; email: OptArgsResolverFn; id: OptArgsResolverFn; - invalidated: OptArgsResolverFn>, ParentType, ContextType>; name: OptArgsResolverFn, ParentType, ContextType>; - organization: OptArgsResolverFn; - organizationId: OptArgsResolverFn; role: OptArgsResolverFn, ParentType, ContextType>; updatedAt: OptArgsResolverFn; uploaded: OptArgsResolverFn>, ParentType, ContextType>; - validated: OptArgsResolverFn>, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -1457,14 +1447,10 @@ export type UserRelationResolvers; email?: RequiredResolverFn; id?: RequiredResolverFn; - invalidated?: RequiredResolverFn>, ParentType, ContextType>; name?: RequiredResolverFn, ParentType, ContextType>; - organization?: RequiredResolverFn; - organizationId?: RequiredResolverFn; role?: RequiredResolverFn, ParentType, ContextType>; updatedAt?: RequiredResolverFn; uploaded?: RequiredResolverFn>, ParentType, ContextType>; - validated?: RequiredResolverFn>, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/scripts/seed.ts b/scripts/seed.ts index 23ab6bdb..12ee45c7 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -8,6 +8,46 @@ export default async () => { // Seeds automatically with `yarn rw prisma migrate dev` and `yarn rw prisma migrate reset` // + const organization: Prisma.OrganizationCreateArgs['data'] = + await db.organization.create({ + data: { + name: 'Example Organization', + }, + }) + + const agency: Prisma.AgencyCreateArgs['data'] = await db.agency.create({ + data: { + name: 'Example Agency', + code: 'EA', + organizationId: organization.id, + }, + }) + + const users: Prisma.UserCreateManyInput[] = [ + { + email: 'ORGANIZATIONSTAFF@testemail.test', + name: 'Organization Staff', + role: 'ORGANIZATION_STAFF', + agencyId: agency.id, + }, + { + email: 'ORGANIZATIONADMIN@testemail.test', + name: 'Organization Admin', + role: 'ORGANIZATION_ADMIN', + agencyId: agency.id, + }, + { + email: 'USDRADMIN@testemail.test', + name: 'USDR Admin', + role: 'USDR_ADMIN', + agencyId: agency.id, + }, + ] + + await db.user.createMany({ + data: users, + }) + const inputTemplates: Prisma.InputTemplateCreateArgs['data'][] = [ { name: 'Input Template 1', diff --git a/web/src/components/User/EditUserCell/EditUserCell.tsx b/web/src/components/User/EditUserCell/EditUserCell.tsx index c90761ac..62e535b5 100644 --- a/web/src/components/User/EditUserCell/EditUserCell.tsx +++ b/web/src/components/User/EditUserCell/EditUserCell.tsx @@ -14,7 +14,7 @@ export const QUERY = gql` email name agencyId - organizationId + # organizationId role createdAt updatedAt @@ -28,7 +28,7 @@ const UPDATE_USER_MUTATION = gql` email name agencyId - organizationId + # organizationId role createdAt updatedAt diff --git a/web/src/components/User/UserCell/UserCell.tsx b/web/src/components/User/UserCell/UserCell.tsx index caa43470..ab711bf5 100644 --- a/web/src/components/User/UserCell/UserCell.tsx +++ b/web/src/components/User/UserCell/UserCell.tsx @@ -11,7 +11,7 @@ export const QUERY = gql` email name agencyId - organizationId + # organizationId role createdAt updatedAt diff --git a/web/src/components/User/UsersCell/UsersCell.tsx b/web/src/components/User/UsersCell/UsersCell.tsx index 56350fa4..9725ff62 100644 --- a/web/src/components/User/UsersCell/UsersCell.tsx +++ b/web/src/components/User/UsersCell/UsersCell.tsx @@ -13,8 +13,9 @@ export const QUERY = gql` name agency { name + organizationId } - organizationId + # organizationId role createdAt updatedAt diff --git a/web/types/graphql.d.ts b/web/types/graphql.d.ts index 4f2bc99d..418d60da 100644 --- a/web/types/graphql.d.ts +++ b/web/types/graphql.d.ts @@ -133,7 +133,6 @@ export type CreateUserInput = { agencyId?: InputMaybe; email: Scalars['String']; name?: InputMaybe; - organizationId?: InputMaybe; role?: InputMaybe; }; @@ -677,7 +676,6 @@ export type UpdateUserInput = { agencyId?: InputMaybe; email?: InputMaybe; name?: InputMaybe; - organizationId?: InputMaybe; role?: InputMaybe; }; @@ -734,14 +732,10 @@ export type User = { createdAt: Scalars['DateTime']; email: Scalars['String']; id: Scalars['Int']; - invalidated: Array>; name?: Maybe; - organization: Organization; - organizationId: Scalars['Int']; role?: Maybe; updatedAt: Scalars['DateTime']; uploaded: Array>; - validated: Array>; }; export type FindAgenciesByOrganizationIdVariables = Exact<{ @@ -934,7 +928,7 @@ export type EditUserByIdVariables = Exact<{ }>; -export type EditUserById = { __typename?: 'Query', user?: { __typename?: 'User', id: number, email: string, name?: string | null, agencyId?: number | null, organizationId: number, role?: RoleEnum | null, createdAt: string, updatedAt: string } | null }; +export type EditUserById = { __typename?: 'Query', user?: { __typename?: 'User', id: number, email: string, name?: string | null, agencyId?: number | null, role?: RoleEnum | null, createdAt: string, updatedAt: string } | null }; export type UpdateUserMutationVariables = Exact<{ id: Scalars['Int']; @@ -942,7 +936,7 @@ export type UpdateUserMutationVariables = Exact<{ }>; -export type UpdateUserMutation = { __typename?: 'Mutation', updateUser: { __typename?: 'User', id: number, email: string, name?: string | null, agencyId?: number | null, organizationId: number, role?: RoleEnum | null, createdAt: string, updatedAt: string } }; +export type UpdateUserMutation = { __typename?: 'Mutation', updateUser: { __typename?: 'User', id: number, email: string, name?: string | null, agencyId?: number | null, role?: RoleEnum | null, createdAt: string, updatedAt: string } }; export type CreateUserMutationVariables = Exact<{ input: CreateUserInput; @@ -963,7 +957,7 @@ export type FindUserByIdVariables = Exact<{ }>; -export type FindUserById = { __typename?: 'Query', user?: { __typename?: 'User', id: number, email: string, name?: string | null, agencyId?: number | null, organizationId: number, role?: RoleEnum | null, createdAt: string, updatedAt: string } | null }; +export type FindUserById = { __typename?: 'Query', user?: { __typename?: 'User', id: number, email: string, name?: string | null, agencyId?: number | null, role?: RoleEnum | null, createdAt: string, updatedAt: string } | null }; export type agenciesUnderUserOrganizationVariables = Exact<{ organizationId: Scalars['Int']; @@ -977,4 +971,4 @@ export type FindUsersByOrganizationIdVariables = Exact<{ }>; -export type FindUsersByOrganizationId = { __typename?: 'Query', usersByOrganization: Array<{ __typename?: 'User', id: number, email: string, name?: string | null, organizationId: number, role?: RoleEnum | null, createdAt: string, updatedAt: string, agency?: { __typename?: 'Agency', name: string } | null }> }; +export type FindUsersByOrganizationId = { __typename?: 'Query', usersByOrganization: Array<{ __typename?: 'User', id: number, email: string, name?: string | null, role?: RoleEnum | null, createdAt: string, updatedAt: string, agency?: { __typename?: 'Agency', name: string, organizationId: number } | null }> }; From a39b3d02d1421e4efbb8ddfeab718632183a276d Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Fri, 16 Feb 2024 14:15:37 -0500 Subject: [PATCH 03/10] CPF-103 remove organizationId from api inputs --- api/src/graphql/users.sdl.ts | 2 -- api/src/services/users/users.ts | 9 --------- web/src/components/User/EditUserCell/EditUserCell.tsx | 2 -- web/src/components/User/UserCell/UserCell.tsx | 1 - web/src/components/User/UsersCell/UsersCell.tsx | 2 -- web/types/graphql.d.ts | 2 +- 6 files changed, 1 insertion(+), 17 deletions(-) diff --git a/api/src/graphql/users.sdl.ts b/api/src/graphql/users.sdl.ts index ac2bc87c..137a66e6 100644 --- a/api/src/graphql/users.sdl.ts +++ b/api/src/graphql/users.sdl.ts @@ -28,7 +28,6 @@ export const schema = gql` email: String! name: String agencyId: Int - # organizationId: Int role: RoleEnum } @@ -36,7 +35,6 @@ export const schema = gql` email: String name: String agencyId: Int - # organizationId: Int role: RoleEnum } diff --git a/api/src/services/users/users.ts b/api/src/services/users/users.ts index 3dc3789d..0a96d2aa 100644 --- a/api/src/services/users/users.ts +++ b/api/src/services/users/users.ts @@ -101,19 +101,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() - }, } diff --git a/web/src/components/User/EditUserCell/EditUserCell.tsx b/web/src/components/User/EditUserCell/EditUserCell.tsx index 62e535b5..1ba01fb3 100644 --- a/web/src/components/User/EditUserCell/EditUserCell.tsx +++ b/web/src/components/User/EditUserCell/EditUserCell.tsx @@ -14,7 +14,6 @@ export const QUERY = gql` email name agencyId - # organizationId role createdAt updatedAt @@ -28,7 +27,6 @@ const UPDATE_USER_MUTATION = gql` email name agencyId - # organizationId role createdAt updatedAt diff --git a/web/src/components/User/UserCell/UserCell.tsx b/web/src/components/User/UserCell/UserCell.tsx index ab711bf5..7fc37045 100644 --- a/web/src/components/User/UserCell/UserCell.tsx +++ b/web/src/components/User/UserCell/UserCell.tsx @@ -11,7 +11,6 @@ export const QUERY = gql` email name agencyId - # organizationId role createdAt updatedAt diff --git a/web/src/components/User/UsersCell/UsersCell.tsx b/web/src/components/User/UsersCell/UsersCell.tsx index 9725ff62..a4d865cc 100644 --- a/web/src/components/User/UsersCell/UsersCell.tsx +++ b/web/src/components/User/UsersCell/UsersCell.tsx @@ -13,9 +13,7 @@ export const QUERY = gql` name agency { name - organizationId } - # organizationId role createdAt updatedAt diff --git a/web/types/graphql.d.ts b/web/types/graphql.d.ts index 418d60da..1bd8a741 100644 --- a/web/types/graphql.d.ts +++ b/web/types/graphql.d.ts @@ -971,4 +971,4 @@ export type FindUsersByOrganizationIdVariables = Exact<{ }>; -export type FindUsersByOrganizationId = { __typename?: 'Query', usersByOrganization: Array<{ __typename?: 'User', id: number, email: string, name?: string | null, role?: RoleEnum | null, createdAt: string, updatedAt: string, agency?: { __typename?: 'Agency', name: string, organizationId: number } | null }> }; +export type FindUsersByOrganizationId = { __typename?: 'Query', usersByOrganization: Array<{ __typename?: 'User', id: number, email: string, name?: string | null, role?: RoleEnum | null, createdAt: string, updatedAt: string, agency?: { __typename?: 'Agency', name: string } | null }> }; From 132a77ed412237a2802b2942b38c361b39634b96 Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Fri, 16 Feb 2024 14:52:16 -0500 Subject: [PATCH 04/10] CPF-103 fix tests to include required fields for User model --- .../subrecipients/subrecipients.scenarios.ts | 8 ++++++-- .../services/subrecipients/subrecipients.ts | 3 --- api/src/services/uploads/uploads.scenarios.ts | 18 ++++++++++++++++-- api/src/services/users/users.scenarios.ts | 1 - api/src/services/users/users.test.ts | 2 -- api/src/services/users/users.ts | 2 +- 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/api/src/services/subrecipients/subrecipients.scenarios.ts b/api/src/services/subrecipients/subrecipients.scenarios.ts index 430addd0..110d8b7d 100644 --- a/api/src/services/subrecipients/subrecipients.scenarios.ts +++ b/api/src/services/subrecipients/subrecipients.scenarios.ts @@ -17,9 +17,11 @@ export const standard = defineScenario({ updatedAt: '2023-12-09T14:50:18.317Z', uploadedBy: { create: { + name: 'String', email: 'String', 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' } }, @@ -73,9 +75,11 @@ export const standard = defineScenario({ updatedAt: '2023-12-09T14:50:18.317Z', uploadedBy: { create: { + name: 'String', email: 'String', 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' } }, diff --git a/api/src/services/subrecipients/subrecipients.ts b/api/src/services/subrecipients/subrecipients.ts index 0ad559bd..e6cfcc62 100644 --- a/api/src/services/subrecipients/subrecipients.ts +++ b/api/src/services/subrecipients/subrecipients.ts @@ -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 } }) diff --git a/api/src/services/uploads/uploads.scenarios.ts b/api/src/services/uploads/uploads.scenarios.ts index f19483ba..30634266 100644 --- a/api/src/services/uploads/uploads.scenarios.ts +++ b/api/src/services/uploads/uploads.scenarios.ts @@ -7,7 +7,14 @@ export const standard = defineScenario({ one: { data: { filename: 'String', - uploadedBy: { create: { email: 'String' } }, + uploadedBy: { + create: { + email: 'String', + name: 'String', + role: 'USDR_ADMIN', + agency: { create: { name: 'String', code: 'String' } }, + }, + }, agency: { create: { name: 'String', code: 'String' } }, organization: { create: { name: 'String' } }, reportingPeriod: { @@ -37,7 +44,14 @@ export const standard = defineScenario({ two: { data: { filename: 'String', - uploadedBy: { create: { email: 'String' } }, + uploadedBy: { + create: { + email: 'String', + name: 'String', + role: 'USDR_ADMIN', + agency: { create: { name: 'String', code: 'String' } }, + }, + }, agency: { create: { name: 'String', code: 'String' } }, organization: { create: { name: 'String' } }, reportingPeriod: { diff --git a/api/src/services/users/users.scenarios.ts b/api/src/services/users/users.scenarios.ts index e19cc537..073594e0 100644 --- a/api/src/services/users/users.scenarios.ts +++ b/api/src/services/users/users.scenarios.ts @@ -28,7 +28,6 @@ export const standard = defineScenario< data: { email: 'String', name: 'String', - organization: { create: { name: 'String' } }, agency: { create: { name: 'String', code: 'String' } }, role: 'ORGANIZATION_ADMIN', }, diff --git a/api/src/services/users/users.test.ts b/api/src/services/users/users.test.ts index 0c7fac40..1ebc65a7 100644 --- a/api/src/services/users/users.test.ts +++ b/api/src/services/users/users.test.ts @@ -25,7 +25,6 @@ describe('users', () => { scenario('creates a user', async (scenario: StandardScenario) => { mockCurrentUser({ id: scenario.user.one.id, - organizationId: scenario.user.one.organizationId, email: 'email@example.com', roles: ['USDR_ADMIN'], }) @@ -40,7 +39,6 @@ describe('users', () => { }) expect(result.email).toEqual(scenario.user.one.email) - expect(result.organizationId).toEqual(scenario.organization.one.id) }) scenario('updates a user', async (scenario: StandardScenario) => { diff --git a/api/src/services/users/users.ts b/api/src/services/users/users.ts index 0a96d2aa..9afcacc1 100644 --- a/api/src/services/users/users.ts +++ b/api/src/services/users/users.ts @@ -63,7 +63,7 @@ export const createUser: MutationResolvers['createUser'] = async ({ } return db.user.create({ - data: { ...input, organizationId: agency.organizationId }, + data: input, }) } catch (err) { throw new Error(err) From 20be4e6ed78af5c935f187c0da8652f5702a1d52 Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Fri, 16 Feb 2024 16:12:39 -0500 Subject: [PATCH 05/10] CPF-103 add 'unique' field attribute to user email --- .../20240216211114_make_user_email_unique/migration.sql | 8 ++++++++ api/db/schema.prisma | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 api/db/migrations/20240216211114_make_user_email_unique/migration.sql diff --git a/api/db/migrations/20240216211114_make_user_email_unique/migration.sql b/api/db/migrations/20240216211114_make_user_email_unique/migration.sql new file mode 100644 index 00000000..5e77f8e9 --- /dev/null +++ b/api/db/migrations/20240216211114_make_user_email_unique/migration.sql @@ -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"); diff --git a/api/db/schema.prisma b/api/db/schema.prisma index 11bf84aa..a09fb88b 100644 --- a/api/db/schema.prisma +++ b/api/db/schema.prisma @@ -35,7 +35,7 @@ model Organization { model User { id Int @id @default(autoincrement()) - email String + email String @unique name String agencyId Int createdAt DateTime @default(now()) @db.Timestamptz(6) From 31dfe0d2fdea77fd257e8b3e6133176cf258d639 Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Fri, 16 Feb 2024 17:26:45 -0500 Subject: [PATCH 06/10] CPF-103 use unique emails for testing --- .../services/subrecipients/subrecipients.scenarios.ts | 4 ++-- api/src/services/uploads/uploads.scenarios.ts | 4 ++-- api/src/services/users/users.scenarios.ts | 2 +- api/src/services/users/users.test.ts | 10 +++++----- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/api/src/services/subrecipients/subrecipients.scenarios.ts b/api/src/services/subrecipients/subrecipients.scenarios.ts index 110d8b7d..3736bdbe 100644 --- a/api/src/services/subrecipients/subrecipients.scenarios.ts +++ b/api/src/services/subrecipients/subrecipients.scenarios.ts @@ -18,7 +18,7 @@ export const standard = defineScenario({ uploadedBy: { create: { name: 'String', - email: 'String', + email: 'uniqueemail1@test.com', updatedAt: '2023-12-09T14:50:18.317Z', agency: { create: { name: 'String', code: 'String' } }, role: 'USDR_ADMIN', @@ -76,7 +76,7 @@ export const standard = defineScenario({ uploadedBy: { create: { name: 'String', - email: 'String', + email: 'uniqueemail2@test.com', updatedAt: '2023-12-09T14:50:18.317Z', agency: { create: { name: 'String', code: 'String' } }, role: 'USDR_ADMIN', diff --git a/api/src/services/uploads/uploads.scenarios.ts b/api/src/services/uploads/uploads.scenarios.ts index 30634266..de4a18bd 100644 --- a/api/src/services/uploads/uploads.scenarios.ts +++ b/api/src/services/uploads/uploads.scenarios.ts @@ -9,7 +9,7 @@ export const standard = defineScenario({ filename: 'String', uploadedBy: { create: { - email: 'String', + email: 'uniqueemail1@test.com', name: 'String', role: 'USDR_ADMIN', agency: { create: { name: 'String', code: 'String' } }, @@ -46,7 +46,7 @@ export const standard = defineScenario({ filename: 'String', uploadedBy: { create: { - email: 'String', + email: 'uniqueemail2@test.com', name: 'String', role: 'USDR_ADMIN', agency: { create: { name: 'String', code: 'String' } }, diff --git a/api/src/services/users/users.scenarios.ts b/api/src/services/users/users.scenarios.ts index 073594e0..cdd550f0 100644 --- a/api/src/services/users/users.scenarios.ts +++ b/api/src/services/users/users.scenarios.ts @@ -26,7 +26,7 @@ export const standard = defineScenario< user: { one: { data: { - email: 'String', + email: 'uniqueemail1@test.com', name: 'String', agency: { create: { name: 'String', code: 'String' } }, role: 'ORGANIZATION_ADMIN', diff --git a/api/src/services/users/users.test.ts b/api/src/services/users/users.test.ts index 1ebc65a7..e0e6c5c2 100644 --- a/api/src/services/users/users.test.ts +++ b/api/src/services/users/users.test.ts @@ -25,20 +25,20 @@ describe('users', () => { scenario('creates a user', async (scenario: StandardScenario) => { mockCurrentUser({ id: scenario.user.one.id, - email: 'email@example.com', + email: scenario.user.one.email, roles: ['USDR_ADMIN'], }) const result = await createUser({ input: { - name: scenario.user.one.name, - email: scenario.user.one.email, + email: 'uniqueemail2@test.com', + name: 'String', agencyId: scenario.agency.one.id, - role: 'ORGANIZATION_STAFF', + role: 'USDR_ADMIN', }, }) - expect(result.email).toEqual(scenario.user.one.email) + expect(result.email).toEqual('uniqueemail2@test.com') }) scenario('updates a user', async (scenario: StandardScenario) => { From f6e18c5d9cd661bbebcaa783340afca114d68ad4 Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Mon, 19 Feb 2024 12:52:59 -0500 Subject: [PATCH 07/10] CPF-101 update CreateUserInput fields to be required, use Redwood service validations to ensure valid and unique email address, non-empty name, valid agency --- api/src/graphql/users.sdl.ts | 15 +++++---- api/src/services/users/users.ts | 37 +++++++++++++++++---- api/types/graphql.d.ts | 31 ++++++++--------- web/src/components/User/NewUser/NewUser.tsx | 3 +- web/types/graphql.d.ts | 23 +++++++------ 5 files changed, 68 insertions(+), 41 deletions(-) diff --git a/api/src/graphql/users.sdl.ts b/api/src/graphql/users.sdl.ts index 137a66e6..2558dbb5 100644 --- a/api/src/graphql/users.sdl.ts +++ b/api/src/graphql/users.sdl.ts @@ -8,12 +8,12 @@ export const schema = gql` type User { id: Int! email: String! - name: String - agencyId: Int + name: String! + agencyId: Int! createdAt: DateTime! updatedAt: DateTime! - agency: Agency - role: RoleEnum + agency: Agency! + role: RoleEnum! certified: [ReportingPeriod]! uploaded: [Upload]! } @@ -26,9 +26,10 @@ export const schema = gql` input CreateUserInput { email: String! - name: String - agencyId: Int - role: RoleEnum + name: String! + agencyId: Int! + role: RoleEnum! + isActive: Boolean! } input UpdateUserInput { diff --git a/api/src/services/users/users.ts b/api/src/services/users/users.ts index 9afcacc1..eacc410b 100644 --- a/api/src/services/users/users.ts +++ b/api/src/services/users/users.ts @@ -4,6 +4,13 @@ import type { UserRelationResolvers, } from 'types/graphql' +import { + validate, + validateWith, + validateWithSync, + validateUniqueness, +} from '@redwoodjs/api' + import { AuthenticationError } from '@redwoodjs/graphql-server' import { ROLES } from 'src/lib/constants' @@ -49,19 +56,35 @@ const canCreateUser = (currentUser, userRoleToCreate: string) => { 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 { agencyId } = input - try { - const agency = await db.agency.findUnique({ where: { id: agencyId } }) + // Email input must be a valid email address; also takes care of null and undefined + validate(input.email, { + email: { message: 'Please provide a valid email address' }, + }) + // Email must be unique + await validateUniqueness( + 'user', + { email: input.email }, + { + message: 'This email is already in use', + } + ) + + // Name input must be present and not an empty string + validate(input.name, { + presence: { allowEmptyString: false, message: 'Please provide a name' }, + }) + + await validateWith(async () => { + const agency = await db.agency.findUnique({ where: { id: agencyId } }) if (!agency) { - throw new Error('Agency not found.') + throw 'This agency does not exist.' } + }) + try { return db.user.create({ data: input, }) diff --git a/api/types/graphql.d.ts b/api/types/graphql.d.ts index 17f99d74..1a881889 100644 --- a/api/types/graphql.d.ts +++ b/api/types/graphql.d.ts @@ -149,10 +149,11 @@ export type CreateUploadValidationInput = { }; export type CreateUserInput = { - agencyId?: InputMaybe; + agencyId: Scalars['Int']; email: Scalars['String']; - name?: InputMaybe; - role?: InputMaybe; + isActive: Scalars['Boolean']; + name: Scalars['String']; + role: RoleEnum; }; export type ExpenditureCategory = { @@ -745,14 +746,14 @@ export type UploadValidation = { export type User = { __typename?: 'User'; - agency?: Maybe; - agencyId?: Maybe; + agency: Agency; + agencyId: Scalars['Int']; certified: Array>; createdAt: Scalars['DateTime']; email: Scalars['String']; id: Scalars['Int']; - name?: Maybe; - role?: Maybe; + name: Scalars['String']; + role: RoleEnum; updatedAt: Scalars['DateTime']; uploaded: Array>; }; @@ -1427,28 +1428,28 @@ export type UploadValidationRelationResolvers = { - agency: OptArgsResolverFn, ParentType, ContextType>; - agencyId: OptArgsResolverFn, ParentType, ContextType>; + agency: OptArgsResolverFn; + agencyId: OptArgsResolverFn; certified: OptArgsResolverFn>, ParentType, ContextType>; createdAt: OptArgsResolverFn; email: OptArgsResolverFn; id: OptArgsResolverFn; - name: OptArgsResolverFn, ParentType, ContextType>; - role: OptArgsResolverFn, ParentType, ContextType>; + name: OptArgsResolverFn; + role: OptArgsResolverFn; updatedAt: OptArgsResolverFn; uploaded: OptArgsResolverFn>, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; export type UserRelationResolvers = { - agency?: RequiredResolverFn, ParentType, ContextType>; - agencyId?: RequiredResolverFn, ParentType, ContextType>; + agency?: RequiredResolverFn; + agencyId?: RequiredResolverFn; certified?: RequiredResolverFn>, ParentType, ContextType>; createdAt?: RequiredResolverFn; email?: RequiredResolverFn; id?: RequiredResolverFn; - name?: RequiredResolverFn, ParentType, ContextType>; - role?: RequiredResolverFn, ParentType, ContextType>; + name?: RequiredResolverFn; + role?: RequiredResolverFn; updatedAt?: RequiredResolverFn; uploaded?: RequiredResolverFn>, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; diff --git a/web/src/components/User/NewUser/NewUser.tsx b/web/src/components/User/NewUser/NewUser.tsx index 96dd1c8a..9dda0022 100644 --- a/web/src/components/User/NewUser/NewUser.tsx +++ b/web/src/components/User/NewUser/NewUser.tsx @@ -26,7 +26,8 @@ const NewUser = () => { }) const onSave = (input: CreateUserInput) => { - createUser({ variables: { input } }) + // temp fix for the missing isActive field + createUser({ variables: { input: { ...input, isActive: true } } }) } return ( diff --git a/web/types/graphql.d.ts b/web/types/graphql.d.ts index 1bd8a741..1b2e5922 100644 --- a/web/types/graphql.d.ts +++ b/web/types/graphql.d.ts @@ -130,10 +130,11 @@ export type CreateUploadValidationInput = { }; export type CreateUserInput = { - agencyId?: InputMaybe; + agencyId: Scalars['Int']; email: Scalars['String']; - name?: InputMaybe; - role?: InputMaybe; + isActive: Scalars['Boolean']; + name: Scalars['String']; + role: RoleEnum; }; export type ExpenditureCategory = { @@ -726,14 +727,14 @@ export type UploadValidation = { export type User = { __typename?: 'User'; - agency?: Maybe; - agencyId?: Maybe; + agency: Agency; + agencyId: Scalars['Int']; certified: Array>; createdAt: Scalars['DateTime']; email: Scalars['String']; id: Scalars['Int']; - name?: Maybe; - role?: Maybe; + name: Scalars['String']; + role: RoleEnum; updatedAt: Scalars['DateTime']; uploaded: Array>; }; @@ -928,7 +929,7 @@ export type EditUserByIdVariables = Exact<{ }>; -export type EditUserById = { __typename?: 'Query', user?: { __typename?: 'User', id: number, email: string, name?: string | null, agencyId?: number | null, role?: RoleEnum | null, createdAt: string, updatedAt: string } | null }; +export type EditUserById = { __typename?: 'Query', user?: { __typename?: 'User', id: number, email: string, name: string, agencyId: number, role: RoleEnum, createdAt: string, updatedAt: string } | null }; export type UpdateUserMutationVariables = Exact<{ id: Scalars['Int']; @@ -936,7 +937,7 @@ export type UpdateUserMutationVariables = Exact<{ }>; -export type UpdateUserMutation = { __typename?: 'Mutation', updateUser: { __typename?: 'User', id: number, email: string, name?: string | null, agencyId?: number | null, role?: RoleEnum | null, createdAt: string, updatedAt: string } }; +export type UpdateUserMutation = { __typename?: 'Mutation', updateUser: { __typename?: 'User', id: number, email: string, name: string, agencyId: number, role: RoleEnum, createdAt: string, updatedAt: string } }; export type CreateUserMutationVariables = Exact<{ input: CreateUserInput; @@ -957,7 +958,7 @@ export type FindUserByIdVariables = Exact<{ }>; -export type FindUserById = { __typename?: 'Query', user?: { __typename?: 'User', id: number, email: string, name?: string | null, agencyId?: number | null, role?: RoleEnum | null, createdAt: string, updatedAt: string } | null }; +export type FindUserById = { __typename?: 'Query', user?: { __typename?: 'User', id: number, email: string, name: string, agencyId: number, role: RoleEnum, createdAt: string, updatedAt: string } | null }; export type agenciesUnderUserOrganizationVariables = Exact<{ organizationId: Scalars['Int']; @@ -971,4 +972,4 @@ export type FindUsersByOrganizationIdVariables = Exact<{ }>; -export type FindUsersByOrganizationId = { __typename?: 'Query', usersByOrganization: Array<{ __typename?: 'User', id: number, email: string, name?: string | null, role?: RoleEnum | null, createdAt: string, updatedAt: string, agency?: { __typename?: 'Agency', name: string } | null }> }; +export type FindUsersByOrganizationId = { __typename?: 'Query', usersByOrganization: Array<{ __typename?: 'User', id: number, email: string, name: string, role: RoleEnum, createdAt: string, updatedAt: string, agency: { __typename?: 'Agency', name: string } }> }; From 9e5bdb4bca4b9d7da36c96da005a8a34efaf027b Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Mon, 19 Feb 2024 17:08:15 -0500 Subject: [PATCH 08/10] CPF-101 don't check if logged in user has the same organization as the new user for USDR admins, add agencyId to auth currentUser --- api/src/graphql/users.sdl.ts | 2 +- api/src/lib/auth.ts | 3 +- api/src/services/users/users.ts | 88 +++++++-------------- api/types/graphql.d.ts | 2 +- web/src/components/User/NewUser/NewUser.tsx | 3 +- 5 files changed, 34 insertions(+), 64 deletions(-) diff --git a/api/src/graphql/users.sdl.ts b/api/src/graphql/users.sdl.ts index 2558dbb5..8088f0f5 100644 --- a/api/src/graphql/users.sdl.ts +++ b/api/src/graphql/users.sdl.ts @@ -29,7 +29,7 @@ export const schema = gql` name: String! agencyId: Int! role: RoleEnum! - isActive: Boolean! + isActive: Boolean } input UpdateUserInput { diff --git a/api/src/lib/auth.ts b/api/src/lib/auth.ts index a2386ac5..f11a7aae 100644 --- a/api/src/lib/auth.ts +++ b/api/src/lib/auth.ts @@ -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@example.com', roles: ['USDR_ADMIN'], } diff --git a/api/src/services/users/users.ts b/api/src/services/users/users.ts index eacc410b..180fff01 100644 --- a/api/src/services/users/users.ts +++ b/api/src/services/users/users.ts @@ -4,16 +4,11 @@ import type { UserRelationResolvers, } from 'types/graphql' -import { - validate, - validateWith, - validateWithSync, - validateUniqueness, -} from '@redwoodjs/api' +import { validate, validateWith, validateUniqueness } from '@redwoodjs/api' import { AuthenticationError } from '@redwoodjs/graphql-server' - import { ROLES } from 'src/lib/constants' + import { db } from 'src/lib/db' export const users: QueryResolvers['users'] = () => { @@ -26,71 +21,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, }) => { - const { agencyId } = input + const { email, name, agencyId } = input + const { currentUser } = context + const { USDR_ADMIN } = ROLES - // Email input must be a valid email address; also takes care of null and undefined - validate(input.email, { + validate(email, { email: { message: 'Please provide a valid email address' }, }) - // Email must be unique - await validateUniqueness( - 'user', - { email: input.email }, - { - message: 'This email is already in use', - } - ) - - // Name input must be present and not an empty string - validate(input.name, { + validate(name, { presence: { allowEmptyString: false, message: 'Please provide a name' }, }) - await validateWith(async () => { - const agency = await db.agency.findUnique({ where: { id: agencyId } }) - if (!agency) { - throw 'This agency does not exist.' + validateWith(async () => { + if (currentUser.roles?.includes(USDR_ADMIN)) { + return true } - }) - try { - return db.user.create({ - data: input, + 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 }) => { diff --git a/api/types/graphql.d.ts b/api/types/graphql.d.ts index 1a881889..90c4fa0b 100644 --- a/api/types/graphql.d.ts +++ b/api/types/graphql.d.ts @@ -151,7 +151,7 @@ export type CreateUploadValidationInput = { export type CreateUserInput = { agencyId: Scalars['Int']; email: Scalars['String']; - isActive: Scalars['Boolean']; + isActive?: InputMaybe; name: Scalars['String']; role: RoleEnum; }; diff --git a/web/src/components/User/NewUser/NewUser.tsx b/web/src/components/User/NewUser/NewUser.tsx index 9dda0022..96dd1c8a 100644 --- a/web/src/components/User/NewUser/NewUser.tsx +++ b/web/src/components/User/NewUser/NewUser.tsx @@ -26,8 +26,7 @@ const NewUser = () => { }) const onSave = (input: CreateUserInput) => { - // temp fix for the missing isActive field - createUser({ variables: { input: { ...input, isActive: true } } }) + createUser({ variables: { input } }) } return ( From 763a5f31b37b37b64d3be26afa8108fbef765495 Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Mon, 19 Feb 2024 17:10:12 -0500 Subject: [PATCH 09/10] CPF-101 eslint fixes --- api/src/services/users/users.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/src/services/users/users.ts b/api/src/services/users/users.ts index 180fff01..e853d39e 100644 --- a/api/src/services/users/users.ts +++ b/api/src/services/users/users.ts @@ -5,10 +5,9 @@ import type { } from 'types/graphql' import { validate, validateWith, validateUniqueness } from '@redwoodjs/api' - import { AuthenticationError } from '@redwoodjs/graphql-server' -import { ROLES } from 'src/lib/constants' +import { ROLES } from 'src/lib/constants' import { db } from 'src/lib/db' export const users: QueryResolvers['users'] = () => { From 7fac4fe1097bcfebe92f9505ebb251abfbdb52fb Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Fri, 1 Mar 2024 22:27:32 +0000 Subject: [PATCH 10/10] bring back .vscode/settings.json --- .vscode/settings.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..4639be2e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "diffEditor.ignoreTrimWhitespace": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "editor.formatOnSaveMode": "modifications", + "eslint.debug": true +} \ No newline at end of file