mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
feat(auth): add post-Fayda password setup flow
This commit is contained in:
@@ -66,7 +66,9 @@ function decodePrivateJwk(base64: string): FaydaJwk {
|
||||
|
||||
export default registerAs('fayda', (): FaydaConfig => {
|
||||
const enabled = (process.env.FAYDA_ENABLED ?? 'false').toLowerCase() === 'true';
|
||||
const scope = process.env.FAYDA_SCOPE ?? 'openid profile email';
|
||||
// `profile` covers name/birthdate/gender/picture; `email`, `phone`, `address`
|
||||
// are needed so the matching essential claims aren't rejected as out-of-scope.
|
||||
const scope = process.env.FAYDA_SCOPE ?? 'openid profile email phone address';
|
||||
const acrValues = process.env.FAYDA_ACR_VALUES ?? 'mosip:idp:acr:generated-code';
|
||||
const claimsLocales = process.env.FAYDA_CLAIMS_LOCALES ?? 'en am';
|
||||
const sessionTtl = Number.parseInt(process.env.FAYDA_SESSION_TTL_MINUTES ?? '10', 10);
|
||||
|
||||
@@ -314,7 +314,7 @@ Payment providers send notifications to:
|
||||
.addTag("Excess Baggage", "IAM-protected agent/supervisor endpoints to log excess baggage charges, waive fees, resend payment links, and manage allowance rules per seat class. Public token-based endpoints let passengers self-pay outstanding charges.")
|
||||
.addTag("Packages", "Bundled travel packages with tiered pricing. Public endpoints for browsing and booking; JWT-authenticated endpoints for purchase history; IAM-protected endpoints for admin CRUD and tier management.")
|
||||
.addTag("Audit", "User activity logging, system changes, compliance tracking, and audit trails")
|
||||
.addTag("Auth", "Passenger registration, login, OTP, password reset, and profile management")
|
||||
.addTag("Passenger Auth", "Passenger registration, login, OTP, password reset, Fayda password setup, and profile management")
|
||||
.addTag("Booking", "Complete booking lifecycle: create, modify, cancel, guest checkout. Supports ONE_WAY | ROUND_TRIP | TRANSIT | ROUND_TRIP_TRANSIT booking types. returnLegStatus filter for round-trip no-show management")
|
||||
.addTag("Config", "System settings, feature flags, and configuration management")
|
||||
.addTag("Currencies", "Multi-currency support, exchange rates, and currency conversion")
|
||||
|
||||
@@ -3,10 +3,10 @@ import { ApiTags, ApiOperation, ApiResponse, ApiBody, ApiBearerAuth } from '@nes
|
||||
import { Throttle, SkipThrottle } from '@nestjs/throttler';
|
||||
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
|
||||
import { PassengerAuthService } from './passenger-auth.service';
|
||||
import { RegisterDto, LoginDto } from './auth.dto';
|
||||
import { RegisterDto, LoginDto, FaydaRequestPasswordSetupDto, FaydaVerifyAndLoginDto } from './auth.dto';
|
||||
import { JwtGuard } from '../../common/jwt.guard';
|
||||
|
||||
@ApiTags('Auth')
|
||||
@ApiTags('Passenger Auth')
|
||||
@Controller('auth')
|
||||
@Throttle({ auth: { limit: 5, ttl: 60_000 } })
|
||||
export class AuthController {
|
||||
@@ -118,4 +118,24 @@ export class AuthController {
|
||||
resetPassword(@Param('id') id: string, @Body() body: { tempPassword: string }) {
|
||||
return this.passengerAuthService.resetUserPassword(id, body.tempPassword);
|
||||
}
|
||||
|
||||
@Post('fayda/request-password-setup')
|
||||
@IsPublic()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Send OTP to phone for Fayda-verified account password setup' })
|
||||
@ApiResponse({ status: 200, description: 'OTP sent to registered phone number' })
|
||||
@ApiBody({ type: FaydaRequestPasswordSetupDto })
|
||||
requestFaydaPasswordSetup(@Body() dto: FaydaRequestPasswordSetupDto, @Request() req: any) {
|
||||
return this.passengerAuthService.requestFaydaPasswordSetup(dto.phoneNumber, req);
|
||||
}
|
||||
|
||||
@Post('fayda/verify-and-login')
|
||||
@IsPublic()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Verify OTP and receive session token for Fayda-verified account' })
|
||||
@ApiResponse({ status: 200, description: 'Returns token + requiresPassword flag. Use token with POST /v1/auth/set-fayda-password.' })
|
||||
@ApiBody({ type: FaydaVerifyAndLoginDto })
|
||||
verifyFaydaAndLogin(@Body() dto: FaydaVerifyAndLoginDto) {
|
||||
return this.passengerAuthService.verifyFaydaAndLogin(dto.phoneNumber, dto.otp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IsEmail, IsString, MinLength, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class NameDto {
|
||||
@ApiProperty({ example: 'ቀለሙ ቀጸላ' })
|
||||
@@ -49,3 +49,19 @@ export class LoginDto {
|
||||
@IsString()
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class FaydaRequestPasswordSetupDto {
|
||||
@ApiProperty({ example: '+251911234567', description: 'Phone number of the Fayda-verified account' })
|
||||
@IsString()
|
||||
phoneNumber: string;
|
||||
}
|
||||
|
||||
export class FaydaVerifyAndLoginDto {
|
||||
@ApiProperty({ example: '+251911234567' })
|
||||
@IsString()
|
||||
phoneNumber: string;
|
||||
|
||||
@ApiProperty({ example: '123456', description: '6-digit OTP received via SMS' })
|
||||
@IsString()
|
||||
otp: string;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Injectable,
|
||||
ConflictException,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { ModuleRef, ContextIdFactory } from '@nestjs/core';
|
||||
@@ -10,6 +11,7 @@ import { DataSource } from 'typeorm';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { AuthService as IamAuthService } from '@tria-plc/iamapi-common/module/auth/services/auth.service';
|
||||
import { EUserType } from '@tria-plc/api-common/utils/enums/user.enum';
|
||||
import { EOtpType } from '@tria-plc/iamapi-common/enums/otp.enum';
|
||||
import { PrismaService } from '../../common/prisma.service';
|
||||
import { RegisterDto, LoginDto } from './auth.dto';
|
||||
|
||||
@@ -19,10 +21,13 @@ type IamUserRow = {
|
||||
name: { en: string; am: string } | null;
|
||||
phone_number: string | null;
|
||||
metadata: Record<string, any> | null;
|
||||
verified_by: string | null;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PassengerAuthService {
|
||||
private readonly logger = new Logger(PassengerAuthService.name);
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@InjectDataSource() private readonly dataSource: DataSource,
|
||||
@@ -165,7 +170,7 @@ export class PassengerAuthService {
|
||||
include: { loyalty: true, wallet: true },
|
||||
}),
|
||||
this.dataSource.query<IamUserRow[]>(
|
||||
`SELECT id, email, name, phone_number, metadata FROM iam.users WHERE id = $1 LIMIT 1`,
|
||||
`SELECT id, email, name, phone_number, metadata, verified_by FROM iam.users WHERE id = $1 LIMIT 1`,
|
||||
[iamUserId],
|
||||
),
|
||||
]);
|
||||
@@ -178,7 +183,7 @@ export class PassengerAuthService {
|
||||
email: iam?.email ?? null,
|
||||
phone: iam?.phone_number ?? null,
|
||||
fullName: iam?.name?.en ?? iam?.name?.am ?? null,
|
||||
faydaVerified: iam?.metadata?.faydaVerified ?? false,
|
||||
faydaVerified: iam?.verified_by === 'fayda',
|
||||
createdAt: passenger.createdAt,
|
||||
passenger: {
|
||||
id: passenger.id,
|
||||
@@ -396,6 +401,121 @@ export class PassengerAuthService {
|
||||
return { success: true, message: 'Password reset successfully' };
|
||||
}
|
||||
|
||||
async requestFaydaPasswordSetup(phoneNumber: string, req: any): Promise<{ sent: boolean }> {
|
||||
const phone = this.standardizePhone(phoneNumber);
|
||||
|
||||
const users = await this.dataSource.query<{ id: string; email: string }[]>(
|
||||
`SELECT id, email FROM iam.users WHERE phone_number = $1 AND verified_by = 'fayda' LIMIT 1`,
|
||||
[phone],
|
||||
);
|
||||
this.logger.log(`requestFaydaPasswordSetup: phone=${phone} found=${users.length > 0}`);
|
||||
// Return success regardless to avoid phone enumeration
|
||||
if (!users.length) return { sent: true };
|
||||
const u = users[0];
|
||||
|
||||
const iamAuthService = await this.resolveIamAuthService(req);
|
||||
await iamAuthService.generateVerificationCode({
|
||||
email: u.email,
|
||||
phoneNumber: phone,
|
||||
type: EOtpType.SET_PASSWORD,
|
||||
});
|
||||
|
||||
return { sent: true };
|
||||
}
|
||||
|
||||
async verifyFaydaAndLogin(
|
||||
phoneNumber: string,
|
||||
otp: string,
|
||||
): Promise<{ token: string; refreshToken: string; requiresPassword: boolean; iamUserId: string }> {
|
||||
const phone = this.standardizePhone(phoneNumber);
|
||||
|
||||
const users = await this.dataSource.query<{
|
||||
id: string;
|
||||
email: string;
|
||||
name: { en: string; am: string } | null;
|
||||
username: string;
|
||||
phone_number: string | null;
|
||||
has_set_password: boolean;
|
||||
}[]>(
|
||||
`SELECT id, email, name, username, phone_number, has_set_password
|
||||
FROM iam.users WHERE phone_number = $1 AND verified_by = 'fayda' LIMIT 1`,
|
||||
[phone],
|
||||
);
|
||||
if (!users.length) throw new UnauthorizedException('Invalid phone number or OTP');
|
||||
const u = users[0];
|
||||
|
||||
const verifications = await this.dataSource.query<{
|
||||
id: string; verification_code: string; attempt_count: number;
|
||||
}[]>(
|
||||
`SELECT id, verification_code, attempt_count FROM iam.user_verifications
|
||||
WHERE user_id = $1 AND otp_type = 'set-password' AND "isUsed" = false AND expires_at > NOW()
|
||||
ORDER BY created_at DESC LIMIT 1`,
|
||||
[u.id],
|
||||
);
|
||||
if (!verifications.length) throw new UnauthorizedException('Invalid phone number or OTP');
|
||||
const v = verifications[0];
|
||||
|
||||
if (v.attempt_count >= 5) {
|
||||
await this.dataSource.query(
|
||||
`UPDATE iam.user_verifications SET "isUsed" = true WHERE id = $1`, [v.id],
|
||||
);
|
||||
throw new UnauthorizedException('Too many attempts. Request a new code.');
|
||||
}
|
||||
|
||||
await this.dataSource.query(
|
||||
`UPDATE iam.user_verifications SET attempt_count = attempt_count + 1 WHERE id = $1`, [v.id],
|
||||
);
|
||||
|
||||
const { verifyPassword } = await import('@tria-plc/api-common/utils/argon');
|
||||
const valid = await verifyPassword(otp, v.verification_code);
|
||||
if (!valid) throw new UnauthorizedException('Invalid phone number or OTP');
|
||||
|
||||
await this.dataSource.query(
|
||||
`UPDATE iam.user_verifications SET "isUsed" = true WHERE id = $1`, [v.id],
|
||||
);
|
||||
|
||||
const userInfo = {
|
||||
id: u.id,
|
||||
email: u.email ?? '',
|
||||
name: u.name ?? { en: '', am: '' },
|
||||
userType: 'individual',
|
||||
status: 'accepted',
|
||||
hasSetPassword: u.has_set_password,
|
||||
isPhoneNumberVerified: false,
|
||||
hasFinishedRegistration: false,
|
||||
hasFinishedDMSOnboarding: false,
|
||||
username: u.username,
|
||||
phoneNumber: u.phone_number ?? '',
|
||||
roles: [],
|
||||
permissions: [],
|
||||
employee: [],
|
||||
};
|
||||
|
||||
const sessions = await this.dataSource.query<{ id: string }[]>(
|
||||
`INSERT INTO iam.sessions
|
||||
(id, email, device, "userInfo", expiry_time, refresh_count, status, user_id)
|
||||
VALUES (gen_random_uuid(), $1, 'fayda-otp-setup', $2::jsonb, NOW() + INTERVAL '1 day', 0, 'ACTIVE', $3)
|
||||
ON CONFLICT (user_id, device) DO UPDATE
|
||||
SET status = 'ACTIVE', "userInfo" = EXCLUDED."userInfo",
|
||||
expiry_time = NOW() + INTERVAL '1 day', updated_at = NOW()
|
||||
RETURNING id`,
|
||||
[u.email ?? '', JSON.stringify(userInfo), u.id],
|
||||
);
|
||||
|
||||
const { generateToken, generateRefreshToken } = await import('@tria-plc/api-common/utils/token');
|
||||
const token = generateToken({ id: sessions[0].id });
|
||||
const refreshToken = generateRefreshToken({ id: sessions[0].id });
|
||||
|
||||
return { token, refreshToken, requiresPassword: !u.has_set_password, iamUserId: u.id };
|
||||
}
|
||||
|
||||
private standardizePhone(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}`;
|
||||
}
|
||||
|
||||
private async compensateIamSignup(email: string): Promise<void> {
|
||||
try {
|
||||
const rows = await this.dataSource.query<{ id: string }[]>(
|
||||
|
||||
@@ -74,6 +74,7 @@ export class VerifaydaController {
|
||||
purpose: dto.purpose ?? 'VERIFY',
|
||||
platform: dto.platform ?? 'WEB',
|
||||
userId: req.user?.id,
|
||||
wantsPasswordSetup: dto.wantsPasswordSetup ?? false,
|
||||
});
|
||||
return { authorizationUrl };
|
||||
}
|
||||
|
||||
@@ -21,6 +21,17 @@ export class StartVerificationDto {
|
||||
@IsOptional()
|
||||
@IsIn(['WEB', 'MOBILE'])
|
||||
platform?: 'WEB' | 'MOBILE';
|
||||
|
||||
@ApiPropertyOptional({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
description:
|
||||
'Set to true when the user opts in to full account registration (checkbox). ' +
|
||||
'When true, the /complete response includes a short-lived token and promptPasswordSetup=true ' +
|
||||
'so the frontend can immediately prompt for a password via POST /v1/auth/set-fayda-password.',
|
||||
})
|
||||
@IsOptional()
|
||||
wantsPasswordSetup?: boolean;
|
||||
}
|
||||
|
||||
export class CompleteVerificationResultDto {
|
||||
@@ -29,9 +40,12 @@ export class CompleteVerificationResultDto {
|
||||
|
||||
@ApiProperty() verified: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: 'JWT (LOGIN flow only).' })
|
||||
@ApiPropertyOptional({ description: 'JWT. LOGIN: session token for the authenticated user. VERIFY: short-lived token for calling /v1/auth/set-fayda-password.' })
|
||||
token?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
refreshToken?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Authenticated user summary (LOGIN flow only; same shape as /auth/login).',
|
||||
})
|
||||
@@ -62,6 +76,19 @@ export class CompleteVerificationResultDto {
|
||||
|
||||
@ApiPropertyOptional({ description: 'Whether the verified identity was saved to IAM. False if the IAM write failed.' })
|
||||
userDataSaved?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: 'IAM user ID of the verified identity (VERIFY flow).' })
|
||||
iamUserId?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'True when the IAM account has not yet set a password (VERIFY flow).' })
|
||||
requiresPassword?: boolean;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'True when the user opted in to immediate password setup (wantsPasswordSetup=true at start) ' +
|
||||
'AND they have not yet set a password. Frontend should navigate to the set-password screen.',
|
||||
})
|
||||
promptPasswordSetup?: boolean;
|
||||
}
|
||||
|
||||
export class VerifaydaCallbackDto {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { generateToken, generateRefreshToken } from '@tria-plc/api-common/utils/token';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { PrismaService } from '../../common/prisma.service';
|
||||
import { FaydaConfig, FaydaPlatform } from '../../config/fayda.config';
|
||||
@@ -47,6 +48,7 @@ export interface StartVerificationInput {
|
||||
purpose: VerifaydaPurpose;
|
||||
platform?: FaydaPlatform;
|
||||
userId?: string; // iamUserId of the authenticated user, if any
|
||||
wantsPasswordSetup?: boolean;
|
||||
}
|
||||
|
||||
export interface FaydaUserSummary {
|
||||
@@ -66,6 +68,10 @@ export interface CompleteVerificationResult {
|
||||
purpose: VerifaydaPurpose;
|
||||
verified: boolean;
|
||||
token?: string;
|
||||
refreshToken?: string;
|
||||
requiresPassword?: boolean;
|
||||
promptPasswordSetup?: boolean;
|
||||
iamUserId?: string;
|
||||
user?: FaydaUserSummary;
|
||||
fullName?: string;
|
||||
email?: string;
|
||||
@@ -145,6 +151,7 @@ export class VerifaydaService {
|
||||
codeVerifier,
|
||||
purpose: input.purpose,
|
||||
platform: input.platform ?? 'WEB',
|
||||
saveToAccount: input.wantsPasswordSetup ?? false,
|
||||
iamUserId: input.userId ?? null,
|
||||
expiresAt,
|
||||
},
|
||||
@@ -220,8 +227,18 @@ export class VerifaydaService {
|
||||
const login = await this.issueLoginToken(userId);
|
||||
result = { purpose: 'LOGIN', verified: true, ...login };
|
||||
} else {
|
||||
// VERIFY — prove identity, save to IAM, return verified attributes.
|
||||
const { userDataSaved } = await this.upsertIamUser(normalized);
|
||||
// VERIFY — prove identity, save to IAM, return verified attributes + short-lived token.
|
||||
const { iamUserId, userDataSaved } = await this.upsertIamUser(normalized);
|
||||
|
||||
let sessionToken: { token: string; refreshToken: string; requiresPassword: boolean } | undefined;
|
||||
if (iamUserId) {
|
||||
try {
|
||||
sessionToken = await this.createFaydaSession(iamUserId);
|
||||
} catch (err) {
|
||||
this.logger.warn(`Fayda session creation failed: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
result = {
|
||||
purpose: 'VERIFY',
|
||||
verified: true,
|
||||
@@ -231,6 +248,11 @@ export class VerifaydaService {
|
||||
birthdate: normalized.birthdate,
|
||||
gender: normalized.gender,
|
||||
userDataSaved,
|
||||
iamUserId: iamUserId ?? undefined,
|
||||
token: sessionToken?.token,
|
||||
refreshToken: sessionToken?.refreshToken,
|
||||
requiresPassword: sessionToken?.requiresPassword,
|
||||
promptPasswordSetup: session.saveToAccount && (sessionToken?.requiresPassword ?? false),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -298,14 +320,19 @@ export class VerifaydaService {
|
||||
claims_locales: this.faydaConfig.claimsLocales,
|
||||
});
|
||||
|
||||
// Every claim is marked essential so eSignet shows them locked/pre-checked
|
||||
// on the consent screen — the user cannot toggle any off; they either
|
||||
// consent to all of them or the whole flow is cancelled (?error=...).
|
||||
const claims = {
|
||||
userinfo: {
|
||||
name: { essential: true },
|
||||
phone_number: { essential: true },
|
||||
email: { essential: false },
|
||||
email: { essential: true },
|
||||
birthdate: { essential: true },
|
||||
gender: { essential: false },
|
||||
picture: { essential: false },
|
||||
gender: { essential: true },
|
||||
address: { essential: true },
|
||||
nationality: { essential: true },
|
||||
picture: { essential: true },
|
||||
},
|
||||
id_token: {},
|
||||
};
|
||||
@@ -447,12 +474,16 @@ export class VerifaydaService {
|
||||
phoneNumber: normalized.rawPhoneNumber ?? '',
|
||||
};
|
||||
|
||||
// Step 1 — already verified with same Fayda sub
|
||||
// Step 1 — already linked to this Fayda sub; ensure verified_by is set
|
||||
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) {
|
||||
await this.dataSource.query(
|
||||
`UPDATE iam.users SET verified_by = 'fayda', updated_at = NOW() WHERE id = $1`,
|
||||
[bySub[0].id],
|
||||
);
|
||||
return { iamUserId: bySub[0].id, userDataSaved: true };
|
||||
}
|
||||
|
||||
@@ -497,7 +528,7 @@ export class VerifaydaService {
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
gen_random_uuid(), $1::jsonb, $2, $3, $4, $5::jsonb,
|
||||
'individual', 'accepted', true, false,
|
||||
'individual', 'submitted', true, false,
|
||||
false, 'fayda',
|
||||
NOW(), NOW()
|
||||
) RETURNING id`,
|
||||
@@ -516,6 +547,60 @@ export class VerifaydaService {
|
||||
}
|
||||
}
|
||||
|
||||
private async createFaydaSession(
|
||||
iamUserId: string,
|
||||
): Promise<{ token: string; refreshToken: string; requiresPassword: boolean }> {
|
||||
const rows = await this.dataSource.query<{
|
||||
id: string;
|
||||
email: string;
|
||||
name: { en: string; am: string } | null;
|
||||
username: string;
|
||||
phone_number: string | null;
|
||||
has_set_password: boolean;
|
||||
status: string;
|
||||
}[]>(
|
||||
`SELECT id, email, name, username, phone_number, has_set_password, status
|
||||
FROM iam.users WHERE id = $1 LIMIT 1`,
|
||||
[iamUserId],
|
||||
);
|
||||
if (!rows.length) throw new Error(`IAM user ${iamUserId} not found`);
|
||||
const u = rows[0];
|
||||
|
||||
const userInfo = {
|
||||
id: u.id,
|
||||
email: u.email ?? '',
|
||||
name: u.name ?? { en: '', am: '' },
|
||||
userType: 'individual',
|
||||
status: u.status,
|
||||
hasSetPassword: u.has_set_password,
|
||||
isPhoneNumberVerified: false,
|
||||
hasFinishedRegistration: false,
|
||||
hasFinishedDMSOnboarding: false,
|
||||
username: u.username,
|
||||
phoneNumber: u.phone_number ?? '',
|
||||
roles: [],
|
||||
permissions: [],
|
||||
employee: [],
|
||||
};
|
||||
|
||||
const sessions = await this.dataSource.query<{ id: string }[]>(
|
||||
`INSERT INTO iam.sessions
|
||||
(id, email, device, "userInfo", expiry_time, refresh_count, status, user_id)
|
||||
VALUES (gen_random_uuid(), $1, 'fayda-verify', $2::jsonb, NOW() + INTERVAL '1 day', 0, 'ACTIVE', $3)
|
||||
ON CONFLICT (user_id, device) DO UPDATE
|
||||
SET status = 'ACTIVE', "userInfo" = EXCLUDED."userInfo",
|
||||
expiry_time = NOW() + INTERVAL '1 day', updated_at = NOW()
|
||||
RETURNING id`,
|
||||
[u.email ?? '', JSON.stringify(userInfo), iamUserId],
|
||||
);
|
||||
|
||||
const sessionId = sessions[0].id;
|
||||
const token = generateToken({ id: sessionId });
|
||||
const refreshToken = generateRefreshToken({ id: sessionId });
|
||||
|
||||
return { token, refreshToken, requiresPassword: !u.has_set_password };
|
||||
}
|
||||
|
||||
private async markSessionFailed(
|
||||
state: string,
|
||||
errorCode: string,
|
||||
|
||||
Reference in New Issue
Block a user