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:
Mahi
2026-02-07 10:23:29 -04:00
commit 80566bf0a2
441 changed files with 102418 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams, HttpHeaders, HttpEvent, HttpEventType, HttpRequest } from '@angular/common/http';
import { Observable, map, filter } from 'rxjs';
import { environment } from '../../../environments/environment';
export interface UploadProgress<T> {
progress: number;
loaded: number;
total: number;
complete: boolean;
response?: T;
}
export interface ApiResponse<T> {
success: boolean;
data: T;
timestamp: string;
message?: string;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
hasNextPage: boolean;
}
@Injectable({
providedIn: 'root',
})
export class ApiService {
private readonly http = inject(HttpClient);
private readonly baseUrl = environment.apiBaseUrl;
get<T>(path: string, params?: Record<string, string | number | boolean>): Observable<T> {
let httpParams = new HttpParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
httpParams = httpParams.set(key, String(value));
}
});
}
return this.http
.get<ApiResponse<T>>(`${this.baseUrl}${path}`, { params: httpParams })
.pipe(map((response) => response.data));
}
getRaw<T>(path: string, params?: Record<string, string | number | boolean>): Observable<T> {
let httpParams = new HttpParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
httpParams = httpParams.set(key, String(value));
}
});
}
return this.http.get<T>(`${this.baseUrl}${path}`, { params: httpParams });
}
post<T>(path: string, body: unknown): Observable<T> {
return this.http
.post<ApiResponse<T>>(`${this.baseUrl}${path}`, body)
.pipe(map((response) => response.data));
}
postRaw<T>(path: string, body: unknown): Observable<T> {
return this.http.post<T>(`${this.baseUrl}${path}`, body);
}
put<T>(path: string, body: unknown): Observable<T> {
return this.http
.put<ApiResponse<T>>(`${this.baseUrl}${path}`, body)
.pipe(map((response) => response.data));
}
patch<T>(path: string, body: unknown): Observable<T> {
return this.http
.patch<ApiResponse<T>>(`${this.baseUrl}${path}`, body)
.pipe(map((response) => response.data));
}
delete<T>(path: string): Observable<T> {
return this.http
.delete<ApiResponse<T>>(`${this.baseUrl}${path}`)
.pipe(map((response) => response.data));
}
upload<T>(path: string, formData: FormData): Observable<T> {
return this.http
.post<ApiResponse<T>>(`${this.baseUrl}${path}`, formData)
.pipe(map((response) => response.data));
}
/**
* Upload with progress tracking
* Returns an observable that emits upload progress and final response
*/
uploadWithProgress<T>(path: string, formData: FormData): Observable<UploadProgress<T>> {
const req = new HttpRequest('POST', `${this.baseUrl}${path}`, formData, {
reportProgress: true,
});
return this.http.request<ApiResponse<T>>(req).pipe(
map((event: HttpEvent<ApiResponse<T>>) => {
switch (event.type) {
case HttpEventType.UploadProgress:
const total = event.total || 0;
const loaded = event.loaded;
const progress = total > 0 ? Math.round((loaded / total) * 100) : 0;
return {
progress,
loaded,
total,
complete: false,
} as UploadProgress<T>;
case HttpEventType.Response:
return {
progress: 100,
loaded: event.body?.data ? 1 : 0,
total: 1,
complete: true,
response: event.body?.data,
} as UploadProgress<T>;
default:
return {
progress: 0,
loaded: 0,
total: 0,
complete: false,
} as UploadProgress<T>;
}
})
);
}
download(path: string): Observable<Blob> {
return this.http.get(`${this.baseUrl}${path}`, {
responseType: 'blob',
});
}
getBlob(url: string): Observable<Blob> {
return this.http.get(url, { responseType: 'blob' });
}
}