mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat: ( fayda ) register user after fayda verification
This commit is contained in:
@@ -59,6 +59,9 @@ export class CompleteVerificationResultDto {
|
||||
|
||||
@ApiPropertyOptional({ description: 'Verified gender from Fayda (VERIFY flow).' })
|
||||
gender?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Whether the verified identity was saved to IAM. False if the IAM write failed.' })
|
||||
userDataSaved?: boolean;
|
||||
}
|
||||
|
||||
export class VerifaydaCallbackDto {
|
||||
|
||||
@@ -72,6 +72,7 @@ export interface CompleteVerificationResult {
|
||||
phoneNumber?: string;
|
||||
birthdate?: string;
|
||||
gender?: string;
|
||||
userDataSaved?: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -219,8 +220,8 @@ export class VerifaydaService {
|
||||
const login = await this.issueLoginToken(userId);
|
||||
result = { purpose: 'LOGIN', verified: true, ...login };
|
||||
} else {
|
||||
// VERIFY — prove identity and hand the verified attributes back to the
|
||||
// caller. No domain writes; the session row tracks status as usual.
|
||||
// VERIFY — prove identity, save to IAM, return verified attributes.
|
||||
const { userDataSaved } = await this.upsertIamUser(normalized);
|
||||
result = {
|
||||
purpose: 'VERIFY',
|
||||
verified: true,
|
||||
@@ -229,6 +230,7 @@ export class VerifaydaService {
|
||||
phoneNumber: normalized.phoneNumber,
|
||||
birthdate: normalized.birthdate,
|
||||
gender: normalized.gender,
|
||||
userDataSaved,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -265,13 +267,13 @@ export class VerifaydaService {
|
||||
}
|
||||
|
||||
async getVerificationStatus(iamUserId: string): Promise<VerificationStatusDto> {
|
||||
const rows = await this.dataSource.query<{ metadata: Record<string, any> | null; name: { en: string; am: string } | null }[]>(
|
||||
`SELECT metadata, name FROM iam.users WHERE id = $1 LIMIT 1`,
|
||||
const rows = await this.dataSource.query<{ verified_by: string | null; updated_at: Date | null; name: { en: string; am: string } | null }[]>(
|
||||
`SELECT verified_by, updated_at, name FROM iam.users WHERE id = $1 LIMIT 1`,
|
||||
[iamUserId],
|
||||
);
|
||||
const iam = rows[0] ?? null;
|
||||
const faydaVerified = iam?.metadata?.faydaVerified === true || iam?.metadata?.faydaVerified === 'true';
|
||||
const faydaVerifiedAt = iam?.metadata?.faydaVerifiedAt ? new Date(iam.metadata.faydaVerifiedAt) : undefined;
|
||||
const faydaVerified = iam?.verified_by === 'fayda';
|
||||
const faydaVerifiedAt = faydaVerified && iam?.updated_at ? new Date(iam.updated_at) : undefined;
|
||||
const fullName = iam?.name?.en ?? iam?.name?.am ?? undefined;
|
||||
return { verified: faydaVerified, verifiedAt: faydaVerifiedAt, fullName };
|
||||
}
|
||||
@@ -387,18 +389,39 @@ export class VerifaydaService {
|
||||
}
|
||||
|
||||
private normalizeUserInfo(raw: FaydaUserInfo): NormalizedFaydaUserInfo {
|
||||
const nameEn = raw['name#en'] as string | undefined;
|
||||
const nameAm = raw['name#am'] as string | undefined;
|
||||
const genderEn = raw['gender#en'] as string | undefined;
|
||||
const genderAm = raw['gender#am'] as string | undefined;
|
||||
const addressEn = raw['address#en'] as string | undefined;
|
||||
const addressAm = raw['address#am'] as string | undefined;
|
||||
const rawPhone = (raw.phone_number ?? raw['phone_number#en'] ?? raw['phone_number#am'] ?? raw.phone) as string | undefined;
|
||||
|
||||
return {
|
||||
sub: raw.sub,
|
||||
fullName: raw.name ?? raw['name#en'] ?? raw['name#am'],
|
||||
phoneNumber:
|
||||
raw.phone_number ?? raw['phone_number#en'] ?? raw['phone_number#am'] ?? raw.phone,
|
||||
email: raw.email,
|
||||
gender: raw.gender,
|
||||
birthdate: raw.birthdate,
|
||||
picture: raw.picture,
|
||||
fullName: (raw.name as string | undefined) ?? nameEn ?? nameAm,
|
||||
phoneNumber: rawPhone ? this.standardizePhoneNumber(rawPhone) : undefined,
|
||||
rawPhoneNumber: rawPhone,
|
||||
email: raw.email as string | undefined,
|
||||
gender: genderEn ?? genderAm ?? (raw.gender as string | undefined),
|
||||
birthdate: raw.birthdate as string | undefined,
|
||||
picture: raw.picture as string | undefined,
|
||||
nameEn,
|
||||
nameAm,
|
||||
genderEn,
|
||||
genderAm,
|
||||
addressEn,
|
||||
addressAm,
|
||||
};
|
||||
}
|
||||
|
||||
private standardizePhoneNumber(phone: string): string {
|
||||
const digits = phone.replace(/\D/g, '');
|
||||
if (digits.startsWith('251')) return `+${digits}`;
|
||||
if (digits.startsWith('0')) return `+251${digits.slice(1)}`;
|
||||
return `+${digits}`;
|
||||
}
|
||||
|
||||
// LOGIN via Fayda is now handled entirely by the IAM package's own OIDC flow.
|
||||
// This method is kept as a stub so completeVerification() still compiles;
|
||||
// it throws immediately without touching the database.
|
||||
@@ -411,6 +434,88 @@ export class VerifaydaService {
|
||||
});
|
||||
}
|
||||
|
||||
private async upsertIamUser(
|
||||
normalized: NormalizedFaydaUserInfo,
|
||||
): Promise<{ iamUserId: string | null; userDataSaved: boolean }> {
|
||||
try {
|
||||
const iamMetadata = {
|
||||
sub: normalized.sub,
|
||||
address: { am: normalized.addressAm ?? '', en: normalized.addressEn ?? '' },
|
||||
email: normalized.email ?? '',
|
||||
gender: { am: normalized.genderAm ?? '', en: normalized.genderEn ?? '' },
|
||||
name: { am: normalized.nameAm ?? '', en: normalized.nameEn ?? '' },
|
||||
phoneNumber: normalized.rawPhoneNumber ?? '',
|
||||
};
|
||||
|
||||
// Step 1 — already verified with same Fayda sub
|
||||
const bySub = await this.dataSource.query<{ id: string }[]>(
|
||||
`SELECT id FROM iam.users WHERE metadata->>'sub' = $1 LIMIT 1`,
|
||||
[normalized.sub],
|
||||
);
|
||||
if (bySub.length > 0) {
|
||||
return { iamUserId: bySub[0].id, userDataSaved: true };
|
||||
}
|
||||
|
||||
// Step 2 — existing user by phone or email, not yet Fayda-verified
|
||||
const conditions: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
if (normalized.phoneNumber) {
|
||||
params.push(normalized.phoneNumber);
|
||||
conditions.push(`phone_number = $${params.length}`);
|
||||
}
|
||||
if (normalized.email) {
|
||||
params.push(normalized.email);
|
||||
conditions.push(`email = $${params.length}`);
|
||||
}
|
||||
if (conditions.length > 0) {
|
||||
const byContact = await this.dataSource.query<{ id: string }[]>(
|
||||
`SELECT id FROM iam.users WHERE ${conditions.join(' OR ')} LIMIT 1`,
|
||||
params,
|
||||
);
|
||||
if (byContact.length > 0) {
|
||||
const existingId = byContact[0].id;
|
||||
await this.dataSource.query(
|
||||
`UPDATE iam.users
|
||||
SET metadata = COALESCE(metadata, '{}'::jsonb) || $1::jsonb,
|
||||
verified_by = 'fayda',
|
||||
updated_at = NOW()
|
||||
WHERE id = $2`,
|
||||
[JSON.stringify(iamMetadata), existingId],
|
||||
);
|
||||
return { iamUserId: existingId, userDataSaved: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3 — new user
|
||||
const name = { am: normalized.nameAm ?? '', en: normalized.nameEn ?? '' };
|
||||
const username = normalized.phoneNumber ?? normalized.email ?? normalized.sub;
|
||||
const inserted = await this.dataSource.query<{ id: string }[]>(
|
||||
`INSERT INTO iam.users (
|
||||
id, name, username, email, phone_number, metadata,
|
||||
user_type, status, is_active, has_set_password,
|
||||
is_phone_number_verified, verified_by,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
gen_random_uuid(), $1::jsonb, $2, $3, $4, $5::jsonb,
|
||||
'individual', 'accepted', true, false,
|
||||
false, 'fayda',
|
||||
NOW(), NOW()
|
||||
) RETURNING id`,
|
||||
[
|
||||
JSON.stringify(name),
|
||||
username,
|
||||
normalized.email ?? null,
|
||||
normalized.phoneNumber ?? null,
|
||||
JSON.stringify(iamMetadata),
|
||||
],
|
||||
);
|
||||
return { iamUserId: inserted[0].id, userDataSaved: true };
|
||||
} catch (err) {
|
||||
this.logger.error(`Fayda IAM upsert failed: ${(err as Error).message}`);
|
||||
return { iamUserId: null, userDataSaved: false };
|
||||
}
|
||||
}
|
||||
|
||||
private async markSessionFailed(
|
||||
state: string,
|
||||
errorCode: string,
|
||||
|
||||
@@ -27,10 +27,19 @@ export interface FaydaUserInfo {
|
||||
|
||||
export interface NormalizedFaydaUserInfo {
|
||||
sub: string;
|
||||
// Convenience / display fields
|
||||
fullName?: string;
|
||||
phoneNumber?: string;
|
||||
phoneNumber?: string; // standardized e.g. +251911234567
|
||||
email?: string;
|
||||
gender?: string;
|
||||
birthdate?: string;
|
||||
picture?: string;
|
||||
// Raw localized fields — preserved for IAM-identical writes
|
||||
nameEn?: string;
|
||||
nameAm?: string;
|
||||
genderEn?: string;
|
||||
genderAm?: string;
|
||||
addressEn?: string;
|
||||
addressAm?: string;
|
||||
rawPhoneNumber?: string; // unstandardized, stored in IAM metadata
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user