mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 00:38:11 +00:00
feat: ( fayda ) implement verify with fayda
This commit is contained in:
@@ -1271,7 +1271,7 @@ model FaydaVerificationSession {
|
|||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
state String @unique
|
state String @unique
|
||||||
codeVerifier String
|
codeVerifier String
|
||||||
purpose String @default("PURCHASE")
|
purpose String @default("VERIFY") // VERIFY | LOGIN
|
||||||
platform String @default("WEB") // WEB | MOBILE — recorded for audit
|
platform String @default("WEB") // WEB | MOBILE — recorded for audit
|
||||||
saveToAccount Boolean @default(false)
|
saveToAccount Boolean @default(false)
|
||||||
status String @default("PENDING")
|
status String @default("PENDING")
|
||||||
|
|||||||
@@ -55,8 +55,9 @@ export class VerifaydaController {
|
|||||||
summary: 'Start a VeriFayda 2.0 verification session',
|
summary: 'Start a VeriFayda 2.0 verification session',
|
||||||
description: `Creates a verification session and returns the eSignet authorize URL the frontend should send the user to.
|
description: `Creates a verification session and returns the eSignet authorize URL the frontend should send the user to.
|
||||||
|
|
||||||
- Works for **logged-in users** and **guests**. If a valid bearer token is present, the verification is tied to that user; when \`saveToAccount\` is true their account is marked verified on success.
|
- Works for **logged-in users** and **guests**. If a valid bearer token is present, the verification is tied to that user.
|
||||||
- For a **PURCHASE** flow, pass \`bookingId\` to stamp the booking's seats as Fayda-verified.
|
- **VERIFY** (default): the user proves their identity and \`/complete\` returns the verified attributes (name, email, phone, dob, gender).
|
||||||
|
- **LOGIN**: \`/complete\` resolves/creates the user and returns a JWT.
|
||||||
- The returned \`authorizationUrl\` already carries the PKCE \`code_challenge\`, CSRF \`state\`, requested \`claims\`, and \`code_challenge_method=S256\`. The frontend simply navigates to it (full page or popup).`,
|
- The returned \`authorizationUrl\` already carries the PKCE \`code_challenge\`, CSRF \`state\`, requested \`claims\`, and \`code_challenge_method=S256\`. The frontend simply navigates to it (full page or popup).`,
|
||||||
})
|
})
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
@@ -73,11 +74,9 @@ export class VerifaydaController {
|
|||||||
@Req() req: RequestWithOptionalUser,
|
@Req() req: RequestWithOptionalUser,
|
||||||
): Promise<{ authorizationUrl: string }> {
|
): Promise<{ authorizationUrl: string }> {
|
||||||
const authorizationUrl = await this.service.startVerification({
|
const authorizationUrl = await this.service.startVerification({
|
||||||
purpose: dto.purpose ?? 'PURCHASE',
|
purpose: dto.purpose ?? 'VERIFY',
|
||||||
platform: dto.platform ?? 'WEB',
|
platform: dto.platform ?? 'WEB',
|
||||||
userId: req.user?.userId,
|
userId: req.user?.userId,
|
||||||
bookingId: dto.bookingId,
|
|
||||||
saveToAccount: dto.saveToAccount,
|
|
||||||
});
|
});
|
||||||
return { authorizationUrl };
|
return { authorizationUrl };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,16 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { IsBoolean, IsIn, IsOptional, IsString } from 'class-validator';
|
import { IsIn, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class StartVerificationDto {
|
export class StartVerificationDto {
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
enum: ['LOGIN', 'PURCHASE'],
|
enum: ['LOGIN', 'VERIFY'],
|
||||||
default: 'PURCHASE',
|
default: 'VERIFY',
|
||||||
description: 'Reason for verification.',
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsIn(['LOGIN', 'PURCHASE'])
|
|
||||||
purpose?: 'LOGIN' | 'PURCHASE';
|
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
|
||||||
description:
|
description:
|
||||||
'Booking the verification should attach to (PURCHASE flow). If omitted, the session is anchored only to the user.',
|
'Reason for verification. VERIFY returns the verified identity attributes; LOGIN resolves/creates a user and returns a JWT.',
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsIn(['LOGIN', 'VERIFY'])
|
||||||
bookingId?: string;
|
purpose?: 'LOGIN' | 'VERIFY';
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
|
||||||
description:
|
|
||||||
'When true and the user is logged in, copy faydaVerified=true / faydaSub onto their User record after verification.',
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
saveToAccount?: boolean;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
enum: ['WEB', 'MOBILE'],
|
enum: ['WEB', 'MOBILE'],
|
||||||
@@ -39,8 +24,8 @@ export class StartVerificationDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CompleteVerificationResultDto {
|
export class CompleteVerificationResultDto {
|
||||||
@ApiProperty({ enum: ['LOGIN', 'PURCHASE'] })
|
@ApiProperty({ enum: ['LOGIN', 'VERIFY'] })
|
||||||
purpose: 'LOGIN' | 'PURCHASE';
|
purpose: 'LOGIN' | 'VERIFY';
|
||||||
|
|
||||||
@ApiProperty() verified: boolean;
|
@ApiProperty() verified: boolean;
|
||||||
|
|
||||||
@@ -58,10 +43,22 @@ export class CompleteVerificationResultDto {
|
|||||||
agentId?: string;
|
agentId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({ description: 'Verified full name from Fayda (VERIFY flow).' })
|
||||||
description: 'Verified full name from Fayda (PURCHASE flow).',
|
|
||||||
})
|
|
||||||
fullName?: string;
|
fullName?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Verified email from Fayda (VERIFY flow).' })
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Verified phone number from Fayda (VERIFY flow).' })
|
||||||
|
phoneNumber?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Verified date of birth from Fayda, ISO yyyy-MM-dd (VERIFY flow).',
|
||||||
|
})
|
||||||
|
birthdate?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Verified gender from Fayda (VERIFY flow).' })
|
||||||
|
gender?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VerifaydaCallbackDto {
|
export class VerifaydaCallbackDto {
|
||||||
|
|||||||
@@ -96,13 +96,12 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
|||||||
prisma.faydaVerificationSession.create.mockResolvedValue({});
|
prisma.faydaVerificationSession.create.mockResolvedValue({});
|
||||||
|
|
||||||
const url = await service.startVerification({
|
const url = await service.startVerification({
|
||||||
purpose: 'PURCHASE',
|
purpose: 'VERIFY',
|
||||||
userId: 'user-1',
|
userId: 'user-1',
|
||||||
saveToAccount: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const created = prisma.faydaVerificationSession.create.mock.calls[0][0].data;
|
const created = prisma.faydaVerificationSession.create.mock.calls[0][0].data;
|
||||||
expect(created.purpose).toBe('PURCHASE');
|
expect(created.purpose).toBe('VERIFY');
|
||||||
expect(created.platform).toBe('WEB');
|
expect(created.platform).toBe('WEB');
|
||||||
expect(typeof created.state).toBe('string');
|
expect(typeof created.state).toBe('string');
|
||||||
expect(typeof created.codeVerifier).toBe('string');
|
expect(typeof created.codeVerifier).toBe('string');
|
||||||
@@ -139,7 +138,7 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
|||||||
jwt,
|
jwt,
|
||||||
);
|
);
|
||||||
await expect(
|
await expect(
|
||||||
disabledService.startVerification({ purpose: 'PURCHASE' }),
|
disabledService.startVerification({ purpose: 'VERIFY' }),
|
||||||
).rejects.toMatchObject({ status: 503 });
|
).rejects.toMatchObject({ status: 503 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -150,14 +149,12 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
|||||||
id: 'session-1',
|
id: 'session-1',
|
||||||
state: 'state-abc',
|
state: 'state-abc',
|
||||||
codeVerifier: 'verifier-xyz',
|
codeVerifier: 'verifier-xyz',
|
||||||
purpose: 'PURCHASE',
|
purpose: 'VERIFY',
|
||||||
platform: 'WEB',
|
platform: 'WEB',
|
||||||
saveToAccount: false,
|
|
||||||
status: 'PENDING',
|
status: 'PENDING',
|
||||||
errorCode: null,
|
errorCode: null,
|
||||||
errorDescription: null,
|
errorDescription: null,
|
||||||
userId: null,
|
userId: null,
|
||||||
bookingId: null,
|
|
||||||
expiresAt: new Date(Date.now() + 60_000),
|
expiresAt: new Date(Date.now() + 60_000),
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
@@ -209,18 +206,16 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('completeVerification — PURCHASE', () => {
|
describe('completeVerification — VERIFY', () => {
|
||||||
function pendingSession(overrides: Partial<any> = {}) {
|
function pendingSession(overrides: Partial<any> = {}) {
|
||||||
return {
|
return {
|
||||||
id: 'session-1',
|
id: 'session-1',
|
||||||
state: 'state-abc',
|
state: 'state-abc',
|
||||||
codeVerifier: 'verifier-xyz',
|
codeVerifier: 'verifier-xyz',
|
||||||
purpose: 'PURCHASE',
|
purpose: 'VERIFY',
|
||||||
platform: 'WEB',
|
platform: 'WEB',
|
||||||
saveToAccount: false,
|
|
||||||
status: 'PENDING',
|
status: 'PENDING',
|
||||||
userId: null,
|
userId: null,
|
||||||
bookingId: null,
|
|
||||||
expiresAt: new Date(Date.now() + 60_000),
|
expiresAt: new Date(Date.now() + 60_000),
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
@@ -238,19 +233,23 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
|||||||
(global as any).fetch = jest.fn(() => Promise.resolve(queue.shift()));
|
(global as any).fetch = jest.fn(() => Promise.resolve(queue.shift()));
|
||||||
}
|
}
|
||||||
|
|
||||||
it('stamps the booking seats and returns { verified, fullName }', async () => {
|
it('returns the verified identity attributes and writes no domain rows', async () => {
|
||||||
prisma.faydaVerificationSession.findUnique.mockResolvedValue(
|
prisma.faydaVerificationSession.findUnique.mockResolvedValue(pendingSession());
|
||||||
pendingSession({ bookingId: 'booking-1' }),
|
|
||||||
);
|
|
||||||
prisma.faydaVerificationSession.update.mockResolvedValue({});
|
prisma.faydaVerificationSession.update.mockResolvedValue({});
|
||||||
prisma.bookingSeat.updateMany.mockResolvedValue({ count: 1 });
|
|
||||||
|
|
||||||
mockFetchSequence(
|
mockFetchSequence(
|
||||||
{ json: async () => ({ access_token: 'tok', token_type: 'Bearer' }) },
|
{ json: async () => ({ access_token: 'tok', token_type: 'Bearer' }) },
|
||||||
{
|
{
|
||||||
headers: new Headers({ 'content-type': 'application/json' }),
|
headers: new Headers({ 'content-type': 'application/json' }),
|
||||||
text: async () =>
|
text: async () =>
|
||||||
JSON.stringify({ sub: 'fayda-sub-1', name: 'Test User' }),
|
JSON.stringify({
|
||||||
|
sub: 'fayda-sub-1',
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
phone_number: '+251911000000',
|
||||||
|
birthdate: '1990-05-01',
|
||||||
|
gender: 'Male',
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -260,65 +259,17 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
purpose: 'PURCHASE',
|
purpose: 'VERIFY',
|
||||||
verified: true,
|
verified: true,
|
||||||
fullName: 'Test User',
|
fullName: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
phoneNumber: '+251911000000',
|
||||||
|
birthdate: '1990-05-01',
|
||||||
|
gender: 'Male',
|
||||||
});
|
});
|
||||||
expect(result.token).toBeUndefined();
|
expect(result.token).toBeUndefined();
|
||||||
expect(prisma.bookingSeat.updateMany).toHaveBeenCalledWith({
|
expect(result.user).toBeUndefined();
|
||||||
where: { bookingId: 'booking-1' },
|
expect(prisma.bookingSeat.updateMany).not.toHaveBeenCalled();
|
||||||
data: expect.objectContaining({ faydaSub: 'fayda-sub-1' }),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('saves to the User account when saveToAccount=true and no conflict', async () => {
|
|
||||||
prisma.faydaVerificationSession.findUnique.mockResolvedValue(
|
|
||||||
pendingSession({ userId: 'user-1', saveToAccount: true }),
|
|
||||||
);
|
|
||||||
prisma.user.findFirst.mockResolvedValue(null);
|
|
||||||
prisma.user.update.mockResolvedValue({});
|
|
||||||
prisma.faydaVerificationSession.update.mockResolvedValue({});
|
|
||||||
|
|
||||||
mockFetchSequence(
|
|
||||||
{ json: async () => ({ access_token: 'tok', token_type: 'Bearer' }) },
|
|
||||||
{
|
|
||||||
headers: new Headers({ 'content-type': 'application/json' }),
|
|
||||||
text: async () =>
|
|
||||||
JSON.stringify({ sub: 'fayda-sub-2', name: 'Test User' }),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await service.completeVerification({
|
|
||||||
code: 'authcode',
|
|
||||||
state: 'state-abc',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.verified).toBe(true);
|
|
||||||
expect(prisma.user.update).toHaveBeenCalledWith({
|
|
||||||
where: { id: 'user-1' },
|
|
||||||
data: expect.objectContaining({ faydaVerified: true, faydaSub: 'fayda-sub-2' }),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws identity_conflict (409) when faydaSub belongs to another user', async () => {
|
|
||||||
prisma.faydaVerificationSession.findUnique.mockResolvedValue(
|
|
||||||
pendingSession({ userId: 'user-1', saveToAccount: true }),
|
|
||||||
);
|
|
||||||
prisma.user.findFirst.mockResolvedValue({ id: 'other-user' });
|
|
||||||
prisma.faydaVerificationSession.updateMany.mockResolvedValue({ count: 1 });
|
|
||||||
|
|
||||||
mockFetchSequence(
|
|
||||||
{ json: async () => ({ access_token: 'tok', token_type: 'Bearer' }) },
|
|
||||||
{
|
|
||||||
headers: new Headers({ 'content-type': 'application/json' }),
|
|
||||||
text: async () =>
|
|
||||||
JSON.stringify({ sub: 'fayda-sub-3', name: 'Test User' }),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
service.completeVerification({ code: 'authcode', state: 'state-abc' }),
|
|
||||||
).rejects.toMatchObject({ status: 409 });
|
|
||||||
expect(prisma.user.update).not.toHaveBeenCalled();
|
expect(prisma.user.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -353,11 +304,8 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to localized name (name#en) when name is missing', async () => {
|
it('falls back to localized name (name#en) when name is missing', async () => {
|
||||||
prisma.faydaVerificationSession.findUnique.mockResolvedValue(
|
prisma.faydaVerificationSession.findUnique.mockResolvedValue(pendingSession());
|
||||||
pendingSession({ bookingId: 'booking-2' }),
|
|
||||||
);
|
|
||||||
prisma.faydaVerificationSession.update.mockResolvedValue({});
|
prisma.faydaVerificationSession.update.mockResolvedValue({});
|
||||||
prisma.bookingSeat.updateMany.mockResolvedValue({ count: 1 });
|
|
||||||
|
|
||||||
mockFetchSequence(
|
mockFetchSequence(
|
||||||
{ json: async () => ({ access_token: 'tok', token_type: 'Bearer' }) },
|
{ json: async () => ({ access_token: 'tok', token_type: 'Bearer' }) },
|
||||||
@@ -377,9 +325,6 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
|||||||
state: 'state-abc',
|
state: 'state-abc',
|
||||||
});
|
});
|
||||||
expect(result.fullName).toBe('English Name');
|
expect(result.fullName).toBe('English Name');
|
||||||
expect(prisma.bookingSeat.updateMany.mock.calls[0][0].data.faydaVerifiedName).toBe(
|
|
||||||
'English Name',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -391,10 +336,8 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
|||||||
codeVerifier: 'verifier-xyz',
|
codeVerifier: 'verifier-xyz',
|
||||||
purpose: 'LOGIN',
|
purpose: 'LOGIN',
|
||||||
platform: 'WEB',
|
platform: 'WEB',
|
||||||
saveToAccount: false,
|
|
||||||
status: 'PENDING',
|
status: 'PENDING',
|
||||||
userId: null,
|
userId: null,
|
||||||
bookingId: null,
|
|
||||||
expiresAt: new Date(Date.now() + 60_000),
|
expiresAt: new Date(Date.now() + 60_000),
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ export interface StartVerificationInput {
|
|||||||
purpose: VerifaydaPurpose;
|
purpose: VerifaydaPurpose;
|
||||||
platform?: FaydaPlatform;
|
platform?: FaydaPlatform;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
bookingId?: string;
|
|
||||||
saveToAccount?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FaydaUserSummary {
|
export interface FaydaUserSummary {
|
||||||
@@ -63,7 +61,8 @@ export interface FaydaUserSummary {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Result of completing a verification. `verified` is always true on success.
|
* Result of completing a verification. `verified` is always true on success.
|
||||||
* LOGIN additionally returns a JWT + user; PURCHASE returns the verified name.
|
* LOGIN additionally returns a JWT + user; VERIFY returns the verified identity
|
||||||
|
* attributes (name, email, phone, dob, gender) for the caller to consume.
|
||||||
*/
|
*/
|
||||||
export interface CompleteVerificationResult {
|
export interface CompleteVerificationResult {
|
||||||
purpose: VerifaydaPurpose;
|
purpose: VerifaydaPurpose;
|
||||||
@@ -71,6 +70,10 @@ export interface CompleteVerificationResult {
|
|||||||
token?: string;
|
token?: string;
|
||||||
user?: FaydaUserSummary;
|
user?: FaydaUserSummary;
|
||||||
fullName?: string;
|
fullName?: string;
|
||||||
|
email?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
birthdate?: string;
|
||||||
|
gender?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -134,15 +137,13 @@ export class VerifaydaService {
|
|||||||
codeVerifier,
|
codeVerifier,
|
||||||
purpose: input.purpose,
|
purpose: input.purpose,
|
||||||
platform: input.platform ?? 'WEB',
|
platform: input.platform ?? 'WEB',
|
||||||
saveToAccount: input.saveToAccount ?? false,
|
|
||||||
userId: input.userId ?? null,
|
userId: input.userId ?? null,
|
||||||
bookingId: input.bookingId ?? null,
|
|
||||||
expiresAt,
|
expiresAt,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Fayda verification started: purpose=${input.purpose} platform=${input.platform ?? 'WEB'} userId=${input.userId ?? 'none'} bookingId=${input.bookingId ?? 'none'}`,
|
`Fayda verification started: purpose=${input.purpose} platform=${input.platform ?? 'WEB'} userId=${input.userId ?? 'none'}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.buildAuthorizationUrl({ state, codeChallenge });
|
return this.buildAuthorizationUrl({ state, codeChallenge });
|
||||||
@@ -206,17 +207,22 @@ export class VerifaydaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let result: CompleteVerificationResult;
|
let result: CompleteVerificationResult;
|
||||||
if (session.purpose === 'PURCHASE') {
|
if (session.purpose === 'LOGIN') {
|
||||||
await this.handlePurchaseSuccess(session, normalized);
|
|
||||||
result = {
|
|
||||||
purpose: 'PURCHASE',
|
|
||||||
verified: true,
|
|
||||||
fullName: normalized.fullName,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const { userId } = await this.handleLoginSuccess(normalized);
|
const { userId } = await this.handleLoginSuccess(normalized);
|
||||||
const login = await this.issueLoginToken(userId);
|
const login = await this.issueLoginToken(userId);
|
||||||
result = { purpose: 'LOGIN', verified: true, ...login };
|
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.
|
||||||
|
result = {
|
||||||
|
purpose: 'VERIFY',
|
||||||
|
verified: true,
|
||||||
|
fullName: normalized.fullName,
|
||||||
|
email: normalized.email,
|
||||||
|
phoneNumber: normalized.phoneNumber,
|
||||||
|
birthdate: normalized.birthdate,
|
||||||
|
gender: normalized.gender,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prisma.faydaVerificationSession.update({
|
await this.prisma.faydaVerificationSession.update({
|
||||||
@@ -413,49 +419,6 @@ export class VerifaydaService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handlePurchaseSuccess(
|
|
||||||
session: {
|
|
||||||
id: string;
|
|
||||||
userId: string | null;
|
|
||||||
bookingId: string | null;
|
|
||||||
saveToAccount: boolean;
|
|
||||||
},
|
|
||||||
normalized: NormalizedFaydaUserInfo,
|
|
||||||
): Promise<void> {
|
|
||||||
if (session.bookingId) {
|
|
||||||
await this.prisma.bookingSeat.updateMany({
|
|
||||||
where: { bookingId: session.bookingId },
|
|
||||||
data: {
|
|
||||||
faydaVerifiedAt: new Date(),
|
|
||||||
faydaSub: normalized.sub,
|
|
||||||
faydaVerifiedName: normalized.fullName ?? null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.userId && session.saveToAccount) {
|
|
||||||
const conflict = await this.prisma.user.findFirst({
|
|
||||||
where: {
|
|
||||||
faydaSub: normalized.sub,
|
|
||||||
NOT: { id: session.userId },
|
|
||||||
},
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
if (conflict) {
|
|
||||||
throw new FaydaIdentityConflictException();
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.prisma.user.update({
|
|
||||||
where: { id: session.userId },
|
|
||||||
data: {
|
|
||||||
faydaVerified: true,
|
|
||||||
faydaVerifiedAt: new Date(),
|
|
||||||
faydaSub: normalized.sub,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the User for a LOGIN flow and returns its id (the caller mints the
|
* Resolves the User for a LOGIN flow and returns its id (the caller mints the
|
||||||
* JWT via {@link issueLoginToken}). Resolution order:
|
* JWT via {@link issueLoginToken}). Resolution order:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type VerifaydaPurpose = 'LOGIN' | 'PURCHASE';
|
export type VerifaydaPurpose = 'LOGIN' | 'VERIFY';
|
||||||
|
|
||||||
export interface FaydaTokenResponse {
|
export interface FaydaTokenResponse {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user