speedrun-cli 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/bin/cli.js +224 -0
- package/index.js +12 -0
- package/package.json +74 -0
- package/src/constants.js +65 -0
- package/src/generator.js +271 -0
- package/src/index.js +13 -0
- package/src/moduleGenerator.js +586 -0
- package/src/postSetup.js +365 -0
- package/src/prompts.js +189 -0
- package/src/utils.js +112 -0
- package/templates/README.md +81 -0
- package/templates/base/eslint.config.mjs +34 -0
- package/templates/base/gitignore +78 -0
- package/templates/base/nest-cli.json +8 -0
- package/templates/base/src/common/constants/cookie.config.ts +20 -0
- package/templates/base/src/common/decorators/get-user.decorator.ts +13 -0
- package/templates/base/src/common/decorators/public.decorator.ts +4 -0
- package/templates/base/src/common/decorators/roles.decorator.ts +4 -0
- package/templates/base/src/common/dtos/pagination.dto.ts +29 -0
- package/templates/base/src/common/filters/http-exception.filter.ts +108 -0
- package/templates/base/src/common/guards/auth.guard.ts +51 -0
- package/templates/base/src/common/guards/refresh-token.guard.ts +39 -0
- package/templates/base/src/common/guards/roles.guard.ts +45 -0
- package/templates/base/src/common/interceptors/response.interceptor.ts +55 -0
- package/templates/base/src/common/interfaces/api-response.interface.ts +26 -0
- package/templates/base/src/common/middleware/correlation-id.middleware.ts +20 -0
- package/templates/base/src/common/validators/password.validator.ts +37 -0
- package/templates/base/src/config/config.module.ts +14 -0
- package/templates/base/src/config/logger.config.ts +109 -0
- package/templates/base/src/main.ts +84 -0
- package/templates/base/src/modules/auth/auth.controller.ts +133 -0
- package/templates/base/src/modules/auth/dtos/login.dto.ts +11 -0
- package/templates/base/src/modules/auth/dtos/signup.dto.ts +8 -0
- package/templates/base/src/modules/health/health.controller.ts +20 -0
- package/templates/base/src/modules/health/health.module.ts +7 -0
- package/templates/base/src/modules/users/dtos/update-profile.dto.ts +12 -0
- package/templates/base/src/modules/users/dtos/update-user.dto.ts +13 -0
- package/templates/base/src/modules/users/users.controller.ts +65 -0
- package/templates/base/test/app.e2e-spec.ts +24 -0
- package/templates/base/test/jest-e2e.json +9 -0
- package/templates/base/tsconfig.build.json +4 -0
- package/templates/base/tsconfig.json +24 -0
- package/templates/base-crud/CRUD_README.md +385 -0
- package/templates/base-crud/src/common/base/base.controller.ts +321 -0
- package/templates/base-crud/src/common/base/base.service.ts +192 -0
- package/templates/base-crud/src/common/base/index.ts +20 -0
- package/templates/base-crud/src/common/base/swagger/api-response.dto.ts +82 -0
- package/templates/base-crud/src/common/base/swagger/paginated.dto.ts +102 -0
- package/templates/base-crud/src/modules/products/dto/create-product.dto.ts +44 -0
- package/templates/base-crud/src/modules/products/dto/product.dto.ts +38 -0
- package/templates/base-crud/src/modules/products/dto/update-product.dto.ts +12 -0
- package/templates/base-crud/src/modules/products/products.controller.ts +94 -0
- package/templates/base-crud-drizzle/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-drizzle/src/modules/products/products.module.ts +24 -0
- package/templates/base-crud-drizzle/src/modules/products/products.service.ts +140 -0
- package/templates/base-crud-drizzle/src/modules/products/schema/products.schema.ts +39 -0
- package/templates/base-crud-mongoose/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-mongoose/src/modules/products/products.module.ts +26 -0
- package/templates/base-crud-mongoose/src/modules/products/products.service.ts +133 -0
- package/templates/base-crud-mongoose/src/modules/products/schemas/product.schema.ts +67 -0
- package/templates/base-crud-prisma/src/modules/products/products.module.ts +12 -0
- package/templates/base-crud-prisma/src/modules/products/products.service.ts +100 -0
- package/templates/base-crud-typeorm/src/modules/products/entities/product.entity.ts +58 -0
- package/templates/base-crud-typeorm/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-typeorm/src/modules/products/products.module.ts +25 -0
- package/templates/base-crud-typeorm/src/modules/products/products.service.ts +102 -0
- package/templates/database/mongodb/.env.example +22 -0
- package/templates/database/mysql/.env.example +24 -0
- package/templates/database/mysql/drizzle.config.ts +13 -0
- package/templates/database/mysql/package.json +5 -0
- package/templates/database/mysql/prisma/schema.prisma +55 -0
- package/templates/database/mysql/src/database/drizzle.ts +13 -0
- package/templates/database/mysql/src/database/schema.ts +58 -0
- package/templates/database/postgres/.env.example +24 -0
- package/templates/database/postgres/drizzle.config.ts +13 -0
- package/templates/database/postgres/package.json +8 -0
- package/templates/database/postgres/prisma/schema.prisma +55 -0
- package/templates/database/sqlite/.env.example +24 -0
- package/templates/database/sqlite/drizzle.config.ts +13 -0
- package/templates/database/sqlite/package.json +8 -0
- package/templates/database/sqlite/prisma/schema.prisma +48 -0
- package/templates/database/sqlite/src/database/drizzle.ts +11 -0
- package/templates/database/sqlite/src/database/schema.ts +52 -0
- package/templates/orm/drizzle/drizzle.config.ts +13 -0
- package/templates/orm/drizzle/package.json +90 -0
- package/templates/orm/drizzle/src/app.module.ts +73 -0
- package/templates/orm/drizzle/src/config/env.validation.ts +55 -0
- package/templates/orm/drizzle/src/database/database.module.ts +25 -0
- package/templates/orm/drizzle/src/database/drizzle.ts +13 -0
- package/templates/orm/drizzle/src/database/schema.ts +60 -0
- package/templates/orm/drizzle/src/database/seed.ts +87 -0
- package/templates/orm/drizzle/src/modules/auth/auth.module.ts +12 -0
- package/templates/orm/drizzle/src/modules/auth/auth.service.ts +298 -0
- package/templates/orm/drizzle/src/modules/health/health.controller.ts +34 -0
- package/templates/orm/drizzle/src/modules/health/health.module.ts +7 -0
- package/templates/orm/drizzle/src/modules/users/users.module.ts +10 -0
- package/templates/orm/drizzle/src/modules/users/users.service.ts +152 -0
- package/templates/orm/mongoose/.env.example +22 -0
- package/templates/orm/mongoose/package.json +86 -0
- package/templates/orm/mongoose/src/app.module.ts +73 -0
- package/templates/orm/mongoose/src/config/env.validation.ts +55 -0
- package/templates/orm/mongoose/src/database/database.module.ts +19 -0
- package/templates/orm/mongoose/src/database/seed.ts +107 -0
- package/templates/orm/mongoose/src/modules/auth/auth.module.ts +21 -0
- package/templates/orm/mongoose/src/modules/auth/auth.service.ts +272 -0
- package/templates/orm/mongoose/src/modules/auth/dtos/login.dto.ts +10 -0
- package/templates/orm/mongoose/src/modules/auth/dtos/signup.dto.ts +8 -0
- package/templates/orm/mongoose/src/modules/health/health.controller.ts +26 -0
- package/templates/orm/mongoose/src/modules/health/health.module.ts +7 -0
- package/templates/orm/mongoose/src/modules/users/dtos/update-profile.dto.ts +24 -0
- package/templates/orm/mongoose/src/modules/users/dtos/update-user.dto.ts +13 -0
- package/templates/orm/mongoose/src/modules/users/users.module.ts +19 -0
- package/templates/orm/mongoose/src/modules/users/users.service.ts +179 -0
- package/templates/orm/mongoose/src/schemas/index.ts +2 -0
- package/templates/orm/mongoose/src/schemas/refresh-token.schema.ts +55 -0
- package/templates/orm/mongoose/src/schemas/user.schema.ts +53 -0
- package/templates/orm/prisma/package.json +102 -0
- package/templates/orm/prisma/prisma/schema.prisma +60 -0
- package/templates/orm/prisma/prisma/seed.ts +69 -0
- package/templates/orm/prisma/src/app.module.ts +55 -0
- package/templates/orm/prisma/src/config/env.validation.ts +56 -0
- package/templates/orm/prisma/src/modules/auth/auth.module.ts +17 -0
- package/templates/orm/prisma/src/modules/auth/auth.service.ts +308 -0
- package/templates/orm/prisma/src/modules/health/health.controller.ts +26 -0
- package/templates/orm/prisma/src/modules/health/health.module.ts +10 -0
- package/templates/orm/prisma/src/modules/users/users.module.ts +11 -0
- package/templates/orm/prisma/src/modules/users/users.service.ts +105 -0
- package/templates/orm/prisma/src/prisma/prisma.module.ts +9 -0
- package/templates/orm/prisma/src/prisma/prisma.service.ts +16 -0
- package/templates/orm/typeorm/package.json +100 -0
- package/templates/orm/typeorm/src/app.module.ts +55 -0
- package/templates/orm/typeorm/src/config/env.validation.ts +58 -0
- package/templates/orm/typeorm/src/database/data-source.ts +19 -0
- package/templates/orm/typeorm/src/database/database.module.ts +27 -0
- package/templates/orm/typeorm/src/database/seed.ts +76 -0
- package/templates/orm/typeorm/src/entities/index.ts +2 -0
- package/templates/orm/typeorm/src/entities/refresh-token.entity.ts +45 -0
- package/templates/orm/typeorm/src/entities/user.entity.ts +51 -0
- package/templates/orm/typeorm/src/modules/auth/auth.module.ts +19 -0
- package/templates/orm/typeorm/src/modules/auth/auth.service.ts +286 -0
- package/templates/orm/typeorm/src/modules/health/health.controller.ts +24 -0
- package/templates/orm/typeorm/src/modules/health/health.module.ts +9 -0
- package/templates/orm/typeorm/src/modules/users/users.module.ts +13 -0
- package/templates/orm/typeorm/src/modules/users/users.service.ts +108 -0
- package/templates/swagger/src/common/dtos/pagination.dto.ts +43 -0
- package/templates/swagger/src/main.ts +116 -0
- package/templates/swagger/src/modules/auth/auth.controller.ts +235 -0
- package/templates/swagger/src/modules/auth/dtos/login.dto.ts +20 -0
- package/templates/swagger/src/modules/auth/dtos/signup.dto.ts +14 -0
- package/templates/swagger/src/modules/users/dtos/update-profile.dto.ts +19 -0
- package/templates/swagger/src/modules/users/dtos/update-user.dto.ts +23 -0
- package/templates/swagger/src/modules/users/users.controller.ts +214 -0
- package/templates/swagger-mongoose/src/modules/auth/dtos/login.dto.ts +20 -0
- package/templates/swagger-mongoose/src/modules/auth/dtos/signup.dto.ts +14 -0
- package/templates/swagger-mongoose/src/modules/users/dtos/update-profile.dto.ts +40 -0
- package/templates/swagger-mongoose/src/modules/users/dtos/update-user.dto.ts +23 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const envSchema = z.object({
|
|
4
|
+
NODE_ENV: z
|
|
5
|
+
.enum(['development', 'production', 'test'])
|
|
6
|
+
.default('development'),
|
|
7
|
+
PORT: z.coerce.number().default(8080),
|
|
8
|
+
|
|
9
|
+
// Database
|
|
10
|
+
DATABASE_URL: z.string().min(1, 'DATABASE_URL is required'),
|
|
11
|
+
|
|
12
|
+
// JWT Secrets
|
|
13
|
+
JWT_ACCESS_SECRET: z
|
|
14
|
+
.string()
|
|
15
|
+
.min(32, 'JWT_ACCESS_SECRET must be at least 32 characters'),
|
|
16
|
+
JWT_REFRESH_SECRET: z
|
|
17
|
+
.string()
|
|
18
|
+
.min(32, 'JWT_REFRESH_SECRET must be at least 32 characters'),
|
|
19
|
+
JWT_ACCESS_EXPIRY: z
|
|
20
|
+
.string()
|
|
21
|
+
.regex(/^\d+[smhd]$/, 'JWT_ACCESS_EXPIRY must be in format: 60m, 1h, etc.')
|
|
22
|
+
.default('15m'),
|
|
23
|
+
JWT_REFRESH_EXPIRY: z
|
|
24
|
+
.string()
|
|
25
|
+
.regex(/^\d+[smhd]$/, 'JWT_REFRESH_EXPIRY must be in format: 7d, 30d, etc.')
|
|
26
|
+
.default('7d'),
|
|
27
|
+
|
|
28
|
+
// CORS
|
|
29
|
+
CORS_ORIGIN: z
|
|
30
|
+
.string()
|
|
31
|
+
.refine((val) => {
|
|
32
|
+
const origins = val.split(',').map((o) => o.trim());
|
|
33
|
+
const urlRegex = /^https?:\/\/.+/;
|
|
34
|
+
return origins.every((origin) => urlRegex.test(origin) || origin === '*');
|
|
35
|
+
}, 'CORS_ORIGIN must be valid URL(s) or "*"')
|
|
36
|
+
.default('http://localhost:3000'),
|
|
37
|
+
|
|
38
|
+
// Logging
|
|
39
|
+
LOG_LEVEL: z
|
|
40
|
+
.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace'])
|
|
41
|
+
.default('info'),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export type Env = z.infer<typeof envSchema>;
|
|
45
|
+
|
|
46
|
+
export function validate(config: Record<string, unknown>) {
|
|
47
|
+
const result = envSchema.safeParse(config);
|
|
48
|
+
if (!result.success) {
|
|
49
|
+
const errors = result.error.issues
|
|
50
|
+
.map((e) => `${e.path.join('.')}: ${e.message}`)
|
|
51
|
+
.join('\n');
|
|
52
|
+
throw new Error(`Environment validation failed:\n${errors}`);
|
|
53
|
+
}
|
|
54
|
+
return result.data;
|
|
55
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { MongooseModule } from '@nestjs/mongoose';
|
|
3
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
4
|
+
|
|
5
|
+
@Module({
|
|
6
|
+
imports: [
|
|
7
|
+
MongooseModule.forRootAsync({
|
|
8
|
+
imports: [ConfigModule],
|
|
9
|
+
useFactory: (configService: ConfigService) => ({
|
|
10
|
+
uri: configService.get<string>('DATABASE_URL'),
|
|
11
|
+
// Connection options
|
|
12
|
+
retryWrites: true,
|
|
13
|
+
w: 'majority',
|
|
14
|
+
}),
|
|
15
|
+
inject: [ConfigService],
|
|
16
|
+
}),
|
|
17
|
+
],
|
|
18
|
+
})
|
|
19
|
+
export class DatabaseModule {}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { connect, connection } from 'mongoose';
|
|
2
|
+
import * as bcrypt from 'bcrypt';
|
|
3
|
+
import * as dotenv from 'dotenv';
|
|
4
|
+
|
|
5
|
+
// Load environment variables
|
|
6
|
+
dotenv.config();
|
|
7
|
+
|
|
8
|
+
const SALT_ROUNDS = 12;
|
|
9
|
+
|
|
10
|
+
interface UserDoc {
|
|
11
|
+
fullName: string;
|
|
12
|
+
email: string;
|
|
13
|
+
passwordHash: string;
|
|
14
|
+
role: 'USER' | 'ADMIN';
|
|
15
|
+
isActive: boolean;
|
|
16
|
+
createdAt: Date;
|
|
17
|
+
updatedAt: Date;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function seed() {
|
|
21
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
22
|
+
|
|
23
|
+
if (!databaseUrl) {
|
|
24
|
+
console.error('❌ DATABASE_URL is not defined in environment variables');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log('🌱 Starting database seed...\n');
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Connect to MongoDB
|
|
32
|
+
await connect(databaseUrl);
|
|
33
|
+
console.log('✓ Connected to MongoDB');
|
|
34
|
+
|
|
35
|
+
const db = connection.db;
|
|
36
|
+
if (!db) {
|
|
37
|
+
throw new Error('Failed to get database instance from connection');
|
|
38
|
+
}
|
|
39
|
+
const usersCollection = db.collection('users');
|
|
40
|
+
|
|
41
|
+
// Check if admin already exists
|
|
42
|
+
const existingAdmin = await usersCollection.findOne({ email: 'admin@example.com' });
|
|
43
|
+
|
|
44
|
+
if (existingAdmin) {
|
|
45
|
+
console.log('⚠️ Admin user already exists, skipping...');
|
|
46
|
+
} else {
|
|
47
|
+
// Create admin user
|
|
48
|
+
const adminPasswordHash = await bcrypt.hash('Admin@123', SALT_ROUNDS);
|
|
49
|
+
const now = new Date();
|
|
50
|
+
|
|
51
|
+
const adminUser: UserDoc = {
|
|
52
|
+
fullName: 'Admin User',
|
|
53
|
+
email: 'admin@example.com',
|
|
54
|
+
passwordHash: adminPasswordHash,
|
|
55
|
+
role: 'ADMIN',
|
|
56
|
+
isActive: true,
|
|
57
|
+
createdAt: now,
|
|
58
|
+
updatedAt: now,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
await usersCollection.insertOne(adminUser);
|
|
62
|
+
console.log('✓ Created admin user');
|
|
63
|
+
console.log(' Email: admin@example.com');
|
|
64
|
+
console.log(' Password: Admin@123');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check if regular user already exists
|
|
68
|
+
const existingUser = await usersCollection.findOne({ email: 'user@example.com' });
|
|
69
|
+
|
|
70
|
+
if (existingUser) {
|
|
71
|
+
console.log('⚠️ Regular user already exists, skipping...');
|
|
72
|
+
} else {
|
|
73
|
+
// Create regular user
|
|
74
|
+
const userPasswordHash = await bcrypt.hash('User@123', SALT_ROUNDS);
|
|
75
|
+
const now = new Date();
|
|
76
|
+
|
|
77
|
+
const regularUser: UserDoc = {
|
|
78
|
+
fullName: 'Regular User',
|
|
79
|
+
email: 'user@example.com',
|
|
80
|
+
passwordHash: userPasswordHash,
|
|
81
|
+
role: 'USER',
|
|
82
|
+
isActive: true,
|
|
83
|
+
createdAt: now,
|
|
84
|
+
updatedAt: now,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
await usersCollection.insertOne(regularUser);
|
|
88
|
+
console.log('✓ Created regular user');
|
|
89
|
+
console.log(' Email: user@example.com');
|
|
90
|
+
console.log(' Password: User@123');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Create indexes
|
|
94
|
+
console.log('\n📇 Creating indexes...');
|
|
95
|
+
await usersCollection.createIndex({ email: 1 }, { unique: true });
|
|
96
|
+
console.log('✓ Created unique index on users.email');
|
|
97
|
+
|
|
98
|
+
console.log('\n✅ Database seed completed successfully!\n');
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('❌ Seed failed:', error);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
} finally {
|
|
103
|
+
await connection.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
seed();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { JwtModule } from '@nestjs/jwt';
|
|
3
|
+
import { MongooseModule } from '@nestjs/mongoose';
|
|
4
|
+
import { AuthController } from './auth.controller';
|
|
5
|
+
import { AuthService } from './auth.service';
|
|
6
|
+
import { User, UserSchema } from '../../schemas/user.schema';
|
|
7
|
+
import { RefreshToken, RefreshTokenSchema } from '../../schemas/refresh-token.schema';
|
|
8
|
+
|
|
9
|
+
@Module({
|
|
10
|
+
imports: [
|
|
11
|
+
MongooseModule.forFeature([
|
|
12
|
+
{ name: User.name, schema: UserSchema },
|
|
13
|
+
{ name: RefreshToken.name, schema: RefreshTokenSchema },
|
|
14
|
+
]),
|
|
15
|
+
JwtModule.register({}),
|
|
16
|
+
],
|
|
17
|
+
controllers: [AuthController],
|
|
18
|
+
providers: [AuthService],
|
|
19
|
+
exports: [AuthService, JwtModule],
|
|
20
|
+
})
|
|
21
|
+
export class AuthModule {}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Injectable,
|
|
3
|
+
UnauthorizedException,
|
|
4
|
+
ConflictException,
|
|
5
|
+
ForbiddenException,
|
|
6
|
+
NotFoundException,
|
|
7
|
+
} from '@nestjs/common';
|
|
8
|
+
import { ConfigService } from '@nestjs/config';
|
|
9
|
+
import { JwtService } from '@nestjs/jwt';
|
|
10
|
+
import { InjectModel } from '@nestjs/mongoose';
|
|
11
|
+
import { Model } from 'mongoose';
|
|
12
|
+
import * as bcrypt from 'bcrypt';
|
|
13
|
+
import { User, UserDocument, UserRole } from '../../schemas/user.schema';
|
|
14
|
+
import { RefreshToken, RefreshTokenDocument } from '../../schemas/refresh-token.schema';
|
|
15
|
+
import { SignupDto } from './dtos/signup.dto';
|
|
16
|
+
import { LoginDto } from './dtos/login.dto';
|
|
17
|
+
|
|
18
|
+
const SALT_ROUNDS = 12;
|
|
19
|
+
|
|
20
|
+
@Injectable()
|
|
21
|
+
export class AuthService {
|
|
22
|
+
constructor(
|
|
23
|
+
@InjectModel(User.name) private userModel: Model<UserDocument>,
|
|
24
|
+
@InjectModel(RefreshToken.name) private refreshTokenModel: Model<RefreshTokenDocument>,
|
|
25
|
+
private jwtService: JwtService,
|
|
26
|
+
private configService: ConfigService,
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
async signup(signupDto: SignupDto) {
|
|
30
|
+
const { fullName, email, password } = signupDto;
|
|
31
|
+
|
|
32
|
+
// Check if user already exists
|
|
33
|
+
const existingUser = await this.userModel.findOne({ email: email.toLowerCase() });
|
|
34
|
+
if (existingUser) {
|
|
35
|
+
throw new ConflictException('User with this email already exists');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Hash password and create user
|
|
39
|
+
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
|
|
40
|
+
const user = await this.userModel.create({
|
|
41
|
+
fullName,
|
|
42
|
+
email: email.toLowerCase(),
|
|
43
|
+
passwordHash,
|
|
44
|
+
role: UserRole.USER,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
id: user._id.toString(),
|
|
49
|
+
fullName: user.fullName,
|
|
50
|
+
email: user.email,
|
|
51
|
+
role: user.role,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async login(loginDto: LoginDto, deviceInfo?: string, ipAddress?: string) {
|
|
56
|
+
const { email, password } = loginDto;
|
|
57
|
+
|
|
58
|
+
// Find user
|
|
59
|
+
const user = await this.userModel.findOne({ email: email.toLowerCase() });
|
|
60
|
+
if (!user) {
|
|
61
|
+
throw new UnauthorizedException('Invalid email or password');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check if user is active
|
|
65
|
+
if (!user.isActive) {
|
|
66
|
+
throw new UnauthorizedException('Account is deactivated');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Verify password
|
|
70
|
+
const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
|
|
71
|
+
if (!isPasswordValid) {
|
|
72
|
+
throw new UnauthorizedException('Invalid email or password');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Generate tokens
|
|
76
|
+
const tokens = await this.generateTokens(user);
|
|
77
|
+
|
|
78
|
+
// Store refresh token in database
|
|
79
|
+
await this.storeRefreshToken(
|
|
80
|
+
tokens.refreshToken,
|
|
81
|
+
user._id.toString(),
|
|
82
|
+
deviceInfo || 'Unknown Device',
|
|
83
|
+
ipAddress || 'Unknown IP',
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Clean up expired tokens for this user
|
|
87
|
+
await this.cleanupExpiredTokens(user._id.toString());
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
user: {
|
|
91
|
+
id: user._id.toString(),
|
|
92
|
+
fullName: user.fullName,
|
|
93
|
+
email: user.email,
|
|
94
|
+
role: user.role,
|
|
95
|
+
},
|
|
96
|
+
...tokens,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async logout(userId: string, refreshToken?: string) {
|
|
101
|
+
if (refreshToken) {
|
|
102
|
+
// Find and revoke the specific refresh token by comparing hashes
|
|
103
|
+
const storedTokens = await this.refreshTokenModel.find({ userId });
|
|
104
|
+
|
|
105
|
+
for (const storedToken of storedTokens) {
|
|
106
|
+
const matches = await bcrypt.compare(refreshToken, storedToken.token);
|
|
107
|
+
if (matches) {
|
|
108
|
+
await this.refreshTokenModel.updateOne(
|
|
109
|
+
{ _id: storedToken._id },
|
|
110
|
+
{ isRevoked: true },
|
|
111
|
+
);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
// Logout from all devices - revoke all refresh tokens for this user
|
|
117
|
+
await this.refreshTokenModel.updateMany(
|
|
118
|
+
{ userId, isRevoked: false },
|
|
119
|
+
{ isRevoked: true },
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { message: 'Logged out successfully' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async refreshToken(
|
|
127
|
+
userId: string,
|
|
128
|
+
rt: string,
|
|
129
|
+
deviceInfo?: string,
|
|
130
|
+
ipAddress?: string,
|
|
131
|
+
) {
|
|
132
|
+
// Find the user
|
|
133
|
+
const user = await this.userModel.findById(userId);
|
|
134
|
+
if (!user) {
|
|
135
|
+
throw new ForbiddenException('Invalid refresh token');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Find matching refresh token in database by comparing hashes
|
|
139
|
+
const storedTokens = await this.refreshTokenModel.find({
|
|
140
|
+
userId,
|
|
141
|
+
isRevoked: false,
|
|
142
|
+
expiresAt: { $gt: new Date() },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
let validToken: RefreshTokenDocument | null = null;
|
|
146
|
+
for (const storedToken of storedTokens) {
|
|
147
|
+
const matches = await bcrypt.compare(rt, storedToken.token);
|
|
148
|
+
if (matches) {
|
|
149
|
+
validToken = storedToken;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!validToken) {
|
|
155
|
+
throw new UnauthorizedException('Invalid refresh token');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Revoke old refresh token (token rotation)
|
|
159
|
+
await this.refreshTokenModel.updateOne(
|
|
160
|
+
{ _id: validToken._id },
|
|
161
|
+
{ isRevoked: true },
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Generate new tokens
|
|
165
|
+
const tokens = await this.generateTokens(user);
|
|
166
|
+
|
|
167
|
+
// Store new refresh token
|
|
168
|
+
await this.storeRefreshToken(
|
|
169
|
+
tokens.refreshToken,
|
|
170
|
+
user._id.toString(),
|
|
171
|
+
deviceInfo || 'Unknown Device',
|
|
172
|
+
ipAddress || 'Unknown IP',
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return tokens;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async getMe(userId: string) {
|
|
179
|
+
const user = await this.userModel.findById(userId);
|
|
180
|
+
if (!user) {
|
|
181
|
+
throw new NotFoundException('User not found');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
id: user._id.toString(),
|
|
186
|
+
fullName: user.fullName,
|
|
187
|
+
email: user.email,
|
|
188
|
+
role: user.role,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Helper Methods
|
|
193
|
+
|
|
194
|
+
async hashData(data: string): Promise<string> {
|
|
195
|
+
return bcrypt.hash(data, SALT_ROUNDS);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async generateTokens(user: UserDocument): Promise<{ accessToken: string; refreshToken: string }> {
|
|
199
|
+
const payload = {
|
|
200
|
+
sub: user._id.toString(),
|
|
201
|
+
email: user.email,
|
|
202
|
+
role: user.role,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const accessExpiry = (this.configService.get<string>('JWT_ACCESS_EXPIRY') || '15m') as any;
|
|
206
|
+
const refreshExpiry = (this.configService.get<string>('JWT_REFRESH_EXPIRY') || '7d') as any;
|
|
207
|
+
|
|
208
|
+
const [accessToken, refreshToken] = await Promise.all([
|
|
209
|
+
this.jwtService.signAsync(payload, {
|
|
210
|
+
secret: this.configService.get<string>('JWT_ACCESS_SECRET'),
|
|
211
|
+
expiresIn: accessExpiry,
|
|
212
|
+
}),
|
|
213
|
+
this.jwtService.signAsync(payload, {
|
|
214
|
+
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
|
|
215
|
+
expiresIn: refreshExpiry,
|
|
216
|
+
}),
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
return { accessToken, refreshToken };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private async storeRefreshToken(
|
|
223
|
+
token: string,
|
|
224
|
+
userId: string,
|
|
225
|
+
userAgent: string,
|
|
226
|
+
ipAddress: string,
|
|
227
|
+
) {
|
|
228
|
+
const expiresIn = this.configService.get<string>('JWT_REFRESH_EXPIRY') || '7d';
|
|
229
|
+
const expiresAt = this.calculateExpiry(expiresIn);
|
|
230
|
+
|
|
231
|
+
// Hash the refresh token before storing
|
|
232
|
+
const hashedToken = await this.hashData(token);
|
|
233
|
+
|
|
234
|
+
await this.refreshTokenModel.create({
|
|
235
|
+
token: hashedToken,
|
|
236
|
+
userId,
|
|
237
|
+
userAgent,
|
|
238
|
+
ipAddress,
|
|
239
|
+
expiresAt,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private calculateExpiry(expiresIn: string): Date {
|
|
244
|
+
const match = expiresIn.match(/^(\d+)([smhd])$/);
|
|
245
|
+
if (!match) {
|
|
246
|
+
return new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // Default 7 days
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const value = parseInt(match[1], 10);
|
|
250
|
+
const unit = match[2];
|
|
251
|
+
|
|
252
|
+
const multipliers: Record<string, number> = {
|
|
253
|
+
s: 1000,
|
|
254
|
+
m: 60 * 1000,
|
|
255
|
+
h: 60 * 60 * 1000,
|
|
256
|
+
d: 24 * 60 * 60 * 1000,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return new Date(Date.now() + value * multipliers[unit]);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private async cleanupExpiredTokens(userId: string) {
|
|
263
|
+
// Remove expired/revoked refresh tokens for this user
|
|
264
|
+
await this.refreshTokenModel.deleteMany({
|
|
265
|
+
userId,
|
|
266
|
+
$or: [
|
|
267
|
+
{ isRevoked: true },
|
|
268
|
+
{ expiresAt: { $lt: new Date() } },
|
|
269
|
+
],
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Controller, Get } from '@nestjs/common';
|
|
2
|
+
import { Connection } from 'mongoose';
|
|
3
|
+
import { InjectConnection } from '@nestjs/mongoose';
|
|
4
|
+
import { Public } from '../../common/decorators/public.decorator';
|
|
5
|
+
|
|
6
|
+
@Controller('health')
|
|
7
|
+
export class HealthController {
|
|
8
|
+
constructor(@InjectConnection() private connection: Connection) {}
|
|
9
|
+
|
|
10
|
+
@Public()
|
|
11
|
+
@Get()
|
|
12
|
+
async check() {
|
|
13
|
+
const dbStatus = this.connection.readyState === 1 ? 'healthy' : 'unhealthy';
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
status: dbStatus === 'healthy' ? 'ok' : 'error',
|
|
17
|
+
timestamp: new Date().toISOString(),
|
|
18
|
+
services: {
|
|
19
|
+
database: {
|
|
20
|
+
status: dbStatus,
|
|
21
|
+
type: 'mongodb',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IsEmail,
|
|
3
|
+
IsOptional,
|
|
4
|
+
IsString,
|
|
5
|
+
MaxLength,
|
|
6
|
+
MinLength,
|
|
7
|
+
} from 'class-validator';
|
|
8
|
+
|
|
9
|
+
export class UpdateProfileDto {
|
|
10
|
+
@IsOptional()
|
|
11
|
+
@IsString()
|
|
12
|
+
@MinLength(2)
|
|
13
|
+
@MaxLength(100)
|
|
14
|
+
fullName?: string;
|
|
15
|
+
|
|
16
|
+
@IsOptional()
|
|
17
|
+
@IsEmail()
|
|
18
|
+
email?: string;
|
|
19
|
+
|
|
20
|
+
@IsOptional()
|
|
21
|
+
@IsString()
|
|
22
|
+
@MinLength(6)
|
|
23
|
+
password?: string;
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { IsBoolean, IsEnum, IsOptional } from 'class-validator';
|
|
2
|
+
import { UpdateProfileDto } from './update-profile.dto';
|
|
3
|
+
import { UserRole } from '../../../schemas/user.schema';
|
|
4
|
+
|
|
5
|
+
export class UpdateUserDto extends UpdateProfileDto {
|
|
6
|
+
@IsOptional()
|
|
7
|
+
@IsEnum(UserRole, { message: 'Role must be a valid UserRole' })
|
|
8
|
+
role?: UserRole;
|
|
9
|
+
|
|
10
|
+
@IsOptional()
|
|
11
|
+
@IsBoolean()
|
|
12
|
+
isActive?: boolean;
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { MongooseModule } from '@nestjs/mongoose';
|
|
3
|
+
import { UsersController } from './users.controller';
|
|
4
|
+
import { UsersService } from './users.service';
|
|
5
|
+
import { User, UserSchema } from '../../schemas/user.schema';
|
|
6
|
+
import { RefreshToken, RefreshTokenSchema } from '../../schemas/refresh-token.schema';
|
|
7
|
+
|
|
8
|
+
@Module({
|
|
9
|
+
imports: [
|
|
10
|
+
MongooseModule.forFeature([
|
|
11
|
+
{ name: User.name, schema: UserSchema },
|
|
12
|
+
{ name: RefreshToken.name, schema: RefreshTokenSchema },
|
|
13
|
+
]),
|
|
14
|
+
],
|
|
15
|
+
controllers: [UsersController],
|
|
16
|
+
providers: [UsersService],
|
|
17
|
+
exports: [UsersService],
|
|
18
|
+
})
|
|
19
|
+
export class UsersModule {}
|