Files
Goa-gel-fullstack/backend/src/modules/auth/auth.service.ts
Mahi d9de183e51 feat: Runtime configuration and Docker deployment improvements
Frontend:
- Add runtime configuration service for deployment-time API URL injection
- Create docker-entrypoint.sh to generate config.json from environment variables
- Update ApiService, ApprovalService, and DocumentViewer to use RuntimeConfigService
- Add APP_INITIALIZER to load runtime config before app starts

Backend:
- Fix init-blockchain.js to properly quote mnemonic phrases in .env file
- Improve docker-entrypoint.sh with health checks and better error handling

Docker:
- Add API_BASE_URL environment variable to frontend container
- Update docker-compose.yml with clear documentation for remote deployment
- Reorganize .env.example with clear categories (REQUIRED FOR REMOTE, PRODUCTION, AUTO-GENERATED)

Workflow fixes:
- Fix DepartmentApproval interface to match backend schema
- Fix stage transformation for 0-indexed stageOrder
- Fix workflow list to show correct stage count from definition.stages

Cleanup:
- Move development artifacts to .trash directory
- Remove root-level package.json (was only for utility scripts)
- Add .trash/ to .gitignore
2026-02-08 18:45:01 -04:00

211 lines
6.0 KiB
TypeScript

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)
*/
async emailPasswordLogin(dto: EmailPasswordLoginDto): Promise<{
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);
}
}