because I got bored of customising my CV for every job

chore: add remaining configuration and query files

+24
.env.example
··· 1 + # Database 2 + POSTGRES_USER=cv 3 + POSTGRES_PASSWORD=cv 4 + POSTGRES_DB=cv 5 + DB_PORT=5432 6 + DATABASE_URL=postgresql://cv:cv@db:5432/cv 7 + 8 + # Server 9 + SERVER_PORT=3000 10 + JWT_SECRET=your-super-secret-jwt-key-here 11 + JWT_ACCESS_TOKEN_EXPIRY=15m 12 + JWT_REFRESH_TOKEN_EXPIRY=7d 13 + 14 + # Client 15 + CLIENT_PORT=5173 16 + VITE_SERVER_URL=http://localhost:3000 17 + 18 + # Docs 19 + DOCS_PORT=3001 20 + VITE_CLIENT_URL=http://localhost:5173 21 + 22 + # Prisma 23 + PRISMA_ENABLE_TRACING=false 24 + VITE_DOCS_URL=http://localhost:3001
+16
apps/client/src/features/user/queries/my-skills.graphql
··· 1 + query MySkills { 2 + me { 3 + experience { 4 + edges { 5 + node { 6 + id 7 + skills { 8 + id 9 + name 10 + description 11 + } 12 + } 13 + } 14 + } 15 + } 16 + }
+29
apps/server/prisma/models/cv.prisma
··· 1 + model CVTemplate { 2 + id String @id @default(cuid()) 3 + name String 4 + description String? 5 + createdAt DateTime @default(now()) 6 + updatedAt DateTime @updatedAt 7 + 8 + // CVs using this template 9 + cvs CV[] 10 + 11 + @@map("cv_templates") 12 + } 13 + 14 + model CV { 15 + id String @id @default(cuid()) 16 + userId String 17 + templateId String 18 + title String 19 + introduction String? 20 + createdAt DateTime @default(now()) 21 + updatedAt DateTime @updatedAt 22 + 23 + // Relations 24 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) 25 + template CVTemplate @relation(fields: [templateId], references: [id], onDelete: Restrict) 26 + applications Application[] 27 + 28 + @@map("cvs") 29 + }
+91
apps/server/prisma/models/job-experience.prisma
··· 1 + model Skill { 2 + id String @id @default(cuid()) 3 + name String @unique 4 + description String? 5 + createdAt DateTime @default(now()) 6 + updatedAt DateTime @updatedAt 7 + 8 + // Job experiences that use this skill 9 + jobExperiences UserJobExperience[] 10 + 11 + // Vacancies requiring this skill 12 + vacancies Vacancy[] 13 + 14 + // Education entries that use this skill 15 + educations Education[] 16 + 17 + @@map("skills") 18 + } 19 + 20 + model Company { 21 + id String @id @default(cuid()) 22 + name String @unique 23 + description String? 24 + website String? 25 + createdAt DateTime @default(now()) 26 + updatedAt DateTime @updatedAt 27 + 28 + // Job experiences at this company 29 + jobExperiences UserJobExperience[] 30 + 31 + // Vacancies at this company 32 + vacancies Vacancy[] 33 + 34 + @@map("companies") 35 + } 36 + 37 + model Role { 38 + id String @id @default(cuid()) 39 + name String @unique 40 + description String? 41 + createdAt DateTime @default(now()) 42 + updatedAt DateTime @updatedAt 43 + 44 + // Job experiences with this role 45 + jobExperiences UserJobExperience[] 46 + 47 + // Vacancies with this role 48 + vacancies Vacancy[] 49 + 50 + @@map("roles") 51 + } 52 + 53 + model Level { 54 + id String @id @default(cuid()) 55 + name String @unique 56 + description String? 57 + createdAt DateTime @default(now()) 58 + updatedAt DateTime @updatedAt 59 + 60 + // Job experiences at this level 61 + jobExperiences UserJobExperience[] 62 + 63 + // Vacancies at this level 64 + vacancies Vacancy[] 65 + 66 + @@map("levels") 67 + } 68 + 69 + model UserJobExperience { 70 + id String @id @default(cuid()) 71 + userId String 72 + companyId String 73 + roleId String 74 + levelId String 75 + startDate DateTime 76 + endDate DateTime? 77 + description String? 78 + createdAt DateTime @default(now()) 79 + updatedAt DateTime @updatedAt 80 + 81 + // Relations 82 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) 83 + company Company @relation(fields: [companyId], references: [id], onDelete: Cascade) 84 + role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) 85 + level Level @relation(fields: [levelId], references: [id], onDelete: Cascade) 86 + 87 + // Skills used in this job experience 88 + skills Skill[] 89 + 90 + @@map("user_job_experiences") 91 + }
+43
apps/server/prisma/models/organization.prisma
··· 1 + model Organization { 2 + id String @id @default(cuid()) 3 + name String @unique 4 + description String? 5 + createdAt DateTime @default(now()) 6 + updatedAt DateTime @updatedAt 7 + 8 + // Users in this organization 9 + memberships Membership[] 10 + 11 + @@map("organizations") 12 + } 13 + 14 + model OrganizationRole { 15 + id String @id @default(cuid()) 16 + name String @unique 17 + description String? 18 + color String @default("#6366f1") 19 + createdAt DateTime @default(now()) 20 + updatedAt DateTime @updatedAt 21 + 22 + // Memberships with this role 23 + memberships Membership[] 24 + 25 + @@map("organization_roles") 26 + } 27 + 28 + model Membership { 29 + id String @id @default(cuid()) 30 + userId String 31 + organizationId String 32 + organizationRoleId String 33 + createdAt DateTime @default(now()) 34 + updatedAt DateTime @updatedAt 35 + 36 + // Relations 37 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) 38 + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) 39 + role OrganizationRole @relation(fields: [organizationRoleId], references: [id], onDelete: Cascade) 40 + 41 + @@unique([userId, organizationId]) 42 + @@map("memberships") 43 + }
+61
apps/server/prisma/models/user.prisma
··· 1 + model User { 2 + id String @id @default(cuid()) 3 + email String @unique 4 + name String 5 + password String 6 + createdAt DateTime @default(now()) 7 + updatedAt DateTime @updatedAt 8 + 9 + // Job experiences 10 + jobExperiences UserJobExperience[] 11 + 12 + // Organizations 13 + memberships Membership[] 14 + 15 + // Vacancies owned by user 16 + ownedVacancies Vacancy[] @relation("VacancyOwner") 17 + 18 + // CVs 19 + cvs CV[] 20 + 21 + // Applications 22 + applications Application[] 23 + 24 + // Education history 25 + educationHistory Education[] 26 + 27 + @@map("users") 28 + } 29 + 30 + model Institution { 31 + id String @id @default(cuid()) 32 + name String @unique 33 + description String? 34 + createdAt DateTime @default(now()) 35 + updatedAt DateTime @updatedAt 36 + 37 + // Education entries at this institution 38 + educationEntries Education[] 39 + 40 + @@map("institutions") 41 + } 42 + 43 + model Education { 44 + id String @id @default(cuid()) 45 + userId String 46 + institutionId String 47 + degree String 48 + fieldOfStudy String? 49 + startDate DateTime 50 + endDate DateTime? 51 + description String? 52 + createdAt DateTime @default(now()) 53 + updatedAt DateTime @updatedAt 54 + 55 + // Relations 56 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) 57 + institution Institution @relation(fields: [institutionId], references: [id], onDelete: Cascade) 58 + skills Skill[] 59 + 60 + @@map("educations") 61 + }
+78
apps/server/prisma/models/vacancy.prisma
··· 1 + model JobType { 2 + id String @id @default(cuid()) 3 + name String @unique 4 + description String? 5 + createdAt DateTime @default(now()) 6 + updatedAt DateTime @updatedAt 7 + 8 + // Vacancies with this job type 9 + vacancies Vacancy[] 10 + 11 + @@map("job_types") 12 + } 13 + 14 + model ApplicationStatus { 15 + id String @id @default(cuid()) 16 + name String @unique 17 + description String? 18 + createdAt DateTime @default(now()) 19 + updatedAt DateTime @updatedAt 20 + 21 + // Applications with this status 22 + applications Application[] 23 + 24 + @@map("application_statuses") 25 + } 26 + 27 + model Vacancy { 28 + id String @id @default(cuid()) 29 + ownerId String 30 + title String 31 + companyId String 32 + roleId String 33 + levelId String? 34 + jobTypeId String? 35 + description String? 36 + requirements String? 37 + location String? 38 + minSalary Int? 39 + maxSalary Int? 40 + applicationUrl String? 41 + deadline DateTime? 42 + isActive Boolean @default(true) 43 + isPublic Boolean @default(false) 44 + createdAt DateTime @default(now()) 45 + updatedAt DateTime @updatedAt 46 + 47 + // Relations 48 + owner User @relation("VacancyOwner", fields: [ownerId], references: [id], onDelete: Cascade) 49 + company Company @relation(fields: [companyId], references: [id], onDelete: Cascade) 50 + role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) 51 + level Level? @relation(fields: [levelId], references: [id], onDelete: Cascade) 52 + jobType JobType? @relation(fields: [jobTypeId], references: [id], onDelete: Cascade) 53 + skills Skill[] 54 + applications Application[] 55 + 56 + @@map("vacancies") 57 + } 58 + 59 + model Application { 60 + id String @id @default(cuid()) 61 + userId String 62 + vacancyId String 63 + cvId String? 64 + coverLetter String? 65 + statusId String 66 + appliedAt DateTime @default(now()) 67 + createdAt DateTime @default(now()) 68 + updatedAt DateTime @updatedAt 69 + 70 + // Relations 71 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) 72 + vacancy Vacancy @relation(fields: [vacancyId], references: [id], onDelete: Cascade) 73 + cv CV? @relation(fields: [cvId], references: [id], onDelete: SetNull) 74 + status ApplicationStatus @relation(fields: [statusId], references: [id], onDelete: Restrict) 75 + 76 + @@unique([userId, vacancyId]) 77 + @@map("applications") 78 + }
+13
apps/server/src/config/config.module.ts
··· 1 + import { Global, Module } from "@nestjs/common"; 2 + import { JwtConfigService } from "./jwt.config"; 3 + 4 + /** 5 + * Global configuration module 6 + * Provides configuration services throughout the application 7 + */ 8 + @Global() 9 + @Module({ 10 + providers: [JwtConfigService], 11 + exports: [JwtConfigService], 12 + }) 13 + export class AppConfigModule {}
+46
apps/server/src/config/env.validation.ts
··· 1 + import * as Joi from "joi"; 2 + 3 + // Custom validator for JWT expiry format (e.g., "15m", "7d", "1h") 4 + const jwtExpirySchema = Joi.string() 5 + .pattern(/^(\d+)([smhd])$/) 6 + .messages({ 7 + "string.pattern.base": 8 + 'JWT expiry must be in format: number + unit (s=seconds, m=minutes, h=hours, d=days). Example: "15m", "7d"', 9 + }); 10 + 11 + export const envValidationSchema = Joi.object({ 12 + // Database Configuration 13 + POSTGRES_USER: Joi.string().required(), 14 + POSTGRES_PASSWORD: Joi.string().required(), 15 + POSTGRES_DB: Joi.string().required(), 16 + DATABASE_URL: Joi.string().uri().required(), 17 + 18 + // Server Configuration 19 + PORT: Joi.number().default(3000), 20 + SERVER_PORT: Joi.number().default(3000), 21 + NODE_ENV: Joi.string() 22 + .valid("development", "production", "test") 23 + .default("development"), 24 + 25 + // JWT Configuration 26 + JWT_SECRET: Joi.string().min(16).required().messages({ 27 + "string.min": "JWT_SECRET must be at least 16 characters long for security", 28 + "any.required": "JWT_SECRET is required", 29 + }), 30 + JWT_ACCESS_TOKEN_EXPIRY: jwtExpirySchema.default("15m"), 31 + JWT_REFRESH_TOKEN_EXPIRY: jwtExpirySchema.default("7d"), 32 + 33 + // Prisma Configuration 34 + PRISMA_ENABLE_TRACING: Joi.boolean().default(false), 35 + 36 + // Client Configuration (optional for server) 37 + CLIENT_PORT: Joi.number().optional(), 38 + VITE_SERVER_URL: Joi.string().uri().optional(), 39 + GRAPHQL_SCHEMA_URL: Joi.string().uri().optional(), 40 + 41 + // UI Configuration (optional for server) 42 + UI_PORT: Joi.number().optional(), 43 + 44 + // Database Port 45 + DB_PORT: Joi.number().default(5432), 46 + });
+77
apps/server/src/config/jwt.config.ts
··· 1 + import { Injectable } from "@nestjs/common"; 2 + import { ConfigService } from "@nestjs/config"; 3 + 4 + /** 5 + * JWT Configuration Service 6 + * Handles parsing and providing JWT-related configuration values 7 + */ 8 + @Injectable() 9 + export class JwtConfigService { 10 + constructor(private configService: ConfigService) {} 11 + 12 + /** 13 + * Gets the access token expiry string (e.g., "15m") 14 + */ 15 + getAccessTokenExpiry(): string { 16 + return this.configService.getOrThrow<string>("JWT_ACCESS_TOKEN_EXPIRY"); 17 + } 18 + 19 + /** 20 + * Gets the refresh token expiry string (e.g., "7d") 21 + */ 22 + getRefreshTokenExpiry(): string { 23 + return this.configService.getOrThrow<string>("JWT_REFRESH_TOKEN_EXPIRY"); 24 + } 25 + 26 + /** 27 + * Gets the JWT secret 28 + */ 29 + getSecret(): string { 30 + return this.configService.getOrThrow<string>("JWT_SECRET"); 31 + } 32 + 33 + /** 34 + * Calculates expiry date from JWT expiry string format 35 + * Format: number + unit (s=seconds, m=minutes, h=hours, d=days) 36 + * Examples: "15m", "7d", "1h" 37 + */ 38 + calculateExpiryDate(expiryString: string): Date { 39 + const expires_at = new Date(); 40 + 41 + // Parse expiry string (e.g., "15m", "1h", "7d") 42 + // Format is already validated by Joi schema, but we handle edge cases safely 43 + const match = expiryString.match(/^(\d+)([smhd])$/); 44 + if (!(match?.[1] && match[2])) { 45 + // This should never happen due to Joi validation, but provides type safety 46 + expires_at.setMinutes(expires_at.getMinutes() + 15); 47 + return expires_at; 48 + } 49 + 50 + const value = Number.parseInt(match[1], 10); 51 + const unit = match[2]; 52 + 53 + switch (unit) { 54 + case "s": 55 + expires_at.setSeconds(expires_at.getSeconds() + value); 56 + break; 57 + case "m": 58 + expires_at.setMinutes(expires_at.getMinutes() + value); 59 + break; 60 + case "h": 61 + expires_at.setHours(expires_at.getHours() + value); 62 + break; 63 + case "d": 64 + expires_at.setDate(expires_at.getDate() + value); 65 + break; 66 + } 67 + 68 + return expires_at; 69 + } 70 + 71 + /** 72 + * Calculates the access token expiry date 73 + */ 74 + calculateAccessTokenExpiryDate(): Date { 75 + return this.calculateExpiryDate(this.getAccessTokenExpiry()); 76 + } 77 + }