first commit

This commit is contained in:
alexandrump
2026-02-09 01:02:53 +01:00
commit 82f3464565
90 changed files with 4788 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AdminAuthGuard implements CanActivate {
constructor(private readonly jwt: JwtService, private readonly config: ConfigService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
const authHeader = req.headers['authorization'];
const apiKeyHeader = req.headers['x-api-key'];
const envKey = this.config.get('API_ADMIN_KEY') || process.env.API_ADMIN_KEY || 'dev-admin-key';
let token: string | undefined;
if (authHeader) {
const raw = Array.isArray(authHeader) ? authHeader[0] : authHeader;
if (typeof raw === 'string' && raw.startsWith('Bearer ')) token = raw.slice(7).trim();
else if (typeof raw === 'string' && !raw.startsWith('Bearer ')) token = raw.trim();
}
if (token) {
try {
const secret = this.config.get('JWT_SECRET') || process.env.JWT_SECRET || 'dev-secret';
const payload: any = this.jwt.verify(token, { secret });
if (payload && payload.role === 'admin') {
req.user = { id: payload.sub, email: payload.email, role: payload.role };
return true;
}
} catch (e) {
// fall through to api key check
}
}
// fallback to API key header (x-api-key) or plain authorization header value
let provided: string | undefined;
if (apiKeyHeader) provided = Array.isArray(apiKeyHeader) ? apiKeyHeader[0] : apiKeyHeader;
if (!provided && authHeader && typeof authHeader === 'string' && !authHeader.startsWith('Bearer ')) provided = authHeader;
if (provided && provided === envKey) return true;
throw new UnauthorizedException('Invalid admin credentials');
}
}

View File

@@ -0,0 +1,23 @@
import { Controller, Post, Body, BadRequestException } from '@nestjs/common';
import { AuthService } from './auth.service';
class RegisterDto { email: string; password: string }
class LoginDto { email: string; password: string }
@Controller('auth')
export class AuthController {
constructor(private readonly auth: AuthService) {}
@Post('register')
async register(@Body() body: RegisterDto) {
if (!body.email || !body.password) throw new BadRequestException('email and password required');
return this.auth.register(body.email, body.password);
}
@Post('login')
async login(@Body() body: LoginDto) {
const user = await this.auth.validateUser(body.email, body.password);
if (!user) throw new BadRequestException('invalid credentials');
return this.auth.login(user);
}
}

View File

@@ -0,0 +1,31 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UserEntity } from './user.entity';
import { JwtStrategy } from './jwt.strategy';
import { PlansModule } from '../plans/plans.module';
@Module({
imports: [
ConfigModule,
TypeOrmModule.forFeature([UserEntity]),
PlansModule,
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
secret: cfg.get('JWT_SECRET') || 'dev-secret',
signOptions: { expiresIn: '7d' },
}),
}),
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
exports: [AuthService, JwtModule],
})
export class AuthModule {}

View File

@@ -0,0 +1,46 @@
import { Injectable, ConflictException, UnauthorizedException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserEntity } from './user.entity';
import * as bcrypt from 'bcrypt';
import { JwtService } from '@nestjs/jwt';
import { PlansService } from '../plans/plans.service';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(UserEntity) private readonly users: Repository<UserEntity>,
private readonly jwtService: JwtService,
private readonly plans: PlansService,
) {}
async register(email: string, password: string, role = 'user') {
const existing = await this.users.findOne({ where: { email } });
if (existing) throw new ConflictException('Email already registered');
const hash = await bcrypt.hash(password, 10);
const defaultPlan = await this.plans.getDefaultPlan();
const user = this.users.create({ email, passwordHash: hash, role, planId: defaultPlan?.id });
await this.users.save(user);
return { id: user.id, email: user.email, role: user.role };
}
async validateUser(email: string, password: string) {
const user = await this.users.findOne({
where: { email },
select: ['id', 'email', 'passwordHash', 'role', 'planId'],
});
if (!user) return null;
const ok = await bcrypt.compare(password, user.passwordHash);
if (!ok) return null;
return user;
}
async login(user: UserEntity) {
const payload = { sub: user.id, email: user.email, role: user.role };
return { access_token: this.jwtService.sign(payload) };
}
async findById(id: string) {
return this.users.findOne({ where: { id } });
}
}

View File

@@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.get('JWT_SECRET') || 'dev-secret',
});
}
async validate(payload: any) {
return { userId: payload.sub, email: payload.email, role: payload.role };
}
}

View File

@@ -0,0 +1,30 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { PlanEntity } from '../entities/plan.entity';
@Entity('users')
export class UserEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column({ nullable: false, select: false })
passwordHash: string;
@Column({ default: 'user' })
role: string; // 'user' | 'admin'
@Column({ type: 'uuid', nullable: true })
planId?: string | null;
@ManyToOne(() => PlanEntity, { nullable: true })
@JoinColumn({ name: 'planId' })
plan?: PlanEntity | null;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}