Commit fa6af417 by Augusto

Initial commit

parents
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
.env
{
"singleQuote": true,
"trailingComma": "all"
}
\ No newline at end of file
{
"eslint.enable": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "never"
}
}
\ No newline at end of file
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
ScoutingSystem is a modular monolith for managing scouting data, transfers, players, and reports. It uses feature flags to enable/disable endpoint groups per client subscription (data, scouting, transfer; auth is mandatory).
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Architecture
- Monolith with modules in `src/modules/*`
- Feature flags via `FeatureFlagGuard` and `@FeatureFlag()`
- Database via Drizzle (`src/database/*`)
- Global error handling (`src/common/filters/global-exception.filter.ts`)
See:
- `docs/COMPLETE_ARCHITECTURE.md`
- `docs/MODULE_ARCHITECTURE.md`
- `docs/FEATURE_FLAGS.md`
- `docs/API_ENDPOINTS.md`
- `docs/ERROR_SYSTEM.md`
## Feature Flags (Per Client)
Admin endpoints to manage subscriptions:
```http
GET /api/admin/feature-flags/status
GET /api/admin/feature-flags/subscriptions
GET /api/admin/feature-flags/subscriptions/:clientId
POST /api/admin/feature-flags/subscriptions
PUT /api/admin/feature-flags/subscriptions/:clientId
DELETE /api/admin/feature-flags/subscriptions/:clientId
GET /api/admin/feature-flags/check/:clientId/:feature
```
## Modules
- Auth: `src/modules/auth/*`
- Data Import: `src/modules/data-import/*`
- Players: `src/modules/players/*`
- Transfer: `src/modules/transfer/*`
- Reports: `src/modules/reports/*`
- Admin: `src/modules/admin/*`
## Quickstart
```bash
npm install
npm run start:dev
# Swagger: http://localhost:3000/api/docs
```
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/database/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url:
process.env.DATABASE_URL ||
'postgresql://username:password@localhost:5432/scoutingsystem',
},
});
CREATE TABLE "categories" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"description" text,
"type" text NOT NULL,
"is_active" boolean DEFAULT true,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "categories_name_unique" UNIQUE("name")
);
--> statement-breakpoint
CREATE TABLE "client_subscriptions" (
"id" serial PRIMARY KEY NOT NULL,
"client_id" text NOT NULL,
"features" json NOT NULL,
"expires_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "client_subscriptions_client_id_unique" UNIQUE("client_id")
);
--> statement-breakpoint
CREATE TABLE "global_settings" (
"id" serial PRIMARY KEY NOT NULL,
"category" text NOT NULL,
"key" text NOT NULL,
"name" text NOT NULL,
"description" text,
"value" json NOT NULL,
"color" text,
"is_active" boolean DEFAULT true,
"sort_order" integer DEFAULT 0,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "user_sessions" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"token" text NOT NULL,
"device_id" text NOT NULL,
"device_info" json,
"expires_at" timestamp NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "user_sessions_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "user_settings" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"category" text NOT NULL,
"key" text NOT NULL,
"value" json NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL,
"password_hash" text NOT NULL,
"role" text DEFAULT 'viewer' NOT NULL,
"is_active" boolean DEFAULT true,
"email_verified_at" timestamp,
"last_login_at" timestamp,
"two_factor_enabled" boolean DEFAULT false,
"two_factor_secret" text,
"failed_login_attempts" integer DEFAULT 0,
"locked_until" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"deleted_at" timestamp,
CONSTRAINT "users_email_unique" UNIQUE("email")
);
--> statement-breakpoint
ALTER TABLE "user_sessions" ADD CONSTRAINT "user_sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_settings" ADD CONSTRAINT "user_settings_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "categories_name_idx" ON "categories" USING btree ("name");--> statement-breakpoint
CREATE INDEX "categories_type_idx" ON "categories" USING btree ("type");--> statement-breakpoint
CREATE INDEX "categories_is_active_idx" ON "categories" USING btree ("is_active");--> statement-breakpoint
CREATE UNIQUE INDEX "client_subscriptions_client_id_idx" ON "client_subscriptions" USING btree ("client_id");--> statement-breakpoint
CREATE INDEX "client_subscriptions_expires_at_idx" ON "client_subscriptions" USING btree ("expires_at");--> statement-breakpoint
CREATE INDEX "global_settings_category_idx" ON "global_settings" USING btree ("category");--> statement-breakpoint
CREATE INDEX "global_settings_key_idx" ON "global_settings" USING btree ("key");--> statement-breakpoint
CREATE INDEX "global_settings_is_active_idx" ON "global_settings" USING btree ("is_active");--> statement-breakpoint
CREATE INDEX "global_settings_sort_order_idx" ON "global_settings" USING btree ("sort_order");--> statement-breakpoint
CREATE UNIQUE INDEX "user_sessions_token_idx" ON "user_sessions" USING btree ("token");--> statement-breakpoint
CREATE INDEX "user_sessions_user_id_idx" ON "user_sessions" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "user_sessions_device_id_idx" ON "user_sessions" USING btree ("device_id");--> statement-breakpoint
CREATE INDEX "user_sessions_expires_at_idx" ON "user_sessions" USING btree ("expires_at");--> statement-breakpoint
CREATE INDEX "user_settings_user_id_idx" ON "user_settings" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "user_settings_category_idx" ON "user_settings" USING btree ("category");--> statement-breakpoint
CREATE INDEX "user_settings_key_idx" ON "user_settings" USING btree ("key");--> statement-breakpoint
CREATE UNIQUE INDEX "user_settings_user_category_key_idx" ON "user_settings" USING btree ("user_id","category","key");--> statement-breakpoint
CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");--> statement-breakpoint
CREATE INDEX "users_role_idx" ON "users" USING btree ("role");--> statement-breakpoint
CREATE INDEX "users_is_active_idx" ON "users" USING btree ("is_active");--> statement-breakpoint
CREATE INDEX "users_two_factor_enabled_idx" ON "users" USING btree ("two_factor_enabled");--> statement-breakpoint
CREATE INDEX "users_locked_until_idx" ON "users" USING btree ("locked_until");
\ No newline at end of file
CREATE TABLE "client_modules" (
"id" serial PRIMARY KEY NOT NULL,
"client_id" text NOT NULL,
"scouting" boolean DEFAULT false,
"data_analytics" boolean DEFAULT false,
"transfers" boolean DEFAULT false,
"is_active" boolean DEFAULT true,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "client_modules_client_id_unique" UNIQUE("client_id")
);
--> statement-breakpoint
CREATE UNIQUE INDEX "client_modules_client_id_idx" ON "client_modules" USING btree ("client_id");--> statement-breakpoint
CREATE INDEX "client_modules_is_active_idx" ON "client_modules" USING btree ("is_active");
\ No newline at end of file
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1761215819193,
"tag": "0000_lyrical_hannibal_king",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1761216792037,
"tag": "0001_loose_gideon",
"breakpoints": true
}
]
}
\ No newline at end of file
# =============================================================================
# ScoutingSystem Environment Configuration
# =============================================================================
# This is your actual environment configuration.
# Keep this file secure and never commit it to Git!
# =============================================================================
# -----------------------------------------------------------------------------
# Application Settings
# -----------------------------------------------------------------------------
NODE_ENV=development
PORT=3000
# -----------------------------------------------------------------------------
# Database Configuration (PostgreSQL)
# -----------------------------------------------------------------------------
# Note: Remove ?schema=public from DATABASE_URL (not supported by Drizzle)
DATABASE_URL="postgresql://augusto:021222@localhost:5432/scoutingsystem"
# Database Connection Pooling (Performance Optimization)
DB_POOL_MAX=10 # Maximum number of connections in pool
DB_IDLE_TIMEOUT=20 # Seconds before closing idle connections
DB_CONNECT_TIMEOUT=10 # Seconds to wait for connection
# -----------------------------------------------------------------------------
# JWT Authentication
# -----------------------------------------------------------------------------
# Your current JWT secret (already generated with openssl rand -base64 32)
JWT_SECRET="e9a1b9512abb01dbf612e496f8ada4bb3655fdb1a8c04c82cd3c577553e07e0152f829057dfde882a44ac29c6e110d882a84efae78a78bc6d8979bd1d6210d26"
JWT_EXPIRES_IN=24h
# -----------------------------------------------------------------------------
# Security Settings
# -----------------------------------------------------------------------------
BCRYPT_ROUNDS=12
# -----------------------------------------------------------------------------
# Superadmin Basic Auth (for Control Center)
# -----------------------------------------------------------------------------
# These credentials allow external control centers to manage all client instances
# USERNAME: Plain text (must match exactly when authenticating)
# PASSWORD: Bcrypt hashed for security (original: SuperAdmin@ScoutingSystem2025!)
SUPERADMIN_USERNAME=superadmin
SUPERADMIN_PASSWORD=$2b$12$atF4HIRjbkT2uOOb.s7gK.o8Ki1q.jRRHtYGVm8z.FCCrwDS4G5wa
# =============================================================================
# IMPORTANT NOTES:
# =============================================================================
# 1. For production, use a hashed SUPERADMIN_PASSWORD (starts with $2b$12$...)
# 2. Always use HTTPS in production
# 3. Rotate JWT_SECRET and SUPERADMIN_PASSWORD every 90 days
# 4. Never commit this file to Git (it's in .gitignore)
# =============================================================================
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'commonjs',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn'
},
},
);
\ No newline at end of file
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "scoutingsystem",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"db:generate": "drizzle-kit generate:pg",
"db:migrate": "ts-node src/database/migrate.ts",
"db:studio": "drizzle-kit studio",
"db:push": "drizzle-kit push:pg",
"db:import-users": "ts-node src/database/import-users.ts",
"db:import-users-json": "ts-node src/database/import-users-from-json.ts"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.1",
"@types/bcrypt": "^6.0.0",
"@types/qrcode": "^1.5.5",
"@types/speakeasy": "^2.0.10",
"bcrypt": "^6.0.0",
"better-sqlite3": "^12.4.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"drizzle-kit": "^0.31.5",
"drizzle-orm": "^0.44.6",
"drizzle-zod": "^0.8.3",
"postgres": "^3.4.7",
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"speakeasy": "^2.0.0",
"swagger-ui-express": "^5.0.1",
"zod": "^4.1.12"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/better-sqlite3": "^7.6.13",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});
import { Controller, Get } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
@ApiTags('health')
@Controller()
export class AppController {
@Get()
@ApiOperation({ summary: 'Root endpoint - API information' })
@ApiResponse({ status: 200, description: 'API information' })
getApiInfo() {
return {
message: 'ScoutingSystem API is running',
version: '1.0.0',
timestamp: new Date().toISOString(),
};
}
}
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { DatabaseModule } from './database/database.module';
import { FeatureFlagModule } from './common/services/feature-flag.module';
import { UsersModule } from './modules/users/users.module';
import { AuthModule } from './modules/auth/auth.module';
import { SuperAdminModule } from './modules/superadmin/superadmin.module';
import { SettingsModule } from './modules/settings/settings.module';
@Module({
imports: [
// Load .env file globally
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
DatabaseModule,
FeatureFlagModule,
UsersModule,
AuthModule,
SuperAdminModule,
SettingsModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
// Service can be removed or used for application-level logic
}
/**
* Role Groups for Authorization
* Centralized role definitions to avoid duplication across controllers
*
* NOTE: For client-specific customizations, see src/config/roles.config.ts
* This file imports from the configuration, making it easy to customize per client.
*
* To customize roles for a specific client deployment:
* 1. Edit src/config/roles.config.ts
* 2. Or create src/config/roles.config.{clientName}.ts for multiple clients
* 3. Change the import below to load the correct configuration
*/
import { CLIENT_ROLES } from '../../config/roles.config';
// Export configured roles
// This allows the application to use role groups defined in the configuration
export const ROLE_GROUPS = CLIENT_ROLES;
// Legacy/Fallback definitions (kept for reference)
// These match the default configuration but can be fully overridden
export const ROLE_GROUPS_DEFAULT = {
SUPER_ADMIN: ['superadmin'],
ALL_MANAGEMENT: ['superadmin', 'admin', 'groupmanager', 'president'],
SCOUTS: [
'superadmin',
'admin',
'groupmanager',
'president',
'sportsdirector',
'chiefscout',
'staffscout',
],
DATA_ANALYSTS: [
'superadmin',
'admin',
'groupmanager',
'president',
'sportsdirector',
'chiefdataanalyst',
'staffdataanalyst',
],
SCOUTS_AND_ANALYSTS: [
'superadmin',
'admin',
'groupmanager',
'president',
'sportsdirector',
'chiefscout',
'chiefdataanalyst',
'staffscout',
'staffdataanalyst',
],
TRANSFER_MANAGERS: [
'superadmin',
'admin',
'groupmanager',
'president',
'sportsdirector',
'chieftransfermarket',
'stafftransfermarket',
],
DATA_IMPORTERS: [
'admin',
'groupmanager',
'president',
'sportsdirector',
'chiefscout',
'staffscout',
],
TRANSFER_APPROVERS: [
'superadmin',
'admin',
'groupmanager',
'president',
'sportsdirector',
'chieftransfermarket',
],
} as const;
/**
* Helper function to get all unique roles in the system
*/
export function getAllSystemRoles(): string[] {
const allRoles = new Set<string>();
Object.values(ROLE_GROUPS).forEach((roles) => {
roles.forEach((role) => allRoles.add(role));
});
return Array.from(allRoles);
}
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
import { SetMetadata } from '@nestjs/common';
export type FeatureType = 'data' | 'scouting' | 'transfer' | 'auth';
export const FEATURE_FLAG_KEY = 'featureFlag';
/**
* Decorator to mark endpoints that require specific features
* Usage: @FeatureFlag('data') or @FeatureFlag(['data', 'scouting'])
*/
export const FeatureFlag = (features: FeatureType | FeatureType[]) =>
SetMetadata(
FEATURE_FLAG_KEY,
Array.isArray(features) ? features : [features],
);
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
import { HttpStatus } from '@nestjs/common';
import { BaseError } from './base.error';
/**
* Admin management related errors
*/
export class AdminError extends BaseError {
constructor(message: string, context?: Record<string, any>) {
super(message, HttpStatus.INTERNAL_SERVER_ERROR, 'ADMIN_ERROR', context);
}
}
export class SystemOverviewRetrievalFailedError extends BaseError {
constructor(reason?: string) {
super(
'Failed to retrieve system overview',
HttpStatus.INTERNAL_SERVER_ERROR,
'SYSTEM_OVERVIEW_RETRIEVAL_FAILED',
{ reason },
);
}
}
export class SystemMetricsRetrievalFailedError extends BaseError {
constructor(reason?: string) {
super(
'Failed to retrieve system metrics',
HttpStatus.INTERNAL_SERVER_ERROR,
'SYSTEM_METRICS_RETRIEVAL_FAILED',
{ reason },
);
}
}
export class ServiceStatusRetrievalFailedError extends BaseError {
constructor(reason?: string) {
super(
'Failed to retrieve service statuses',
HttpStatus.INTERNAL_SERVER_ERROR,
'SERVICE_STATUS_RETRIEVAL_FAILED',
{ reason },
);
}
}
export class AdminAccessDeniedError extends BaseError {
constructor(operation: string, userRole: string) {
super(
`Admin access denied for operation: ${operation}`,
HttpStatus.FORBIDDEN,
'ADMIN_ACCESS_DENIED',
{ operation, userRole },
);
}
}
export class SystemConfigurationError extends BaseError {
constructor(configKey: string, reason?: string) {
super(
`System configuration error for key: ${configKey}`,
HttpStatus.INTERNAL_SERVER_ERROR,
'SYSTEM_CONFIGURATION_ERROR',
{ configKey, reason },
);
}
}
export class SystemMaintenanceModeError extends BaseError {
constructor() {
super(
'System is currently in maintenance mode',
HttpStatus.SERVICE_UNAVAILABLE,
'SYSTEM_MAINTENANCE_MODE',
);
}
}
export class SystemResourceExhaustedError extends BaseError {
constructor(resource: string) {
super(
`System resource '${resource}' is exhausted`,
HttpStatus.SERVICE_UNAVAILABLE,
'SYSTEM_RESOURCE_EXHAUSTED',
{ resource },
);
}
}
import { HttpStatus } from '@nestjs/common';
import { BaseError } from './base.error';
/**
* Authentication related errors
*/
export class AuthenticationError extends BaseError {
constructor(message: string, context?: Record<string, any>) {
super(message, HttpStatus.UNAUTHORIZED, 'AUTH_ERROR', context);
}
}
export class InvalidCredentialsError extends BaseError {
constructor(email?: string) {
super(
'Invalid email or password provided',
HttpStatus.UNAUTHORIZED,
'INVALID_CREDENTIALS',
email ? { email } : undefined,
);
}
}
export class TokenExpiredError extends BaseError {
constructor() {
super(
'Authentication token has expired',
HttpStatus.UNAUTHORIZED,
'TOKEN_EXPIRED',
);
}
}
export class InvalidTokenError extends BaseError {
constructor() {
super(
'Invalid or malformed authentication token',
HttpStatus.UNAUTHORIZED,
'INVALID_TOKEN',
);
}
}
export class TokenNotFoundError extends BaseError {
constructor() {
super(
'Authentication token not provided',
HttpStatus.UNAUTHORIZED,
'TOKEN_NOT_FOUND',
);
}
}
export class UserNotFoundError extends BaseError {
constructor(identifier?: string) {
super(
'User not found',
HttpStatus.NOT_FOUND,
'USER_NOT_FOUND',
identifier ? { identifier } : undefined,
);
}
}
export class UserAlreadyExistsError extends BaseError {
constructor(email?: string) {
super(
'User with this email already exists',
HttpStatus.CONFLICT,
'USER_ALREADY_EXISTS',
email ? { email } : undefined,
);
}
}
export class AccountLockedError extends BaseError {
constructor(lockReason?: string) {
super(
'User account is locked',
HttpStatus.FORBIDDEN,
'ACCOUNT_LOCKED',
lockReason ? { lockReason } : undefined,
);
}
}
export class AccountDisabledError extends BaseError {
constructor() {
super('User account is disabled', HttpStatus.FORBIDDEN, 'ACCOUNT_DISABLED');
}
}
export class LogoutFailedError extends BaseError {
constructor(reason?: string) {
super(
'Failed to logout user',
HttpStatus.BAD_REQUEST,
'LOGOUT_FAILED',
reason ? { reason } : undefined,
);
}
}
import { HttpStatus } from '@nestjs/common';
import { BaseError } from './base.error';
/**
* Authorization related errors
*/
export class AuthorizationError extends BaseError {
constructor(message: string, context?: Record<string, any>) {
super(message, HttpStatus.FORBIDDEN, 'AUTHORIZATION_ERROR', context);
}
}
export class InsufficientPermissionsError extends BaseError {
constructor(requiredRole?: string, userRole?: string) {
super(
'Insufficient permissions to perform this action',
HttpStatus.FORBIDDEN,
'INSUFFICIENT_PERMISSIONS',
{
requiredRole,
userRole,
},
);
}
}
export class ServiceAccessDeniedError extends BaseError {
constructor(serviceName: string, userRole?: string) {
super(
`Access denied to service: ${serviceName}`,
HttpStatus.FORBIDDEN,
'SERVICE_ACCESS_DENIED',
{
serviceName,
userRole,
},
);
}
}
export class ModuleAccessDeniedError extends BaseError {
constructor(moduleName: string, userRole?: string) {
super(
`Access denied to module: ${moduleName}`,
HttpStatus.FORBIDDEN,
'MODULE_ACCESS_DENIED',
{
moduleName,
userRole,
},
);
}
}
export class RoleRequiredError extends BaseError {
constructor(requiredRoles: string[], userRole?: string) {
super(
`One of the following roles is required: ${requiredRoles.join(', ')}`,
HttpStatus.FORBIDDEN,
'ROLE_REQUIRED',
{
requiredRoles,
userRole,
},
);
}
}
export class AdminAccessRequiredError extends BaseError {
constructor() {
super(
'Administrator access is required for this operation',
HttpStatus.FORBIDDEN,
'ADMIN_ACCESS_REQUIRED',
);
}
}
export class SuperAdminAccessRequiredError extends BaseError {
constructor() {
super(
'Super Administrator access is required for this operation',
HttpStatus.FORBIDDEN,
'SUPER_ADMIN_ACCESS_REQUIRED',
);
}
}
import { HttpException, HttpStatus } from '@nestjs/common';
/**
* Base error class for all custom errors in the application
*/
export abstract class BaseError extends HttpException {
public readonly errorCode: string;
public readonly timestamp: Date;
public readonly context?: Record<string, any>;
constructor(
message: string,
status: HttpStatus,
errorCode: string,
context?: Record<string, any>,
) {
super(
{
message,
errorCode,
timestamp: new Date().toISOString(),
context,
},
status,
);
this.errorCode = errorCode;
this.timestamp = new Date();
this.context = context;
}
}
import { BaseError } from './base.error';
// ============================================================================
// DATA IMPORT ERRORS
// ============================================================================
export class DataImportError extends BaseError {
constructor(message: string, code: string, statusCode: number = 400) {
super(message, statusCode, code);
this.name = 'DataImportError';
}
}
export class PlayerImportError extends DataImportError {
constructor(message: string, code: string = 'PLAYER_IMPORT_ERROR') {
super(message, code, 400);
this.name = 'PlayerImportError';
}
}
export class MatchImportError extends DataImportError {
constructor(message: string, code: string = 'MATCH_IMPORT_ERROR') {
super(message, code, 400);
this.name = 'MatchImportError';
}
}
export class MatchEventImportError extends DataImportError {
constructor(message: string, code: string = 'MATCH_EVENT_IMPORT_ERROR') {
super(message, code, 400);
this.name = 'MatchEventImportError';
}
}
export class PlayerStatisticsImportError extends DataImportError {
constructor(
message: string,
code: string = 'PLAYER_STATISTICS_IMPORT_ERROR',
) {
super(message, code, 400);
this.name = 'PlayerStatisticsImportError';
}
}
export class RefereeImportError extends DataImportError {
constructor(message: string, code: string = 'REFEREE_IMPORT_ERROR') {
super(message, code, 400);
this.name = 'RefereeImportError';
}
}
export class BulkImportError extends DataImportError {
constructor(message: string, code: string = 'BULK_IMPORT_ERROR') {
super(message, code, 400);
this.name = 'BulkImportError';
}
}
// ============================================================================
// SPECIFIC DATA IMPORT ERROR CODES
// ============================================================================
export const DATA_IMPORT_ERROR_CODES = {
// Player import errors
PLAYER_ALREADY_EXISTS: 'PLAYER_ALREADY_EXISTS',
PLAYER_VALIDATION_FAILED: 'PLAYER_VALIDATION_FAILED',
PLAYER_SAVE_FAILED: 'PLAYER_SAVE_FAILED',
PLAYER_BATCH_IMPORT_FAILED: 'PLAYER_BATCH_IMPORT_FAILED',
// Match import errors
MATCH_ALREADY_EXISTS: 'MATCH_ALREADY_EXISTS',
MATCH_VALIDATION_FAILED: 'MATCH_VALIDATION_FAILED',
MATCH_SAVE_FAILED: 'MATCH_SAVE_FAILED',
MATCH_BATCH_IMPORT_FAILED: 'MATCH_BATCH_IMPORT_FAILED',
// Match event import errors
MATCH_EVENT_ALREADY_EXISTS: 'MATCH_EVENT_ALREADY_EXISTS',
MATCH_EVENT_VALIDATION_FAILED: 'MATCH_EVENT_VALIDATION_FAILED',
MATCH_EVENT_SAVE_FAILED: 'MATCH_EVENT_SAVE_FAILED',
MATCH_EVENT_BATCH_IMPORT_FAILED: 'MATCH_EVENT_BATCH_IMPORT_FAILED',
// Player statistics import errors
PLAYER_STATISTICS_ALREADY_EXISTS: 'PLAYER_STATISTICS_ALREADY_EXISTS',
PLAYER_STATISTICS_VALIDATION_FAILED: 'PLAYER_STATISTICS_VALIDATION_FAILED',
PLAYER_STATISTICS_SAVE_FAILED: 'PLAYER_STATISTICS_SAVE_FAILED',
PLAYER_STATISTICS_BATCH_IMPORT_FAILED:
'PLAYER_STATISTICS_BATCH_IMPORT_FAILED',
// Referee import errors
REFEREE_ALREADY_EXISTS: 'REFEREE_ALREADY_EXISTS',
REFEREE_VALIDATION_FAILED: 'REFEREE_VALIDATION_FAILED',
REFEREE_SAVE_FAILED: 'REFEREE_SAVE_FAILED',
REFEREE_BATCH_IMPORT_FAILED: 'REFEREE_BATCH_IMPORT_FAILED',
// Bulk import errors
BULK_IMPORT_VALIDATION_FAILED: 'BULK_IMPORT_VALIDATION_FAILED',
BULK_IMPORT_PARTIAL_FAILURE: 'BULK_IMPORT_PARTIAL_FAILURE',
BULK_IMPORT_COMPLETE_FAILURE: 'BULK_IMPORT_COMPLETE_FAILURE',
// General data import errors
INVALID_DATA_FORMAT: 'INVALID_DATA_FORMAT',
MISSING_REQUIRED_FIELDS: 'MISSING_REQUIRED_FIELDS',
DATABASE_CONNECTION_ERROR: 'DATABASE_CONNECTION_ERROR',
UNKNOWN_IMPORT_ERROR: 'UNKNOWN_IMPORT_ERROR',
} as const;
// ============================================================================
// ERROR FACTORY FUNCTIONS
// ============================================================================
export const createPlayerImportError = (message: string, code?: string) => {
return new PlayerImportError(message, code);
};
export const createMatchImportError = (message: string, code?: string) => {
return new MatchImportError(message, code);
};
export const createMatchEventImportError = (message: string, code?: string) => {
return new MatchEventImportError(message, code);
};
export const createPlayerStatisticsImportError = (
message: string,
code?: string,
) => {
return new PlayerStatisticsImportError(message, code);
};
export const createRefereeImportError = (message: string, code?: string) => {
return new RefereeImportError(message, code);
};
export const createBulkImportError = (message: string, code?: string) => {
return new BulkImportError(message, code);
};
// ============================================================================
// COMMON ERROR MESSAGES
// ============================================================================
export const DATA_IMPORT_ERROR_MESSAGES = {
PLAYER_ALREADY_EXISTS:
'Player with this external ID already exists in the system',
MATCH_ALREADY_EXISTS:
'Match with this external ID already exists in the system',
MATCH_EVENT_ALREADY_EXISTS:
'Match event with this external ID already exists in the system',
PLAYER_STATISTICS_ALREADY_EXISTS:
'Player statistics for this player and match already exist',
REFEREE_ALREADY_EXISTS:
'Referee with this external ID already exists in the system',
INVALID_DATA_FORMAT: 'Invalid data format provided for import',
MISSING_REQUIRED_FIELDS: 'Required fields are missing from the import data',
DATABASE_CONNECTION_ERROR: 'Failed to connect to database during import',
UNKNOWN_IMPORT_ERROR: 'An unknown error occurred during data import',
BULK_IMPORT_PARTIAL_FAILURE: 'Bulk import completed with some failures',
BULK_IMPORT_COMPLETE_FAILURE: 'Bulk import failed completely',
} as const;
import { HttpStatus } from '@nestjs/common';
import { BaseError } from './base.error';
/**
* Database related errors
*/
export class DatabaseError extends BaseError {
constructor(message: string, context?: Record<string, any>) {
super(message, HttpStatus.INTERNAL_SERVER_ERROR, 'DATABASE_ERROR', context);
}
}
export class DatabaseConnectionError extends BaseError {
constructor(reason?: string) {
super(
'Failed to connect to database',
HttpStatus.SERVICE_UNAVAILABLE,
'DATABASE_CONNECTION_ERROR',
{ reason },
);
}
}
export class DatabaseQueryError extends BaseError {
constructor(query: string, reason?: string) {
super(
'Database query failed',
HttpStatus.INTERNAL_SERVER_ERROR,
'DATABASE_QUERY_ERROR',
{ query, reason },
);
}
}
export class DatabaseTransactionError extends BaseError {
constructor(operation: string, reason?: string) {
super(
`Database transaction failed for operation: ${operation}`,
HttpStatus.INTERNAL_SERVER_ERROR,
'DATABASE_TRANSACTION_ERROR',
{ operation, reason },
);
}
}
export class DatabaseConstraintError extends BaseError {
constructor(constraint: string, reason?: string) {
super(
`Database constraint violation: ${constraint}`,
HttpStatus.CONFLICT,
'DATABASE_CONSTRAINT_ERROR',
{ constraint, reason },
);
}
}
export class DatabaseTimeoutError extends BaseError {
constructor(operation: string, timeoutMs: number) {
super(
`Database operation '${operation}' timed out after ${timeoutMs}ms`,
HttpStatus.REQUEST_TIMEOUT,
'DATABASE_TIMEOUT',
{ operation, timeoutMs },
);
}
}
export class DatabaseMigrationError extends BaseError {
constructor(version: string, reason?: string) {
super(
`Database migration failed for version: ${version}`,
HttpStatus.INTERNAL_SERVER_ERROR,
'DATABASE_MIGRATION_ERROR',
{ version, reason },
);
}
}
export class DatabaseBackupError extends BaseError {
constructor(operation: string, reason?: string) {
super(
`Database backup operation failed: ${operation}`,
HttpStatus.INTERNAL_SERVER_ERROR,
'DATABASE_BACKUP_ERROR',
{ operation, reason },
);
}
}
export class DatabaseRestoreError extends BaseError {
constructor(operation: string, reason?: string) {
super(
`Database restore operation failed: ${operation}`,
HttpStatus.INTERNAL_SERVER_ERROR,
'DATABASE_RESTORE_ERROR',
{ operation, reason },
);
}
}
// Base error class
export { BaseError } from './base.error';
// Authentication errors
export {
AuthenticationError,
InvalidCredentialsError,
TokenExpiredError,
InvalidTokenError,
TokenNotFoundError,
UserNotFoundError,
UserAlreadyExistsError,
AccountLockedError,
AccountDisabledError,
LogoutFailedError,
} from './auth.errors';
// Authorization errors
export {
AuthorizationError,
InsufficientPermissionsError,
ServiceAccessDeniedError,
ModuleAccessDeniedError,
RoleRequiredError,
AdminAccessRequiredError,
SuperAdminAccessRequiredError,
} from './authorization.errors';
// Validation errors
export {
ValidationError,
InvalidInputError,
MissingRequiredFieldError,
InvalidEmailFormatError,
InvalidPasswordFormatError,
InvalidIdFormatError,
InvalidAmountError,
InvalidDateRangeError,
InvalidPeriodError,
InvalidServiceNameError,
InvalidTransferDataError,
} from './validation.errors';
// Service management errors
export {
ServiceError,
ServiceNotAvailableError,
ServiceNotRunningError,
ServiceAlreadyRunningError,
ServiceStartFailedError,
ServiceStopFailedError,
ServiceHealthCheckFailedError,
ServiceTimeoutError,
ServiceCommunicationError,
ServiceConfigurationError,
} from './service.errors';
// Player management errors
export {
PlayerError,
PlayerNotFoundError,
PlayerReportGenerationFailedError,
PlayerPerformanceAnalysisFailedError,
PlayerScoutingDataNotFoundError,
PlayerScoutingDataRetrievalFailedError,
PlayerDataInsufficientError,
PlayerReportAccessDeniedError,
PlayerPerformanceDataOutdatedError,
} from './player.errors';
// Transfer management errors
export {
TransferError,
TransferNotFoundError,
TransferInitiationFailedError,
TransferCancellationFailedError,
TransferApprovalFailedError,
TransferStatusRetrievalFailedError,
TransferAlreadyCompletedError,
TransferAlreadyCancelledError,
TransferInsufficientFundsError,
TransferInvalidAccountsError,
TransferSameAccountError,
TransferAccessDeniedError,
TransferApprovalAccessDeniedError,
} from './transfer.errors';
// Reports errors
export {
ReportsError,
TeamReportGenerationFailedError,
PerformanceReportGenerationFailedError,
ScoutingReportGenerationFailedError,
TeamNotFoundError,
MissionNotFoundError,
ReportDataInsufficientError,
ReportAccessDeniedError,
ReportExportFailedError,
ReportTemplateNotFoundError,
} from './reports.errors';
// Admin errors
export {
AdminError,
SystemOverviewRetrievalFailedError,
SystemMetricsRetrievalFailedError,
ServiceStatusRetrievalFailedError,
AdminAccessDeniedError,
SystemConfigurationError,
SystemMaintenanceModeError,
SystemResourceExhaustedError,
} from './admin.errors';
// Database errors
export {
DatabaseError,
DatabaseConnectionError,
DatabaseQueryError,
DatabaseTransactionError,
DatabaseConstraintError,
DatabaseTimeoutError,
DatabaseMigrationError,
DatabaseBackupError,
DatabaseRestoreError,
} from './database.errors';
// Data import errors
export {
DataImportError,
PlayerImportError,
MatchImportError,
MatchEventImportError,
PlayerStatisticsImportError,
RefereeImportError,
BulkImportError,
DATA_IMPORT_ERROR_CODES,
DATA_IMPORT_ERROR_MESSAGES,
createPlayerImportError,
createMatchImportError,
createMatchEventImportError,
createPlayerStatisticsImportError,
createRefereeImportError,
createBulkImportError,
} from './data-import.errors';
import { HttpStatus } from '@nestjs/common';
import { BaseError } from './base.error';
/**
* Player management related errors
*/
export class PlayerError extends BaseError {
constructor(message: string, context?: Record<string, any>) {
super(message, HttpStatus.INTERNAL_SERVER_ERROR, 'PLAYER_ERROR', context);
}
}
export class PlayerNotFoundError extends BaseError {
constructor(playerId: string) {
super(
`Player with ID '${playerId}' not found`,
HttpStatus.NOT_FOUND,
'PLAYER_NOT_FOUND',
{ playerId },
);
}
}
export class PlayerReportGenerationFailedError extends BaseError {
constructor(playerId: string, reason?: string) {
super(
`Failed to generate report for player '${playerId}'`,
HttpStatus.INTERNAL_SERVER_ERROR,
'PLAYER_REPORT_GENERATION_FAILED',
{ playerId, reason },
);
}
}
export class PlayerPerformanceAnalysisFailedError extends BaseError {
constructor(playerId: string, period?: string, reason?: string) {
super(
`Failed to analyze performance for player '${playerId}'`,
HttpStatus.INTERNAL_SERVER_ERROR,
'PLAYER_PERFORMANCE_ANALYSIS_FAILED',
{ playerId, period, reason },
);
}
}
export class PlayerScoutingDataNotFoundError extends BaseError {
constructor(playerId: string) {
super(
`No scouting data found for player '${playerId}'`,
HttpStatus.NOT_FOUND,
'PLAYER_SCOUTING_DATA_NOT_FOUND',
{ playerId },
);
}
}
export class PlayerScoutingDataRetrievalFailedError extends BaseError {
constructor(playerId: string, reason?: string) {
super(
`Failed to retrieve scouting data for player '${playerId}'`,
HttpStatus.INTERNAL_SERVER_ERROR,
'PLAYER_SCOUTING_DATA_RETRIEVAL_FAILED',
{ playerId, reason },
);
}
}
export class PlayerDataInsufficientError extends BaseError {
constructor(playerId: string, dataType: string) {
super(
`Insufficient ${dataType} data for player '${playerId}'`,
HttpStatus.UNPROCESSABLE_ENTITY,
'PLAYER_DATA_INSUFFICIENT',
{ playerId, dataType },
);
}
}
export class PlayerReportAccessDeniedError extends BaseError {
constructor(playerId: string, userRole: string) {
super(
`Access denied to player report for player '${playerId}'`,
HttpStatus.FORBIDDEN,
'PLAYER_REPORT_ACCESS_DENIED',
{ playerId, userRole },
);
}
}
export class PlayerPerformanceDataOutdatedError extends BaseError {
constructor(playerId: string, lastUpdate: string) {
super(
`Performance data for player '${playerId}' is outdated`,
HttpStatus.UNPROCESSABLE_ENTITY,
'PLAYER_PERFORMANCE_DATA_OUTDATED',
{ playerId, lastUpdate },
);
}
}
import { HttpStatus } from '@nestjs/common';
import { BaseError } from './base.error';
/**
* Reports generation related errors
*/
export class ReportsError extends BaseError {
constructor(message: string, context?: Record<string, any>) {
super(message, HttpStatus.INTERNAL_SERVER_ERROR, 'REPORTS_ERROR', context);
}
}
export class TeamReportGenerationFailedError extends BaseError {
constructor(teamId: string, reason?: string) {
super(
`Failed to generate team report for team '${teamId}'`,
HttpStatus.INTERNAL_SERVER_ERROR,
'TEAM_REPORT_GENERATION_FAILED',
{ teamId, reason },
);
}
}
export class PerformanceReportGenerationFailedError extends BaseError {
constructor(reason?: string) {
super(
'Failed to generate performance report',
HttpStatus.INTERNAL_SERVER_ERROR,
'PERFORMANCE_REPORT_GENERATION_FAILED',
{ reason },
);
}
}
export class ScoutingReportGenerationFailedError extends BaseError {
constructor(missionId: string, reason?: string) {
super(
`Failed to generate scouting report for mission '${missionId}'`,
HttpStatus.INTERNAL_SERVER_ERROR,
'SCOUTING_REPORT_GENERATION_FAILED',
{ missionId, reason },
);
}
}
export class TeamNotFoundError extends BaseError {
constructor(teamId: string) {
super(
`Team with ID '${teamId}' not found`,
HttpStatus.NOT_FOUND,
'TEAM_NOT_FOUND',
{ teamId },
);
}
}
export class MissionNotFoundError extends BaseError {
constructor(missionId: string) {
super(
`Scouting mission with ID '${missionId}' not found`,
HttpStatus.NOT_FOUND,
'MISSION_NOT_FOUND',
{ missionId },
);
}
}
export class ReportDataInsufficientError extends BaseError {
constructor(reportType: string, reason?: string) {
super(
`Insufficient data to generate ${reportType} report`,
HttpStatus.UNPROCESSABLE_ENTITY,
'REPORT_DATA_INSUFFICIENT',
{ reportType, reason },
);
}
}
export class ReportAccessDeniedError extends BaseError {
constructor(reportType: string, userRole: string) {
super(
`Access denied to ${reportType} report`,
HttpStatus.FORBIDDEN,
'REPORT_ACCESS_DENIED',
{ reportType, userRole },
);
}
}
export class ReportExportFailedError extends BaseError {
constructor(reportType: string, format: string, reason?: string) {
super(
`Failed to export ${reportType} report in ${format} format`,
HttpStatus.INTERNAL_SERVER_ERROR,
'REPORT_EXPORT_FAILED',
{ reportType, format, reason },
);
}
}
export class ReportTemplateNotFoundError extends BaseError {
constructor(templateName: string) {
super(
`Report template '${templateName}' not found`,
HttpStatus.NOT_FOUND,
'REPORT_TEMPLATE_NOT_FOUND',
{ templateName },
);
}
}
import { HttpStatus } from '@nestjs/common';
import { BaseError } from './base.error';
/**
* Service management related errors
*/
export class ServiceError extends BaseError {
constructor(message: string, context?: Record<string, any>) {
super(message, HttpStatus.SERVICE_UNAVAILABLE, 'SERVICE_ERROR', context);
}
}
export class ServiceNotAvailableError extends BaseError {
constructor(serviceName: string) {
super(
`Service '${serviceName}' is not available`,
HttpStatus.SERVICE_UNAVAILABLE,
'SERVICE_NOT_AVAILABLE',
{ serviceName },
);
}
}
export class ServiceNotRunningError extends BaseError {
constructor(serviceName: string) {
super(
`Service '${serviceName}' is not running`,
HttpStatus.SERVICE_UNAVAILABLE,
'SERVICE_NOT_RUNNING',
{ serviceName },
);
}
}
export class ServiceAlreadyRunningError extends BaseError {
constructor(serviceName: string) {
super(
`Service '${serviceName}' is already running`,
HttpStatus.CONFLICT,
'SERVICE_ALREADY_RUNNING',
{ serviceName },
);
}
}
export class ServiceStartFailedError extends BaseError {
constructor(serviceName: string, reason?: string) {
super(
`Failed to start service '${serviceName}'`,
HttpStatus.INTERNAL_SERVER_ERROR,
'SERVICE_START_FAILED',
{ serviceName, reason },
);
}
}
export class ServiceStopFailedError extends BaseError {
constructor(serviceName: string, reason?: string) {
super(
`Failed to stop service '${serviceName}'`,
HttpStatus.INTERNAL_SERVER_ERROR,
'SERVICE_STOP_FAILED',
{ serviceName, reason },
);
}
}
export class ServiceHealthCheckFailedError extends BaseError {
constructor(serviceName: string, reason?: string) {
super(
`Health check failed for service '${serviceName}'`,
HttpStatus.SERVICE_UNAVAILABLE,
'SERVICE_HEALTH_CHECK_FAILED',
{ serviceName, reason },
);
}
}
export class ServiceTimeoutError extends BaseError {
constructor(serviceName: string, timeoutMs: number) {
super(
`Service '${serviceName}' request timed out after ${timeoutMs}ms`,
HttpStatus.REQUEST_TIMEOUT,
'SERVICE_TIMEOUT',
{ serviceName, timeoutMs },
);
}
}
export class ServiceCommunicationError extends BaseError {
constructor(serviceName: string, reason?: string) {
super(
`Failed to communicate with service '${serviceName}'`,
HttpStatus.BAD_GATEWAY,
'SERVICE_COMMUNICATION_ERROR',
{ serviceName, reason },
);
}
}
export class ServiceConfigurationError extends BaseError {
constructor(serviceName: string, configKey?: string) {
super(
`Configuration error for service '${serviceName}'`,
HttpStatus.INTERNAL_SERVER_ERROR,
'SERVICE_CONFIGURATION_ERROR',
{ serviceName, configKey },
);
}
}
import { HttpStatus } from '@nestjs/common';
import { BaseError } from './base.error';
/**
* Transfer management related errors
*/
export class TransferError extends BaseError {
constructor(message: string, context?: Record<string, any>) {
super(message, HttpStatus.INTERNAL_SERVER_ERROR, 'TRANSFER_ERROR', context);
}
}
export class TransferNotFoundError extends BaseError {
constructor(transferId: string) {
super(
`Transfer with ID '${transferId}' not found`,
HttpStatus.NOT_FOUND,
'TRANSFER_NOT_FOUND',
{ transferId },
);
}
}
export class TransferInitiationFailedError extends BaseError {
constructor(reason?: string) {
super(
'Failed to initiate transfer',
HttpStatus.INTERNAL_SERVER_ERROR,
'TRANSFER_INITIATION_FAILED',
{ reason },
);
}
}
export class TransferCancellationFailedError extends BaseError {
constructor(transferId: string, reason?: string) {
super(
`Failed to cancel transfer '${transferId}'`,
HttpStatus.INTERNAL_SERVER_ERROR,
'TRANSFER_CANCELLATION_FAILED',
{ transferId, reason },
);
}
}
export class TransferApprovalFailedError extends BaseError {
constructor(transferId: string, reason?: string) {
super(
`Failed to approve transfer '${transferId}'`,
HttpStatus.INTERNAL_SERVER_ERROR,
'TRANSFER_APPROVAL_FAILED',
{ transferId, reason },
);
}
}
export class TransferStatusRetrievalFailedError extends BaseError {
constructor(transferId: string, reason?: string) {
super(
`Failed to retrieve status for transfer '${transferId}'`,
HttpStatus.INTERNAL_SERVER_ERROR,
'TRANSFER_STATUS_RETRIEVAL_FAILED',
{ transferId, reason },
);
}
}
export class TransferAlreadyCompletedError extends BaseError {
constructor(transferId: string) {
super(
`Transfer '${transferId}' is already completed`,
HttpStatus.CONFLICT,
'TRANSFER_ALREADY_COMPLETED',
{ transferId },
);
}
}
export class TransferAlreadyCancelledError extends BaseError {
constructor(transferId: string) {
super(
`Transfer '${transferId}' is already cancelled`,
HttpStatus.CONFLICT,
'TRANSFER_ALREADY_CANCELLED',
{ transferId },
);
}
}
export class TransferInsufficientFundsError extends BaseError {
constructor(amount: number, available: number) {
super(
'Insufficient funds for transfer',
HttpStatus.BAD_REQUEST,
'TRANSFER_INSUFFICIENT_FUNDS',
{ amount, available },
);
}
}
export class TransferInvalidAccountsError extends BaseError {
constructor(fromAccount?: string, toAccount?: string) {
super(
'Invalid source or destination account for transfer',
HttpStatus.BAD_REQUEST,
'TRANSFER_INVALID_ACCOUNTS',
{ fromAccount, toAccount },
);
}
}
export class TransferSameAccountError extends BaseError {
constructor(account: string) {
super(
'Cannot transfer to the same account',
HttpStatus.BAD_REQUEST,
'TRANSFER_SAME_ACCOUNT',
{ account },
);
}
}
export class TransferAccessDeniedError extends BaseError {
constructor(transferId: string, userRole: string) {
super(
`Access denied to transfer '${transferId}'`,
HttpStatus.FORBIDDEN,
'TRANSFER_ACCESS_DENIED',
{ transferId, userRole },
);
}
}
export class TransferApprovalAccessDeniedError extends BaseError {
constructor(transferId: string, userRole: string) {
super(
`Insufficient permissions to approve transfer '${transferId}'`,
HttpStatus.FORBIDDEN,
'TRANSFER_APPROVAL_ACCESS_DENIED',
{ transferId, userRole },
);
}
}
import { HttpStatus } from '@nestjs/common';
import { BaseError } from './base.error';
/**
* Validation related errors
*/
export class ValidationError extends BaseError {
constructor(message: string, context?: Record<string, any>) {
super(message, HttpStatus.BAD_REQUEST, 'VALIDATION_ERROR', context);
}
}
export class InvalidInputError extends BaseError {
constructor(field: string, value: any, reason?: string) {
super(
`Invalid input for field '${field}'`,
HttpStatus.BAD_REQUEST,
'INVALID_INPUT',
{
field,
value,
reason,
},
);
}
}
export class MissingRequiredFieldError extends BaseError {
constructor(field: string) {
super(
`Required field '${field}' is missing`,
HttpStatus.BAD_REQUEST,
'MISSING_REQUIRED_FIELD',
{ field },
);
}
}
export class InvalidEmailFormatError extends BaseError {
constructor(email: string) {
super(
'Invalid email format provided',
HttpStatus.BAD_REQUEST,
'INVALID_EMAIL_FORMAT',
{ email },
);
}
}
export class InvalidPasswordFormatError extends BaseError {
constructor() {
super(
'Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, and one number',
HttpStatus.BAD_REQUEST,
'INVALID_PASSWORD_FORMAT',
);
}
}
export class InvalidIdFormatError extends BaseError {
constructor(id: string, type: string = 'ID') {
super(
`Invalid ${type} format: ${id}`,
HttpStatus.BAD_REQUEST,
'INVALID_ID_FORMAT',
{ id, type },
);
}
}
export class InvalidAmountError extends BaseError {
constructor(amount: number) {
super(
'Transfer amount must be a positive number',
HttpStatus.BAD_REQUEST,
'INVALID_AMOUNT',
{ amount },
);
}
}
export class InvalidDateRangeError extends BaseError {
constructor(startDate: string, endDate: string) {
super(
'Start date must be before end date',
HttpStatus.BAD_REQUEST,
'INVALID_DATE_RANGE',
{ startDate, endDate },
);
}
}
export class InvalidPeriodError extends BaseError {
constructor(period: string) {
super(
`Invalid period format: ${period}. Valid formats: 'last-month', 'season', 'current-season'`,
HttpStatus.BAD_REQUEST,
'INVALID_PERIOD',
{ period },
);
}
}
export class InvalidServiceNameError extends BaseError {
constructor(serviceName: string) {
super(
`Invalid service name: ${serviceName}`,
HttpStatus.BAD_REQUEST,
'INVALID_SERVICE_NAME',
{ serviceName },
);
}
}
export class InvalidTransferDataError extends BaseError {
constructor(reason: string) {
super(
`Invalid transfer data: ${reason}`,
HttpStatus.BAD_REQUEST,
'INVALID_TRANSFER_DATA',
{ reason },
);
}
}
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { BaseError } from '../errors/base.error';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(GlobalExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status: HttpStatus;
let message: string;
let errorCode: string;
let timestamp: string;
let context: Record<string, any> | undefined;
if (exception instanceof BaseError) {
// Handle custom application errors
status = exception.getStatus();
const errorResponse = exception.getResponse() as any;
message = errorResponse.message;
errorCode = errorResponse.errorCode;
timestamp = errorResponse.timestamp;
context = errorResponse.context;
} else if (exception instanceof HttpException) {
// Handle standard HTTP exceptions
status = exception.getStatus();
const errorResponse = exception.getResponse();
message =
typeof errorResponse === 'string'
? errorResponse
: (errorResponse as any).message || 'Internal server error';
errorCode = 'HTTP_EXCEPTION';
timestamp = new Date().toISOString();
} else {
// Handle unexpected errors
status = HttpStatus.INTERNAL_SERVER_ERROR;
message = 'Internal server error';
errorCode = 'INTERNAL_SERVER_ERROR';
timestamp = new Date().toISOString();
}
// Log the error
this.logger.error(
`${request.method} ${request.url} - ${status} - ${message}`,
exception instanceof Error ? exception.stack : undefined,
);
// Prepare error response
const errorResponse = {
statusCode: status,
message,
errorCode,
timestamp,
path: request.url,
method: request.method,
...(context && { context }),
};
response.status(status).json(errorResponse);
}
}
export { GlobalExceptionFilter } from './global-exception.filter';
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
} from '@nestjs/common';
import { AuthService } from '../../modules/auth/auth.service';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private readonly authService: AuthService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('No token provided');
}
try {
// First validate the JWT token
const user = await this.authService.validateToken(token);
if (!user) {
throw new UnauthorizedException('Invalid token');
}
// Then check if the session exists in the database (for single-device login)
const session = await this.authService.validateSession(token);
if (!session) {
throw new UnauthorizedException('Session expired or invalid');
}
request.user = user;
return true;
} catch (error) {
if (error instanceof UnauthorizedException) {
throw error;
}
throw new UnauthorizedException('Invalid token');
}
}
private extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
Logger,
} from '@nestjs/common';
import * as bcrypt from 'bcrypt';
@Injectable()
export class BasicAuthGuard implements CanActivate {
private readonly logger = new Logger(BasicAuthGuard.name);
// Superadmin credentials from environment
private readonly SUPERADMIN_USERNAME = process.env.SUPERADMIN_USERNAME;
private readonly SUPERADMIN_PASSWORD = process.env.SUPERADMIN_PASSWORD;
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
// Get Authorization header
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Basic ')) {
this.logger.warn('Missing or invalid Basic Auth header');
throw new UnauthorizedException('Basic Authentication required');
}
try {
// Decode Base64 credentials
const base64Credentials = authHeader.split(' ')[1];
const credentials = Buffer.from(base64Credentials, 'base64').toString(
'ascii',
);
const [username, password] = credentials.split(':');
// Validate credentials exist
if (!this.SUPERADMIN_USERNAME || !this.SUPERADMIN_PASSWORD) {
this.logger.error(
'SUPERADMIN_USERNAME or SUPERADMIN_PASSWORD not configured',
);
throw new UnauthorizedException('Server configuration error');
}
// Validate username and password
if (username !== this.SUPERADMIN_USERNAME) {
this.logger.warn(`Invalid superadmin username attempt: ${username}`);
throw new UnauthorizedException('Invalid credentials');
}
// Check if password is hashed (starts with $2b$) or plain text
let isValidPassword = false;
if (this.SUPERADMIN_PASSWORD.startsWith('$2b$')) {
// Hashed password - use bcrypt compare
isValidPassword = await bcrypt.compare(
password,
this.SUPERADMIN_PASSWORD,
);
} else {
// Plain text password (for development only)
isValidPassword = password === this.SUPERADMIN_PASSWORD;
if (isValidPassword && process.env.NODE_ENV === 'production') {
this.logger.warn(
'⚠️ Using plain text superadmin password in production!',
);
}
}
if (!isValidPassword) {
this.logger.warn('Invalid superadmin password attempt');
throw new UnauthorizedException('Invalid credentials');
}
// Add superadmin info to request
request.user = {
id: 0,
email: 'superadmin@system',
name: 'SuperAdmin',
role: 'superadmin',
clientId: 'system',
isActive: true,
authType: 'basic',
};
this.logger.log(`SuperAdmin authenticated via Basic Auth: ${username}`);
return true;
} catch (error) {
if (error instanceof UnauthorizedException) {
throw error;
}
this.logger.error('Basic Auth validation error:', error);
throw new UnauthorizedException('Authentication failed');
}
}
}
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
Logger,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { FeatureFlagService } from '../services/feature-flag.service';
import {
FEATURE_FLAG_KEY,
FeatureType,
} from '../decorators/feature-flag.decorator';
@Injectable()
export class FeatureFlagGuard implements CanActivate {
private readonly logger = new Logger(FeatureFlagGuard.name);
constructor(
private readonly reflector: Reflector,
private readonly featureFlagService: FeatureFlagService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// Get required features from decorator
const requiredFeatures = this.reflector.getAllAndOverride<FeatureType[]>(
FEATURE_FLAG_KEY,
[context.getHandler(), context.getClass()],
);
// If no feature requirements, allow access
if (!requiredFeatures || requiredFeatures.length === 0) {
return true;
}
// Get client ID from request (you might need to adjust this based on your auth system)
const request = context.switchToHttp().getRequest();
const clientId = this.extractClientId(request);
if (!clientId) {
this.logger.warn('No client ID found in request');
throw new ForbiddenException('Client identification required');
}
// Check if client has all required features
const featureChecks = await Promise.all(
requiredFeatures.map((feature) =>
this.featureFlagService.isFeatureEnabled(clientId, feature),
),
);
const hasAllFeatures = featureChecks.every((enabled) => enabled);
if (!hasAllFeatures) {
const enabledFeatures =
await this.featureFlagService.getEnabledFeatures(clientId);
const missingFeaturesChecks = await Promise.all(
requiredFeatures.map(async (feature) => ({
feature,
enabled: await this.featureFlagService.isFeatureEnabled(
clientId,
feature,
),
})),
);
const missingFeatures = missingFeaturesChecks
.filter((check) => !check.enabled)
.map((check) => check.feature);
this.logger.warn(
`Client ${clientId} attempted to access endpoint requiring features: ${requiredFeatures.join(', ')}. ` +
`Enabled features: ${enabledFeatures.join(', ')}. Missing: ${missingFeatures.join(', ')}`,
);
throw new ForbiddenException(
`Access denied. Required features: ${requiredFeatures.join(', ')}. ` +
`Your subscription includes: ${enabledFeatures.join(', ')}`,
);
}
this.logger.debug(
`Client ${clientId} granted access to endpoint requiring features: ${requiredFeatures.join(', ')}`,
);
return true;
}
private extractClientId(request: any): string | null {
// Try to get client ID from different sources
// Adjust this based on your authentication system
// Option 1: From JWT token payload
if (request.user?.clientId) {
return request.user.clientId;
}
// Option 2: From user ID (if clientId is same as userId)
if (request.user?.id) {
return request.user.id;
}
// Option 3: From headers
if (request.headers['x-client-id']) {
return request.headers['x-client-id'];
}
// Option 4: From query parameters
if (request.query.clientId) {
return request.query.clientId;
}
return null;
}
}
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
Logger,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { BasicAuthGuard } from './basic-auth.guard';
import { AuthService } from '../../modules/auth/auth.service';
/**
* FlexibleAuthGuard - Accepts either JWT or Basic Auth
* Useful for superadmin endpoints that can be accessed from:
* 1. Normal users with JWT tokens (logged in via web interface)
* 2. Superadmin control center with Basic Auth (external management)
*/
@Injectable()
export class FlexibleAuthGuard implements CanActivate {
private readonly logger = new Logger(FlexibleAuthGuard.name);
private readonly basicAuthGuard: BasicAuthGuard;
constructor(private readonly authService: AuthService) {
this.basicAuthGuard = new BasicAuthGuard();
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
// Try Basic Auth first (for superadmin control center)
if (authHeader?.startsWith('Basic ')) {
try {
const result = await this.basicAuthGuard.canActivate(context);
this.logger.debug('Authenticated via Basic Auth');
return result;
} catch (error) {
// Basic auth failed, try JWT
this.logger.debug('Basic Auth failed, trying JWT...');
}
}
// Try JWT Auth (for normal logged-in users)
if (authHeader?.startsWith('Bearer ')) {
try {
// Extract token and validate
const token = authHeader.substring(7);
const user = await this.authService.validateToken(token);
if (!user) {
throw new UnauthorizedException('Invalid token');
}
// Attach user to request
request.user = user;
this.logger.debug('Authenticated via JWT');
return true;
} catch (error) {
this.logger.debug('JWT Auth failed');
throw new UnauthorizedException(
'Invalid credentials - provide valid JWT or Basic Auth',
);
}
}
throw new UnauthorizedException(
'Authentication required - provide JWT or Basic Auth',
);
}
}
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
throw new ForbiddenException('User not authenticated');
}
const hasRole = requiredRoles.some((role) => user.role === role);
if (!hasRole) {
throw new ForbiddenException(
`Access denied. Required roles: ${requiredRoles.join(', ')}`,
);
}
return true;
}
}
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url, user } = request;
const now = Date.now();
this.logger.log(`→ ${method} ${url} | User: ${user?.email || 'Anonymous'}`);
return next.handle().pipe(
tap({
next: () => {
const response = context.switchToHttp().getResponse();
const { statusCode } = response;
const elapsed = Date.now() - now;
this.logger.log(` ${method} ${url} ${statusCode} | ${elapsed}ms`);
},
error: (error) => {
const elapsed = Date.now() - now;
this.logger.error(
` ${method} ${url} ERROR | ${elapsed}ms`,
error.stack,
);
},
}),
);
}
}
import { Module, Global } from '@nestjs/common';
import { FeatureFlagService } from './feature-flag.service';
import { DatabaseModule } from '../../database/database.module';
@Global()
@Module({
imports: [DatabaseModule],
providers: [FeatureFlagService],
exports: [FeatureFlagService],
})
export class FeatureFlagModule {}
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import {
ClientSubscription,
SubscriptionUpdate,
} from '../../interfaces/feature-flags/feature-flags';
import { DatabaseService } from '../../database/database.service';
import { clientSubscriptions } from '../../database/schema';
import { eq } from 'drizzle-orm';
@Injectable()
export class FeatureFlagService implements OnModuleInit {
private readonly logger = new Logger(FeatureFlagService.name);
constructor(private readonly databaseService: DatabaseService) {}
async onModuleInit() {
// Initialize with default subscriptions for testing
await this.initializeDefaultSubscriptions();
}
private async initializeDefaultSubscriptions() {
try {
// Check if default subscriptions exist
const existingSubscriptions = await this.databaseService
.getDatabase()
.select()
.from(clientSubscriptions)
.limit(1);
// Only initialize if database is empty
if (existingSubscriptions.length === 0) {
await this.databaseService
.getDatabase()
.insert(clientSubscriptions)
.values([
{
clientId: 'admin',
features: {
data: true,
scouting: true,
transfer: true,
auth: true,
},
expiresAt: null,
},
{
clientId: 'client-data-only',
features: {
data: true,
scouting: false,
transfer: false,
auth: true,
},
expiresAt: null,
},
{
clientId: 'client-data-scouting',
features: {
data: true,
scouting: true,
transfer: false,
auth: true,
},
expiresAt: null,
},
]);
this.logger.log('Initialized default feature flag subscriptions');
} else {
this.logger.log('Feature flag subscriptions already exist');
}
} catch (error) {
this.logger.error(
'Failed to initialize default subscriptions:',
error.message,
);
}
}
/**
* Check if a feature is enabled for a specific client
*/
async isFeatureEnabled(
clientId: string,
feature: keyof ClientSubscription['features'],
): Promise<boolean> {
try {
const [subscription] = await this.databaseService
.getDatabase()
.select()
.from(clientSubscriptions)
.where(eq(clientSubscriptions.clientId, clientId))
.limit(1);
if (!subscription) {
this.logger.warn(`No subscription found for client: ${clientId}`);
return false;
}
// Check if subscription has expired
if (subscription.expiresAt && subscription.expiresAt < new Date()) {
this.logger.warn(`Subscription expired for client: ${clientId}`);
return false;
}
// Auth is always enabled
if (feature === 'auth') {
return true;
}
return (subscription.features as any)[feature] || false;
} catch (error) {
this.logger.error(
`Error checking feature for client ${clientId}:`,
error.message,
);
return false;
}
}
/**
* Get all enabled features for a client
*/
async getEnabledFeatures(clientId: string): Promise<string[]> {
try {
const [subscription] = await this.databaseService
.getDatabase()
.select()
.from(clientSubscriptions)
.where(eq(clientSubscriptions.clientId, clientId))
.limit(1);
if (!subscription) {
return ['auth']; // Auth is always available
}
return Object.entries(subscription.features as object)
.filter(([_, enabled]) => enabled)
.map(([feature, _]) => feature);
} catch (error) {
this.logger.error(
`Error getting features for client ${clientId}:`,
error.message,
);
return ['auth'];
}
}
/**
* Update client subscription
*/
async updateClientSubscription(
clientId: string,
subscription: SubscriptionUpdate,
): Promise<boolean> {
try {
const [existing] = await this.databaseService
.getDatabase()
.select()
.from(clientSubscriptions)
.where(eq(clientSubscriptions.clientId, clientId))
.limit(1);
const features = {
data: subscription.features?.data ?? existing?.features.data ?? false,
scouting:
subscription.features?.scouting ??
existing?.features.scouting ??
false,
transfer:
subscription.features?.transfer ??
existing?.features.transfer ??
false,
auth: true,
};
if (existing) {
// Update existing subscription
await this.databaseService
.getDatabase()
.update(clientSubscriptions)
.set({
features: features as any,
expiresAt: subscription.expiresAt ?? existing.expiresAt,
updatedAt: new Date(),
})
.where(eq(clientSubscriptions.clientId, clientId));
} else {
// Insert new subscription
await this.databaseService
.getDatabase()
.insert(clientSubscriptions)
.values({
clientId,
features: features as any,
expiresAt: subscription.expiresAt ?? null,
});
}
this.logger.log(`Updated subscription for client: ${clientId}`);
return true;
} catch (error) {
this.logger.error(
`Failed to update subscription for client ${clientId}:`,
error,
);
return false;
}
}
/**
* Remove client subscription
*/
async removeClientSubscription(clientId: string): Promise<boolean> {
try {
await this.databaseService
.getDatabase()
.delete(clientSubscriptions)
.where(eq(clientSubscriptions.clientId, clientId));
this.logger.log(`Removed subscription for client: ${clientId}`);
return true;
} catch (error) {
this.logger.error(
`Failed to remove subscription for client ${clientId}:`,
error.message,
);
return false;
}
}
/**
* Get all client subscriptions (for admin purposes)
*/
async getAllSubscriptions(): Promise<ClientSubscription[]> {
try {
const subscriptions = await this.databaseService
.getDatabase()
.select()
.from(clientSubscriptions);
return subscriptions.map((sub) => ({
clientId: sub.clientId,
features: sub.features as any,
expiresAt: sub.expiresAt ?? undefined,
}));
} catch (error) {
this.logger.error('Failed to get all subscriptions:', error.message);
return [];
}
}
/**
* Get subscription for a specific client
*/
async getClientSubscription(
clientId: string,
): Promise<ClientSubscription | null> {
try {
const [subscription] = await this.databaseService
.getDatabase()
.select()
.from(clientSubscriptions)
.where(eq(clientSubscriptions.clientId, clientId))
.limit(1);
if (!subscription) {
return null;
}
return {
clientId: subscription.clientId,
features: subscription.features as any,
expiresAt: subscription.expiresAt ?? undefined,
};
} catch (error) {
this.logger.error(
`Failed to get subscription for client ${clientId}:`,
error.message,
);
return null;
}
}
/**
* Check if client has any active subscription
*/
async hasActiveSubscription(clientId: string): Promise<boolean> {
try {
const [subscription] = await this.databaseService
.getDatabase()
.select()
.from(clientSubscriptions)
.where(eq(clientSubscriptions.clientId, clientId))
.limit(1);
if (!subscription) {
return false;
}
// Check if subscription has expired
if (subscription.expiresAt && subscription.expiresAt < new Date()) {
return false;
}
return true;
} catch (error) {
this.logger.error(
`Failed to check active subscription for client ${clientId}:`,
error.message,
);
return false;
}
}
/**
* Get feature status for all clients (for monitoring)
*/
async getFeatureStatus(): Promise<
Record<string, { total: number; enabled: number }>
> {
const status = {
data: { total: 0, enabled: 0 },
scouting: { total: 0, enabled: 0 },
transfer: { total: 0, enabled: 0 },
auth: { total: 0, enabled: 0 },
};
try {
const subscriptions = await this.getAllSubscriptions();
for (const subscription of subscriptions) {
const isActive = await this.hasActiveSubscription(
subscription.clientId,
);
if (isActive) {
Object.keys(status).forEach((feature) => {
status[feature].total++;
if (
subscription.features[
feature as keyof ClientSubscription['features']
]
) {
status[feature].enabled++;
}
});
}
}
} catch (error) {
this.logger.error('Failed to get feature status:', error.message);
}
return status;
}
}
import {
InvalidIdFormatError,
InvalidAmountError,
InvalidTransferDataError,
} from '../errors';
/**
* Validation utility class for common validation patterns
* Centralizes validation logic to avoid duplication
*/
export class ValidationUtils {
private static readonly ID_REGEX = /^[a-zA-Z0-9-_]+$/;
/**
* Validates ID format (alphanumeric, dashes, underscores)
* @throws InvalidIdFormatError if validation fails
*/
static validateId(id: string | undefined, fieldName: string): void {
if (!id || !this.ID_REGEX.test(id)) {
throw new InvalidIdFormatError(id || '', fieldName);
}
}
/**
* Validates that amount is positive
* @throws InvalidAmountError if validation fails
*/
static validatePositiveAmount(amount: number | undefined): void {
if (!amount || amount <= 0) {
throw new InvalidAmountError(amount || 0);
}
}
/**
* Validates that a string is not empty
* @throws InvalidTransferDataError if validation fails
*/
static validateNotEmpty(value: string | undefined, fieldName: string): void {
if (!value || value.trim().length === 0) {
throw new InvalidTransferDataError(`${fieldName} cannot be empty`);
}
}
/**
* Validates that two values are not the same
* @throws InvalidTransferDataError if validation fails
*/
static validateNotSame(
value1: string,
value2: string,
fieldName: string,
): void {
if (value1 === value2) {
throw new InvalidTransferDataError(
`${fieldName} cannot be the same value`,
);
}
}
}
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z
.enum(['development', 'production', 'test'])
.default('development'),
PORT: z
.string()
.default('3000')
.transform((val) => parseInt(val, 10)),
DATABASE_URL: z.string().min(1, 'DATABASE_URL is required'),
DB_POOL_MAX: z
.string()
.default('10')
.transform((val) => parseInt(val, 10)),
DB_IDLE_TIMEOUT: z
.string()
.default('20')
.transform((val) => parseInt(val, 10)),
DB_CONNECT_TIMEOUT: z
.string()
.default('10')
.transform((val) => parseInt(val, 10)),
JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
JWT_EXPIRES_IN: z.string().default('24h'),
BCRYPT_ROUNDS: z
.string()
.default('12')
.transform((val) => parseInt(val, 10)),
// Superadmin Basic Auth (for control center access)
SUPERADMIN_USERNAME: z.string().optional(),
SUPERADMIN_PASSWORD: z.string().optional(),
});
export type Env = z.infer<typeof envSchema>;
export function validateEnv(): Env {
try {
return envSchema.parse(process.env);
} catch (error) {
console.error('❌ Invalid environment variables:');
if (error instanceof z.ZodError) {
error.issues.forEach((err) => {
console.error(` - ${err.path.join('.')}: ${err.message}`);
});
}
console.error(
'\n💡 Please check your .env file and ensure all required variables are set.',
);
process.exit(1);
}
}
/**
* Client-Specific Role Configuration
*
* This file defines role groups that can be easily customized per client deployment.
* Each client can have their own role structure without modifying core application code.
*
* To customize for a specific client:
* 1. Copy this file to: `src/config/roles.config.{clientName}.ts`
* 2. Modify the CLIENT_ROLES constant below
* 3. Use environment variable to load the correct config: CLIENT_CONFIG=clientName
*
* Example: CLIENT_CONFIG=barcelona would load roles.config.barcelona.ts
*/
/**
* Define your organization's roles here
* These are the individual roles that exist in your system
*/
export const AVAILABLE_ROLES = {
// Top-level management
SUPER_ADMIN: 'superadmin',
ADMIN: 'admin',
GROUP_MANAGER: 'groupmanager',
PRESIDENT: 'president',
// Department heads
SPORTS_DIRECTOR: 'sportsdirector',
CHIEF_SCOUT: 'chiefscout',
CHIEF_DATA_ANALYST: 'chiefdataanalyst',
CHIEF_TRANSFER_MARKET: 'chieftransfermarket',
// Staff members
STAFF_SCOUT: 'staffscout',
STAFF_DATA_ANALYST: 'staffdataanalyst',
STAFF_TRANSFER_MARKET: 'stafftransfermarket',
// Read-only
VIEWER: 'viewer',
} as const;
/**
* Define role groups based on functionality
* Customize these groups according to your organization's structure
*/
export const CLIENT_ROLES = {
/**
* Super administrators - Full system access
*/
SUPER_ADMIN: [AVAILABLE_ROLES.SUPER_ADMIN],
/**
* All management levels
*/
ALL_MANAGEMENT: [
AVAILABLE_ROLES.SUPER_ADMIN,
AVAILABLE_ROLES.ADMIN,
AVAILABLE_ROLES.GROUP_MANAGER,
AVAILABLE_ROLES.PRESIDENT,
],
/**
* Scouting department - Can access scouting reports and player data
*/
SCOUTS: [
AVAILABLE_ROLES.SUPER_ADMIN,
AVAILABLE_ROLES.ADMIN,
AVAILABLE_ROLES.GROUP_MANAGER,
AVAILABLE_ROLES.PRESIDENT,
AVAILABLE_ROLES.SPORTS_DIRECTOR,
AVAILABLE_ROLES.CHIEF_SCOUT,
AVAILABLE_ROLES.STAFF_SCOUT,
],
/**
* Data analytics department - Can access performance data and analytics
*/
DATA_ANALYSTS: [
AVAILABLE_ROLES.SUPER_ADMIN,
AVAILABLE_ROLES.ADMIN,
AVAILABLE_ROLES.GROUP_MANAGER,
AVAILABLE_ROLES.PRESIDENT,
AVAILABLE_ROLES.SPORTS_DIRECTOR,
AVAILABLE_ROLES.CHIEF_DATA_ANALYST,
AVAILABLE_ROLES.STAFF_DATA_ANALYST,
],
/**
* Combined scouts and data analysts - Access to both scouting and data
*/
SCOUTS_AND_ANALYSTS: [
AVAILABLE_ROLES.SUPER_ADMIN,
AVAILABLE_ROLES.ADMIN,
AVAILABLE_ROLES.GROUP_MANAGER,
AVAILABLE_ROLES.PRESIDENT,
AVAILABLE_ROLES.SPORTS_DIRECTOR,
AVAILABLE_ROLES.CHIEF_SCOUT,
AVAILABLE_ROLES.CHIEF_DATA_ANALYST,
AVAILABLE_ROLES.STAFF_SCOUT,
AVAILABLE_ROLES.STAFF_DATA_ANALYST,
],
/**
* Transfer market department - Can manage transfers
*/
TRANSFER_MANAGERS: [
AVAILABLE_ROLES.SUPER_ADMIN,
AVAILABLE_ROLES.ADMIN,
AVAILABLE_ROLES.GROUP_MANAGER,
AVAILABLE_ROLES.PRESIDENT,
AVAILABLE_ROLES.SPORTS_DIRECTOR,
AVAILABLE_ROLES.CHIEF_TRANSFER_MARKET,
AVAILABLE_ROLES.STAFF_TRANSFER_MARKET,
],
/**
* Transfer approvers - Can approve transfers (higher authority)
*/
TRANSFER_APPROVERS: [
AVAILABLE_ROLES.SUPER_ADMIN,
AVAILABLE_ROLES.ADMIN,
AVAILABLE_ROLES.GROUP_MANAGER,
AVAILABLE_ROLES.PRESIDENT,
AVAILABLE_ROLES.SPORTS_DIRECTOR,
AVAILABLE_ROLES.CHIEF_TRANSFER_MARKET,
],
/**
* Data importers - Can import external data
*/
DATA_IMPORTERS: [
AVAILABLE_ROLES.ADMIN,
AVAILABLE_ROLES.GROUP_MANAGER,
AVAILABLE_ROLES.PRESIDENT,
AVAILABLE_ROLES.SPORTS_DIRECTOR,
AVAILABLE_ROLES.CHIEF_SCOUT,
AVAILABLE_ROLES.STAFF_SCOUT,
],
} as const;
/**
* Helper function to check if a role belongs to a group
*/
export function hasRole(
userRole: string,
allowedRoles: readonly string[],
): boolean {
return allowedRoles.includes(userRole);
}
/**
* Get all roles in the system
*/
export function getAllRoles(): string[] {
return Object.values(AVAILABLE_ROLES);
}
/**
* Validate if a role exists in the system
*/
export function isValidRole(role: string): boolean {
return getAllRoles().includes(role);
}
import { Module, Global } from '@nestjs/common';
import { DatabaseService } from './database.service';
@Global()
@Module({
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}
import {
Injectable,
Logger,
OnModuleInit,
OnModuleDestroy,
} from '@nestjs/common';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
@Injectable()
export class DatabaseService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(DatabaseService.name);
private db: ReturnType<typeof drizzle>;
private sql: postgres.Sql;
async onModuleInit() {
try {
const databaseUrl =
process.env.DATABASE_URL ||
'postgresql://username:password@localhost:5432/scoutingsystem';
// Configure connection pooling for optimal performance
this.sql = postgres(databaseUrl, {
max: parseInt(process.env.DB_POOL_MAX || '10', 10), // Maximum pool size
idle_timeout: parseInt(process.env.DB_IDLE_TIMEOUT || '20', 10), // Seconds before idle connection closes
connect_timeout: parseInt(process.env.DB_CONNECT_TIMEOUT || '10', 10), // Connection timeout in seconds
max_lifetime: 60 * 30, // Max connection lifetime: 30 minutes
onnotice: () => {}, // Silence notices for cleaner logs
});
this.db = drizzle(this.sql);
this.logger.log(
`PostgreSQL database connected with connection pooling (max: ${process.env.DB_POOL_MAX || '10'})`,
);
} catch (error) {
this.logger.error('Failed to initialize database:', error);
throw error;
}
}
getDatabase() {
return this.db;
}
async onModuleDestroy() {
if (this.sql) {
await this.sql.end();
this.logger.log('Database connection closed');
}
}
}
import * as bcrypt from 'bcrypt';
import postgres from 'postgres';
import * as fs from 'fs';
import * as path from 'path';
/**
* User Import Script from JSON
*
* This script reads users from a JSON file and imports them into the database.
*
* Usage:
* 1. Modify src/database/users.json with your desired users
* 2. Run: npm run db:import-users-json
* 3. Or run directly: ts-node src/database/import-users-from-json.ts
*/
interface UserImportData {
name: string;
email: string;
password: string;
role: string;
isActive?: boolean;
emailVerifiedAt?: string;
twoFactorEnabled?: boolean;
}
interface UsersJsonData {
users: UserImportData[];
metadata: {
version: string;
description: string;
created: string;
totalUsers: number;
roles: string[];
};
}
// ============================================================================
// IMPORT FUNCTIONS
// ============================================================================
async function hashPassword(password: string): Promise<string> {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}
async function checkUserExists(sql: any, email: string): Promise<boolean> {
const existingUser = await sql`
SELECT id FROM users WHERE email = ${email} LIMIT 1
`;
return existingUser.length > 0;
}
async function importUser(sql: any, userData: UserImportData): Promise<void> {
const hashedPassword = await hashPassword(userData.password);
// Parse emailVerifiedAt if it's a string
const emailVerifiedAt = userData.emailVerifiedAt
? new Date(userData.emailVerifiedAt)
: null;
await sql`
INSERT INTO users (
name, email, password_hash, role, is_active,
email_verified_at, two_factor_enabled, created_at, updated_at
) VALUES (
${userData.name}, ${userData.email}, ${hashedPassword}, ${userData.role},
${userData.isActive ?? true}, ${emailVerifiedAt},
${userData.twoFactorEnabled ?? false}, ${new Date()}, ${new Date()}
)
`;
}
function loadUsersFromJson(): UserImportData[] {
const jsonPath = path.join(__dirname, 'users.json');
if (!fs.existsSync(jsonPath)) {
throw new Error(`Users JSON file not found at: ${jsonPath}`);
}
const jsonContent = fs.readFileSync(jsonPath, 'utf8');
const data: UsersJsonData = JSON.parse(jsonContent);
if (!data.users || !Array.isArray(data.users)) {
throw new Error('Invalid JSON format. Expected "users" array.');
}
return data.users;
}
async function importUsersFromJson(): Promise<void> {
// Get database URL from environment
const databaseUrl =
process.env.DATABASE_URL ||
'postgresql://augusto:021222@localhost:5432/scoutingsystem';
console.log('🚀 Starting user import process from JSON...');
// Load users from JSON file
let usersToImport: UserImportData[];
try {
usersToImport = loadUsersFromJson();
console.log(`📊 Found ${usersToImport.length} users to import from JSON`);
} catch (error) {
console.error('❌ Error loading users from JSON:', error);
return;
}
let importedCount = 0;
let skippedCount = 0;
let errorCount = 0;
// Create direct database connection
const sql = postgres(databaseUrl);
try {
for (const userData of usersToImport) {
try {
// Check if user already exists
const userExists = await checkUserExists(sql, userData.email);
if (userExists) {
console.log(`⏭️ Skipping ${userData.email} - already exists`);
skippedCount++;
continue;
}
// Import user
await importUser(sql, userData);
console.log(` Imported ${userData.name} (${userData.email})`);
importedCount++;
} catch (error) {
console.error(` Error importing ${userData.email}:`, error);
errorCount++;
}
}
} finally {
// Close database connection
await sql.end();
}
console.log('\n📈 Import Summary:');
console.log(` Successfully imported: ${importedCount} users`);
console.log(`⏭️ Skipped (already exist): ${skippedCount} users`);
console.log(`❌ Errors: ${errorCount} users`);
console.log(`📊 Total processed: ${usersToImport.length} users`);
if (errorCount > 0) {
console.log('\n⚠️ Some users failed to import. Check the errors above.');
} else {
console.log('\n🎉 All users imported successfully!');
}
}
// ============================================================================
// EXECUTION
// ============================================================================
if (require.main === module) {
importUsersFromJson()
.then(() => {
console.log('\n🏁 User import process completed.');
process.exit(0);
})
.catch((error) => {
console.error('\n💥 Fatal error during user import:', error);
process.exit(1);
});
}
export {
importUsersFromJson,
loadUsersFromJson,
type UserImportData,
type UsersJsonData,
};
import * as bcrypt from 'bcrypt';
import postgres from 'postgres';
/**
* User Import Script
*
* This script allows you to import users into the database.
* You can customize the users array below and run this script.
*
* Usage:
* 1. Modify the users array below with your desired users
* 2. Run: npm run db:import-users
* 3. Or run directly: ts-node src/database/import-users.ts
*/
interface UserImportData {
name: string;
email: string;
password: string; // Will be hashed automatically
role: string;
isActive?: boolean;
emailVerifiedAt?: Date;
twoFactorEnabled?: boolean;
}
// ============================================================================
// USER DATA - CUSTOMIZE THIS ARRAY
// ============================================================================
const usersToImport: UserImportData[] = [
{
name: 'Super Administrator',
email: 'superadmin@scoutingsystem.com',
password: 'SuperAdmin@2025!',
role: 'superadmin',
isActive: true,
emailVerifiedAt: new Date(),
twoFactorEnabled: false,
},
{
name: 'System Administrator',
email: 'admin@scoutingsystem.com',
password: 'Admin@2025!',
role: 'admin',
isActive: true,
emailVerifiedAt: new Date(),
twoFactorEnabled: false,
},
{
name: 'Group Manager',
email: 'groupmanager@scoutingsystem.com',
password: 'GroupManager@2025!',
role: 'groupmanager',
isActive: true,
emailVerifiedAt: new Date(),
twoFactorEnabled: false,
},
{
name: 'Club President',
email: 'president@scoutingsystem.com',
password: 'President@2025!',
role: 'president',
isActive: true,
emailVerifiedAt: new Date(),
twoFactorEnabled: false,
},
{
name: 'Sports Director',
email: 'sportsdirector@scoutingsystem.com',
password: 'SportsDirector@2025!',
role: 'sportsdirector',
isActive: true,
emailVerifiedAt: new Date(),
twoFactorEnabled: false,
},
{
name: 'Chief Scout',
email: 'chiefscout@scoutingsystem.com',
password: 'ChiefScout@2025!',
role: 'chiefscout',
isActive: true,
emailVerifiedAt: new Date(),
twoFactorEnabled: false,
},
{
name: 'Staff Scout',
email: 'staffscout@scoutingsystem.com',
password: 'StaffScout@2025!',
role: 'staffscout',
isActive: true,
emailVerifiedAt: new Date(),
twoFactorEnabled: false,
},
{
name: 'Chief Data Analyst',
email: 'chiefdataanalyst@scoutingsystem.com',
password: 'ChiefDataAnalyst@2025!',
role: 'chiefdataanalyst',
isActive: true,
emailVerifiedAt: new Date(),
twoFactorEnabled: false,
},
{
name: 'Staff Data Analyst',
email: 'staffdataanalyst@scoutingsystem.com',
password: 'StaffDataAnalyst@2025!',
role: 'staffdataanalyst',
isActive: true,
emailVerifiedAt: new Date(),
twoFactorEnabled: false,
},
{
name: 'Chief Transfer Market',
email: 'chieftransfermarket@scoutingsystem.com',
password: 'ChiefTransferMarket@2025!',
role: 'chieftransfermarket',
isActive: true,
emailVerifiedAt: new Date(),
twoFactorEnabled: false,
},
{
name: 'Staff Transfer Market',
email: 'stafftransfermarket@scoutingsystem.com',
password: 'StaffTransferMarket@2025!',
role: 'stafftransfermarket',
isActive: true,
emailVerifiedAt: new Date(),
twoFactorEnabled: false,
},
{
name: 'Viewer User',
email: 'viewer@scoutingsystem.com',
password: 'Viewer@2025!',
role: 'viewer',
isActive: true,
emailVerifiedAt: new Date(),
twoFactorEnabled: false,
},
];
// ============================================================================
// IMPORT FUNCTIONS
// ============================================================================
async function hashPassword(password: string): Promise<string> {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}
async function checkUserExists(sql: any, email: string): Promise<boolean> {
const existingUser = await sql`
SELECT id FROM users WHERE email = ${email} LIMIT 1
`;
return existingUser.length > 0;
}
async function importUser(sql: any, userData: UserImportData): Promise<void> {
const hashedPassword = await hashPassword(userData.password);
await sql`
INSERT INTO users (
name, email, password_hash, role, is_active,
email_verified_at, two_factor_enabled, created_at, updated_at
) VALUES (
${userData.name}, ${userData.email}, ${hashedPassword}, ${userData.role},
${userData.isActive ?? true}, ${userData.emailVerifiedAt},
${userData.twoFactorEnabled ?? false}, ${new Date()}, ${new Date()}
)
`;
}
async function importUsers(): Promise<void> {
// Get database URL from environment
const databaseUrl =
process.env.DATABASE_URL ||
'postgresql://augusto:021222@localhost:5432/scoutingsystem';
console.log('🚀 Starting user import process...');
console.log(`📊 Found ${usersToImport.length} users to import`);
let importedCount = 0;
let skippedCount = 0;
let errorCount = 0;
// Create direct database connection
const sql = postgres(databaseUrl);
try {
for (const userData of usersToImport) {
try {
// Check if user already exists
const userExists = await checkUserExists(sql, userData.email);
if (userExists) {
console.log(`⏭️ Skipping ${userData.email} - already exists`);
skippedCount++;
continue;
}
// Import user
await importUser(sql, userData);
console.log(` Imported ${userData.name} (${userData.email})`);
importedCount++;
} catch (error) {
console.error(` Error importing ${userData.email}:`, error);
errorCount++;
}
}
} finally {
// Close database connection
await sql.end();
}
console.log('\n📈 Import Summary:');
console.log(` Successfully imported: ${importedCount} users`);
console.log(`⏭️ Skipped (already exist): ${skippedCount} users`);
console.log(` Errors: ${errorCount} users`);
console.log(`📊 Total processed: ${usersToImport.length} users`);
if (errorCount > 0) {
console.log('\n⚠️ Some users failed to import. Check the errors above.');
} else {
console.log('\n🎉 All users imported successfully!');
}
}
// ============================================================================
// EXECUTION
// ============================================================================
if (require.main === module) {
importUsers()
.then(() => {
console.log('\n🏁 User import process completed.');
process.exit(0);
})
.catch((error) => {
console.error('\n💥 Fatal error during user import:', error);
process.exit(1);
});
}
export { importUsers, usersToImport, type UserImportData };
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
async function runMigrations() {
const databaseUrl =
process.env.DATABASE_URL ||
'postgresql://username:password@localhost:5432/scoutingsystem';
const sql = postgres(databaseUrl);
const db = drizzle(sql);
try {
console.log('Running database migrations...');
await migrate(db, { migrationsFolder: './drizzle' });
console.log('✅ Database migrations completed successfully');
} catch (error) {
console.error('❌ Migration failed:', error);
process.exit(1);
} finally {
await sql.end();
}
}
runMigrations();
import {
pgTable,
text,
integer,
boolean,
timestamp,
serial,
json,
uniqueIndex,
index,
} from 'drizzle-orm/pg-core';
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
// ============================================================================
// USERS & AUTHENTICATION
// ============================================================================
export const users = pgTable(
'users',
{
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
passwordHash: text('password_hash').notNull(),
role: text('role')
.notNull()
.default('viewer')
.$type<
| 'superadmin'
| 'admin'
| 'groupmanager'
| 'president'
| 'sportsdirector'
| 'chiefscout'
| 'chieftransfermarket'
| 'chiefdataanalyst'
| 'stafftransfermarket'
| 'staffscout'
| 'staffdataanalyst'
| 'viewer'
>(),
isActive: boolean('is_active').default(true),
emailVerifiedAt: timestamp('email_verified_at'),
lastLoginAt: timestamp('last_login_at'),
twoFactorEnabled: boolean('two_factor_enabled').default(false),
twoFactorSecret: text('two_factor_secret'),
failedLoginAttempts: integer('failed_login_attempts').default(0),
lockedUntil: timestamp('locked_until'),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
deletedAt: timestamp('deleted_at'),
},
(table) => ({
emailIdx: uniqueIndex('users_email_idx').on(table.email),
roleIdx: index('users_role_idx').on(table.role),
isActiveIdx: index('users_is_active_idx').on(table.isActive),
twoFactorEnabledIdx: index('users_two_factor_enabled_idx').on(
table.twoFactorEnabled,
),
lockedUntilIdx: index('users_locked_until_idx').on(table.lockedUntil),
}),
);
export const userSessions = pgTable(
'user_sessions',
{
id: serial('id').primaryKey(),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
token: text('token').notNull().unique(),
deviceId: text('device_id').notNull(),
deviceInfo: json('device_info').$type<{
userAgent?: string;
ipAddress?: string;
platform?: string;
}>(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
},
(table) => ({
tokenIdx: uniqueIndex('user_sessions_token_idx').on(table.token),
userIdIdx: index('user_sessions_user_id_idx').on(table.userId),
deviceIdIdx: index('user_sessions_device_id_idx').on(table.deviceId),
expiresAtIdx: index('user_sessions_expires_at_idx').on(table.expiresAt),
}),
);
// ============================================================================
// SETTINGS
// ============================================================================
export const categories = pgTable(
'categories',
{
id: serial('id').primaryKey(),
name: text('name').notNull().unique(),
description: text('description'),
type: text('type').notNull().$type<'global' | 'user'>(),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
nameIdx: uniqueIndex('categories_name_idx').on(table.name),
typeIdx: index('categories_type_idx').on(table.type),
isActiveIdx: index('categories_is_active_idx').on(table.isActive),
}),
);
export const globalSettings = pgTable(
'global_settings',
{
id: serial('id').primaryKey(),
category: text('category').notNull(), // 'grades', 'formations', 'positions', 'seasons', 'labels'
key: text('key').notNull(),
name: text('name').notNull(),
description: text('description'),
value: json('value').notNull(),
color: text('color'), // For grades and other color-coded settings
isActive: boolean('is_active').default(true),
sortOrder: integer('sort_order').default(0),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
categoryIdx: index('global_settings_category_idx').on(table.category),
keyIdx: index('global_settings_key_idx').on(table.key),
isActiveIdx: index('global_settings_is_active_idx').on(table.isActive),
sortOrderIdx: index('global_settings_sort_order_idx').on(table.sortOrder),
}),
);
export const userSettings = pgTable(
'user_settings',
{
id: serial('id').primaryKey(),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
category: text('category').notNull(), // 'dashboard', 'player_page', 'reports', etc.
key: text('key').notNull(),
value: json('value').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
userIdIdx: index('user_settings_user_id_idx').on(table.userId),
categoryIdx: index('user_settings_category_idx').on(table.category),
keyIdx: index('user_settings_key_idx').on(table.key),
userCategoryKeyIdx: uniqueIndex('user_settings_user_category_key_idx').on(
table.userId,
table.category,
table.key,
),
}),
);
// ============================================================================
// FEATURE FLAGS & CLIENT SUBSCRIPTIONS
// ============================================================================
export const clientSubscriptions = pgTable(
'client_subscriptions',
{
id: serial('id').primaryKey(),
clientId: text('client_id').notNull().unique(),
features: json('features').notNull().$type<{
data: boolean;
scouting: boolean;
transfer: boolean;
auth: boolean;
}>(),
expiresAt: timestamp('expires_at'),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
clientIdIdx: uniqueIndex('client_subscriptions_client_id_idx').on(
table.clientId,
),
expiresAtIdx: index('client_subscriptions_expires_at_idx').on(
table.expiresAt,
),
}),
);
export const clientModules = pgTable(
'client_modules',
{
id: serial('id').primaryKey(),
clientId: text('client_id').notNull().unique().default('default-client'),
scouting: boolean('scouting').default(false),
dataAnalytics: boolean('data_analytics').default(false),
transfers: boolean('transfers').default(false),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
clientIdIdx: uniqueIndex('client_modules_client_id_idx').on(table.clientId),
isActiveIdx: index('client_modules_is_active_idx').on(table.isActive),
}),
);
// ============================================================================
// ZOD SCHEMAS FOR VALIDATION
// ============================================================================
// Users
export const insertUserSchema = createInsertSchema(users);
export const selectUserSchema = createSelectSchema(users);
export const insertUserSessionSchema = createInsertSchema(userSessions);
export const selectUserSessionSchema = createSelectSchema(userSessions);
// Categories
export const insertCategorySchema = createInsertSchema(categories);
export const selectCategorySchema = createSelectSchema(categories);
// Settings
export const insertGlobalSettingSchema = createInsertSchema(globalSettings);
export const selectGlobalSettingSchema = createSelectSchema(globalSettings);
export const insertUserSettingSchema = createInsertSchema(userSettings);
export const selectUserSettingSchema = createSelectSchema(userSettings);
// Feature Flags
export const insertClientSubscriptionSchema =
createInsertSchema(clientSubscriptions);
export const selectClientSubscriptionSchema =
createSelectSchema(clientSubscriptions);
// Client Modules
export const insertClientModuleSchema = createInsertSchema(clientModules);
export const selectClientModuleSchema = createSelectSchema(clientModules);
// ============================================================================
// TYPE EXPORTS
// ============================================================================
// Users
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type UserSession = typeof userSessions.$inferSelect;
export type NewUserSession = typeof userSessions.$inferInsert;
// Categories
export type Category = typeof categories.$inferSelect;
export type NewCategory = typeof categories.$inferInsert;
// Settings
export type GlobalSetting = typeof globalSettings.$inferSelect;
export type NewGlobalSetting = typeof globalSettings.$inferInsert;
export type UserSetting = typeof userSettings.$inferSelect;
export type NewUserSetting = typeof userSettings.$inferInsert;
// Feature Flags
export type ClientSubscriptionRecord = typeof clientSubscriptions.$inferSelect;
export type NewClientSubscriptionRecord =
typeof clientSubscriptions.$inferInsert;
// Client Modules
export type ClientModule = typeof clientModules.$inferSelect;
export type NewClientModule = typeof clientModules.$inferInsert;
// Re-export everything from the main schema file
export * from '../schema';
{
"users": [
{
"name": "Super Administrator",
"email": "superadmin@scoutingsystem.com",
"password": "SuperAdmin@2025!",
"role": "superadmin",
"isActive": true,
"emailVerifiedAt": "2024-01-01T00:00:00.000Z",
"twoFactorEnabled": false
},
{
"name": "System Administrator",
"email": "admin@scoutingsystem.com",
"password": "Admin@2025!",
"role": "admin",
"isActive": true,
"emailVerifiedAt": "2024-01-01T00:00:00.000Z",
"twoFactorEnabled": false
},
{
"name": "Group Manager",
"email": "groupmanager@scoutingsystem.com",
"password": "GroupManager@2025!",
"role": "groupmanager",
"isActive": true,
"emailVerifiedAt": "2024-01-01T00:00:00.000Z",
"twoFactorEnabled": false
},
{
"name": "Club President",
"email": "president@scoutingsystem.com",
"password": "President@2025!",
"role": "president",
"isActive": true,
"emailVerifiedAt": "2024-01-01T00:00:00.000Z",
"twoFactorEnabled": false
},
{
"name": "Sports Director",
"email": "sportsdirector@scoutingsystem.com",
"password": "SportsDirector@2025!",
"role": "sportsdirector",
"isActive": true,
"emailVerifiedAt": "2024-01-01T00:00:00.000Z",
"twoFactorEnabled": false
},
{
"name": "Chief Scout",
"email": "chiefscout@scoutingsystem.com",
"password": "ChiefScout@2025!",
"role": "chiefscout",
"isActive": true,
"emailVerifiedAt": "2024-01-01T00:00:00.000Z",
"twoFactorEnabled": false
},
{
"name": "Staff Scout",
"email": "staffscout@scoutingsystem.com",
"password": "StaffScout@2025!",
"role": "staffscout",
"isActive": true,
"emailVerifiedAt": "2024-01-01T00:00:00.000Z",
"twoFactorEnabled": false
},
{
"name": "Chief Data Analyst",
"email": "chiefdataanalyst@scoutingsystem.com",
"password": "ChiefDataAnalyst@2025!",
"role": "chiefdataanalyst",
"isActive": true,
"emailVerifiedAt": "2024-01-01T00:00:00.000Z",
"twoFactorEnabled": false
},
{
"name": "Staff Data Analyst",
"email": "staffdataanalyst@scoutingsystem.com",
"password": "StaffDataAnalyst@2025!",
"role": "staffdataanalyst",
"isActive": true,
"emailVerifiedAt": "2024-01-01T00:00:00.000Z",
"twoFactorEnabled": false
},
{
"name": "Chief Transfer Market",
"email": "chieftransfermarket@scoutingsystem.com",
"password": "ChiefTransferMarket@2025!",
"role": "chieftransfermarket",
"isActive": true,
"emailVerifiedAt": "2024-01-01T00:00:00.000Z",
"twoFactorEnabled": false
},
{
"name": "Staff Transfer Market",
"email": "stafftransfermarket@scoutingsystem.com",
"password": "StaffTransferMarket@2025!",
"role": "stafftransfermarket",
"isActive": true,
"emailVerifiedAt": "2024-01-01T00:00:00.000Z",
"twoFactorEnabled": false
},
{
"name": "Viewer User",
"email": "viewer@scoutingsystem.com",
"password": "Viewer@2025!",
"role": "viewer",
"isActive": true,
"emailVerifiedAt": "2024-01-01T00:00:00.000Z",
"twoFactorEnabled": false
}
],
"metadata": {
"version": "1.0.0",
"description": "Default users for ScoutingSystem application",
"created": "2024-01-01T00:00:00.000Z",
"totalUsers": 12,
"roles": [
"superadmin",
"admin",
"groupmanager",
"president",
"sportsdirector",
"chiefscout",
"staffscout",
"chiefdataanalyst",
"staffdataanalyst",
"chieftransfermarket",
"stafftransfermarket",
"viewer"
]
}
}
/**
* Current user payload extracted from JWT token
*/
export interface CurrentUserPayload {
id: number;
email: string;
name: string;
role: string;
clientId: string | number;
authType?: 'jwt' | 'basic'; // Track authentication method
}
/**
* Validated user returned from authentication
*/
export interface ValidatedUser extends CurrentUserPayload {
isActive?: boolean;
}
/**
* JWT token payload structure
*/
export interface JwtPayload {
sub: number;
email: string;
name: string;
role: string;
clientId: string | number;
iat?: number;
exp?: number;
}
export interface ApiSuccess<T> {
success: true;
data: T;
message?: string;
}
export interface ApiError {
success: false;
error: string;
code?: string;
}
export type ApiResponse<T> = ApiSuccess<T> | ApiError;
export interface PaginationQuery {
page?: number;
pageSize?: number;
}
export interface PaginatedResult<T> {
items: T[];
total: number;
page: number;
pageSize: number;
}
export type FeatureType = 'data' | 'scouting' | 'transfer' | 'auth';
export interface ClientFeatures {
data: boolean;
scouting: boolean;
transfer: boolean;
auth: boolean;
}
export interface ClientSubscription {
clientId: string;
features: ClientFeatures;
expiresAt?: Date;
}
export interface SubscriptionUpdate {
features?: Partial<ClientFeatures>;
expiresAt?: Date;
}
export * from './auth/auth';
export * from './feature-flags/feature-flags';
export * from './common/api';
export * from './common/pagination';
import { NestFactory } from '@nestjs/core';
import { Logger, ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
import { GlobalExceptionFilter } from './common/filters/global-exception.filter';
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
import { validateEnv } from './config/env.validation';
async function bootstrap() {
// Validate environment variables on startup
const env = validateEnv();
const logger = new Logger('Bootstrap');
try {
const app = await NestFactory.create(AppModule);
// Enable global validation pipe
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
// Enable global logging interceptor
app.useGlobalInterceptors(new LoggingInterceptor());
// Enable CORS for cross-origin requests
app.enableCors();
// Set global prefix for all routes
app.setGlobalPrefix('api');
// Set global exception filter
app.useGlobalFilters(new GlobalExceptionFilter());
// Setup Swagger documentation
const config = new DocumentBuilder()
.setTitle('ScoutingSystem')
.setDescription('ScoutingSystem API')
.setVersion('1.0.0')
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'Enter your JWT token obtained from /auth/login',
in: 'header',
},
'bearer',
)
.addBasicAuth(
{
type: 'http',
scheme: 'basic',
description: 'Superadmin credentials (for control centers)',
},
'basic',
)
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document, {
swaggerOptions: {
persistAuthorization: true,
tagsSorter: 'alpha',
operationsSorter: 'alpha',
docExpansion: 'none',
filter: true,
tryItOutEnabled: true,
},
customSiteTitle: 'ScoutingSystem API Docs',
customfavIcon: 'https://swagger.io/favicon.ico',
customCss: `
.swagger-ui .topbar { display: none }
.swagger-ui .info { margin: 20px 0; }
.swagger-ui .info .title { font-size: 36px; }
`,
});
const port = env.PORT;
await app.listen(port);
logger.log(
`🚀 ScoutingSystem API is running on: http://localhost:${port}/api`,
);
logger.log(
`📚 Swagger documentation available at: http://localhost:${port}/docs`,
);
} catch (error) {
logger.error('❌ Failed to start application:', error);
process.exit(1);
}
}
bootstrap();
import {
Controller,
Post,
Body,
HttpCode,
HttpStatus,
UseGuards,
Request,
Delete,
Param,
ParseIntPipe,
Patch,
UnauthorizedException,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
ApiBody,
ApiParam,
} from '@nestjs/swagger';
import { AuthService } from './auth.service';
import {
LoginDto,
AuthResponseDto,
Setup2FAResponseDto,
Verify2FADto,
} from './dto';
import { ChangePasswordDto } from '../users/dto/change-password.dto';
import { JwtAuthGuard } from '../../common/guards/auth.guard';
@ApiTags('Authentication')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@ApiOperation({
summary: 'User login',
description:
'Authenticates a user and returns a JWT token. Supports 2FA and single-device login.',
})
@ApiBody({ type: LoginDto })
@ApiResponse({
status: 200,
description: 'Login successful',
type: AuthResponseDto,
})
@ApiResponse({
status: 401,
description: 'Unauthorized - invalid credentials',
})
@ApiResponse({
status: 400,
description: 'Bad request - validation failed',
})
@HttpCode(HttpStatus.OK)
async login(
@Body() loginDto: LoginDto,
@Request() req: any,
): Promise<AuthResponseDto> {
const deviceInfo = {
userAgent: req.headers['user-agent'],
ipAddress: req.ip || req.connection.remoteAddress,
platform: req.headers['sec-ch-ua-platform'] || 'Unknown',
};
return this.authService.login(loginDto, deviceInfo);
}
@Post('logout')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('bearer')
@ApiOperation({
summary: 'Logout from current device',
description: 'Logs out the user from the current device only.',
})
@ApiResponse({
status: 204,
description: 'Logout successful',
})
@ApiResponse({
status: 401,
description: 'Unauthorized - invalid token',
})
@HttpCode(HttpStatus.NO_CONTENT)
async logout(@Request() req: any): Promise<void> {
const userId = req.user.id;
const deviceId = req.headers['x-device-id'];
return this.authService.logout(userId, deviceId);
}
@Post('logout-all')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('bearer')
@ApiOperation({
summary: 'Logout from all devices',
description:
'Logs out the user from all devices (single-device login enforcement).',
})
@ApiResponse({
status: 204,
description: 'Logout from all devices successful',
})
@ApiResponse({
status: 401,
description: 'Unauthorized - invalid token',
})
@HttpCode(HttpStatus.NO_CONTENT)
async logoutAll(@Request() req: any): Promise<void> {
const userId = req.user.id;
return this.authService.logoutFromAllDevices(userId);
}
@Post('2fa/setup')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('bearer')
@ApiOperation({
summary: 'Setup 2FA',
description:
'Initiates two-factor authentication setup. Returns QR code and backup codes.',
})
@ApiResponse({
status: 200,
description: '2FA setup initiated successfully',
type: Setup2FAResponseDto,
})
@ApiResponse({
status: 400,
description: 'Bad request - 2FA already enabled',
})
@ApiResponse({
status: 401,
description: 'Unauthorized - invalid token',
})
async setup2FA(@Request() req: any): Promise<Setup2FAResponseDto> {
const userId = req.user.id;
return this.authService.setup2FA(userId);
}
@Post('2fa/verify')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('bearer')
@ApiOperation({
summary: 'Verify and enable 2FA',
description: 'Verifies the 2FA code and enables two-factor authentication.',
})
@ApiBody({ type: Verify2FADto })
@ApiResponse({
status: 204,
description: '2FA enabled successfully',
})
@ApiResponse({
status: 400,
description: 'Bad request - invalid 2FA code or setup not initiated',
})
@ApiResponse({
status: 401,
description: 'Unauthorized - invalid token',
})
@HttpCode(HttpStatus.NO_CONTENT)
async verifyAndEnable2FA(
@Request() req: any,
@Body() verifyDto: Verify2FADto,
): Promise<void> {
const userId = req.user.id;
return this.authService.verifyAndEnable2FA(userId, verifyDto);
}
@Delete('2fa/disable')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('bearer')
@ApiOperation({
summary: 'Disable 2FA',
description:
'Disables two-factor authentication. Requires password verification and logs out from all devices.',
})
@ApiBody({
schema: {
type: 'object',
properties: {
password: {
type: 'string',
description: 'Current password for verification',
example: 'currentPassword123',
},
},
required: ['password'],
},
})
@ApiResponse({
status: 204,
description: '2FA disabled successfully',
})
@ApiResponse({
status: 401,
description: 'Unauthorized - invalid password or token',
})
@HttpCode(HttpStatus.NO_CONTENT)
async disable2FA(
@Request() req: any,
@Body() body: { password: string },
): Promise<void> {
const userId = req.user.id;
return this.authService.disable2FA(userId, body.password);
}
@Patch('change-password')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('bearer')
@ApiOperation({
summary: 'Change password',
description:
'Allows users to change their password by providing current password and new password.',
})
@ApiBody({ type: ChangePasswordDto })
@ApiResponse({
status: 204,
description: 'Password changed successfully',
})
@ApiResponse({
status: 401,
description: 'Unauthorized - invalid current password',
})
@ApiResponse({
status: 400,
description: 'Bad request - validation failed',
})
@HttpCode(HttpStatus.NO_CONTENT)
async changePassword(
@Request() req: any,
@Body() changePasswordDto: ChangePasswordDto,
): Promise<void> {
const userId = req.user.id;
return this.authService.changePassword(userId, changePasswordDto);
}
}
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from '../users/users.module';
import { DatabaseModule } from '../../database/database.module';
@Module({
imports: [
DatabaseModule,
UsersModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET') || 'your-secret-key',
signOptions: { expiresIn: '24h' },
}),
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class UserInfoDto {
@ApiProperty({
description: 'User ID',
example: 1,
})
id: number;
@ApiProperty({
description: 'User name',
example: 'John Doe',
})
name: string;
@ApiProperty({
description: 'User email',
example: 'john.doe@example.com',
})
email: string;
@ApiProperty({
description: 'User role',
example: 'viewer',
})
role: string;
@ApiProperty({
description: 'Whether two-factor authentication is enabled',
example: false,
})
twoFactorEnabled: boolean;
}
export class AuthResponseDto {
@ApiProperty({
description: 'JWT access token for authentication',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
})
accessToken: string;
@ApiProperty({
description: 'User information',
type: UserInfoDto,
})
user: UserInfoDto;
@ApiPropertyOptional({
description: 'Whether two-factor authentication is required',
example: false,
})
requiresTwoFactor?: boolean;
}
export * from './login.dto';
export * from './auth-response.dto';
export * from './setup-2fa.dto';
export * from './verify-2fa.dto';
import { IsEmail, IsString, IsOptional, MinLength } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class LoginDto {
@ApiProperty({
description: 'Email address of the user',
example: 'john.doe@example.com',
})
@IsEmail()
email: string;
@ApiProperty({
description: 'Password for the user account',
example: 'securePassword123',
minLength: 6,
})
@IsString()
@MinLength(6)
password: string;
@ApiPropertyOptional({
description: 'Two-factor authentication code (required if 2FA is enabled)',
example: '123456',
})
@IsOptional()
@IsString()
twoFactorCode?: string;
@ApiPropertyOptional({
description: 'Device identifier for single-device login enforcement',
example: 'device_abc123',
})
@IsOptional()
@IsString()
deviceId?: string;
}
import { ApiProperty } from '@nestjs/swagger';
export class Setup2FAResponseDto {
@ApiProperty({
description: 'QR code URL for authenticator app setup',
example: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...',
})
qrCodeUrl: string;
@ApiProperty({
description: 'Two-factor authentication secret key',
example: 'JBSWY3DPEHPK3PXP',
})
secret: string;
@ApiProperty({
description: 'Backup codes for account recovery',
example: ['ABC123', 'DEF456', 'GHI789'],
type: [String],
})
backupCodes: string[];
}
import { IsString, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class Verify2FADto {
@ApiProperty({
description: 'Two-factor authentication code from authenticator app',
example: '123456',
minLength: 6,
})
@IsString()
@MinLength(6)
code: string;
}
import {
IsString,
IsOptional,
IsBoolean,
IsNumber,
IsObject,
IsNotEmpty,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateGlobalSettingDto {
@ApiProperty({
description:
'Setting category (grades, formations, positions, seasons, labels)',
example: 'grades',
enum: ['grades', 'formations', 'positions', 'seasons', 'labels'],
})
@IsString()
@IsNotEmpty()
category: string;
@ApiProperty({
description: 'Unique key within the category',
example: 'excellent',
})
@IsString()
@IsNotEmpty()
key: string;
@ApiProperty({
description: 'Display name for the setting',
example: 'Excellent Grade',
})
@IsString()
@IsNotEmpty()
name: string;
@ApiPropertyOptional({
description: 'Optional description of the setting',
example: 'Outstanding performance grade',
})
@IsString()
@IsOptional()
description?: string;
@ApiProperty({
description: 'Setting value (can be any JSON object)',
example: { min: 90, max: 100 },
})
@IsObject()
value: any;
@ApiPropertyOptional({
description: 'Color code for visual representation (hex format)',
example: '#28a745',
})
@IsString()
@IsOptional()
color?: string;
@ApiPropertyOptional({
description: 'Whether the setting is active',
example: true,
default: true,
})
@IsBoolean()
@IsOptional()
isActive?: boolean;
@ApiPropertyOptional({
description: 'Sort order for display',
example: 1,
default: 0,
})
@IsNumber()
@IsOptional()
sortOrder?: number;
}
export class UpdateGlobalSettingDto {
@ApiPropertyOptional({
description: 'Display name for the setting',
example: 'Excellent Grade',
})
@IsString()
@IsOptional()
name?: string;
@ApiPropertyOptional({
description: 'Optional description of the setting',
example: 'Outstanding performance grade',
})
@IsString()
@IsOptional()
description?: string;
@ApiPropertyOptional({
description: 'Setting value (can be any JSON object)',
example: { min: 90, max: 100 },
})
@IsObject()
@IsOptional()
value?: any;
@ApiPropertyOptional({
description: 'Color code for visual representation (hex format)',
example: '#28a745',
})
@IsString()
@IsOptional()
color?: string;
@ApiPropertyOptional({
description: 'Whether the setting is active',
example: true,
})
@IsBoolean()
@IsOptional()
isActive?: boolean;
@ApiPropertyOptional({
description: 'Sort order for display',
example: 1,
})
@IsNumber()
@IsOptional()
sortOrder?: number;
}
export class CreateCategoryDto {
@ApiProperty({
description: 'Category name',
example: 'custom_fields',
})
@IsString()
@IsNotEmpty()
name: string;
@ApiProperty({
description: 'Category description',
example: 'Custom fields for additional player data',
})
@IsString()
@IsOptional()
description?: string;
@ApiProperty({
description: 'Category type (global or user)',
example: 'global',
enum: ['global', 'user'],
})
@IsString()
@IsNotEmpty()
type: 'global' | 'user';
@ApiProperty({
description: 'Whether the category is active',
example: true,
default: true,
})
@IsBoolean()
@IsOptional()
isActive?: boolean;
}
export class CategoryResponseDto {
@ApiProperty({
description: 'Unique identifier',
example: 1,
})
id: number;
@ApiProperty({
description: 'Category name',
example: 'custom_fields',
})
name: string;
@ApiProperty({
description: 'Category description',
example: 'Custom fields for additional player data',
})
description?: string;
@ApiProperty({
description: 'Category type',
example: 'global',
})
type: 'global' | 'user';
@ApiProperty({
description: 'Whether the category is active',
example: true,
})
isActive: boolean;
@ApiProperty({
description: 'Creation timestamp',
example: '2024-01-01T00:00:00.000Z',
})
createdAt: Date;
@ApiProperty({
description: 'Last update timestamp',
example: '2024-01-01T00:00:00.000Z',
})
updatedAt: Date;
}
export class GlobalSettingResponseDto {
@ApiProperty({
description: 'Unique identifier',
example: 1,
})
id: number;
@ApiProperty({
description: 'Setting category',
example: 'grades',
})
category: string;
@ApiProperty({
description: 'Setting key',
example: 'excellent',
})
key: string;
@ApiProperty({
description: 'Display name',
example: 'Excellent Grade',
})
name: string;
@ApiPropertyOptional({
description: 'Setting description',
example: 'Outstanding performance grade',
})
description?: string;
@ApiProperty({
description: 'Setting value',
example: { min: 90, max: 100 },
})
value: any;
@ApiPropertyOptional({
description: 'Color code',
example: '#28a745',
})
color?: string;
@ApiProperty({
description: 'Whether setting is active',
example: true,
})
isActive: boolean;
@ApiProperty({
description: 'Sort order',
example: 1,
})
sortOrder: number;
@ApiProperty({
description: 'Creation timestamp',
example: '2024-01-01T00:00:00.000Z',
})
createdAt: Date;
@ApiProperty({
description: 'Last update timestamp',
example: '2024-01-01T00:00:00.000Z',
})
updatedAt: Date;
}
This diff is collapsed. Click to expand it.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment