feat: Goa GEL Blockchain e-Licensing Platform - Full Stack Implementation
Complete implementation of the Goa Government e-Licensing platform with: Backend: - NestJS API with JWT authentication - PostgreSQL database with Knex ORM - Redis caching and session management - MinIO document storage - Hyperledger Besu blockchain integration - Multi-department workflow system - Comprehensive API tests (266/282 passing) Frontend: - Angular 21 with standalone components - Angular Material + TailwindCSS UI - Visual workflow builder - Document upload with progress tracking - Blockchain explorer integration - Role-based dashboards (Admin, Department, Citizen) - E2E tests with Playwright (37 tests) Infrastructure: - Docker Compose orchestration - Blockscout blockchain explorer - Development and production configurations
This commit is contained in:
212
backend/src/modules/auth/auth.service.ts
Normal file
212
backend/src/modules/auth/auth.service.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user