first commit
This commit is contained in:
44
apps/api/src/auth/admin.guard.ts
Normal file
44
apps/api/src/auth/admin.guard.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
23
apps/api/src/auth/auth.controller.ts
Normal file
23
apps/api/src/auth/auth.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
31
apps/api/src/auth/auth.module.ts
Normal file
31
apps/api/src/auth/auth.module.ts
Normal 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 {}
|
||||
46
apps/api/src/auth/auth.service.ts
Normal file
46
apps/api/src/auth/auth.service.ts
Normal 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 } });
|
||||
}
|
||||
}
|
||||
19
apps/api/src/auth/jwt.strategy.ts
Normal file
19
apps/api/src/auth/jwt.strategy.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
30
apps/api/src/auth/user.entity.ts
Normal file
30
apps/api/src/auth/user.entity.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user