Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ NODE_ENV=development

# Pino log level
# Valid values: trace, debug, info, warn, error, fatal
LOG_LEVEL=info
LOG_LEVEL=debug

# CORS allowed origins - comma-separated list of allowed origins
# Multiple origins example: http://localhost:3000,https://app.example.com
Expand All @@ -54,7 +54,7 @@ DATABASE_PORT=5432

# Database name
# Default: app
DATABASE_NAME=app
DATABASE_NAME=odma

# Database username
# Default: dev
Expand Down Expand Up @@ -87,7 +87,7 @@ AUTH_SALT_ROUNDS=10

# JWT issuer - identifies who issued the token
# Default: App
AUTH_JWT_ISSUER=App
AUTH_JWT_ISSUER=Odma

# JWT secret key - MUST be changed in production
# Used to sign and verify JWT tokens
Expand Down
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ DATABASE_PORT=5432

# Database name
# Default: app
DATABASE_NAME=app
DATABASE_NAME=odma

# Database username
# Default: dev
Expand All @@ -75,6 +75,11 @@ DATABASE_SSL=false
# Default: false
DATABASE_LOGGING=false

# Run database migrations automatically on application startup
# Accepts: true, false, yes, no, 1, 0
# Default: false
DATABASE_AUTO_MIGRATIONS=false

# -----------------------------------------------------------------------------
# AUTHENTICATION CONFIGURATION
# From: apps/backend/src/config/auth.config.ts
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ node_modules/

# Build outputs
dist/
dist
build/
.output/
.next/
Expand Down
2 changes: 0 additions & 2 deletions .npmrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ link-workspace-packages=true

# strict-peer-dependencies=false
public-hoist-pattern[]=tmp*
public-hoist-pattern[]=nodemon*
public-hoist-pattern[]=quill*
public-hoist-pattern[]=eventemitter*
public-hoist-pattern[]=events*

Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ARG PNPM_HOME="/root/.local/share/pnpm"
ARG PNPM_VERSION="10.17.1"
ARG PNPM_VERSION="10.19.0"

