feat: implement naming convention, deployment automation, and infrastructure updates

- Add comprehensive naming convention (provider-region-resource-env-purpose)
- Implement Terraform locals for centralized naming
- Update all Terraform resources to use new naming convention
- Create deployment automation framework (18 phase scripts)
- Add Azure setup scripts (provider registration, quota checks)
- Update deployment scripts config with naming functions
- Create complete deployment documentation (guide, steps, quick reference)
- Add frontend portal implementations (public and internal)
- Add UI component library (18 components)
- Enhance Entra VerifiedID integration with file utilities
- Add API client package for all services
- Create comprehensive documentation (naming, deployment, next steps)

Infrastructure:
- Resource groups, storage accounts with new naming
- Terraform configuration updates
- Outputs with naming convention examples

Deployment:
- Automated deployment scripts for all 15 phases
- State management and logging
- Error handling and validation

Documentation:
- Naming convention guide and implementation summary
- Complete deployment guide (296 steps)
- Next steps and quick start guides
- Azure prerequisites and setup completion docs

Note: ESLint warnings present - will be addressed in follow-up commit
This commit is contained in:
defiQUG
2025-11-12 08:22:51 -08:00
parent 9e46f3f316
commit 8649ad4124
136 changed files with 17251 additions and 147 deletions

View File

@@ -0,0 +1,23 @@
{
"name": "@the-order/api-client",
"version": "0.1.0",
"private": true,
"description": "API client library for The Order services",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"lint": "eslint src --ext .ts",
"type-check": "tsc --noEmit"
},
"dependencies": {
"axios": "^1.6.2",
"@the-order/schemas": "workspace:*"
},
"devDependencies": {
"@types/node": "^20.10.6",
"typescript": "^5.3.3"
}
}

View File

@@ -0,0 +1,53 @@
import { IdentityClient } from './identity';
import { EResidencyClient } from './eresidency';
import { IntakeClient } from './intake';
import { FinanceClient } from './finance';
import { DataroomClient } from './dataroom';
export class ApiClient {
public readonly identity: IdentityClient;
public readonly eresidency: EResidencyClient;
public readonly intake: IntakeClient;
public readonly finance: FinanceClient;
public readonly dataroom: DataroomClient;
constructor(baseURL?: string) {
// Initialize service clients - each manages its own axios instance
this.identity = new IdentityClient();
this.eresidency = new EResidencyClient();
this.intake = new IntakeClient();
this.finance = new FinanceClient();
this.dataroom = new DataroomClient();
}
setAuthToken(token: string): void {
if (typeof window !== 'undefined') {
localStorage.setItem('auth_token', token);
}
// Update all service clients
this.identity.setAuthToken(token);
this.eresidency.setAuthToken(token);
this.intake.setAuthToken(token);
this.finance.setAuthToken(token);
this.dataroom.setAuthToken(token);
}
clearAuth(): void {
// Clear tokens in all service clients
this.identity.clearAuthToken();
this.eresidency.clearAuthToken();
this.intake.clearAuthToken();
this.finance.clearAuthToken();
this.dataroom.clearAuthToken();
}
}
// Singleton instance
let apiClientInstance: ApiClient | null = null;
export function getApiClient(): ApiClient {
if (!apiClientInstance) {
apiClientInstance = new ApiClient();
}
return apiClientInstance;
}

View File

