diff --git a/src/app/omegaquotes/OmegaquotesQuote.tsx b/src/app/omegaquotes/OmegaquotesQuote.tsx index 94a42acbe..e21cfa0c1 100644 --- a/src/app/omegaquotes/OmegaquotesQuote.tsx +++ b/src/app/omegaquotes/OmegaquotesQuote.tsx @@ -1,5 +1,5 @@ -import Date from '@/components/Date/Date' import styles from './OmegaquotesQuote.module.scss' +import Date from '@/components/Date/Date' import type { OmegaquoteFiltered } from '@/services/omegaquotes/types' export type OmegaquoteQuotePropTypes = { diff --git a/src/auth/authorizer/AuthResult.ts b/src/auth/authorizer/AuthResult.ts index e803853c6..ac2a07a44 100644 --- a/src/auth/authorizer/AuthResult.ts +++ b/src/auth/authorizer/AuthResult.ts @@ -4,17 +4,33 @@ import type { SessionType, UserGuaranteeOption } from '@/auth/session/Session' export type AuthStatus = 'AUTHORIZED' | 'UNAUTHORIZED' | 'AUTHORIZED_NO_USER' | 'UNAUTHENTICATED' -export type AuthResultType = { +export type AuthResultTypeWithoutStatus< + UserGuatantee extends UserGuaranteeOption, + Authorized extends boolean, + PrismaWhereFilter extends object | undefined = undefined +> = { session: SessionType, errorMessage?: string, authorized: Authorized, - status: AuthStatus, + prismaWhereFilter: PrismaWhereFilter | undefined, } -export type AuthResultTypeAny = AuthResultType +export type AuthResultType< + UserGuatantee extends UserGuaranteeOption, + Authorized extends boolean, + PrismaWhereFilter extends object | undefined = undefined +> = AuthResultTypeWithoutStatus & { + status: AuthStatus +} + +export type AuthResultTypeAny = AuthResultType -export class AuthResult { - private authResult: Omit, 'status'> +export class AuthResult< + const UserGuatantee extends UserGuaranteeOption, + const Authorized extends boolean, + const PrismaWhereFilter extends object | undefined = undefined +> { + private authResult: AuthResultTypeWithoutStatus public get authorized() { return this.authResult.authorized } @@ -23,10 +39,20 @@ export class AuthResult, authorized: Authorized, errorMessage?: string) { + public get prismaWhereFilter(): PrismaWhereFilter | undefined { + return this.authResult.prismaWhereFilter + } + + public constructor( + session: SessionType, + authorized: Authorized, + prismaWhereFilter: PrismaWhereFilter | undefined, + errorMessage?: string + ) { this.authResult = { session, authorized, + prismaWhereFilter, errorMessage, } } @@ -51,7 +77,7 @@ export class AuthResult { + public toJsObject(): AuthResultType { return { session: { // Note: spread is neccessary if the session stored on the AuthResult is the Session class and @@ -61,16 +87,25 @@ export class AuthResult( - authResult: AuthResultType - ): AuthResult { - return new AuthResult(authResult.session, authResult.authorized, authResult.errorMessage) + public static fromJsObject< + const UserGuatantee_ extends UserGuaranteeOption, + const Authorized_ extends boolean, + const PrismaWhereFilter_ extends object | undefined = undefined + >( + authResult: AuthResultType + ): AuthResult { + return new AuthResult( + authResult.session, authResult.authorized, authResult.prismaWhereFilter, authResult.errorMessage + ) } - public redirectOnUnauthorized({ returnUrl }: { returnUrl?: string }) : AuthResult { + public redirectOnUnauthorized( + { returnUrl }: { returnUrl?: string } + ) : AuthResult { if (!this.authorized) { if (this.session.user) { if (!this.session.user.acceptedTerms) { @@ -86,6 +121,6 @@ export class AuthResult = { auth: (session: SessionMaybeUser) => UserRequieredOut extends 'USER_REQUIERED_FOR_AUTHORIZED' - ? (AuthResult<'HAS_USER', true> | AuthResult<'HAS_USER' | 'NO_USER', false>) - : (AuthResult<'HAS_USER' | 'NO_USER', true> | AuthResult<'HAS_USER' | 'NO_USER', false>) + ? (AuthResult<'HAS_USER', true, PrismaWhereFilter> | AuthResult<'HAS_USER' | 'NO_USER', false, undefined>) + : (AuthResult<'HAS_USER' | 'NO_USER', true, PrismaWhereFilter> | AuthResult<'HAS_USER' | 'NO_USER', false, undefined>) } export type AuthorizerStaticFieldsBound< DynamicFields extends object, UserRequieredOut extends UserRequieredOutOpt = 'USER_NOT_REQUIERED_FOR_AUTHORIZED' | 'USER_REQUIERED_FOR_AUTHORIZED', + PrismaWhereFilter extends object | undefined = undefined > = { - dynamicFields: (dynamicFields: DynamicFields) => AuthorizerDynamicFieldsBound, + dynamicFields: (dynamicFields: DynamicFields) => AuthorizerDynamicFieldsBound, } export type Authorizer< StaticFields extends object, DynamicFields extends object, UserRequieredOut extends UserRequieredOutOpt, + PrismaWhereFilter extends object | undefined = undefined > = { - staticFields: (staticFields: StaticFields) => AuthorizerStaticFieldsBound + staticFields: (staticFields: StaticFields) => + AuthorizerStaticFieldsBound } - export function AuthorizerFactory< StaticFields extends object, DynamicFields extends object, const UserRequieredOut extends UserRequieredOutOpt, + const PrismaWhereFilter extends object | undefined = undefined >( - authCheck: ((f: { + authCheck: ((_: { session: SessionMaybeUser, staticFields: StaticFields, dynamicFields: DynamicFields - }) => UserRequieredOut extends 'USER_REQUIERED_FOR_AUTHORIZED' ? ({ - success: true, - session: SessionUser, - errorMessage?: string, - } | { - success: false, - session: SessionMaybeUser - errorMessage?: string, - }) : ({ - success: boolean, - session: SessionMaybeUser - errorMessage?: string, - })) -): Authorizer { + }) => UserRequieredOut extends 'USER_REQUIERED_FOR_AUTHORIZED' ? ( + ({ + success: true, + session: SessionUser, + errorMessage?: string, + prismaWhereFilter?: PrismaWhereFilter, + } & (PrismaWhereFilter extends undefined ? object : { prismaWhereFilter: PrismaWhereFilter })) | { + success: false, + session: SessionMaybeUser, + errorMessage?: string, + } + ) : ( + ({ + success: true, + session: SessionMaybeUser, + errorMessage?: string, + prismaWhereFilter?: PrismaWhereFilter, + } & (PrismaWhereFilter extends undefined ? object : { prismaWhereFilter: PrismaWhereFilter })) | { + success: false, + session: SessionMaybeUser, + errorMessage?: string, + } + ) + ) +): Authorizer { return { staticFields: (staticFields) => ( { dynamicFields: (dynamicFields) => ( { auth: (session) => { - const { session: sessionOut, success, errorMessage } = authCheck({ + const results = authCheck({ session, staticFields, dynamicFields }) - if (success) { - return new AuthResult(sessionOut, true) + if (results.success) { + return new AuthResult( + results.session, + true, + results.prismaWhereFilter! + ) } - return new AuthResult(sessionOut, false, errorMessage) + return new AuthResult(results.session, false, undefined, results.errorMessage) } } ) diff --git a/src/auth/authorizer/RequireJWT.ts b/src/auth/authorizer/RequireJWT.ts index aaf7c2df8..701e6bd58 100644 --- a/src/auth/authorizer/RequireJWT.ts +++ b/src/auth/authorizer/RequireJWT.ts @@ -27,6 +27,6 @@ export const RequireJWT = AuthorizerFactory< return { success: true, - session + session, } }) diff --git a/src/auth/authorizer/RequireVisibilityFilter.ts b/src/auth/authorizer/RequireVisibilityFilter.ts new file mode 100644 index 000000000..bd1f15798 --- /dev/null +++ b/src/auth/authorizer/RequireVisibilityFilter.ts @@ -0,0 +1,15 @@ +import { AuthorizerFactory } from './Authorizer' +import { visibilityFilter } from '@/auth/visibility/visibilityFilter' +import type { Permission } from '@/prisma-generated-pn-types' + +export const RequireVisibilityFilter = AuthorizerFactory< + { bypassPermission: Permission }, + Record, + 'USER_NOT_REQUIERED_FOR_AUTHORIZED', + ReturnType | undefined +> (({ session, staticFields }) => ({ + success: true, + prismaWhereFilter: + session.permissions.includes(staticFields.bypassPermission) ? undefined : visibilityFilter(session.memberships), + session, +})) diff --git a/src/auth/visibility/visibilityFilter.ts b/src/auth/visibility/visibilityFilter.ts index 48986e9b5..e37e7331a 100644 --- a/src/auth/visibility/visibilityFilter.ts +++ b/src/auth/visibility/visibilityFilter.ts @@ -31,3 +31,5 @@ export function visibilityFilter(memberships: MembershipFiltered[]) { } } as const satisfies Prisma.VisibilityWhereInput } + +export type VisibilityFilter = ReturnType diff --git a/src/hooks/useAuthorizer.ts b/src/hooks/useAuthorizer.ts index e8c826a66..b9b4c2f99 100644 --- a/src/hooks/useAuthorizer.ts +++ b/src/hooks/useAuthorizer.ts @@ -12,20 +12,20 @@ function useAuthorizer({ authorizer }: { authorizer: AuthorizerDynamicFieldsBound<'USER_NOT_REQUIERED_FOR_AUTHORIZED'> -}): AuthResult +}): AuthResult function useAuthorizer({ authorizer }: { authorizer: AuthorizerDynamicFieldsBound<'USER_REQUIERED_FOR_AUTHORIZED'> -}): AuthResult | AuthResult<'HAS_USER', true> +}): AuthResult | AuthResult<'HAS_USER', true, object | undefined> function useAuthorizer({ authorizer }: { authorizer: AuthorizerDynamicFieldsBound -}): AuthResult { +}): AuthResult { const session = useSession() if (session.loading) { - return new AuthResult(Session.empty(), false) + return new AuthResult(Session.empty(), false, undefined) } return authorizer.auth(session.session) } diff --git a/src/prisma/seeder/src/SeedSpecialImageCollections.ts b/src/prisma/seeder/src/SeedSpecialImageCollections.ts index 30b66daaa..0e8ae23f2 100644 --- a/src/prisma/seeder/src/SeedSpecialImageCollections.ts +++ b/src/prisma/seeder/src/SeedSpecialImageCollections.ts @@ -1,5 +1,5 @@ -import type { PrismaClient } from '@/prisma-generated-pn-client' import { SpecialCollection } from '@/prisma-generated-pn-types' +import type { PrismaClient } from '@/prisma-generated-pn-client' export default async function SeedSpecialImageCollections(prisma: PrismaClient) { const keys = Object.keys(SpecialCollection) as SpecialCollection[] diff --git a/src/prisma/seeder/src/development/seedDevUsers.ts b/src/prisma/seeder/src/development/seedDevUsers.ts index 65db68456..4eeb9f55c 100644 --- a/src/prisma/seeder/src/development/seedDevUsers.ts +++ b/src/prisma/seeder/src/development/seedDevUsers.ts @@ -1,12 +1,12 @@ import { hashAndEncryptPassword } from '@/auth/passwordHash' import { type SeederImage, seedImage } from '@/seeder/src/seedImages' +import { OmegaMembershipLevel, type Prisma } from '@/prisma-generated-pn-types' import { v4 as uuid } from 'uuid' import { randomInt } from 'crypto' import { readdir } from 'fs/promises' import { join, dirname } from 'path' import { fileURLToPath } from 'url' import type { PrismaClient } from '@/prisma-generated-pn-client' -import { OmegaMembershipLevel, type Prisma } from '@/prisma-generated-pn-types' const fileName = fileURLToPath(import.meta.url) const directoryName = dirname(fileName) diff --git a/src/prisma/seeder/src/dobbelOmega/dobbelOmega.ts b/src/prisma/seeder/src/dobbelOmega/dobbelOmega.ts index 74d038237..bc6ae9208 100644 --- a/src/prisma/seeder/src/dobbelOmega/dobbelOmega.ts +++ b/src/prisma/seeder/src/dobbelOmega/dobbelOmega.ts @@ -10,9 +10,9 @@ import { UserMigrator } from './migrateUsers' import migrateCommittees from './migrateCommittees' import seedProdPermissions from './seedProdPermissions' import manifest from '@/seeder/src/logger' +import { PrismaClient as PrismaClientOw } from '@/prisma-generated-ow-basic/client' import { PrismaPg } from '@prisma/adapter-pg' import type { PrismaClient as PrismaClientPn } from '@/prisma-generated-pn-client' -import { PrismaClient as PrismaClientOw } from '@/prisma-generated-ow-basic/client' /** * !DobbelOmega! diff --git a/src/prisma/seeder/src/seedOmegaMembershipGroups.ts b/src/prisma/seeder/src/seedOmegaMembershipGroups.ts index 46ffaee56..7b5e9d59a 100644 --- a/src/prisma/seeder/src/seedOmegaMembershipGroups.ts +++ b/src/prisma/seeder/src/seedOmegaMembershipGroups.ts @@ -1,5 +1,5 @@ -import type { PrismaClient } from '@/prisma-generated-pn-client' import { OmegaMembershipLevel } from '@/prisma-generated-pn-types' +import type { PrismaClient } from '@/prisma-generated-pn-client' export default async function seedOmegaMembershipGroups(prisma: PrismaClient) { const levels = Object.values(OmegaMembershipLevel) diff --git a/src/services/serviceOperation.ts b/src/services/serviceOperation.ts index aa4ecf139..d57e24703 100644 --- a/src/services/serviceOperation.ts +++ b/src/services/serviceOperation.ts @@ -59,11 +59,13 @@ export type ServiceOperationOperation< ParamsSchema extends z.ZodTypeAny | undefined, DataSchema extends z.ZodTypeAny | undefined, Return, + PrismaWhereFilter extends object | undefined > = ( args: & ParamsObject & DataObject & ServiceOperationContext + , prismaWhereFilter: PrismaWhereFilter | undefined ) => Promise | Return export type SubServiceOperationConfig< @@ -74,12 +76,13 @@ export type SubServiceOperationConfig< OperationImplementationFields extends object | undefined, Return, OpensTransaction extends boolean = false, + PrismaWhereFilter extends object | undefined = undefined > = { paramsSchema?: ((implementationFields: ParamsSchemaImplementationFields) => ParamsSchema) | undefined, dataSchema?: ((implementationFields: DataSchemaImplementationFields) => DataSchema) | undefined, opensTransaction?: OpensTransaction, operation: (implementationFields: OperationImplementationFields) => - ServiceOperationOperation + ServiceOperationOperation } export type ArgsAuthGetterAndOwnershipCheck< @@ -97,16 +100,23 @@ export type AuthorizerGetter< OpensTransaction extends boolean, ParamsSchema extends z.ZodTypeAny | undefined, DataSchema extends z.ZodTypeAny | undefined, - ImplementationParamsSchema extends z.ZodTypeAny | undefined + ImplementationParamsSchema extends z.ZodTypeAny | undefined, + PrismaWhereFilter extends object | undefined > = ( args: ArgsAuthGetterAndOwnershipCheck -) => AuthorizerDynamicFieldsBound | Promise +) => + | AuthorizerDynamicFieldsBound<'USER_NOT_REQUIERED_FOR_AUTHORIZED' | 'USER_REQUIERED_FOR_AUTHORIZED', PrismaWhereFilter> + | Promise< + AuthorizerDynamicFieldsBound< + 'USER_NOT_REQUIERED_FOR_AUTHORIZED' | 'USER_REQUIERED_FOR_AUTHORIZED', PrismaWhereFilter + > + > export type OwnershipCheck< OpensTransaction extends boolean, ParamsSchema extends z.ZodTypeAny | undefined, DataSchema extends z.ZodTypeAny | undefined, - ImplementationParamsSchema extends z.ZodTypeAny | undefined + ImplementationParamsSchema extends z.ZodTypeAny | undefined, > = ( args: ArgsAuthGetterAndOwnershipCheck ) => boolean | Promise @@ -143,13 +153,16 @@ export type ServiceOperationImplementationConfig< ParamsSchemaImplementationFields extends object | undefined, DataSchemaImplementationFields extends object | undefined, OperationImplementationFields extends object | undefined, + PrismaWhereFilter extends object | undefined > = ServiceOperationImplementationConfigInternalCall< ImplementationParamsSchema, ParamsSchemaImplementationFields, DataSchemaImplementationFields, OperationImplementationFields > & { - authorizer: AuthorizerGetter, + authorizer: AuthorizerGetter< + OpensTransaction, ParamsSchema, DataSchema, ImplementationParamsSchema, PrismaWhereFilter | undefined + >, ownershipCheck: OwnershipCheck, } @@ -218,7 +231,7 @@ export type ServiceOperation< Return, ParamsSchema extends z.ZodTypeAny | undefined = undefined, DataSchema extends z.ZodTypeAny | undefined = undefined, - ImplementationParamsSchema extends z.ZodTypeAny | undefined = undefined + ImplementationParamsSchema extends z.ZodTypeAny | undefined = undefined, > = { /** * Pass a specific prisma client to the service operation. Usefull when using the service operation inside a transaction. @@ -243,6 +256,7 @@ export function defineSubOperation< ParamsSchemaImplementationFields extends object | undefined = undefined, DataSchemaImplementationFields extends object | undefined = undefined, OperationImplementationFields extends object | undefined = undefined, + PrismaWhereFilter extends object | undefined = undefined >( serviceOperationConfig: SubServiceOperationConfig< ParamsSchema, @@ -251,7 +265,8 @@ export function defineSubOperation< DataSchemaImplementationFields, OperationImplementationFields, Return, - OpensTransaction + OpensTransaction, + PrismaWhereFilter > ) { const implement = ( @@ -262,7 +277,8 @@ export function defineSubOperation< DataSchema, ParamsSchemaImplementationFields, DataSchemaImplementationFields, - OperationImplementationFields + OperationImplementationFields, + PrismaWhereFilter > ): ServiceOperation => { const expectedArgsArePresent = ( @@ -377,23 +393,30 @@ export function defineSubOperation< // Then, authorize user. // This has to be done after the validation because the authorizer might use the data to authorize the user. - if (!bypassAuth) { - if (!implementationArgs.authorizer) { - throw new Smorekopp( - 'UNAUTHENTICATED', - 'This service operation is not externally callable.' + // Disable es lint next line: + + const prismaWhereFilter: PrismaWhereFilter | undefined = await (async () => { + if (!bypassAuth) { + if (!implementationArgs.authorizer) { + throw new Smorekopp( + 'UNAUTHENTICATED', + 'This service operation is not externally callable.' + ) + } + + const authorizer = await prismaErrorWrapper( + () => implementationArgs.authorizer({ ...args, prisma }) ) - } + const authResult = authorizer.auth(session) - const authorizer = await prismaErrorWrapper( - () => implementationArgs.authorizer({ ...args, prisma }) - ) - const authResult = authorizer.auth(session) + if (!authResult.authorized) { + throw new Smorekopp(authResult.status, authResult.getErrorMessage) + } - if (!authResult.authorized) { - throw new Smorekopp(authResult.status, authResult.getErrorMessage) + return authResult.prismaWhereFilter } - } + return undefined + })() const ownershipCheckResult = await prismaErrorWrapper( () => implementationArgs.ownershipCheck({ @@ -408,11 +431,10 @@ export function defineSubOperation< `) } - // Finally, call the operation. return prismaErrorWrapper(() => serviceOperationConfig.operation( implementationArgs.operationImplementationFields! - )({ ...args, prisma, bypassAuth, session }) + )({ ...args, prisma, bypassAuth, session }, prismaWhereFilter) ) }) } @@ -455,12 +477,13 @@ export function defineOperation< Return, ParamsSchema extends z.ZodTypeAny | undefined = undefined, DataSchema extends z.ZodTypeAny | undefined = undefined, + PrismaWhereFilter extends object | undefined = undefined >({ paramsSchema, dataSchema, opensTransaction, authorizer, operation }: { paramsSchema?: ParamsSchema, dataSchema?: DataSchema, opensTransaction?: OpensTransaction, - authorizer: AuthorizerGetter, - operation: ServiceOperationOperation + authorizer: AuthorizerGetter, + operation: ServiceOperationOperation }): ServiceOperation { return defineSubOperation< Return, @@ -469,7 +492,8 @@ export function defineOperation< DataSchema, undefined, undefined, - undefined + undefined, + PrismaWhereFilter >({ opensTransaction, operation: () => operation, @@ -502,4 +526,3 @@ export type SubServiceOperation< DataSchemaImplementationFields, OperationImplementationFields >> - diff --git a/tests/PrismaTestEnvironment.ts b/tests/PrismaTestEnvironment.ts index e67eef2c9..28cf3154f 100644 --- a/tests/PrismaTestEnvironment.ts +++ b/tests/PrismaTestEnvironment.ts @@ -1,10 +1,10 @@ import { PrismaClient } from '@/prisma-generated-pn-client' import { v4 } from 'uuid' import NodeEnvironment from 'jest-environment-node' +import { PrismaPg } from '@prisma/adapter-pg' import { execSync } from 'child_process' import { URL } from 'url' import type { EnvironmentContext, JestEnvironmentConfig } from '@jest/environment' -import { PrismaPg } from '@prisma/adapter-pg' /** * Generates a modified version of the database URL environment variable