From 16c085e6b51453b02ce5a2fba863206e358f3d40 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 24 Oct 2025 00:01:05 +0200 Subject: [PATCH 1/6] feat: added timeout for prisma service --- .circleci/config.yml | 1 + .env.sample | 11 ++++++++--- src/shared/member-prisma/member-prisma.service.ts | 8 ++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0d57867..cba9536 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -68,6 +68,7 @@ workflows: branches: only: - develop + - pm-2539 # Production builds are exectuted only on tagged commits to the # master branch. diff --git a/.env.sample b/.env.sample index 01e24c6..c68c255 100644 --- a/.env.sample +++ b/.env.sample @@ -143,6 +143,11 @@ SERVICEACC02_CID="devadmin1" SERVICEACC02_SECRET="devadmin1" SERVICEACC02_UID="100000027" -# Note: Registration default password is no longer configurable; for social/SSO -# registrations without a provided password, a unique 16-character random -# password is generated at registration time. +# Note: Registration default password is no longer configurable; for social/SSO +# registrations without a provided password, a unique 16-character random +# password is generated at registration time. + + +# Prisma configuration + +IDENTITY_SERVICE_PRISMA_TIMEOUT=10000 \ No newline at end of file diff --git a/src/shared/member-prisma/member-prisma.service.ts b/src/shared/member-prisma/member-prisma.service.ts index 67f421f..a0ebab3 100644 --- a/src/shared/member-prisma/member-prisma.service.ts +++ b/src/shared/member-prisma/member-prisma.service.ts @@ -6,6 +6,14 @@ export class MemberPrismaService extends MemberPrismaClient implements OnModuleInit, OnModuleDestroy { + constructor() { + super({ + transactionOptions: { + timeout: process.env.IDENTITY_SERVICE_PRISMA_TIMEOUT || 10000, + }, + }); + } + async onModuleInit() { await this.$connect(); } From 3c015388f5603e10edc4e465b56574d6d129264e Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Sat, 25 Oct 2025 00:40:15 +0200 Subject: [PATCH 2/6] fix: parse to int before applying timeout --- src/shared/member-prisma/member-prisma.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/shared/member-prisma/member-prisma.service.ts b/src/shared/member-prisma/member-prisma.service.ts index a0ebab3..8591d0b 100644 --- a/src/shared/member-prisma/member-prisma.service.ts +++ b/src/shared/member-prisma/member-prisma.service.ts @@ -9,7 +9,9 @@ export class MemberPrismaService constructor() { super({ transactionOptions: { - timeout: process.env.IDENTITY_SERVICE_PRISMA_TIMEOUT || 10000, + timeout: process.env.IDENTITY_SERVICE_PRISMA_TIMEOUT + ? parseInt(process.env.IDENTITY_SERVICE_PRISMA_TIMEOUT, 10) + : 10000, }, }); } From bfeae659dcb1438ecf3033c1c8a0554d912cc5af Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Mon, 27 Oct 2025 21:33:02 +1100 Subject: [PATCH 3/6] Fix error seen in dev --- src/api/role/role.service.spec.ts | 7 +++++-- src/api/role/role.service.ts | 17 +++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/api/role/role.service.spec.ts b/src/api/role/role.service.spec.ts index f454f75..fdf5913 100644 --- a/src/api/role/role.service.spec.ts +++ b/src/api/role/role.service.spec.ts @@ -390,13 +390,16 @@ describe('RoleService', () => { ).rejects.toThrow(NotFoundException); }); - it('should throw ConflictException if assignment already exists', async () => { + it('should ignore duplicate assignment if already exists', async () => { mockPrisma.role.count.mockResolvedValue(1); mockPrisma.roleAssignment.create.mockRejectedValue({ code: 'P2002' }); await expect( service.assignRoleToSubject(roleId, subjectId, operatorId), - ).rejects.toThrow(ConflictException); + ).resolves.toBeUndefined(); + expect(mockLogger.warn).toHaveBeenCalledWith( + `Attempt to assign role ${roleId} to subject ${subjectId} which already exists. Ignoring duplicate.`, + ); }); }); diff --git a/src/api/role/role.service.ts b/src/api/role/role.service.ts index 469d666..868c167 100644 --- a/src/api/role/role.service.ts +++ b/src/api/role/role.service.ts @@ -376,18 +376,15 @@ export class RoleService { } catch (error) { if (error.code === Constants.prismaUniqueConflictcode) { this.logger.warn( - `Attempt to assign role ${roleId} to subject ${subjectId} which already exists.`, + `Attempt to assign role ${roleId} to subject ${subjectId} which already exists. Ignoring duplicate.`, ); - throw new ConflictException( - `Role ${roleId} is already assigned to subject ${subjectId}.`, - ); - } else { - this.logger.error( - `Failed to assign role ${roleId} to subject ${subjectId}: ${error.message}`, - error.stack, - ); - throw error; + return; } + this.logger.error( + `Failed to assign role ${roleId} to subject ${subjectId}: ${error.message}`, + error.stack, + ); + throw error; } } From d3c2038a015909618b38b5026ab224030e926f89 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Tue, 28 Oct 2025 15:27:07 +0200 Subject: [PATCH 4/6] Add Trivy scanner workflow for vulnerability scanning --- .github/workflows/trivy.yaml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/trivy.yaml diff --git a/.github/workflows/trivy.yaml b/.github/workflows/trivy.yaml new file mode 100644 index 0000000..7b9fa48 --- /dev/null +++ b/.github/workflows/trivy.yaml @@ -0,0 +1,34 @@ +name: Trivy Scanner + +permissions: + contents: read + security-events: write +on: + push: + branches: + - main + - dev + pull_request: +jobs: + trivy-scan: + name: Use Trivy + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Trivy scanner in repo mode + uses: aquasecurity/trivy-action@0.33.1 + with: + scan-type: "fs" + ignore-unfixed: true + format: "sarif" + output: "trivy-results.sarif" + severity: "CRITICAL,HIGH,UNKNOWN" + scanners: vuln,secret,misconfig,license + github-pat: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: "trivy-results.sarif" From e1a4f4a2a3af73d53db3175d13bf77dca668f5aa Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 30 Oct 2025 11:15:57 +1100 Subject: [PATCH 5/6] Migration tweaks and a fix for roles not being saved properly --- legacy_migrate/src/migrate.ts | 216 +++++++++++++++++++++++++++++++++- prisma/schema.prisma | 29 +++++ 2 files changed, 244 insertions(+), 1 deletion(-) diff --git a/legacy_migrate/src/migrate.ts b/legacy_migrate/src/migrate.ts index 0de1786..e1275cb 100644 --- a/legacy_migrate/src/migrate.ts +++ b/legacy_migrate/src/migrate.ts @@ -64,6 +64,7 @@ interface CliOptions { const cliOptions = parseCliOptions(process.argv.slice(2)); const CHANGE_WINDOW_START = cliOptions.mode === 'delta' ? cliOptions.since : null; const deltaTablesLogged = new Set(); +const deltaTableStats = new Map(); if (cliOptions.mode === 'delta') { console.log( @@ -81,6 +82,12 @@ function applyChangeWindow>( fields: string[], label: string ): T { + if (!deltaTableStats.has(label)) { + deltaTableStats.set(label, { fields, fetches: 0, matched: 0 }); + } else if (fields.length && deltaTableStats.get(label)!.fields.length === 0) { + deltaTableStats.get(label)!.fields = fields; + } + if (!CHANGE_WINDOW_START) { return args; } @@ -88,7 +95,7 @@ function applyChangeWindow>( if (fields.length === 0) { if (!deltaTablesLogged.has(label)) { deltaTablesLogged.add(label); - console.log(`[delta] ${label}: no timestamp fields; exporting full set`); + console.log(`[delta] ${label}: no timestamp fields; exporting full set (delta filter skipped)`); } return args; } @@ -102,6 +109,7 @@ function applyChangeWindow>( console.log( `[delta] ${label}: filtering rows where ${fields.join(' OR ')} ≥ ${CHANGE_WINDOW_START.toISOString()}` ); + console.log(`[delta] ${label}: resolved where clause ${JSON.stringify(deltaWhere)}`); } const baseArgs: any = { ...args }; @@ -113,6 +121,66 @@ function applyChangeWindow>( return baseArgs; } +function recordDeltaFetch(label: string, count: number) { + if (!deltaTableStats.has(label)) { + deltaTableStats.set(label, { fields: [], fetches: 0, matched: 0 }); + } + const stats = deltaTableStats.get(label)!; + stats.fetches += 1; + stats.matched += count; + if (cliOptions.mode === 'delta' && count === 0 && stats.fetches === 1) { + console.warn(`[delta] ${label}: first fetch returned 0 rows during delta migration.`); + } +} + +function resolvePrismaDelegate(label: string) { + if (!label) { + return null; + } + const [clientKey, ...modelParts] = label.split('.'); + const modelName = modelParts.join('.'); + const client = clientKey === 'target' ? target : clientKey === 'sourceAuth' ? sourceAuth : clientKey === 'sourceIdentity' ? sourceIdentity : null; + if (!client || !modelName) { + return null; + } + const delegate = (client as any)[modelName]; + if (!delegate || typeof delegate.count !== 'function') { + return null; + } + return delegate; +} + +function buildDeltaWhereClause(fields: string[]) { + if (!CHANGE_WINDOW_START || !fields || fields.length === 0) { + return undefined; + } + return { + OR: fields.map((field) => ({ [field]: { gte: CHANGE_WINDOW_START } })) + }; +} + +function logDeltaTableSummary() { + if (deltaTableStats.size === 0) { + if (cliOptions.mode === 'delta') { + console.log('[delta] No delta table statistics were collected.'); + } + return; + } + + console.log('[delta] Table fetch summary:'); + deltaTableStats.forEach((stats, label) => { + const fieldsSummary = stats.fields.length ? stats.fields.join(', ') : 'n/a'; + console.log(`[delta] ${label}: matched=${stats.matched}, fetches=${stats.fetches}, fields=${fieldsSummary}`); + if (cliOptions.mode === 'delta') { + if (stats.matched === 0) { + console.warn(`[delta] ${label}: matched 0 records; verify incremental filters for this table.`); + } else if (stats.matched > 100_000) { + console.warn(`[delta] ${label}: processed ${stats.matched} records during delta run; confirm the since-date filter is correct.`); + } + } + }); +} + function parseCliOptions(argv: string[]): CliOptions { let mode: RunMode | null = null; let sinceRaw: string | null = process.env.MIGRATE_SINCE ?? null; @@ -263,6 +331,7 @@ async function migrateRoles() { 'sourceAuth.role' ) ); + recordDeltaFetch('sourceAuth.role', batch.length); if (batch.length === 0) break; await target.$transaction( @@ -304,6 +373,7 @@ async function migrateClients() { 'sourceAuth.client' ) ); + recordDeltaFetch('sourceAuth.client', batch.length); if (batch.length === 0) break; await target.$transaction( @@ -350,6 +420,7 @@ async function migrateRoleAssignments() { 'sourceAuth.roleAssignment' ) ); + recordDeltaFetch('sourceAuth.roleAssignment', batch.length); if (batch.length === 0) break; await target.$transaction( @@ -406,6 +477,7 @@ async function migrateAchievementTypeLu() { 'sourceIdentity.achievement_type_lu' ) ); + recordDeltaFetch('sourceIdentity.achievement_type_lu', batch.length); if (batch.length === 0) break; await target.$transaction( @@ -445,6 +517,7 @@ async function migrateCountry() { 'sourceIdentity.country' ) ); + recordDeltaFetch('sourceIdentity.country', batch.length); if (batch.length === 0) break; await target.$transaction( @@ -502,6 +575,7 @@ async function migrateEmailStatusLu() { 'sourceIdentity.email_status_lu' ) ); + recordDeltaFetch('sourceIdentity.email_status_lu', batch.length); if (batch.length === 0) break; await target.$transaction( @@ -544,6 +618,7 @@ async function migrateEmailTypeLu() { 'sourceIdentity.email_type_lu' ) ); + recordDeltaFetch('sourceIdentity.email_type_lu', batch.length); if (batch.length === 0) break; await target.$transaction( @@ -586,6 +661,7 @@ async function migrateInvalidHandles() { 'sourceIdentity.invalid_handles' ) ); + recordDeltaFetch('sourceIdentity.invalid_handles', batch.length); if (batch.length === 0) break; await target.$transaction( @@ -625,6 +701,7 @@ async function migrateSecurityStatusLu() { 'sourceIdentity.security_status_lu' ) ); + recordDeltaFetch('sourceIdentity.security_status_lu', batch.length); if (batch.length === 0) break; await target.$transaction( @@ -664,6 +741,7 @@ async function migrateSecurityGroups() { 'sourceIdentity.security_groups' ) ); + recordDeltaFetch('sourceIdentity.security_groups', batch.length); if (batch.length === 0) break; await target.$transaction( @@ -1458,6 +1536,139 @@ async function migrateUserStatus() { // ===== Main ===== +async function validateDeltaMigration() { + console.log('[delta] Performing preflight validation…'); + if (cliOptions.mode !== 'delta') { + console.log('[delta] Run mode is full; delta validation skipped.'); + return; + } + + if (!CHANGE_WINDOW_START) { + throw new Error('[delta] Delta mode requires a since-date; none was resolved.'); + } + + const now = new Date(); + if (CHANGE_WINDOW_START > now) { + throw new Error(`[delta] Since-date ${CHANGE_WINDOW_START.toISOString()} is in the future.`); + } + + const windowHours = (now.getTime() - CHANGE_WINDOW_START.getTime()) / 36e5; + if (windowHours > 24 * 30) { + const windowDays = Math.round(windowHours / 24); + console.warn(`[delta] Since-date is ${windowDays} days old; ensure this wide window is intentional.`); + } + + const connectionChecks = [ + { label: 'target', client: target }, + { label: 'sourceAuth', client: sourceAuth }, + { label: 'sourceIdentity', client: sourceIdentity } + ]; + + for (const { label, client } of connectionChecks) { + try { + await client.$queryRawUnsafe('SELECT 1'); + console.log(`[delta] Connectivity check OK: ${label}`); + } catch (err: any) { + throw new Error(`[delta] Connectivity check failed for ${label}: ${err.message}`); + } + } + + console.log(`[delta] Change window start: ${CHANGE_WINDOW_START.toISOString()}`); + console.log(`[delta] Parallel worker limit: ${PARALLEL_LIMIT}`); +} + +async function performReferentialIntegrityChecks() { + if (cliOptions.mode !== 'delta') { + return; + } + + console.log('[delta] Validating basic referential integrity on target…'); + const checks: Array<{ label: string; query: () => Promise }> = [ + { + label: 'roleAssignment.role', + query: () => target.roleAssignment.count({ where: { role: { is: null } } }) + }, + { + label: 'userEmail.user', + query: () => target.userEmail.count({ where: { user: { is: null } } }) + }, + { + label: 'userStatus.user', + query: () => target.userStatus.count({ where: { user: { is: null } } }) + } + ]; + + for (const check of checks) { + try { + const orphans = await check.query(); + if (orphans > 0) { + console.warn(`[delta] Referential check failed for ${check.label}: ${orphans} orphan record(s).`); + } else { + console.log(`[delta] Referential check passed for ${check.label}.`); + } + } catch (err: any) { + console.warn(`[delta] Referential check errored for ${check.label}: ${err.message}`); + } + } +} + +async function verifyDeltaMigration() { + if (cliOptions.mode !== 'delta') { + logDeltaTableSummary(); + return; + } + + console.log('[delta] Verifying delta migration results…'); + const discrepancies: Array<{ label: string; source: number; target: number }> = []; + + for (const [label, stats] of deltaTableStats.entries()) { + if (!stats.fields.length) { + console.log(`[delta] ${label}: no delta fields registered; skipping count comparison.`); + continue; + } + + const sourceDelegate = resolvePrismaDelegate(label); + const targetLabel = label.startsWith('sourceAuth.') || label.startsWith('sourceIdentity.') + ? `target.${label.split('.').slice(1).join('.')}` + : null; + if (!sourceDelegate || !targetLabel) { + console.warn(`[delta] ${label}: unable to resolve source delegate; skipping verification.`); + continue; + } + + const targetDelegate = resolvePrismaDelegate(targetLabel); + if (!targetDelegate) { + console.warn(`[delta] ${label}: unable to resolve target delegate (${targetLabel}); skipping.`); + continue; + } + + const whereClause = buildDeltaWhereClause(stats.fields); + try { + const [sourceCount, targetCount] = await Promise.all([ + sourceDelegate.count({ where: whereClause }), + targetDelegate.count({ where: whereClause }) + ]); + if (sourceCount !== targetCount) { + discrepancies.push({ label, source: sourceCount, target: targetCount }); + console.warn(`[delta] ${label}: count mismatch (source=${sourceCount}, target=${targetCount}).`); + } else { + console.log(`[delta] ${label}: counts OK (${sourceCount}).`); + } + } catch (err: any) { + console.warn(`[delta] ${label}: unable to verify counts (${err.message}).`); + } + } + + await performReferentialIntegrityChecks(); + logDeltaTableSummary(); + + if (discrepancies.length) { + console.warn(`[delta] Count verification identified ${discrepancies.length} table(s) with mismatches.`); + } else { + console.log('[delta] Source and target counts match for monitored tables.'); + } +} + async function main() { const descriptor = cliOptions.mode === 'delta' @@ -1465,6 +1676,8 @@ async function main() { : 'mode=full'; console.log(`Starting migration… (${descriptor})`); + await validateDeltaMigration(); + // // 1) Auth (MySQL) → target await runParallel('auth (roles + clients)', [migrateRoles, migrateClients]); await migrateRoleAssignments(); @@ -1499,6 +1712,7 @@ async function main() { migrateUserStatus, ]); + await verifyDeltaMigration(); console.log('✓ Migration complete.'); } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 08ec1aa..1391cff 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,10 +1,12 @@ generator client { provider = "prisma-client-js" + previewFeatures = ["multiSchema"] } datasource db { provider = "postgresql" url = env("IDENTITY_DB_URL") + schemas = ["identity"] } // IDENTITY SCHEMA MODELS @@ -16,6 +18,7 @@ model achievement_type_lu { achievement_type_id Decimal @id(map: "achv_type_lu_pkey") @db.Decimal(5, 0) achievement_type_desc String @db.VarChar(64) user_achievement user_achievement[] + @@schema("identity") } model country { @@ -31,6 +34,7 @@ model country { iso_alpha2_code String? @db.VarChar(2) iso_alpha3_code String? @db.VarChar(3) + @@schema("identity") } model dice_connection { @@ -45,6 +49,7 @@ model dice_connection { @@index([connection]) + @@schema("identity") } model email { @@ -64,6 +69,7 @@ model email { @@index([user_id, primary_ind], map: "email_user_id_idx") @@index([user_id, email_type_id, status_id]) @@index([address, status_id]) + @@schema("identity") } model email_status_lu { @@ -73,6 +79,7 @@ model email_status_lu { modify_date DateTime? @default(now()) @db.Timestamp(6) email email[] user_email_xref user_email_xref[] // Added back-relation to user_email_xref + @@schema("identity") } model email_type_lu { @@ -82,6 +89,7 @@ model email_type_lu { modify_date DateTime? @default(now()) @db.Timestamp(6) email email[] + @@schema("identity") } model id_sequences { @@ -90,12 +98,14 @@ model id_sequences { block_size Decimal @db.Decimal(10, 0) exhausted Decimal @default(0) @db.Decimal(1, 0) + @@schema("identity") } model invalid_handles { invalid_handle_id Int @id(map: "pk_invalid_hand556") invalid_handle String @db.VarChar(20) + @@schema("identity") } @@ -105,6 +115,7 @@ model security_groups { challenge_group_ind Int @default(0) @db.SmallInt create_user_id Decimal? @db.Decimal(12, 0) user_group_xref user_group_xref[] + @@schema("identity") } model security_status_lu { @@ -112,6 +123,7 @@ model security_status_lu { status_desc String? @db.VarChar(200) user_group_xref user_group_xref[] + @@schema("identity") } model security_user { @@ -122,6 +134,7 @@ model security_user { modify_date DateTime? @db.Timestamp(6) user_group_xref user_group_xref[] + @@schema("identity") } @@ -130,6 +143,7 @@ model social_login_provider { name String? @db.VarChar(50) user_social_login user_social_login[] + @@schema("identity") } model sso_login_provider { @@ -140,6 +154,7 @@ model sso_login_provider { identify_handle_enabled Boolean @default(true) user_sso_login user_sso_login[] + @@schema("identity") } model user { @@ -178,6 +193,7 @@ model user { @@index([handle_lower], map: "user_lower_handle_idx") @@index([open_id]) @@index([status, handle_lower]) + @@schema("identity") } model user_2fa { @@ -191,6 +207,7 @@ model user_2fa { modified_at DateTime @default(now()) @db.Timestamp(6) user user @relation(fields: [user_id], references: [user_id], onDelete: NoAction, onUpdate: NoAction) + @@schema("identity") } /// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. @@ -206,6 +223,7 @@ model user_achievement { // Define composite primary key @@id([user_id, achievement_type_id], map: "user_achievement_pkey") // Added @@id // @@ignore // Removed @@ignore + @@schema("identity") } model user_group_xref { @@ -221,6 +239,7 @@ model user_group_xref { @@unique([login_id, group_id], map: "user_grp_xref_i2") + @@schema("identity") } model user_otp_email { @@ -235,6 +254,7 @@ model user_otp_email { @@unique([user_id, mode]) + @@schema("identity") } model user_social_login { @@ -252,6 +272,7 @@ model user_social_login { @@id([user_id, social_login_provider_id], map: "user_social_prkey") @@index([social_user_id, social_login_provider_id]) + @@schema("identity") } model user_sso_login { @@ -266,6 +287,7 @@ model user_sso_login { @@id([user_id, provider_id], map: "user_sso_prkey") // Primary key @@index([sso_user_id, provider_id], map: "idx_user_social_login_sso_user_id_provider_id") // Index only, NOT unique + @@schema("identity") } model user_status { @@ -278,6 +300,7 @@ model user_status { @@id([user_id, user_status_type_id], map: "userstatus_pk") + @@schema("identity") } model user_status_lu { @@ -285,6 +308,7 @@ model user_status_lu { description String? @db.VarChar(100) user_status user_status[] + @@schema("identity") } model user_status_type_lu { @@ -292,6 +316,7 @@ model user_status_type_lu { description String? @db.VarChar(100) user_status user_status[] + @@schema("identity") } // Explicit definition for the user-email join table @@ -310,6 +335,7 @@ model user_email_xref { @@id([user_id, email_id], map: "user_email_xref_pkey") // Define composite primary key @@index([email_id], map: "user_email_xref_email_idx") @@index([user_id, status_id]) + @@schema("identity") } @@ -329,6 +355,7 @@ model Client { modifiedAt DateTime? @db.Timestamp(0) @@map("client") // Map model name to table name + @@schema("identity") } model Role { @@ -342,6 +369,7 @@ model Role { roleAssignments RoleAssignment[] // Relation field @@map("role") // Map model name to table name + @@schema("identity") } model RoleAssignment { @@ -361,4 +389,5 @@ model RoleAssignment { @@index([roleId], map: "role_id_idx") @@index([subjectId, subjectType]) @@map("role_assignment") + @@schema("identity") } From c558b5ef7ce285aa83faa8b8eb3f9b6b5d019872 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 30 Oct 2025 20:38:33 +1100 Subject: [PATCH 6/6] Add Topcoder User at initial creation instead of after user validation --- src/api/user/user.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/api/user/user.service.ts b/src/api/user/user.service.ts index d4e9272..8c259a8 100644 --- a/src/api/user/user.service.ts +++ b/src/api/user/user.service.ts @@ -953,6 +953,10 @@ export class UserService { this.logger.log('Primary Role to be saved: ' + primaryRole); // assign primary role await this.roleService.assignRoleByName(primaryRole, userId, userId); + // Assign 'Topcoder User' at the same time, to avoid weird issues after the first login + if (primaryRole == 'Topcoder Talent') { + await this.roleService.assignRoleByName('Topcoder User', userId, userId); + } } private async getNextUserId(): Promise {