diff --git a/common/exceptions/custom-exceptions.ts b/common/exceptions/custom-exceptions.ts index 6448ff7..8f17ee3 100644 --- a/common/exceptions/custom-exceptions.ts +++ b/common/exceptions/custom-exceptions.ts @@ -2,7 +2,23 @@ import { HttpException, HttpStatus } from '@nestjs/common'; // Base exception export class BaseException extends HttpException { - protected constructor(code: string, message: string, status: HttpStatus) { - super({ code, message }, status); + protected constructor( + errorCode: string, + errorMessage: string, + status: HttpStatus + ) { + super({ code: errorCode, message: errorMessage }, status); } } + +/* + * Organization Exceptions + * */ +export class OrganizationNotFoundException extends BaseException { + +} +// export class NotPartOfOrganizationException extends BaseException { +// constructor(code: string, message: string, status: HttpStatus) { +// super(); +// } +// } diff --git a/common/exceptions/error_codes.ts b/common/exceptions/error_codes.ts new file mode 100644 index 0000000..063e82a --- /dev/null +++ b/common/exceptions/error_codes.ts @@ -0,0 +1,17 @@ +import { HttpStatus } from "@nestjs/common"; + +interface AppError { + errorCode: string, + message: string, + status: HttpStatus +} + +type ErrorsType = Record + +export const ORGANIZATION_ERRORS: ErrorsType = { + NOT_FOUND: { + errorCode: 'ORG_001', + message: 'Organization not found', + status: HttpStatus.NOT_FOUND + } +} diff --git a/common/keys.ts b/common/keys.ts index c105005..0546e23 100644 --- a/common/keys.ts +++ b/common/keys.ts @@ -1,2 +1,5 @@ export const PUBLIC_KEY = '__PUBLIC_KEY__'; export const ROLE_KEY = '__ROLE_KEY__'; +export const ORG_ROLE_KEY = '__ORG_ROLE_KEY__' +export const ORG_ROLES_ALL_KEY = '__ORG_ROLE_ALL_KEY__'; +export const CAN_PERFORM_KEY = '__CAN_PERFORM_KEY__'; diff --git a/core/als/request-context.service.ts b/core/als/request-context.service.ts index 691a491..7d313f8 100644 --- a/core/als/request-context.service.ts +++ b/core/als/request-context.service.ts @@ -47,4 +47,12 @@ export class RequestContextService { get isTransaction(): boolean { return !!this.get().tx; } + + get orgId(): string { + return this.orgId + } + + set orgId(id: string) { + this.set('orgId', id) + } } diff --git a/core/als/request-context.type.ts b/core/als/request-context.type.ts index d416f05..04cb6f7 100644 --- a/core/als/request-context.type.ts +++ b/core/als/request-context.type.ts @@ -6,5 +6,6 @@ export interface RequestContext { correlationId?: string; headers: Record; user?: JwtPayload; + orgId?: string; tx?: Prisma.TransactionClient; } diff --git a/prisma/generated/prisma/internal/class.ts b/prisma/generated/prisma/internal/class.ts index 853736c..6fe283f 100644 --- a/prisma/generated/prisma/internal/class.ts +++ b/prisma/generated/prisma/internal/class.ts @@ -20,7 +20,7 @@ const config: runtime.GetPrismaClientConfig = { "clientVersion": "7.3.0", "engineVersion": "9d6ad21cbbceab97458517b147a6a09ff43aa735", "activeProvider": "postgresql", - "inlineSchema": "model OrganizationJoinRequest {\n userId String\n orgId String\n status ORGANIZATION_JOIN_REQUEST @default(PENDING)\n requestType ORGANIZATION_JOIN_REQUEST_TYPE\n requestedOn DateTime @default(now())\n role ORG_ROLE @default(member)\n updatedAt DateTime @updatedAt\n rejectReason String?\n requestMessage String?\n\n organization Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@unique([userId, orgId])\n @@map(\"organization_join_request\")\n}\n\nenum ORGANIZATION_JOIN_REQUEST {\n PENDING\n ACCEPTED\n REJECTED\n CANCELLED\n}\n\nenum ORGANIZATION_JOIN_REQUEST_TYPE {\n INVITED\n REQUESTED\n}\n\nmodel OrganizationUserJoinTable {\n userId String\n orgId String\n role ORG_ROLE @default(member)\n joinedDate DateTime @default(now())\n\n organization Organization @relation(fields: [orgId], references: [id], onDelete: Restrict)\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@unique([userId, orgId])\n @@map(\"organization_user_join\")\n}\n\nenum ORG_ROLE {\n owner\n admin\n member\n}\n\nmodel Organization {\n id String @id @default(uuid())\n name String\n description String?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n members OrganizationUserJoinTable[]\n requestingMembers OrganizationJoinRequest[]\n\n @@map(\"organization\")\n}\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel User {\n id String @id @default(uuid())\n firstName String\n middleName String?\n lastName String\n email String @unique\n password String\n role USER_ROLE @default(user)\n isVerified Boolean? @default(false) // TODO: Email using queue\n refreshToken String?\n profilePicture String?\n isDeleted Boolean? @default(false)\n deletedAt DateTime?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n organizations OrganizationUserJoinTable[]\n organizationsRequested OrganizationJoinRequest[]\n\n @@map(\"user\")\n}\n\nenum USER_ROLE {\n superadmin\n user\n}\n", + "inlineSchema": "model OrganizationJoinRequest {\n id String @id @default(uuid())\n userId String\n orgId String\n status ORGANIZATION_JOIN_REQUEST @default(PENDING)\n requestType ORGANIZATION_JOIN_REQUEST_TYPE\n requestedOn DateTime @default(now())\n role ORG_ROLE @default(member)\n updatedAt DateTime @updatedAt\n rejectReason String?\n requestMessage String?\n\n organization Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n // @@unique([userId, orgId])\n @@index([userId, orgId])\n @@map(\"organization_join_request\")\n}\n\nenum ORGANIZATION_JOIN_REQUEST {\n PENDING\n ACCEPTED\n REJECTED\n CANCELLED\n}\n\nenum ORGANIZATION_JOIN_REQUEST_TYPE {\n INVITED\n REQUESTED\n}\n\nmodel OrganizationUserJoinTable {\n userId String\n orgId String\n role ORG_ROLE @default(member)\n joinedDate DateTime @default(now())\n\n organization Organization @relation(fields: [orgId], references: [id], onDelete: Restrict)\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@unique([userId, orgId])\n @@map(\"organization_user_join\")\n}\n\nenum ORG_ROLE {\n owner\n admin\n member\n}\n\nmodel Organization {\n id String @id @default(uuid())\n name String\n description String?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n members OrganizationUserJoinTable[]\n requestingMembers OrganizationJoinRequest[]\n\n @@map(\"organization\")\n}\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel User {\n id String @id @default(uuid())\n firstName String\n middleName String?\n lastName String\n email String @unique\n password String\n role USER_ROLE @default(user)\n isVerified Boolean? @default(false) // TODO: Email using queue\n refreshToken String?\n profilePicture String?\n isDeleted Boolean? @default(false)\n deletedAt DateTime?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n organizations OrganizationUserJoinTable[]\n organizationsRequested OrganizationJoinRequest[]\n\n @@map(\"user\")\n}\n\nenum USER_ROLE {\n superadmin\n user\n}\n", "runtimeDataModel": { "models": {}, "enums": {}, @@ -28,7 +28,7 @@ const config: runtime.GetPrismaClientConfig = { } } -config.runtimeDataModel = JSON.parse("{\"models\":{\"OrganizationJoinRequest\":{\"fields\":[{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"orgId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"status\",\"kind\":\"enum\",\"type\":\"ORGANIZATION_JOIN_REQUEST\"},{\"name\":\"requestType\",\"kind\":\"enum\",\"type\":\"ORGANIZATION_JOIN_REQUEST_TYPE\"},{\"name\":\"requestedOn\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"role\",\"kind\":\"enum\",\"type\":\"ORG_ROLE\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"rejectReason\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"requestMessage\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"organization\",\"kind\":\"object\",\"type\":\"Organization\",\"relationName\":\"OrganizationToOrganizationJoinRequest\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"OrganizationJoinRequestToUser\"}],\"dbName\":\"organization_join_request\"},\"OrganizationUserJoinTable\":{\"fields\":[{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"orgId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"role\",\"kind\":\"enum\",\"type\":\"ORG_ROLE\"},{\"name\":\"joinedDate\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"organization\",\"kind\":\"object\",\"type\":\"Organization\",\"relationName\":\"OrganizationToOrganizationUserJoinTable\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"OrganizationUserJoinTableToUser\"}],\"dbName\":\"organization_user_join\"},\"Organization\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"members\",\"kind\":\"object\",\"type\":\"OrganizationUserJoinTable\",\"relationName\":\"OrganizationToOrganizationUserJoinTable\"},{\"name\":\"requestingMembers\",\"kind\":\"object\",\"type\":\"OrganizationJoinRequest\",\"relationName\":\"OrganizationToOrganizationJoinRequest\"}],\"dbName\":\"organization\"},\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"firstName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"middleName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"lastName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"role\",\"kind\":\"enum\",\"type\":\"USER_ROLE\"},{\"name\":\"isVerified\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"refreshToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"profilePicture\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isDeleted\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"deletedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"organizations\",\"kind\":\"object\",\"type\":\"OrganizationUserJoinTable\",\"relationName\":\"OrganizationUserJoinTableToUser\"},{\"name\":\"organizationsRequested\",\"kind\":\"object\",\"type\":\"OrganizationJoinRequest\",\"relationName\":\"OrganizationJoinRequestToUser\"}],\"dbName\":\"user\"}},\"enums\":{},\"types\":{}}") +config.runtimeDataModel = JSON.parse("{\"models\":{\"OrganizationJoinRequest\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"orgId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"status\",\"kind\":\"enum\",\"type\":\"ORGANIZATION_JOIN_REQUEST\"},{\"name\":\"requestType\",\"kind\":\"enum\",\"type\":\"ORGANIZATION_JOIN_REQUEST_TYPE\"},{\"name\":\"requestedOn\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"role\",\"kind\":\"enum\",\"type\":\"ORG_ROLE\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"rejectReason\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"requestMessage\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"organization\",\"kind\":\"object\",\"type\":\"Organization\",\"relationName\":\"OrganizationToOrganizationJoinRequest\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"OrganizationJoinRequestToUser\"}],\"dbName\":\"organization_join_request\"},\"OrganizationUserJoinTable\":{\"fields\":[{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"orgId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"role\",\"kind\":\"enum\",\"type\":\"ORG_ROLE\"},{\"name\":\"joinedDate\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"organization\",\"kind\":\"object\",\"type\":\"Organization\",\"relationName\":\"OrganizationToOrganizationUserJoinTable\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"OrganizationUserJoinTableToUser\"}],\"dbName\":\"organization_user_join\"},\"Organization\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"members\",\"kind\":\"object\",\"type\":\"OrganizationUserJoinTable\",\"relationName\":\"OrganizationToOrganizationUserJoinTable\"},{\"name\":\"requestingMembers\",\"kind\":\"object\",\"type\":\"OrganizationJoinRequest\",\"relationName\":\"OrganizationToOrganizationJoinRequest\"}],\"dbName\":\"organization\"},\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"firstName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"middleName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"lastName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"role\",\"kind\":\"enum\",\"type\":\"USER_ROLE\"},{\"name\":\"isVerified\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"refreshToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"profilePicture\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isDeleted\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"deletedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"organizations\",\"kind\":\"object\",\"type\":\"OrganizationUserJoinTable\",\"relationName\":\"OrganizationUserJoinTableToUser\"},{\"name\":\"organizationsRequested\",\"kind\":\"object\",\"type\":\"OrganizationJoinRequest\",\"relationName\":\"OrganizationJoinRequestToUser\"}],\"dbName\":\"user\"}},\"enums\":{},\"types\":{}}") async function decodeBase64AsWasm(wasmBase64: string): Promise { const { Buffer } = await import('node:buffer') diff --git a/prisma/generated/prisma/internal/prismaNamespace.ts b/prisma/generated/prisma/internal/prismaNamespace.ts index 0df8d51..7a7d258 100644 --- a/prisma/generated/prisma/internal/prismaNamespace.ts +++ b/prisma/generated/prisma/internal/prismaNamespace.ts @@ -743,6 +743,7 @@ export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof export const OrganizationJoinRequestScalarFieldEnum = { + id: 'id', userId: 'userId', orgId: 'orgId', status: 'status', diff --git a/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts b/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts index 1f73dca..a4b831b 100644 --- a/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts +++ b/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts @@ -74,6 +74,7 @@ export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof export const OrganizationJoinRequestScalarFieldEnum = { + id: 'id', userId: 'userId', orgId: 'orgId', status: 'status', diff --git a/prisma/generated/prisma/models/OrganizationJoinRequest.ts b/prisma/generated/prisma/models/OrganizationJoinRequest.ts index cc29063..f82cb42 100644 --- a/prisma/generated/prisma/models/OrganizationJoinRequest.ts +++ b/prisma/generated/prisma/models/OrganizationJoinRequest.ts @@ -25,6 +25,7 @@ export type AggregateOrganizationJoinRequest = { } export type OrganizationJoinRequestMinAggregateOutputType = { + id: string | null userId: string | null orgId: string | null status: $Enums.ORGANIZATION_JOIN_REQUEST | null @@ -37,6 +38,7 @@ export type OrganizationJoinRequestMinAggregateOutputType = { } export type OrganizationJoinRequestMaxAggregateOutputType = { + id: string | null userId: string | null orgId: string | null status: $Enums.ORGANIZATION_JOIN_REQUEST | null @@ -49,6 +51,7 @@ export type OrganizationJoinRequestMaxAggregateOutputType = { } export type OrganizationJoinRequestCountAggregateOutputType = { + id: number userId: number orgId: number status: number @@ -63,6 +66,7 @@ export type OrganizationJoinRequestCountAggregateOutputType = { export type OrganizationJoinRequestMinAggregateInputType = { + id?: true userId?: true orgId?: true status?: true @@ -75,6 +79,7 @@ export type OrganizationJoinRequestMinAggregateInputType = { } export type OrganizationJoinRequestMaxAggregateInputType = { + id?: true userId?: true orgId?: true status?: true @@ -87,6 +92,7 @@ export type OrganizationJoinRequestMaxAggregateInputType = { } export type OrganizationJoinRequestCountAggregateInputType = { + id?: true userId?: true orgId?: true status?: true @@ -172,6 +178,7 @@ export type OrganizationJoinRequestGroupByArgs | string userId?: Prisma.StringFilter<"OrganizationJoinRequest"> | string orgId?: Prisma.StringFilter<"OrganizationJoinRequest"> | string status?: Prisma.EnumORGANIZATION_JOIN_REQUESTFilter<"OrganizationJoinRequest"> | $Enums.ORGANIZATION_JOIN_REQUEST @@ -219,6 +227,7 @@ export type OrganizationJoinRequestWhereInput = { } export type OrganizationJoinRequestOrderByWithRelationInput = { + id?: Prisma.SortOrder userId?: Prisma.SortOrder orgId?: Prisma.SortOrder status?: Prisma.SortOrder @@ -233,7 +242,7 @@ export type OrganizationJoinRequestOrderByWithRelationInput = { } export type OrganizationJoinRequestWhereUniqueInput = Prisma.AtLeast<{ - userId_orgId?: Prisma.OrganizationJoinRequestUserIdOrgIdCompoundUniqueInput + id?: string AND?: Prisma.OrganizationJoinRequestWhereInput | Prisma.OrganizationJoinRequestWhereInput[] OR?: Prisma.OrganizationJoinRequestWhereInput[] NOT?: Prisma.OrganizationJoinRequestWhereInput | Prisma.OrganizationJoinRequestWhereInput[] @@ -248,9 +257,10 @@ export type OrganizationJoinRequestWhereUniqueInput = Prisma.AtLeast<{ requestMessage?: Prisma.StringNullableFilter<"OrganizationJoinRequest"> | string | null organization?: Prisma.XOR user?: Prisma.XOR -}, "userId_orgId"> +}, "id"> export type OrganizationJoinRequestOrderByWithAggregationInput = { + id?: Prisma.SortOrder userId?: Prisma.SortOrder orgId?: Prisma.SortOrder status?: Prisma.SortOrder @@ -269,6 +279,7 @@ export type OrganizationJoinRequestScalarWhereWithAggregatesInput = { AND?: Prisma.OrganizationJoinRequestScalarWhereWithAggregatesInput | Prisma.OrganizationJoinRequestScalarWhereWithAggregatesInput[] OR?: Prisma.OrganizationJoinRequestScalarWhereWithAggregatesInput[] NOT?: Prisma.OrganizationJoinRequestScalarWhereWithAggregatesInput | Prisma.OrganizationJoinRequestScalarWhereWithAggregatesInput[] + id?: Prisma.StringWithAggregatesFilter<"OrganizationJoinRequest"> | string userId?: Prisma.StringWithAggregatesFilter<"OrganizationJoinRequest"> | string orgId?: Prisma.StringWithAggregatesFilter<"OrganizationJoinRequest"> | string status?: Prisma.EnumORGANIZATION_JOIN_REQUESTWithAggregatesFilter<"OrganizationJoinRequest"> | $Enums.ORGANIZATION_JOIN_REQUEST @@ -281,6 +292,7 @@ export type OrganizationJoinRequestScalarWhereWithAggregatesInput = { } export type OrganizationJoinRequestCreateInput = { + id?: string status?: $Enums.ORGANIZATION_JOIN_REQUEST requestType: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE requestedOn?: Date | string @@ -293,6 +305,7 @@ export type OrganizationJoinRequestCreateInput = { } export type OrganizationJoinRequestUncheckedCreateInput = { + id?: string userId: string orgId: string status?: $Enums.ORGANIZATION_JOIN_REQUEST @@ -305,6 +318,7 @@ export type OrganizationJoinRequestUncheckedCreateInput = { } export type OrganizationJoinRequestUpdateInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string status?: Prisma.EnumORGANIZATION_JOIN_REQUESTFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST requestType?: Prisma.EnumORGANIZATION_JOIN_REQUEST_TYPEFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST_TYPE requestedOn?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string @@ -317,6 +331,7 @@ export type OrganizationJoinRequestUpdateInput = { } export type OrganizationJoinRequestUncheckedUpdateInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string orgId?: Prisma.StringFieldUpdateOperationsInput | string status?: Prisma.EnumORGANIZATION_JOIN_REQUESTFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST @@ -329,6 +344,7 @@ export type OrganizationJoinRequestUncheckedUpdateInput = { } export type OrganizationJoinRequestCreateManyInput = { + id?: string userId: string orgId: string status?: $Enums.ORGANIZATION_JOIN_REQUEST @@ -341,6 +357,7 @@ export type OrganizationJoinRequestCreateManyInput = { } export type OrganizationJoinRequestUpdateManyMutationInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string status?: Prisma.EnumORGANIZATION_JOIN_REQUESTFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST requestType?: Prisma.EnumORGANIZATION_JOIN_REQUEST_TYPEFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST_TYPE requestedOn?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string @@ -351,6 +368,7 @@ export type OrganizationJoinRequestUpdateManyMutationInput = { } export type OrganizationJoinRequestUncheckedUpdateManyInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string orgId?: Prisma.StringFieldUpdateOperationsInput | string status?: Prisma.EnumORGANIZATION_JOIN_REQUESTFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST @@ -362,12 +380,8 @@ export type OrganizationJoinRequestUncheckedUpdateManyInput = { requestMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null } -export type OrganizationJoinRequestUserIdOrgIdCompoundUniqueInput = { - userId: string - orgId: string -} - export type OrganizationJoinRequestCountOrderByAggregateInput = { + id?: Prisma.SortOrder userId?: Prisma.SortOrder orgId?: Prisma.SortOrder status?: Prisma.SortOrder @@ -380,6 +394,7 @@ export type OrganizationJoinRequestCountOrderByAggregateInput = { } export type OrganizationJoinRequestMaxOrderByAggregateInput = { + id?: Prisma.SortOrder userId?: Prisma.SortOrder orgId?: Prisma.SortOrder status?: Prisma.SortOrder @@ -392,6 +407,7 @@ export type OrganizationJoinRequestMaxOrderByAggregateInput = { } export type OrganizationJoinRequestMinOrderByAggregateInput = { + id?: Prisma.SortOrder userId?: Prisma.SortOrder orgId?: Prisma.SortOrder status?: Prisma.SortOrder @@ -413,6 +429,10 @@ export type OrganizationJoinRequestOrderByRelationAggregateInput = { _count?: Prisma.SortOrder } +export type StringFieldUpdateOperationsInput = { + set?: string +} + export type EnumORGANIZATION_JOIN_REQUESTFieldUpdateOperationsInput = { set?: $Enums.ORGANIZATION_JOIN_REQUEST } @@ -433,10 +453,6 @@ export type NullableStringFieldUpdateOperationsInput = { set?: string | null } -export type StringFieldUpdateOperationsInput = { - set?: string -} - export type OrganizationJoinRequestCreateNestedManyWithoutOrganizationInput = { create?: Prisma.XOR | Prisma.OrganizationJoinRequestCreateWithoutOrganizationInput[] | Prisma.OrganizationJoinRequestUncheckedCreateWithoutOrganizationInput[] connectOrCreate?: Prisma.OrganizationJoinRequestCreateOrConnectWithoutOrganizationInput | Prisma.OrganizationJoinRequestCreateOrConnectWithoutOrganizationInput[] @@ -522,6 +538,7 @@ export type OrganizationJoinRequestUncheckedUpdateManyWithoutUserNestedInput = { } export type OrganizationJoinRequestCreateWithoutOrganizationInput = { + id?: string status?: $Enums.ORGANIZATION_JOIN_REQUEST requestType: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE requestedOn?: Date | string @@ -533,6 +550,7 @@ export type OrganizationJoinRequestCreateWithoutOrganizationInput = { } export type OrganizationJoinRequestUncheckedCreateWithoutOrganizationInput = { + id?: string userId: string status?: $Enums.ORGANIZATION_JOIN_REQUEST requestType: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE @@ -573,6 +591,7 @@ export type OrganizationJoinRequestScalarWhereInput = { AND?: Prisma.OrganizationJoinRequestScalarWhereInput | Prisma.OrganizationJoinRequestScalarWhereInput[] OR?: Prisma.OrganizationJoinRequestScalarWhereInput[] NOT?: Prisma.OrganizationJoinRequestScalarWhereInput | Prisma.OrganizationJoinRequestScalarWhereInput[] + id?: Prisma.StringFilter<"OrganizationJoinRequest"> | string userId?: Prisma.StringFilter<"OrganizationJoinRequest"> | string orgId?: Prisma.StringFilter<"OrganizationJoinRequest"> | string status?: Prisma.EnumORGANIZATION_JOIN_REQUESTFilter<"OrganizationJoinRequest"> | $Enums.ORGANIZATION_JOIN_REQUEST @@ -585,6 +604,7 @@ export type OrganizationJoinRequestScalarWhereInput = { } export type OrganizationJoinRequestCreateWithoutUserInput = { + id?: string status?: $Enums.ORGANIZATION_JOIN_REQUEST requestType: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE requestedOn?: Date | string @@ -596,6 +616,7 @@ export type OrganizationJoinRequestCreateWithoutUserInput = { } export type OrganizationJoinRequestUncheckedCreateWithoutUserInput = { + id?: string orgId: string status?: $Enums.ORGANIZATION_JOIN_REQUEST requestType: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE @@ -633,6 +654,7 @@ export type OrganizationJoinRequestUpdateManyWithWhereWithoutUserInput = { } export type OrganizationJoinRequestCreateManyOrganizationInput = { + id?: string userId: string status?: $Enums.ORGANIZATION_JOIN_REQUEST requestType: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE @@ -644,6 +666,7 @@ export type OrganizationJoinRequestCreateManyOrganizationInput = { } export type OrganizationJoinRequestUpdateWithoutOrganizationInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string status?: Prisma.EnumORGANIZATION_JOIN_REQUESTFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST requestType?: Prisma.EnumORGANIZATION_JOIN_REQUEST_TYPEFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST_TYPE requestedOn?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string @@ -655,6 +678,7 @@ export type OrganizationJoinRequestUpdateWithoutOrganizationInput = { } export type OrganizationJoinRequestUncheckedUpdateWithoutOrganizationInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string status?: Prisma.EnumORGANIZATION_JOIN_REQUESTFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST requestType?: Prisma.EnumORGANIZATION_JOIN_REQUEST_TYPEFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST_TYPE @@ -666,6 +690,7 @@ export type OrganizationJoinRequestUncheckedUpdateWithoutOrganizationInput = { } export type OrganizationJoinRequestUncheckedUpdateManyWithoutOrganizationInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string status?: Prisma.EnumORGANIZATION_JOIN_REQUESTFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST requestType?: Prisma.EnumORGANIZATION_JOIN_REQUEST_TYPEFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST_TYPE @@ -677,6 +702,7 @@ export type OrganizationJoinRequestUncheckedUpdateManyWithoutOrganizationInput = } export type OrganizationJoinRequestCreateManyUserInput = { + id?: string orgId: string status?: $Enums.ORGANIZATION_JOIN_REQUEST requestType: $Enums.ORGANIZATION_JOIN_REQUEST_TYPE @@ -688,6 +714,7 @@ export type OrganizationJoinRequestCreateManyUserInput = { } export type OrganizationJoinRequestUpdateWithoutUserInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string status?: Prisma.EnumORGANIZATION_JOIN_REQUESTFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST requestType?: Prisma.EnumORGANIZATION_JOIN_REQUEST_TYPEFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST_TYPE requestedOn?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string @@ -699,6 +726,7 @@ export type OrganizationJoinRequestUpdateWithoutUserInput = { } export type OrganizationJoinRequestUncheckedUpdateWithoutUserInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string orgId?: Prisma.StringFieldUpdateOperationsInput | string status?: Prisma.EnumORGANIZATION_JOIN_REQUESTFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST requestType?: Prisma.EnumORGANIZATION_JOIN_REQUEST_TYPEFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST_TYPE @@ -710,6 +738,7 @@ export type OrganizationJoinRequestUncheckedUpdateWithoutUserInput = { } export type OrganizationJoinRequestUncheckedUpdateManyWithoutUserInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string orgId?: Prisma.StringFieldUpdateOperationsInput | string status?: Prisma.EnumORGANIZATION_JOIN_REQUESTFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST requestType?: Prisma.EnumORGANIZATION_JOIN_REQUEST_TYPEFieldUpdateOperationsInput | $Enums.ORGANIZATION_JOIN_REQUEST_TYPE @@ -723,6 +752,7 @@ export type OrganizationJoinRequestUncheckedUpdateManyWithoutUserInput = { export type OrganizationJoinRequestSelect = runtime.Types.Extensions.GetSelect<{ + id?: boolean userId?: boolean orgId?: boolean status?: boolean @@ -737,6 +767,7 @@ export type OrganizationJoinRequestSelect export type OrganizationJoinRequestSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean userId?: boolean orgId?: boolean status?: boolean @@ -751,6 +782,7 @@ export type OrganizationJoinRequestSelectCreateManyAndReturn export type OrganizationJoinRequestSelectUpdateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean userId?: boolean orgId?: boolean status?: boolean @@ -765,6 +797,7 @@ export type OrganizationJoinRequestSelectUpdateManyAndReturn export type OrganizationJoinRequestSelectScalar = { + id?: boolean userId?: boolean orgId?: boolean status?: boolean @@ -776,7 +809,7 @@ export type OrganizationJoinRequestSelectScalar = { requestMessage?: boolean } -export type OrganizationJoinRequestOmit = runtime.Types.Extensions.GetOmit<"userId" | "orgId" | "status" | "requestType" | "requestedOn" | "role" | "updatedAt" | "rejectReason" | "requestMessage", ExtArgs["result"]["organizationJoinRequest"]> +export type OrganizationJoinRequestOmit = runtime.Types.Extensions.GetOmit<"id" | "userId" | "orgId" | "status" | "requestType" | "requestedOn" | "role" | "updatedAt" | "rejectReason" | "requestMessage", ExtArgs["result"]["organizationJoinRequest"]> export type OrganizationJoinRequestInclude = { organization?: boolean | Prisma.OrganizationDefaultArgs user?: boolean | Prisma.UserDefaultArgs @@ -797,6 +830,7 @@ export type $OrganizationJoinRequestPayload } scalars: runtime.Types.Extensions.GetPayloadResult<{ + id: string userId: string orgId: string status: $Enums.ORGANIZATION_JOIN_REQUEST @@ -889,8 +923,8 @@ export interface OrganizationJoinRequestDelegate(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions>> @@ -934,9 +968,9 @@ export interface OrganizationJoinRequestDelegate readonly userId: Prisma.FieldRef<"OrganizationJoinRequest", 'String'> readonly orgId: Prisma.FieldRef<"OrganizationJoinRequest", 'String'> readonly status: Prisma.FieldRef<"OrganizationJoinRequest", 'ORGANIZATION_JOIN_REQUEST'> diff --git a/prisma/migrations/20260311130149_remove_unique_join_req/migration.sql b/prisma/migrations/20260311130149_remove_unique_join_req/migration.sql new file mode 100644 index 0000000..06b4c7d --- /dev/null +++ b/prisma/migrations/20260311130149_remove_unique_join_req/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - The required column `id` was added to the `organization_join_request` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required. + +*/ +-- DropIndex +DROP INDEX "organization_join_request_userId_orgId_key"; + +-- AlterTable +ALTER TABLE "organization_join_request" ADD COLUMN "id" TEXT NOT NULL, +ADD CONSTRAINT "organization_join_request_pkey" PRIMARY KEY ("id"); + +-- CreateIndex +CREATE INDEX "organization_join_request_userId_orgId_idx" ON "organization_join_request"("userId", "orgId"); diff --git a/prisma/models/organization-join-request.prisma b/prisma/models/organization-join-request.prisma index 81dc006..53ddd8e 100644 --- a/prisma/models/organization-join-request.prisma +++ b/prisma/models/organization-join-request.prisma @@ -1,4 +1,5 @@ model OrganizationJoinRequest { + id String @id @default(uuid()) userId String orgId String status ORGANIZATION_JOIN_REQUEST @default(PENDING) @@ -12,7 +13,8 @@ model OrganizationJoinRequest { organization Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@unique([userId, orgId]) + // @@unique([userId, orgId]) + @@index([userId, orgId]) @@map("organization_join_request") } diff --git a/src/auth/decorators/authorization.decorator.ts b/src/auth/decorators/authorization.decorator.ts new file mode 100644 index 0000000..ffabdd7 --- /dev/null +++ b/src/auth/decorators/authorization.decorator.ts @@ -0,0 +1,16 @@ +import { applyDecorators, SetMetadata, UseGuards } from "@nestjs/common"; +import { CAN_PERFORM_KEY } from "common/keys"; +import { ORG_ROLE } from "prisma/generated/prisma/enums"; +import { AuthorizationGuard } from "../guards"; + +/* + *Is this user part of the organization (And optionally, has required role) + * */ + +export function Authorization(role?: ORG_ROLE[]) { + return applyDecorators( + SetMetadata(CAN_PERFORM_KEY, role), + UseGuards(AuthorizationGuard) + ) +} +//export const Authorization = (role?: ORG_ROLE[]) => SetMetadata(CAN_PERFORM_KEY, role) diff --git a/src/auth/decorators/index.ts b/src/auth/decorators/index.ts index ca7ce96..1fac1b8 100644 --- a/src/auth/decorators/index.ts +++ b/src/auth/decorators/index.ts @@ -1,2 +1,3 @@ export * from './public.decorator'; export * from './role.decorator'; +export * from './authorization.decorator'; diff --git a/src/auth/decorators/role.decorator.ts b/src/auth/decorators/role.decorator.ts index f0c0b2c..521a78c 100644 --- a/src/auth/decorators/role.decorator.ts +++ b/src/auth/decorators/role.decorator.ts @@ -1,4 +1,6 @@ import { SetMetadata } from '@nestjs/common'; -import { ROLE_KEY } from 'common/keys'; +import { ORG_ROLE_KEY, ROLE_KEY } from 'common/keys'; export const Roles = (role: string) => SetMetadata(ROLE_KEY, role); + +export const OrgRole = (role: string) => SetMetadata(ORG_ROLE_KEY, role); diff --git a/src/auth/guards/authorization.guard.ts b/src/auth/guards/authorization.guard.ts new file mode 100644 index 0000000..461e714 --- /dev/null +++ b/src/auth/guards/authorization.guard.ts @@ -0,0 +1,57 @@ +import { + BadRequestException, + CanActivate, + ExecutionContext, + ForbiddenException, + Injectable, + UnauthorizedException +} from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import { CAN_PERFORM_KEY } from "common/keys"; +import { RequestContextService } from "core/als/request-context.service"; +import { ORG_ROLE } from "prisma/generated/prisma/enums"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class AuthorizationGuard implements CanActivate { + constructor( + private readonly reqeustContext: RequestContextService, + private readonly reflector: Reflector, + private readonly prisma: PrismaService, + ) { }; + + async canActivate(context: ExecutionContext): Promise { + const requiredRole = this.reflector.getAllAndOverride( + CAN_PERFORM_KEY, + [context.getHandler(), context.getClass()] + ) + + const userId = this.reqeustContext.user.userId; + if (!userId) + throw new UnauthorizedException() + + const request = context.switchToHttp().getRequest() + const orgId = request.params.orgId; + + if (!orgId) + throw new BadRequestException() + + const userIsPartOfOrg = await this.prisma.organizationUserJoinTable.findUnique({ + where: { + userId_orgId: { + userId, + orgId + }, + ...(requiredRole ? { role: { in: requiredRole } } : {}) + }, + select: { + userId: true + } + }) + if (!userIsPartOfOrg) + throw new ForbiddenException() + + this.reqeustContext.orgId = orgId; + return true; + } +} diff --git a/src/auth/guards/index.ts b/src/auth/guards/index.ts index e69de29..7014453 100644 --- a/src/auth/guards/index.ts +++ b/src/auth/guards/index.ts @@ -0,0 +1 @@ +export * from "./authorization.guard" diff --git a/src/auth/guards/rbac.guard.ts b/src/auth/guards/rbac.guard.ts index 5b9be30..c89654d 100644 --- a/src/auth/guards/rbac.guard.ts +++ b/src/auth/guards/rbac.guard.ts @@ -11,7 +11,7 @@ export class RbacGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly requestContext: RequestContextService, - ) {} + ) { } canActivate(context: ExecutionContext) { const requiredRole = this.reflector.getAllAndOverride(ROLE_KEY, [ context.getHandler(), diff --git a/src/organization-membership/dto/index.ts b/src/organization-membership/dto/index.ts index 0231791..6738b86 100644 --- a/src/organization-membership/dto/index.ts +++ b/src/organization-membership/dto/index.ts @@ -1,3 +1,4 @@ export * from './invite-to-org.dto'; export * from './join-request.dto' export * from "./user-invitation-action.dto" +export * from "./org-request-action.dto" diff --git a/src/organization-membership/dto/invite-to-org.dto.ts b/src/organization-membership/dto/invite-to-org.dto.ts index 818bd22..27f721b 100644 --- a/src/organization-membership/dto/invite-to-org.dto.ts +++ b/src/organization-membership/dto/invite-to-org.dto.ts @@ -21,3 +21,14 @@ export class InviteUserToOrganizationRequestDTO { @IsNotEmpty() role: ORG_ROLE; } + +export class CancelInviteUserToOrganizationRequestDTO { + @ApiProperty({ + description: 'Who to cancel', + example: 'bb1c81da-ce8f-4231-aee8-2b976124a589', + type: 'string', + }) + @IsUUID() + @IsNotEmpty() + userId: string; +} diff --git a/src/organization-membership/dto/org-request-action.dto.ts b/src/organization-membership/dto/org-request-action.dto.ts new file mode 100644 index 0000000..bbfc444 --- /dev/null +++ b/src/organization-membership/dto/org-request-action.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger" +import { IsEnum, IsNotEmpty, IsOptional, IsString } from "class-validator" +import { USER_ORG_ACCEPT_REJECT_ACTION } from "../constants" + +export class UserOrganizationRequestActionRequestDTO { + @ApiProperty({ + description: 'Action', + example: USER_ORG_ACCEPT_REJECT_ACTION.ACCEPT, + type: 'string', + }) + @IsEnum(USER_ORG_ACCEPT_REJECT_ACTION) + @IsNotEmpty() + action: USER_ORG_ACCEPT_REJECT_ACTION + + @ApiPropertyOptional({ + description: 'Message(reject reason)', + example: 'Bad sry or smth', + type: 'string', + }) + @IsString() + @IsOptional() + message?: string +} diff --git a/src/organization-membership/dto/user-invitation-action.dto.ts b/src/organization-membership/dto/user-invitation-action.dto.ts index 92c49ab..c89df75 100644 --- a/src/organization-membership/dto/user-invitation-action.dto.ts +++ b/src/organization-membership/dto/user-invitation-action.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator"; +import { IsEnum, IsNotEmpty, IsOptional, IsString } from "class-validator"; import { USER_ORG_ACCEPT_REJECT_ACTION } from "../constants"; diff --git a/src/organization-membership/organization-membership.controller.ts b/src/organization-membership/organization-membership.controller.ts index 949b2b9..9b265aa 100644 --- a/src/organization-membership/organization-membership.controller.ts +++ b/src/organization-membership/organization-membership.controller.ts @@ -2,8 +2,9 @@ import { Body, Controller, Delete, Get, Param, ParseEnumPipe, ParseUUIDPipe, Pat import { OrganizationMembershipService } from './organization-membership.service'; import { RequestContextService } from 'core/als/request-context.service'; import { ApiBearerAuth, ApiOperation, ApiParam, ApiQuery } from '@nestjs/swagger'; -import { JoinRequestToOrganizationRequestDTO, UserOrganizationInvitationActionRequestDTO } from './dto'; -import { ORGANIZATION_JOIN_REQUEST_TYPE } from 'prisma/generated/prisma/enums'; +import { CancelInviteUserToOrganizationRequestDTO, InviteUserToOrganizationRequestDTO, JoinRequestToOrganizationRequestDTO, UserOrganizationInvitationActionRequestDTO, UserOrganizationRequestActionRequestDTO } from './dto'; +import { ORG_ROLE, ORGANIZATION_JOIN_REQUEST_TYPE } from 'prisma/generated/prisma/enums'; +import { Authorization } from 'src/auth/decorators'; /* NOTE: Regarding endpoint path naming * - Since we follow REST style, endpoint are resource based. @@ -42,10 +43,17 @@ export class OrganizationMembershipController { name: 'orgId', type: String, }) - @Delete('organization/:orgId/join-request') - async cancelRequestToJoinOrg(@Param('orgId', new ParseUUIDPipe()) orgId: string) { + @Delete('organization/:orgId/join-request/:id') + async cancelRequestToJoinOrg( + @Param('orgId', new ParseUUIDPipe()) orgId: string, + @Param('id', new ParseUUIDPipe()) joinReqId: string + ) { const user = this.requestContext.user; - return await this.orgMemService.userCancelOrgJoinRequest(user.userId, orgId) + return await this.orgMemService.userCancelOrgJoinRequest( + user.userId, + orgId, + joinReqId + ) } @ApiOperation({ summary: 'Accept or reject an invitation from an organization' }) @@ -53,15 +61,17 @@ export class OrganizationMembershipController { name: 'orgId', type: String, }) - @Patch('organization/:orgId/invitation') + @Patch('organization/:orgId/invitation/:id') async acceptOrRejectInvitation( @Param('orgId', new ParseUUIDPipe()) orgId: string, + @Param('id', new ParseUUIDPipe()) invitationId: string, @Body() body: UserOrganizationInvitationActionRequestDTO ) { const user = this.requestContext.user; return await this.orgMemService.userOrganiaztionRequestAction( user.userId, orgId, + invitationId, body ) } @@ -110,36 +120,66 @@ export class OrganizationMembershipController { * ORGANIZATION OPERATIONS * */ - // @ApiOperation({ summary: 'Invite user to organization' }) - // @ApiParam({ - // name: 'orgId', - // type: String, - // }) - // @Post('organization/:orgId/invitation') - // async inviteUserToOrg( - // @Param('orgId', new ParseUUIDPipe()) orgId: string, - // @Body() body: InviteUserToOrganizationRequestDTO - // ) { - // const user = this.requestContext.user; - // return await this.orgMemService.inviteUserToOrg( - // user.userId, - // orgId, - // body - // ); - // } - // @Get('organization/:id/members') - // async getOrganizationMemebers(@Param('id') orgId: string) { - // return await this.orgMemService.getMemebersOfOrganization(orgId); - // } - // - // @Get('organization/:id/invitations') - // async getOrganizationInvitations(@Param('id') orgId: string) { - // return await this.orgMemService.getOrganizationRequestList(orgId); - // } - // - // @Patch('organization/:id/accept-request') - // acceptJoinRequest() { } - // - // @Patch('organization/:id/reject-request') - // rejectJoinRequest() { } + @ApiOperation({ summary: 'Invite user to organization' }) + @ApiParam({ + name: 'orgId', + type: String, + }) + @Authorization([ORG_ROLE.admin, ORG_ROLE.owner]) + @Post('organization/:orgId/invitation') + async inviteUserToOrg( + @Param('orgId', new ParseUUIDPipe()) orgId: string, + @Body() body: InviteUserToOrganizationRequestDTO + ) { + return await this.orgMemService.inviteUserToOrg( + orgId, + body + ); + } + + @ApiOperation({ summary: 'Cancel a sent invitation to user' }) + @ApiParam({ + name: 'userId', + type: String, + }) + @Authorization([ORG_ROLE.admin, ORG_ROLE.owner]) + @Delete('organization/:orgId/invitation/:id') + async cancelInvitationsToUser( + @Param('orgId', new ParseUUIDPipe()) orgId: string, + @Param('id', new ParseUUIDPipe()) invitationId: string, + @Body() body: CancelInviteUserToOrganizationRequestDTO + ) { + const userId = body.userId; + return await this.orgMemService.orgCancelUserInviteRequest(userId, orgId, invitationId) + } + + @Get('organization/:orgId/members') + @Authorization() + async getOrganizationMemebers(@Param('orgId') orgId: string) { + const user = this.requestContext.user; + return await this.orgMemService.getMemebersOfOrganization(user.userId, orgId); + } + + @Get('organization/:orgId/invitations') + @Authorization([ORG_ROLE.admin, ORG_ROLE.owner]) + async getOrganizationInvitations(@Param('orgId') orgId: string) { + const user = this.requestContext.user; + return await this.orgMemService.getOrganizationRequestList(user.userId, orgId); + } + + @Patch('organization/:orgId/request/:id') + @Authorization([ORG_ROLE.admin, ORG_ROLE.owner]) + async acceptOrRejectRequest( + @Param('orgId', new ParseUUIDPipe()) orgId: string, + @Param('id', new ParseUUIDPipe()) invitationId: string, + @Body() body: UserOrganizationRequestActionRequestDTO + ) { + const user = this.requestContext.user; + return await this.orgMemService.organizationUserJoinRequestAction( + user.userId, + orgId, + invitationId, + body + ) + } } diff --git a/src/organization-membership/organization-membership.service.ts b/src/organization-membership/organization-membership.service.ts index d3d93d2..a315fde 100644 --- a/src/organization-membership/organization-membership.service.ts +++ b/src/organization-membership/organization-membership.service.ts @@ -6,7 +6,7 @@ import { } from '@nestjs/common'; import { OrganizationService } from 'src/organization/organization.service'; import { UserService } from 'src/user/user.service'; -import { InviteUserToOrganizationRequestDTO, UserOrganizationInvitationActionRequestDTO } from './dto'; +import { InviteUserToOrganizationRequestDTO, UserOrganizationInvitationActionRequestDTO, UserOrganizationRequestActionRequestDTO } from './dto'; import { PrismaService } from 'src/prisma/prisma.service'; import { ORGANIZATION_JOIN_REQUEST, @@ -26,60 +26,6 @@ export class OrganizationMembershipService { private readonly prisma: PrismaService, private readonly authorization: AuthorizationService, ) { } - async inviteUserToOrg( - userId: string, - orgId: string, - dto: InviteUserToOrganizationRequestDTO, - ) { - const { invitedUserEmail, ...rest } = dto; - const [orgExists, invitedUser] = await Promise.all([ - this.orgService.findById(orgId), - this.userService.findByEmail(invitedUserEmail), - ]); - - if (!orgExists) throw new NotFoundException('Organization'); - if (!invitedUser) throw new NotFoundException('User'); - - const userAlreadyPart = - await this.prisma.organizationUserJoinTable.findUnique({ - where: { - userId_orgId: { - orgId: orgId, - userId: invitedUser.id, - }, - }, - }); - - if (userAlreadyPart) - throw new BadRequestException('User already part of this organization'); - - const canInviteUser = await this.authorization.canPerformOperation( - userId, - orgId, - USER_ORGANIZATION_OPERATIONS.INVITE_USERS, - ); - if (!canInviteUser) throw new ForbiddenException('Insufficient Permission'); - - try { - const invitation = await this.prisma.organizationJoinRequest.create({ - data: { - ...rest, - userId: invitedUser.id, - orgId, - requestType: ORGANIZATION_JOIN_REQUEST_TYPE.INVITED, - }, - }); - - return invitation; - } catch (err) { - if (err instanceof Prisma.PrismaClientKnownRequestError) { - if (err.code === 'P2002') - throw new BadRequestException('User invitation already sent.'); - } else { - throw err; - } - } - } /* * * USER OPERATIONS @@ -95,22 +41,17 @@ export class OrganizationMembershipService { userAlreadyPartOf ] = await Promise.all([ this.orgService.findById(orgId), - this.prisma.organizationJoinRequest.findUnique({ + this.prisma.organizationJoinRequest.findFirst({ where: { - userId_orgId: { - userId, - orgId: orgId - }, + orgId, + userId, status: ORGANIZATION_JOIN_REQUEST.PENDING }, - select: { orgId: true } }), - this.prisma.organizationUserJoinTable.findUnique({ + this.prisma.organizationUserJoinTable.findFirst({ where: { - userId_orgId: { - orgId: orgId, - userId - } + orgId, + userId, }, select: { userId: true } }) @@ -136,15 +77,15 @@ export class OrganizationMembershipService { async userCancelOrgJoinRequest( userId: string, - orgId: string + orgId: string, + id: string, ) { try { await this.prisma.organizationJoinRequest.update({ where: { - userId_orgId: { - userId, - orgId - }, + id, + userId, + orgId }, data: { status: ORGANIZATION_JOIN_REQUEST.CANCELLED, @@ -174,8 +115,9 @@ export class OrganizationMembershipService { return await this.prisma.organizationJoinRequest.findMany({ where: { userId, - requestType: joinReqType - } + requestType: joinReqType, + }, + include: { user: { select: { firstName: true, email: true } } } }) } @@ -183,24 +125,19 @@ export class OrganizationMembershipService { async userOrganiaztionRequestAction( userId: string, orgId: string, + id: string, dto: UserOrganizationInvitationActionRequestDTO ) { - const [orgExists, hasUserSendRequest] = await Promise.all([ - this.orgService.findById(orgId), - this.prisma.organizationJoinRequest.findUnique({ - where: { - userId_orgId: { - userId, - orgId: orgId - }, - status: ORGANIZATION_JOIN_REQUEST.PENDING, - requestType: ORGANIZATION_JOIN_REQUEST_TYPE.REQUESTED - } - }) - ]) + const hasUserSendRequest = await this.prisma.organizationJoinRequest.findUnique({ + where: { + id, + userId, + orgId, + status: ORGANIZATION_JOIN_REQUEST.PENDING, + requestType: ORGANIZATION_JOIN_REQUEST_TYPE.REQUESTED + } + }) - if (!orgExists) - throw new NotFoundException("Organization") if (!hasUserSendRequest) throw new BadRequestException("No pending join request") @@ -212,10 +149,9 @@ export class OrganizationMembershipService { await tx.organizationJoinRequest.update({ where: { - userId_orgId: { - userId, - orgId: orgId, - } + id, + userId, + orgId, }, data: { status: userAction, @@ -322,9 +258,9 @@ export class OrganizationMembershipService { } async getOrganizationsOfUser(userId: string) { - return await this.prisma.organizationJoinRequest.findMany({ + return await this.prisma.organizationUserJoinTable.findMany({ where: { - userId + userId, }, include: { organization: { @@ -351,41 +287,166 @@ export class OrganizationMembershipService { return true; } + async inviteUserToOrg( + orgId: string, + dto: InviteUserToOrganizationRequestDTO, + ) { + const { invitedUserEmail, ...rest } = dto; + + const invitedUser = await this.userService.findByEmail(invitedUserEmail); + if (!invitedUser) throw new NotFoundException('User'); + + const userAlreadyPart = + await this.prisma.organizationUserJoinTable.findUnique({ + where: { + userId_orgId: { + orgId: orgId, + userId: invitedUser.id, + }, + }, + }); + + if (userAlreadyPart) + throw new BadRequestException('User already part of this organization'); + + // TODO: Test in Authorization and remove + // const canInviteUser = await this.authorization.canPerformOperation( + // userId, + // orgId, + // USER_ORGANIZATION_OPERATIONS.INVITE_USERS, + // ); + // if (!canInviteUser) throw new ForbiddenException('Insufficient Permission'); + + try { + const invitation = await this.prisma.organizationJoinRequest.create({ + data: { + ...rest, + userId: invitedUser.id, + orgId, + requestType: ORGANIZATION_JOIN_REQUEST_TYPE.INVITED, + }, + }); + + return invitation; + } catch (err) { + if (err instanceof Prisma.PrismaClientKnownRequestError) { + if (err.code === 'P2002') + throw new BadRequestException('User invitation already sent.'); + } else { + throw err; + } + } + } + + async orgCancelUserInviteRequest( + userId: string, + orgId: string, + id: string, + ) { + try { + return await this.prisma.organizationJoinRequest.update({ + where: { + id, + userId, + orgId, + requestType: ORGANIZATION_JOIN_REQUEST_TYPE.INVITED, + status: ORGANIZATION_JOIN_REQUEST.PENDING + }, + data: { + status: ORGANIZATION_JOIN_REQUEST.CANCELLED + } + }) + } + catch (err) { + // TODO: Check error type and use it + throw new BadRequestException("Invitation not found") + } + } + + async organizationUserJoinRequestAction( + userId: string, + orgId: string, + id: string, + dto: UserOrganizationRequestActionRequestDTO + ) { + // NOTE: Experiment, don't know if its better + try { + return await this.prisma.$transaction(async (tx) => { + const updatedJoinReq = await tx.organizationJoinRequest.update({ + where: { + id, + orgId, + status: ORGANIZATION_JOIN_REQUEST.PENDING, + requestType: ORGANIZATION_JOIN_REQUEST_TYPE.REQUESTED + }, + data: { + status: dto.action === USER_ORG_ACCEPT_REJECT_ACTION.ACCEPT + ? ORGANIZATION_JOIN_REQUEST.ACCEPTED + : ORGANIZATION_JOIN_REQUEST.REJECTED, + + ...((dto.action === USER_ORG_ACCEPT_REJECT_ACTION.REJECT && dto.message) ? { + rejectReason: dto.message + } : {}) + } + }); + console.log(updatedJoinReq) + + return await tx.organizationUserJoinTable.create({ + data: { + userId: updatedJoinReq.userId, + orgId, + } + }) + }) + } + catch (err) { + console.log(err); + throw new BadRequestException() + } + } + async getOrganizationRequestList( + userId: string, orgId: string, requestType: ORGANIZATION_JOIN_REQUEST_TYPE = ORGANIZATION_JOIN_REQUEST_TYPE.REQUESTED, - status: ORGANIZATION_JOIN_REQUEST = ORGANIZATION_JOIN_REQUEST.PENDING, ) { // TODO: Check can perform return await this.prisma.organizationJoinRequest.findMany({ where: { - orgId: orgId, - status: status, - requestType: requestType, - }, - include: { - user: { - select: { email: true }, - }, - }, - }); + orgId, + requestType: requestType + } + }) } - async getMemebersOfOrganization(orgId: string) { - const orgExists = await this.orgService.findById(orgId); - if (!orgExists) throw new NotFoundException('Organization'); - return await this.prisma.organizationUserJoinTable.findMany({ + async getMemebersOfOrganization(userId: string, orgId: string) { + const members = await this.prisma.organization.findFirst({ where: { - orgId: orgId, + id: orgId, + members: { + some: { + userId: userId + } + } }, include: { - user: { - select: { - email: true, + members: { + include: { + user: { + select: { + firstName: true, + email: true + } + } }, - }, + } }, - }); + }) + + if (!members) + throw new NotFoundException("Organization") + + return members; } } diff --git a/src/organization/organization.controller.ts b/src/organization/organization.controller.ts index 834331e..a764d6b 100644 --- a/src/organization/organization.controller.ts +++ b/src/organization/organization.controller.ts @@ -6,6 +6,7 @@ import { Param, Post, Put, + UseGuards, } from '@nestjs/common'; import { CreateNewOrganizationRequestDTO, @@ -16,6 +17,7 @@ import { OrganizationService } from './organization.service'; import { RequestContextService } from 'core/als/request-context.service'; import { ApiBearerAuth } from '@nestjs/swagger'; import { DataResponse } from 'common/http'; +import { AuthorizationGuard } from 'src/auth/guards'; @Controller('organization') @ApiBearerAuth('access-token') @@ -23,7 +25,7 @@ export class OrganizationController { constructor( private readonly orgService: OrganizationService, private readonly requestContext: RequestContextService, - ) {} + ) { } @Post('') async createNewOrganization( @@ -50,17 +52,18 @@ export class OrganizationController { ); } - @Get(':id') + @Get(':orgId') + @UseGuards(AuthorizationGuard) async getAnOrganization( - @Param('id') id: string, + @Param('orgId') orgId: string, ): Promise> { - const organization = await this.orgService.getOrganizationById(id); + const organization = await this.orgService.getOrganizationById(orgId); return new DataResponse(new OrganizationDTO(organization)); } - @Put(':id') + @Put(':orgId') async updateAnOrganization( - @Param('id') orgId: string, + @Param('orgId') orgId: string, @Body() body: UpdateOrganizationRequestDTO, ): Promise> { const user = this.requestContext.user; @@ -76,9 +79,9 @@ export class OrganizationController { ); } - @Delete(':id') + @Delete(':orgId') async deleteAnOrganization( - @Param('id') orgId: string, + @Param('orgId') orgId: string, ): Promise> { const user = this.requestContext.user; const deletedOrg = await this.orgService.deleteAnOrganization(