Update codebase
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import { Controller, Post, Body, Headers, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('send-otp')
|
||||
async sendOtp(
|
||||
@Headers('x-tenant-id') tenantId: string,
|
||||
@Body('phone') phone: string,
|
||||
) {
|
||||
if (!tenantId) throw new UnauthorizedException('Tenant ID (x-tenant-id) is required');
|
||||
return this.authService.sendOtp(tenantId, phone);
|
||||
}
|
||||
|
||||
@Post('verify-otp')
|
||||
async verifyOtp(
|
||||
@Headers('x-tenant-id') tenantId: string,
|
||||
@Body('phone') phone: string,
|
||||
@Body('code') code: string,
|
||||
) {
|
||||
if (!tenantId) throw new UnauthorizedException('Tenant ID (x-tenant-id) is required');
|
||||
return this.authService.verifyOtp(tenantId, phone, code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
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 { UsersModule } from '../users/users.module';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UsersModule,
|
||||
PassportModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get<string>('JWT_SECRET') || 'change_me_jwt_secret',
|
||||
signOptions: {
|
||||
expiresIn: configService.get<string>('JWT_EXPIRES') || '15m',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private readonly logger = new Logger('Auth');
|
||||
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private jwtService: JwtService,
|
||||
private config: ConfigService,
|
||||
) {}
|
||||
|
||||
private get devCode(): string {
|
||||
// رمز التطوير الثابت — يُستبدل بمحوّل SMS في P2 (راجع docs/07).
|
||||
return '1234';
|
||||
}
|
||||
|
||||
async sendOtp(tenantId: string, phone: string) {
|
||||
// وضع تطوير: الرمز ثابت ويُطبع باللوغ بلا مزوّد SMS.
|
||||
this.logger.log(`OTP for tenant=${tenantId} phone=${phone} => ${this.devCode} (dev)`);
|
||||
return {
|
||||
success: true,
|
||||
message: 'OTP sent',
|
||||
dev_code: this.config.get('auth.otpDevMode') ? this.devCode : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async verifyOtp(tenantId: string, phone: string, code: string) {
|
||||
if (code !== this.devCode) {
|
||||
throw new UnauthorizedException('Invalid OTP code');
|
||||
}
|
||||
|
||||
let user = await this.usersService.findByPhone(tenantId, phone);
|
||||
if (!user) {
|
||||
user = await this.usersService.create(tenantId, phone);
|
||||
}
|
||||
|
||||
return this.issueTokens(user, tenantId);
|
||||
}
|
||||
|
||||
async refresh(refreshToken: string) {
|
||||
let payload: any;
|
||||
try {
|
||||
payload = this.jwtService.verify(refreshToken);
|
||||
} catch {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
if (payload.type !== 'refresh') {
|
||||
throw new UnauthorizedException('Not a refresh token');
|
||||
}
|
||||
const user = await this.usersService.findById(payload.tenant_id, payload.sub);
|
||||
if (!user) throw new UnauthorizedException('User not found');
|
||||
return this.issueTokens(user, user.tenant_id);
|
||||
}
|
||||
|
||||
/** يصدر access + refresh معاً. */
|
||||
private issueTokens(user: User, tenantId: string) {
|
||||
const base = {
|
||||
sub: user.id,
|
||||
phone: user.phone,
|
||||
role: user.role,
|
||||
tenant_id: tenantId,
|
||||
};
|
||||
return {
|
||||
access_token: this.jwtService.sign(base),
|
||||
refresh_token: this.jwtService.sign(
|
||||
{ ...base, type: 'refresh' },
|
||||
{ expiresIn: this.config.get<string>('jwt.refreshExpires') ?? '30d' },
|
||||
),
|
||||
user,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (!requiredRoles) {
|
||||
return true;
|
||||
}
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
return requiredRoles.some((role) => user?.role === role);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(configService: ConfigService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.get<string>('JWT_SECRET') || 'change_me_jwt_secret',
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: any) {
|
||||
return {
|
||||
userId: payload.sub,
|
||||
phone: payload.phone,
|
||||
role: payload.role,
|
||||
tenantId: payload.tenant_id
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
export enum UserRole {
|
||||
RIDER = 'rider',
|
||||
DRIVER = 'driver',
|
||||
DISPATCHER = 'dispatcher',
|
||||
ADMIN = 'admin',
|
||||
}
|
||||
|
||||
@Entity('users')
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'uuid' })
|
||||
tenant_id: string;
|
||||
|
||||
@Column()
|
||||
phone: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
name: string;
|
||||
|
||||
// varchar (لا enum) لمطابقة الهجرة وتفادي إنشاء نوع enum في القاعدة.
|
||||
@Column({ type: 'varchar', default: UserRole.RIDER })
|
||||
role: UserRole;
|
||||
|
||||
@Column({ default: 'active' })
|
||||
status: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { User } from './entities/user.entity';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { User, UserRole } from './entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
async findByPhone(tenantId: string, phone: string): Promise<User | null> {
|
||||
return this.userRepository.findOne({ where: { tenant_id: tenantId, phone } });
|
||||
}
|
||||
|
||||
async findById(tenantId: string, id: string): Promise<User | null> {
|
||||
return this.userRepository.findOne({ where: { tenant_id: tenantId, id } });
|
||||
}
|
||||
|
||||
async create(tenantId: string, phone: string, role: string = 'rider'): Promise<User> {
|
||||
const user = this.userRepository.create({ tenant_id: tenantId, phone, role: role as UserRole });
|
||||
return this.userRepository.save(user);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user