@@ -0,0 +1,140 @@
import axios, { AxiosInstance } from 'axios';
export interface DealRoom {
id: string;
name: string;
description?: string;
status: 'active' | 'archived' | 'closed';
createdAt: string;
updatedAt: string;
participants: string[];
documents: string[];
}
export interface Document {
id: string;
roomId: string;
fileName: string;
fileSize: number;
uploadedBy: string;
uploadedAt: string;
category?: string;
tags?: string[];
}
export class DataroomClient {
protected client: AxiosInstance;
constructor(baseURL?: string) {
const apiBaseURL =
baseURL ||
(typeof window !== 'undefined'
? process.env.NEXT_PUBLIC_DATAROOM_SERVICE_URL || 'http://localhost:4006'
: 'http://localhost:4006');
this.client = axios.create({
baseURL: apiBaseURL,
headers: {
'Content-Type': 'application/json',
},
});
// Set up request interceptor for authentication
this.client.interceptors.request.use(
(config) => {
const token = this.getAuthToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
}
private getAuthToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('auth_token');
}
setAuthToken(token: string): void {
if (typeof window !== 'undefined') {
localStorage.setItem('auth_token', token);
}
}
clearAuthToken(): void {
if (typeof window !== 'undefined') {
localStorage.removeItem('auth_token');
}
}
async createDealRoom(data: {
name: string;
description?: string;
participants?: string[];
}): Promise<DealRoom> {
const response = await this.client.post<DealRoom>('/api/v1/deal-rooms', data);
return response.data;
}
async getDealRoom(roomId: string): Promise<DealRoom> {
const response = await this.client.get<DealRoom>(`/api/v1/deal-rooms/${roomId}`);
return response.data;
}
async listDealRooms(filters?: {
status?: string;
participantId?: string;
page?: number;
pageSize?: number;
}): Promise<{ rooms: DealRoom[]; total: number }> {
const response = await this.client.get<{ rooms: DealRoom[]; total: number }>(
'/api/v1/deal-rooms',
{ params: filters }
);
return response.data;
}
async updateDealRoom(roomId: string, data: Partial<DealRoom>): Promise<DealRoom> {
const response = await this.client.patch<DealRoom>(`/api/v1/deal-rooms/${roomId}`, data);
return response.data;
}
async uploadDocument(roomId: string, file: File | Blob, metadata?: {
category?: string;
tags?: string[];
}): Promise<Document> {
const formData = new FormData();
formData.append('file', file);
if (metadata) {
formData.append('category', metadata.category || '');
if (metadata.tags) {
formData.append('tags', JSON.stringify(metadata.tags));
}
}
const response = await this.client.post<Document>(
`/api/v1/deal-rooms/${roomId}/documents`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
return response.data;
}
async listDocuments(roomId: string): Promise<Document[]> {
const response = await this.client.get<Document[]>(
`/api/v1/deal-rooms/${roomId}/documents`
);
return response.data;
}
async deleteDocument(roomId: string, documentId: string): Promise<void> {
await this.client.delete(`/api/v1/deal-rooms/${roomId}/documents/${documentId}`);
}
}

View File

@@ -0,0 +1,89 @@
import { ApiClient } from './client';
import type { eResidencyApplication, ApplicationStatus } from '@the-order/schemas';
export interface SubmitApplicationRequest {
email: string;
givenName: string;
familyName: string;
dateOfBirth?: string;
nationality?: string;
phone?: string;
address?: {
street?: string;
city?: string;
region?: string;
postalCode?: string;
country?: string;
};
identityDocument?: {
type: 'passport' | 'national_id' | 'drivers_license';
number: string;
issuingCountry: string;
expiryDate?: string;
documentHash?: string;
};
selfieLiveness?: {
imageHash: string;
livenessScore: number;
verifiedAt: string;
};
}
export interface AdjudicateRequest {
decision: 'approve' | 'reject';
reason?: string;
notes?: string;
}
export class EResidencyClient {
constructor(private client: ApiClient) {}
async submitApplication(request: SubmitApplicationRequest) {
return this.client.post<eResidencyApplication>('/applications', request);
}
async getApplication(id: string) {
return this.client.get<eResidencyApplication>(`/applications/${id}`);
}
async getReviewQueue(filters?: {
riskBand?: 'low' | 'medium' | 'high';
status?: ApplicationStatus;
assignedTo?: string;
limit?: number;
offset?: number;
}) {
const params = new URLSearchParams();
if (filters?.riskBand) params.append('riskBand', filters.riskBand);
if (filters?.status) params.append('status', filters.status);
if (filters?.assignedTo) params.append('assignedTo', filters.assignedTo);
if (filters?.limit) params.append('limit', filters.limit.toString());
if (filters?.offset) params.append('offset', filters.offset.toString());
return this.client.get<{
applications: eResidencyApplication[];
total: number;
}>(`/review/queue?${params.toString()}`);
}
async getApplicationForReview(id: string) {
return this.client.get<eResidencyApplication>(`/review/applications/${id}`);
}
async adjudicateApplication(id: string, request: AdjudicateRequest) {
return this.client.post<eResidencyApplication>(`/review/applications/${id}/adjudicate`, request);
}
async revokeCredential(residentNumber: string, reason: string) {
return this.client.post('/applications/revoke', { residentNumber, reason });
}
async getStatus() {
return this.client.get<Array<{
residentNumber: string;
status: string;
issuedAt?: string;
revokedAt?: string;
}>>('/status');
}
}

View File

@@ -0,0 +1,111 @@
import axios, { AxiosInstance } from 'axios';
export interface Payment {
id: string;
amount: number;
currency: string;
status: 'pending' | 'completed' | 'failed' | 'refunded';
paymentMethod: string;
createdAt: string;
description?: string;
}
export interface LedgerEntry {
id: string;
accountId: string;
amount: number;
currency: string;
type: 'debit' | 'credit';
description: string;
timestamp: string;
reference?: string;
}
export class FinanceClient {
protected client: AxiosInstance;
constructor(baseURL?: string) {
const apiBaseURL =
baseURL ||
(typeof window !== 'undefined'
? process.env.NEXT_PUBLIC_FINANCE_SERVICE_URL || 'http://localhost:4005'
: 'http://localhost:4005');
this.client = axios.create({
baseURL: apiBaseURL,
headers: {
'Content-Type': 'application/json',
},
});
// Set up request interceptor for authentication
this.client.interceptors.request.use(
(config) => {
const token = this.getAuthToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
}
private getAuthToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('auth_token');
}
setAuthToken(token: string): void {
if (typeof window !== 'undefined') {
localStorage.setItem('auth_token', token);
}
}
clearAuthToken(): void {
if (typeof window !== 'undefined') {
localStorage.removeItem('auth_token');
}
}
async createPayment(data: {
amount: number;
currency: string;
paymentMethod: string;
description?: string;
}): Promise<Payment> {
const response = await this.client.post<Payment>('/api/v1/payments', data);
return response.data;
}
async getPayment(paymentId: string): Promise<Payment> {
const response = await this.client.get<Payment>(`/api/v1/payments/${paymentId}`);
return response.data;
}
async listPayments(filters?: {
status?: string;
page?: number;
pageSize?: number;
}): Promise<{ payments: Payment[]; total: number }> {
const response = await this.client.get<{ payments: Payment[]; total: number }>('/api/v1/payments', {
params: filters,
});
return response.data;
}
async getLedgerEntries(filters?: {
accountId?: string;
type?: 'debit' | 'credit';
startDate?: string;
endDate?: string;
page?: number;
pageSize?: number;
}): Promise<{ entries: LedgerEntry[]; total: number }> {
const response = await this.client.get<{ entries: LedgerEntry[]; total: number }>(
'/api/v1/ledger',
{ params: filters }
);
return response.data;
}
}

View File

@@ -0,0 +1,114 @@
import { ApiClient } from './client';
import type { eResidentCredential, eCitizenCredential } from '@the-order/schemas';
export interface IssueVCRequest {
subject: string;
credentialSubject: Record<string, unknown>;
expirationDate?: string;
}
export interface VerifyVCRequest {
credential: {
id: string;
proof?: {
jws: string;
verificationMethod: string;
};
};
}
export interface BatchIssuanceRequest {
credentials: Array<{
subject: string;
credentialSubject: Record<string, unknown>;
expirationDate?: string;
}>;
}
export interface CredentialMetrics {
issuedToday: number;
issuedThisWeek: number;
issuedThisMonth: number;
issuedThisYear: number;
successRate: number;
failureRate: number;
totalIssuances: number;
totalFailures: number;
averageIssuanceTime: number;
p50IssuanceTime: number;
p95IssuanceTime: number;
p99IssuanceTime: number;
byCredentialType: Record<string, number>;
byAction: Record<string, number>;
recentIssuances: Array<{
credentialId: string;
credentialType: string[];
issuedAt: Date;
subjectDid: string;
}>;
}
export class IdentityClient {
constructor(private client: ApiClient) {}
async issueCredential(request: IssueVCRequest) {
return this.client.post<{ credential: eResidentCredential | eCitizenCredential }>('/vc/issue', request);
}
async verifyCredential(request: VerifyVCRequest) {
return this.client.post<{ valid: boolean }>('/vc/verify', request);
}
async batchIssue(request: BatchIssuanceRequest) {
return this.client.post<{
jobId: string;
total: number;
accepted: number;
results: Array<{
index: number;
credentialId?: string;
error?: string;
}>;
}>('/vc/issue/batch', request);
}
async revokeCredential(credentialId: string, reason?: string) {
return this.client.post('/vc/revoke', { credentialId, reason });
}
async getMetrics(startDate?: Date, endDate?: Date) {
const params = new URLSearchParams();
if (startDate) params.append('startDate', startDate.toISOString());
if (endDate) params.append('endDate', endDate.toISOString());
return this.client.get<CredentialMetrics>(`/metrics?${params.toString()}`);
}
async getMetricsDashboard() {
return this.client.get<{
summary: CredentialMetrics;
trends: {
daily: Array<{ date: string; count: number }>;
weekly: Array<{ week: string; count: number }>;
monthly: Array<{ month: string; count: number }>;
};
topCredentialTypes: Array<{ type: string; count: number; percentage: number }>;
}>('/metrics/dashboard');
}
async searchAuditLogs(filters: {
credentialId?: string;
issuerDid?: string;
subjectDid?: string;
credentialType?: string | string[];
action?: 'issued' | 'revoked' | 'verified' | 'renewed';
performedBy?: string;
startDate?: string;
endDate?: string;
ipAddress?: string;
page?: number;
pageSize?: number;
}) {
return this.client.post('/metrics/audit/search', filters);
}
}

View File

@@ -0,0 +1,18 @@
export { ApiClient, getApiClient } from './client';
export { IdentityClient } from './identity';
export { EResidencyClient } from './eresidency';
export { IntakeClient } from './intake';
export { FinanceClient } from './finance';
export { DataroomClient } from './dataroom';
// Export types
export type {
IssueVCRequest,
VerifyVCRequest,
BatchIssuanceRequest,
CredentialMetrics,
} from './identity';
export type { SubmitApplicationRequest, AdjudicateRequest } from './eresidency';
export type { DocumentUpload, DocumentMetadata } from './intake';
export type { Payment, LedgerEntry } from './finance';
export type { DealRoom, Document } from './dataroom';

View File

@@ -0,0 +1,104 @@
import axios, { AxiosInstance } from 'axios';
export interface DocumentUpload {
file: File | Blob;
documentType: string;
metadata?: Record<string, unknown>;
}
export interface DocumentMetadata {
id: string;
documentType: string;
fileName: string;
fileSize: number;
uploadedAt: string;
status: 'processing' | 'completed' | 'failed';
metadata?: Record<string, unknown>;
}
export class IntakeClient {
protected client: AxiosInstance;
constructor(baseURL?: string) {
const apiBaseURL =
baseURL ||
(typeof window !== 'undefined'
? process.env.NEXT_PUBLIC_INTAKE_SERVICE_URL || 'http://localhost:4004'
: 'http://localhost:4004');
this.client = axios.create({
baseURL: apiBaseURL,
headers: {
'Content-Type': 'application/json',
},
});
// Set up request interceptor for authentication
this.client.interceptors.request.use(
(config) => {
const token = this.getAuthToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
}
private getAuthToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('auth_token');
}
setAuthToken(token: string): void {
if (typeof window !== 'undefined') {
localStorage.setItem('auth_token', token);
}
}
clearAuthToken(): void {
if (typeof window !== 'undefined') {
localStorage.removeItem('auth_token');
}
}
async uploadDocument(upload: DocumentUpload): Promise<DocumentMetadata> {
const formData = new FormData();
formData.append('file', upload.file);
formData.append('documentType', upload.documentType);
if (upload.metadata) {
formData.append('metadata', JSON.stringify(upload.metadata));
}
const response = await this.client.post<DocumentMetadata>('/api/v1/documents/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
}
async getDocument(documentId: string): Promise<DocumentMetadata> {
const response = await this.client.get<DocumentMetadata>(`/api/v1/documents/${documentId}`);
return response.data;
}
async listDocuments(filters?: {
documentType?: string;
status?: string;
page?: number;
pageSize?: number;
}): Promise<{ documents: DocumentMetadata[]; total: number }> {
const response = await this.client.get<{ documents: DocumentMetadata[]; total: number }>(
'/api/v1/documents',
{ params: filters }
);
return response.data;
}
async deleteDocument(documentId: string): Promise<void> {
await this.client.delete(`/api/v1/documents/${documentId}`);
}
}

View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"composite": true,
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}