FROM node:22.11.0-alpine3.18
FROM node:24.10.0-alpine3.22
# Prerequisites
ARG PORT=3000
ARG PNPM_HOME
Expand Down
42 changes: 22 additions & 20 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "app-starter/backend",
"name": "odma/backend",
"version": "1.0.0",
"description": "NestJS backend with MikroORM",
"author": "",
Expand All @@ -8,6 +8,7 @@
"scripts": {
"build": "nest build",
"openapi:generate": "node scripts/generate-openapi.js",
"invite:admin": "node scripts/invite-admin.js",
"dev": "dotenv -e ../../.env -- nest start --watch",
"start": "dotenv -e ../../.env -- node dist/src/main",
"start:dev": "nest start --watch",
Expand Down Expand Up @@ -35,25 +36,25 @@
"dependencies": {
"@app/config": "workspace:^",
"@app/seed": "workspace:^",
"@mikro-orm/cli": "^6.5.8",
"@mikro-orm/core": "^6.5.8",
"@mikro-orm/migrations": "^6.5.8",
"@mikro-orm/cli": "^6.5.9",
"@mikro-orm/core": "^6.5.9",
"@mikro-orm/migrations": "^6.5.9",
"@mikro-orm/nestjs": "^6.1.1",
"@mikro-orm/postgresql": "^6.5.8",
"@mikro-orm/reflection": "^6.5.8",
"@mikro-orm/seeder": "^6.5.8",
"@mikro-orm/postgresql": "^6.5.9",
"@mikro-orm/reflection": "^6.5.9",
"@mikro-orm/seeder": "^6.5.9",
"@mikro-orm/sql-highlighter": "^1.0.1",
"@nestjs/common": "^11.1.6",
"@nestjs/common": "^11.1.7",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.6",
"@nestjs/core": "^11.1.7",
"@nestjs/jwt": "^11.0.1",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.6",
"@nestjs/platform-express": "^11.1.7",
"@nestjs/serve-static": "^5.0.4",
"@nestjs/swagger": "^11.2.0",
"@nestjs/swagger": "^11.2.1",
"@nestjs/throttler": "^6.4.0",
"@sentry/nestjs": "^10.19.0",
"@sentry/profiling-node": "^10.19.0",
"@sentry/nestjs": "^10.21.0",
"@sentry/profiling-node": "^10.21.0",
"@types/handlebars": "^4.1.0",
"bcrypt": "^6.0.0",
"casual": "^1.6.2",
Expand All @@ -64,10 +65,11 @@
"handlebars": "^4.7.8",
"helmet": "^8.1.0",
"joi": "^18.0.1",
"minimist": "^1.2.8",
"ms": "^2.1.3",
"multer": "^2.0.2",
"nestjs-pino": "^4.4.1",
"nodemailer": "^7.0.9",
"nodemailer": "^7.0.10",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
Expand All @@ -80,18 +82,18 @@
},
"devDependencies": {
"@libsql/client": "^0.15.15",
"@mikro-orm/libsql": "^6.5.8",
"@mikro-orm/libsql": "^6.5.9",
"@nestjs/cli": "^11.0.10",
"@nestjs/schematics": "^11.0.9",
"@nestjs/testing": "^11.1.6",
"@nestjs/testing": "^11.1.7",
"@types/bcrypt": "^6.0.0",
"@types/cookie-parser": "^1.4.9",
"@types/express": "^5.0.3",
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.4",
"@types/jest": "^30.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^2.0.0",
"@types/node": "^24.7.2",
"@types/nodemailer": "^7.0.2",
"@types/node": "^24.9.1",
"@types/nodemailer": "^7.0.3",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/set-cookie-parser": "^2.4.10",
Expand Down
12 changes: 5 additions & 7 deletions apps/backend/scripts/generate-openapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,17 @@ import { resolve, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = fileURLToPath(new URL('.', import.meta.url));
const backendDir = resolve(__dirname, '../');
const appDir = resolve(__dirname, '../');

async function generateOpenApi() {
console.log('🔄 Generating OpenAPI specification...');

try {
// Import from dist (compiled) backend code
const { AppModule } = await import(
join(backendDir, 'dist/src/app.module.js')
);
const { AppModule } = await import(join(appDir, 'dist/src/app.module.js'));
const { NestFactory } = await import('@nestjs/core');
const { generateOpenApiDocument, saveOpenApiSpec } = await import(
join(backendDir, 'dist/src/utils/openapi.js')
join(appDir, 'dist/src/utils/openapi.js')
);
// Create app instance for spec generation only
const app = await NestFactory.create(AppModule, {
Expand All @@ -23,9 +21,9 @@ async function generateOpenApi() {
});
app.setGlobalPrefix('api');
const document = generateOpenApiDocument(app);
const success = saveOpenApiSpec(document);
const isGenerated = saveOpenApiSpec(document);
await app.close();
if (success) {
if (isGenerated) {
console.log('✅ OpenAPI specification generated successfully');
} else {
console.error('❌ Failed to save OpenAPI specification');
Expand Down
58 changes: 58 additions & 0 deletions apps/backend/scripts/invite-admin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import minimist from 'minimist';

const __dirname = fileURLToPath(new URL('.', import.meta.url));
const appDir = resolve(__dirname, '../');

async function inviteAdmin() {
const argv = minimist(process.argv.slice(2));
const { email } = argv;

if (!email || !email.includes('@')) {
console.error('❌ email arg is required and must be a valid email');
console.log('Usage: pnpm invite:admin --email user@example.com');
process.exit(1);
}

console.log(`🔄 Inviting admin user: ${email}...`);

try {
const { AppModule } = await import(join(appDir, 'dist/src/app.module.js'));
const { NestFactory } = await import('@nestjs/core');
const { MikroORM, RequestContext } = await import('@mikro-orm/core');
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log'],
abortOnError: false,
});
const { UserService } = await import(
join(appDir, 'dist/src/modules/user/user.service.js')
);
const { UserRole } = await import(
join(appDir, 'dist/src/database/entities/index.js')
);
const userService = app.get(UserService);
const orm = app.get(MikroORM);
const user = await RequestContext.create(orm.em, async () => {
return await userService.create({
email,
firstName: 'Admin',
lastName: 'User',
role: UserRole.ADMIN,
});
});
console.log('✅ Admin user invited successfully');
console.log(`📧 Email: ${user.email}`);
console.log(`🔑 Invitation email sent`);
await app.close();
process.exit(0);
} catch (error) {
console.error('❌ Error inviting admin:', error.message);
if (error.response) console.error('Error details:', error.response);
console.log('💡 Make sure the backend is built first: pnpm build');
console.log('💡 Usage: pnpm invite:admin --email user@example.com');
process.exit(1);
}
}

inviteAdmin();
2 changes: 1 addition & 1 deletion apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import generalConfig from './config/general.config';
import { join } from 'path';
import mailConfig from './config/mail.config';
import mikroOrmConfig from './config/mikro-orm.config';
import pinoConfig from './config/lib/pino.config';
import pinoConfig from './config/pino.config';
import { validationSchema } from './config/validation';

const isProduction = process.env.NODE_ENV === 'production';
Expand Down
4 changes: 1 addition & 3 deletions apps/backend/src/common/constants/error-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ export const getErrorType = (status: number): string => {
if ([HttpStatus.TOO_MANY_REQUESTS].includes(status)) {
return ErrorTypes.RATE_LIMIT;
}
if (status >= 500) {
return ErrorTypes.INTERNAL;
}
if (status >= 500) return ErrorTypes.INTERNAL;
return ErrorTypes.HTTP;
};
File renamed without changes.
33 changes: 24 additions & 9 deletions apps/backend/src/config/db.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const dbValidationSchema = {
DATABASE_PASSWORD: Joi.string().default('dev'),
DATABASE_SSL: Joi.boolean().default(false),
DATABASE_LOGGING: Joi.boolean().default(false),
DATABASE_AUTO_MIGRATIONS: Joi.boolean().default(true),
};

export interface DbConfig {
Expand All @@ -24,12 +25,26 @@ export interface DbConfig {
logging: boolean;
}

export default registerAs('database', () => ({
host: env.DATABASE_HOST,
port: parseInt(env.DATABASE_PORT as string, 10),
user: env.DATABASE_USERNAME,
password: env.DATABASE_PASSWORD,
dbName: env.DATABASE_NAME,
ssl: yn(env.DATABASE_SSL),
debug: yn(env.DATABASE_LOGGING),
}));
export default registerAs('database', () => {
const useSSL = yn(env.DATABASE_SSL);
const debug = yn(env.DATABASE_LOGGING);
const autoMigrations = yn(env.DATABASE_AUTO_MIGRATIONS);
return {
host: env.DATABASE_HOST,
port: parseInt(env.DATABASE_PORT as string, 10),
user: env.DATABASE_USERNAME,
password: env.DATABASE_PASSWORD,
dbName: env.DATABASE_NAME,
driverOptions: useSSL
? {
connection: {
ssl: {
rejectUnauthorized: false,
},
},
}
: undefined,
debug,
autoMigrations,
};
});
2 changes: 1 addition & 1 deletion apps/backend/src/database/entities/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './base.entity';
export * from './user.entity';
export * from '@/modules/user/entities/user.entity';
23 changes: 23 additions & 0 deletions apps/backend/src/database/migrations/20251023133836-create-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Migration } from '@mikro-orm/migrations';

export class CreateUser extends Migration {
async up(): Promise<void> {
await this.ctx?.schema?.createTable('users', (table) => {
table.uuid('id').primary();
table.datetime('created_at').notNullable();
table.datetime('updated_at').notNullable();
table.datetime('deleted_at').nullable();
table.string('email', 255).notNullable().unique().index();
table.string('password', 255).notNullable();
table.enum('role', ['ADMIN', 'USER']).notNullable().defaultTo('USER');
table.string('first_name', 200).nullable();
table.string('last_name', 200).nullable();
table.text('img_url').nullable();
table.datetime('last_login_at').nullable();
});
}

async down(): Promise<void> {
await this.ctx?.schema?.dropTable('users');
}
}
2 changes: 1 addition & 1 deletion apps/backend/src/database/seeders/DatabaseSeeder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EntityManager } from '@mikro-orm/core';
import { Seeder } from '@mikro-orm/seeder';
import { User } from '../entities/user.entity';
import { User } from '@/database/entities';
import users from '@app/seed/user.json';

export class DatabaseSeeder extends Seeder {
Expand Down
Loading
Loading