Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions migration/1781862303000-AddWalletToSupportIssue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/

/**
* @class
* @implements {MigrationInterface}
*/
module.exports = class AddWalletToSupportIssue1781862303000 {
name = 'AddWalletToSupportIssue1781862303000'

/**
* Adds the source wallet (app the ticket was opened from) to support_issue.
* The column is NOT NULL: every creation path resolves an exact source (X-Client) or fails, so an
* unattributed ticket cannot exist. Legacy rows predate source attribution and carry no exact signal;
* they are backfilled to the DFX default wallet (Config.defaultWalletId = 1) as an explicit one-time
* legacy decision - NOT as a runtime fallback.
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "support_issue" ADD "walletId" integer`);
await queryRunner.query(`UPDATE "support_issue" SET "walletId" = 1 WHERE "walletId" IS NULL`);
await queryRunner.query(`ALTER TABLE "support_issue" ALTER COLUMN "walletId" SET NOT NULL`);
await queryRunner.query(`CREATE INDEX "IDX_f5224979beab23e21df3066a60" ON "support_issue" ("walletId")`);
await queryRunner.query(`ALTER TABLE "support_issue" ADD CONSTRAINT "FK_f5224979beab23e21df3066a60a" FOREIGN KEY ("walletId") REFERENCES "wallet"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}

/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "support_issue" DROP CONSTRAINT "FK_f5224979beab23e21df3066a60a"`);
await queryRunner.query(`DROP INDEX "IDX_f5224979beab23e21df3066a60"`);
await queryRunner.query(`ALTER TABLE "support_issue" DROP COLUMN "walletId"`);
}
}
29 changes: 29 additions & 0 deletions migration/1782911659474-AddUserDataKycFileIdUniqueIndex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/

/**
* Backs the app-level kycFileId uniqueness check (UserDataService.updateUserDataInternal) with a real
* constraint - that check is a read-then-write race between concurrent AML postProcessing calls.
*
* @class
* @implements {MigrationInterface}
*/
module.exports = class AddUserDataKycFileIdUniqueIndex1782911659474 {
name = 'AddUserDataKycFileIdUniqueIndex1782911659474';

/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_8dae6f6af0a6b5dc2ec16c333c" ON "user_data" ("kycFileId") WHERE "kycFileId" IS NOT NULL`);
}

/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_8dae6f6af0a6b5dc2ec16c333c"`);
}
};
26 changes: 26 additions & 0 deletions migration/1782990000000-AddUserDataServiceProviders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/

/**
* @class
* @implements {MigrationInterface}
*/
module.exports = class AddUserDataServiceProviders1782990000000 {
name = 'AddUserDataServiceProviders1782990000000'

/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_data" ADD "serviceProviders" character varying(256)`);
}

/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_data" DROP COLUMN "serviceProviders"`);
}
}
55 changes: 55 additions & 0 deletions migration/1782990000010-BackfillRealUnitServiceProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/

/**
* Backfills the additive RealUnit service-provider marker for existing customers.
* A userData is treated as a RealUnit customer if it has either
* (a) a user onboarded under the RealUnit wallet, or
* (b) a RealUnit (Aktionariat) registration KYC step.
* These are the durable, merge-surviving provenance signals; the RealUnit wallet name on the
* user is used only as historical backfill provenance, never as the runtime scoping anchor.
*
* @class
* @implements {MigrationInterface}
*/
module.exports = class BackfillRealUnitServiceProvider1782990000010 {
name = 'BackfillRealUnitServiceProvider1782990000010'

/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`
UPDATE "user_data" SET "serviceProviders" =
CASE
WHEN "serviceProviders" IS NULL OR "serviceProviders" = '' THEN 'RealUnit'
WHEN ';' || "serviceProviders" || ';' LIKE '%;RealUnit;%' THEN "serviceProviders"
ELSE "serviceProviders" || ';RealUnit'
END
WHERE "id" IN (
SELECT u."userDataId"
FROM "user" u
INNER JOIN "wallet" w ON w."id" = u."walletId"
WHERE w."name" = 'RealUnit'
UNION
SELECT ks."userDataId"
FROM "kyc_step" ks
WHERE ks."name" = 'RealUnitRegistration'
)
`);
}

/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`
UPDATE "user_data"
SET "serviceProviders" =
NULLIF(array_to_string(array_remove(string_to_array("serviceProviders", ';'), 'RealUnit'), ';'), '')
WHERE ';' || "serviceProviders" || ';' LIKE '%;RealUnit;%'
`);
}
}
28 changes: 28 additions & 0 deletions migration/1782990500000-AddTotpLockout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/

/**
* @class
* @implements {MigrationInterface}
*/
module.exports = class AddTotpLockout1782990500000 {
name = 'AddTotpLockout1782990500000';

/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_data" ADD "totpFailedAttempts" integer NOT NULL DEFAULT 0`);
await queryRunner.query(`ALTER TABLE "user_data" ADD "totpBlockedUntil" TIMESTAMP`);
}

/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_data" DROP COLUMN "totpBlockedUntil"`);
await queryRunner.query(`ALTER TABLE "user_data" DROP COLUMN "totpFailedAttempts"`);
}
};
8 changes: 6 additions & 2 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { SharedModule } from './shared/shared.module';
import { GetConfig } from './config/config';
import { IntegrationModule } from './integration/integration.module';
import { TfaEnforcementInterceptor } from './shared/auth/tfa-enforcement.interceptor';
import { SharedModule } from './shared/shared.module';
import { SubdomainsModule } from './subdomains/subdomains.module';

@Module({
imports: [TypeOrmModule.forRoot(GetConfig().database), SharedModule, IntegrationModule, SubdomainsModule],
controllers: [AppController],
providers: [],
// Global backstop enforcing STRICT app-2FA on every route for a mail-origin staff session (tfaRequired).
// Only needs ModuleRef + Reflector (both globally available), so no extra module imports are required.
providers: [{ provide: APP_INTERCEPTOR, useClass: TfaEnforcementInterceptor }],
exports: [],
})
export class AppModule {}
6 changes: 6 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,12 @@ export class Configuration {
noReplyMail: process.env.NOREPLY_MAIL || 'noreply@dfx.swiss',
},
wallet: {
// Explicit entry for the DFX house brand (name of the default wallet, Config.defaultWalletId), so a
// positively DFX-attributed mail is a first-class mapping and not the absence of every other brand.
// Values mirror the previous implicit defaults exactly (default transport, user-v2 template).
DFX: {
template: 'user-v2',
},
onchainlabs: {
template: 'onChainLabs',
},
Expand Down
Loading
Loading