because I got bored of customising my CV for every job
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(auth): add UserRole enum, AdminGuard, and ProfileOwnedResourcePolicy

+89 -13
+1 -1
packages/auth/package.json
··· 30 30 "bcryptjs": "^2.4.3", 31 31 "graphql": "^16.12.0", 32 32 "reflect-metadata": "^0.2.2", 33 - "zod": "^3.25.76" 33 + "zod": "^4.3.6" 34 34 }, 35 35 "devDependencies": { 36 36 "@biomejs/biome": "^2.2.6",
+1
packages/auth/src/authorization/index.ts
··· 4 4 export { Policy } from "./policy.decorator"; 5 5 export type { Policy as IPolicy } from "./policy.interface"; 6 6 export * from "./policy-registry.service"; 7 + export * from "./profile-owned-resource.policy"; 7 8 export * from "./public-resource.policy"; 8 9 export * from "./user-owned-resource.policy";
+40
packages/auth/src/authorization/profile-owned-resource.policy.ts
··· 1 + import { PrismaService } from "@cv/system"; 2 + import type { User } from "../user/user.entity"; 3 + import type { Policy } from "./policy.interface"; 4 + 5 + export abstract class ProfileOwnedResourcePolicy< 6 + TResource extends { profileId: string }, 7 + > implements Policy<TResource> 8 + { 9 + constructor(protected readonly prisma: PrismaService) {} 10 + 11 + view(user: User, resource: TResource): Promise<boolean> { 12 + return this.isOwner(user, resource); 13 + } 14 + 15 + async create( 16 + user: User, 17 + resource?: Partial<TResource>, 18 + ): Promise<boolean> { 19 + if (!resource) return true; 20 + if (!("profileId" in resource) || typeof resource.profileId !== "string") 21 + return false; 22 + return this.isOwner(user, resource as TResource); 23 + } 24 + 25 + update(user: User, resource: TResource): Promise<boolean> { 26 + return this.isOwner(user, resource); 27 + } 28 + 29 + delete(user: User, resource: TResource): Promise<boolean> { 30 + return this.isOwner(user, resource); 31 + } 32 + 33 + private async isOwner(user: User, resource: TResource): Promise<boolean> { 34 + const profile = await this.prisma.profile.findUnique({ 35 + where: { id: resource.profileId }, 36 + select: { userId: true }, 37 + }); 38 + return profile?.userId === user.id; 39 + } 40 + }
+27
packages/auth/src/guards/admin.guard.ts
··· 1 + import { 2 + type CanActivate, 3 + type ExecutionContext, 4 + ForbiddenException, 5 + Injectable, 6 + } from "@nestjs/common"; 7 + import { GqlExecutionContext } from "@nestjs/graphql"; 8 + import type { User } from "../user/user.entity"; 9 + 10 + /** 11 + * Guard that restricts access to admin users. 12 + * Must be stacked after JwtAuthGuard (which populates request.user). 13 + */ 14 + @Injectable() 15 + export class AdminGuard implements CanActivate { 16 + canActivate(context: ExecutionContext): boolean { 17 + const ctx = GqlExecutionContext.create(context); 18 + const request = ctx.getContext().req; 19 + const user = request.user as User | undefined; 20 + 21 + if (!user?.isAdmin) { 22 + throw new ForbiddenException("Admin access required"); 23 + } 24 + 25 + return true; 26 + } 27 + }
+1
packages/auth/src/guards/index.ts
··· 1 + export * from "./admin.guard"; 1 2 export * from "./jwt-auth.guard"; 2 3 export * from "./verified-scope.guard";
+7 -11
packages/auth/src/identity-provider-registry.service.ts
··· 1 1 import { raise } from "@cv/system"; 2 2 import { Injectable, type OnModuleInit } from "@nestjs/common"; 3 3 import { DiscoveryService, Reflector } from "@nestjs/core"; 4 - import { z } from "zod"; 4 + import { z } from "zod/v4"; 5 5 import { 6 6 IDENTITY_PROVIDER_KEY, 7 7 type IdentityProviderMeta, ··· 14 14 }; 15 15 16 16 const identityProviderMetaSchema = z.object({ 17 - name: z.custom<symbol>((val) => typeof val === "symbol", { 18 - message: "name must be a symbol", 19 - }), 17 + name: z.custom<symbol>((val) => typeof val === "symbol"), 20 18 priority: z.number().optional(), 21 19 }); 22 20 23 - const identityProviderInstanceSchema = z 24 - .object({ 25 - authenticate: z.function(), 26 - }) 27 - .passthrough(); 21 + const identityProviderInstanceSchema = z.looseObject({ 22 + authenticate: z.unknown(), 23 + }); 28 24 29 25 @Injectable() 30 26 export class IdentityProviderRegistry<TUser = unknown> implements OnModuleInit { ··· 86 82 return result; 87 83 } catch (error) { 88 84 if (error instanceof z.ZodError) { 89 - const errorMessages = error.errors 85 + const errorMessages = error.issues 90 86 .map(({ path, message }) => `${path.join(".")}: ${message}`) 91 87 .join(", "); 92 88 throw new Error( ··· 108 104 return instance as IdentityProvider<TUser>; 109 105 } catch (error) { 110 106 if (error instanceof z.ZodError) { 111 - const errorMessages = error.errors 107 + const errorMessages = error.issues 112 108 .map((e) => `${e.path.join(".")}: ${e.message}`) 113 109 .join(", "); 114 110 throw new Error(
+10
packages/auth/src/user/user.entity.ts
··· 1 1 import { BaseEntity } from "@cv/system"; 2 2 import type { Credentials } from "./credentials.entity"; 3 3 4 + export enum UserRole { 5 + USER = "USER", 6 + ADMIN = "ADMIN", 7 + } 8 + 4 9 export class User extends BaseEntity { 5 10 constructor( 6 11 id: string, ··· 8 13 createdAt: Date, 9 14 updatedAt: Date, 10 15 public credentials: Credentials | null = null, 16 + public role: UserRole = UserRole.USER, 11 17 ) { 12 18 super(id, createdAt, updatedAt); 19 + } 20 + 21 + get isAdmin(): boolean { 22 + return this.role === UserRole.ADMIN; 13 23 } 14 24 }
+2 -1
packages/auth/src/user/user.mapper.ts
··· 2 2 import { Injectable } from "@nestjs/common"; 3 3 import type { Prisma } from "@prisma/client"; 4 4 import { CredentialsMapper } from "./credentials.mapper"; 5 - import { User } from "./user.entity"; 5 + import { User, UserRole } from "./user.entity"; 6 6 7 7 type PrismaUserWithCredentials = Prisma.UserGetPayload<{ 8 8 include: { credentials: true }; ··· 28 28 prismaUser.createdAt, 29 29 prismaUser.updatedAt, 30 30 credentials, 31 + prismaUser.role as UserRole, 31 32 ); 32 33 } 33 34