2026-02-07 10:23:29 -04:00
|
|
|
import { Injectable, UnauthorizedException, Logger } from '@nestjs/common';
|
|
|
|
|
import { JwtService } from '@nestjs/jwt';
|
|
|
|
|
import { DepartmentsService } from '../departments/departments.service';
|
|
|
|
|
import { ApplicantsService } from '../applicants/applicants.service';
|
|
|
|
|
import { UsersService } from '../users/users.service';
|
|
|
|
|
import { HashUtil } from '../../common/utils';
|
|
|
|
|
import { UserRole } from '../../common/enums';
|
|
|
|
|
import { ERROR_CODES } from '../../common/constants';
|
|
|
|
|
import { JwtPayload } from '../../common/interfaces/request-context.interface';
|
|
|
|
|
import { LoginDto, DigiLockerLoginDto, EmailPasswordLoginDto } from './dto';
|
|
|
|
|
import * as bcrypt from 'bcrypt';
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class AuthService {
|
|
|
|
|
private readonly logger = new Logger(AuthService.name);
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly jwtService: JwtService,
|
|
|
|
|
private readonly departmentsService: DepartmentsService,
|
|
|
|
|
private readonly applicantsService: ApplicantsService,
|
|
|
|
|
private readonly usersService: UsersService,
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate department API key and return JWT token
|
|
|
|
|
*/
|
|
|
|
|
async validateDepartmentApiKey(
|
|
|
|
|
apiKey: string,
|
|
|
|
|
departmentCode: string,
|
|
|
|
|
): Promise<{ accessToken: string; department: { id: string; code: string; name: string } }> {
|
|
|
|
|
const department = await this.departmentsService.findByCode(departmentCode);
|
|
|
|
|
|
|
|
|
|
if (!department) {
|
|
|
|
|
throw new UnauthorizedException({
|
|
|
|
|
code: ERROR_CODES.INVALID_API_KEY,
|
|
|
|
|
message: 'Invalid department code',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!department.isActive) {
|
|
|
|
|
throw new UnauthorizedException({
|
|
|
|
|
code: ERROR_CODES.INVALID_API_KEY,
|
|
|
|
|
message: 'Department is inactive',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isValidApiKey = await HashUtil.comparePassword(apiKey, department.apiKeyHash || '');
|
|
|
|
|
if (!isValidApiKey) {
|
|
|
|
|
throw new UnauthorizedException({
|
|
|
|
|
code: ERROR_CODES.INVALID_API_KEY,
|
|
|
|
|
message: 'Invalid API key',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const payload: JwtPayload = {
|
|
|
|
|
sub: department.id,
|
|
|
|
|
role: UserRole.DEPARTMENT,
|
|
|
|
|
departmentCode: department.code,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
accessToken: this.jwtService.sign(payload),
|
|
|
|
|
department: {
|
|
|
|
|
id: department.id,
|
|
|
|
|
code: department.code,
|
|
|
|
|
name: department.name,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mock DigiLocker login (for POC)
|
|
|
|
|
* In production, this would integrate with actual DigiLocker OAuth
|
|
|
|
|
*/
|
|
|
|
|
async digiLockerLogin(
|
|
|
|
|
dto: DigiLockerLoginDto,
|
|
|
|
|
): Promise<{ accessToken: string; applicant: { id: string; name: string; email: string } }> {
|
|
|
|
|
let applicant = await this.applicantsService.findByDigilockerId(dto.digilockerId);
|
|
|
|
|
|
|
|
|
|
// Auto-create applicant if not exists (mock behavior)
|
|
|
|
|
if (!applicant) {
|
|
|
|
|
this.logger.log(`Creating new applicant for DigiLocker ID: ${dto.digilockerId}`);
|
|
|
|
|
applicant = await this.applicantsService.create({
|
|
|
|
|
digilockerId: dto.digilockerId,
|
|
|
|
|
name: dto.name || 'DigiLocker User',
|
|
|
|
|
email: dto.email || `${dto.digilockerId}@digilocker.gov.in`,
|
|
|
|
|
phone: dto.phone,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!applicant.isActive) {
|
|
|
|
|
throw new UnauthorizedException({
|
|
|
|
|
code: ERROR_CODES.UNAUTHORIZED,
|
|
|
|
|
message: 'Applicant account is inactive',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const payload: JwtPayload = {
|
|
|
|
|
sub: applicant.id,
|
|
|
|
|
email: applicant.email,
|
|
|
|
|
role: UserRole.APPLICANT,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
accessToken: this.jwtService.sign(payload),
|
|
|
|
|
applicant: {
|
|
|
|
|
id: applicant.id,
|
|
|
|
|
name: applicant.name,
|
|
|
|
|
email: applicant.email,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate JWT token and return payload
|
|
|
|
|
*/
|
|
|
|
|
async validateJwtPayload(payload: JwtPayload): Promise<JwtPayload> {
|
|
|
|
|
if (payload.role === UserRole.DEPARTMENT && payload.departmentCode) {
|
|
|
|
|
const department = await this.departmentsService.findByCode(payload.departmentCode);
|
|
|
|
|
if (!department || !department.isActive) {
|
|
|
|
|
throw new UnauthorizedException({
|
|
|
|
|
code: ERROR_CODES.INVALID_TOKEN,
|
|
|
|
|
message: 'Department no longer valid',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else if (payload.role === UserRole.APPLICANT) {
|
|
|
|
|
const applicant = await this.applicantsService.findById(payload.sub);
|
|
|
|
|
if (!applicant || !applicant.isActive) {
|
|
|
|
|
throw new UnauthorizedException({
|
|
|
|
|
code: ERROR_CODES.INVALID_TOKEN,
|
|
|
|
|
message: 'Applicant account no longer valid',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return payload;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Email/Password login for all user types (Admin, Department, Citizen)
|
|
|
|
|
*/
|
2026-02-08 18:44:05 -04:00
|
|
|
async emailPasswordLogin(dto: EmailPasswordLoginDto): Promise<{
|
2026-02-07 10:23:29 -04:00
|
|
|
accessToken: string;
|
|
|
|
|
user: {
|
|
|
|
|
id: string;
|
|
|
|
|
email: string;
|
|
|
|
|
name: string;
|
|
|
|
|
role: string;
|
|
|
|
|
walletAddress: string;
|
|
|
|
|
departmentId?: string;
|
|
|
|
|
};
|
|
|
|
|
}> {
|
|
|
|
|
const user = await this.usersService.findByEmailWithDepartment(dto.email);
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new UnauthorizedException({
|
|
|
|
|
code: ERROR_CODES.INVALID_CREDENTIALS,
|
|
|
|
|
message: 'Invalid email or password',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!user.isActive) {
|
|
|
|
|
throw new UnauthorizedException({
|
|
|
|
|
code: ERROR_CODES.UNAUTHORIZED,
|
|
|
|
|
message: 'User account is inactive',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isValidPassword = await bcrypt.compare(dto.password, user.passwordHash);
|
|
|
|
|
if (!isValidPassword) {
|
|
|
|
|
throw new UnauthorizedException({
|
|
|
|
|
code: ERROR_CODES.INVALID_CREDENTIALS,
|
|
|
|
|
message: 'Invalid email or password',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update last login
|
|
|
|
|
await this.usersService.updateLastLogin(user.id);
|
|
|
|
|
|
|
|
|
|
const payload: JwtPayload = {
|
|
|
|
|
sub: user.id,
|
|
|
|
|
email: user.email,
|
|
|
|
|
role: user.role as UserRole,
|
|
|
|
|
departmentCode: (user as any).department?.code,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
accessToken: this.jwtService.sign(payload),
|
|
|
|
|
user: {
|
|
|
|
|
id: user.id,
|
|
|
|
|
email: user.email,
|
|
|
|
|
name: user.name,
|
|
|
|
|
role: user.role,
|
|
|
|
|
walletAddress: user.walletAddress || '',
|
|
|
|
|
departmentId: user.departmentId,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate admin token (for internal use)
|
|
|
|
|
*/
|
|
|
|
|
generateAdminToken(adminId: string): string {
|
|
|
|
|
const payload: JwtPayload = {
|
|
|
|
|
sub: adminId,
|
|
|
|
|
role: UserRole.ADMIN,
|
|
|
|
|
};
|
|
|
|
|
return this.jwtService.sign(payload);
|
|
|
|
|
}
|
|
|
|
|
}
|