@scryan7371/sdr-security 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/migrations/1700000000001-add-refresh-tokens.js +2 -2
- package/dist/api/migrations/1739490000000-create-app-user.d.ts +9 -0
- package/dist/api/migrations/1739490000000-create-app-user.js +34 -0
- package/dist/api/migrations/1739500000000-create-security-identity.js +1 -1
- package/dist/api/migrations/1739515000000-create-security-user-roles.js +1 -1
- package/dist/api/migrations/1739520000000-create-password-reset-tokens.js +1 -1
- package/dist/api/migrations/1739530000000-create-security-user.js +1 -1
- package/dist/api/migrations/index.d.ts +4 -2
- package/dist/api/migrations/index.js +6 -2
- package/dist/api/migrations/migrations.test.js +21 -0
- package/dist/integration/database.integration.test.js +2 -9
- package/dist/nest/entities/app-user.entity.js +1 -1
- package/dist/nest/entities/password-reset-token.entity.d.ts +1 -0
- package/dist/nest/entities/password-reset-token.entity.js +14 -2
- package/dist/nest/entities/refresh-token.entity.js +2 -2
- package/dist/nest/entities/security-role.entity.d.ts +1 -0
- package/dist/nest/entities/security-role.entity.js +13 -1
- package/dist/nest/entities/security-user-role.entity.d.ts +1 -0
- package/dist/nest/entities/security-user-role.entity.js +14 -2
- package/dist/nest/entities/security-user.entity.js +1 -1
- package/dist/nest/security-auth.service.js +4 -1
- package/dist/nest/security-auth.service.test.js +3 -1
- package/dist/nest/security-workflows.controller.d.ts +4 -4
- package/dist/nest/security-workflows.controller.js +36 -16
- package/dist/nest/security-workflows.service.js +4 -0
- package/package.json +3 -2
- package/src/api/migrations/1700000000001-add-refresh-tokens.ts +2 -2
- package/src/api/migrations/1739490000000-create-app-user.ts +39 -0
- package/src/api/migrations/1739500000000-create-security-identity.ts +1 -1
- package/src/api/migrations/1739515000000-create-security-user-roles.ts +1 -1
- package/src/api/migrations/1739520000000-create-password-reset-tokens.ts +1 -1
- package/src/api/migrations/1739530000000-create-security-user.ts +1 -1
- package/src/api/migrations/index.ts +8 -1
- package/src/api/migrations/migrations.test.ts +30 -1
- package/src/integration/database.integration.test.ts +3 -11
- package/src/nest/entities/app-user.entity.ts +2 -2
- package/src/nest/entities/password-reset-token.entity.ts +12 -3
- package/src/nest/entities/refresh-token.entity.ts +2 -2
- package/src/nest/entities/security-role.entity.ts +10 -2
- package/src/nest/entities/security-user-role.entity.ts +11 -3
- package/src/nest/entities/security-user.entity.ts +1 -1
- package/src/nest/security-auth.service.test.ts +4 -1
- package/src/nest/security-auth.service.ts +5 -2
- package/src/nest/security-workflows.controller.ts +42 -13
- package/src/nest/security-workflows.service.ts +4 -0
|
@@ -7,11 +7,11 @@ class AddRefreshTokens1700000000001 {
|
|
|
7
7
|
const userTableRef = getUserTableReference();
|
|
8
8
|
await queryRunner.query(`
|
|
9
9
|
CREATE TABLE "refresh_token" (
|
|
10
|
-
"id"
|
|
10
|
+
"id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
|
|
11
11
|
"token_hash" varchar NOT NULL,
|
|
12
12
|
"expires_at" timestamptz NOT NULL,
|
|
13
13
|
"revoked_at" timestamptz,
|
|
14
|
-
"userId"
|
|
14
|
+
"userId" uuid,
|
|
15
15
|
"created_at" timestamptz NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
|
16
16
|
CONSTRAINT "FK_refresh_token_user" FOREIGN KEY ("userId") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
|
17
17
|
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CreateAppUser1739490000000 = void 0;
|
|
4
|
+
class CreateAppUser1739490000000 {
|
|
5
|
+
name = "CreateAppUser1739490000000";
|
|
6
|
+
async up(queryRunner) {
|
|
7
|
+
const userTableRef = getUserTableReference();
|
|
8
|
+
await queryRunner.query(`
|
|
9
|
+
CREATE TABLE IF NOT EXISTS ${userTableRef} (
|
|
10
|
+
"id" uuid PRIMARY KEY NOT NULL,
|
|
11
|
+
"email" varchar NOT NULL
|
|
12
|
+
)
|
|
13
|
+
`);
|
|
14
|
+
}
|
|
15
|
+
async down(queryRunner) {
|
|
16
|
+
const userTableRef = getUserTableReference();
|
|
17
|
+
await queryRunner.query(`DROP TABLE IF EXISTS ${userTableRef}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.CreateAppUser1739490000000 = CreateAppUser1739490000000;
|
|
21
|
+
const getSafeIdentifier = (value, fallback) => {
|
|
22
|
+
const resolved = value?.trim() || fallback;
|
|
23
|
+
if (!resolved || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(resolved)) {
|
|
24
|
+
throw new Error(`Invalid SQL identifier: ${resolved}`);
|
|
25
|
+
}
|
|
26
|
+
return resolved;
|
|
27
|
+
};
|
|
28
|
+
const getUserTableReference = () => {
|
|
29
|
+
const table = getSafeIdentifier(process.env.USER_TABLE, "app_user");
|
|
30
|
+
const schema = process.env.USER_TABLE_SCHEMA
|
|
31
|
+
? getSafeIdentifier(process.env.USER_TABLE_SCHEMA, "public")
|
|
32
|
+
: "public";
|
|
33
|
+
return `"${schema}"."${table}"`;
|
|
34
|
+
};
|
|
@@ -8,7 +8,7 @@ class CreateSecurityIdentity1739500000000 {
|
|
|
8
8
|
await queryRunner.query(`
|
|
9
9
|
CREATE TABLE IF NOT EXISTS "security_identity" (
|
|
10
10
|
"id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
|
|
11
|
-
"user_id"
|
|
11
|
+
"user_id" uuid NOT NULL,
|
|
12
12
|
"provider" varchar NOT NULL,
|
|
13
13
|
"provider_subject" varchar NOT NULL,
|
|
14
14
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
@@ -8,7 +8,7 @@ class CreateSecurityUserRoles1739515000000 {
|
|
|
8
8
|
await queryRunner.query(`
|
|
9
9
|
CREATE TABLE IF NOT EXISTS "security_user_role" (
|
|
10
10
|
"id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
|
|
11
|
-
"user_id"
|
|
11
|
+
"user_id" uuid NOT NULL,
|
|
12
12
|
"role_id" uuid NOT NULL,
|
|
13
13
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
14
14
|
CONSTRAINT "FK_security_user_role_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE,
|
|
@@ -8,7 +8,7 @@ class CreatePasswordResetTokens1739520000000 {
|
|
|
8
8
|
await queryRunner.query(`
|
|
9
9
|
CREATE TABLE IF NOT EXISTS "security_password_reset_token" (
|
|
10
10
|
"id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
|
|
11
|
-
"user_id"
|
|
11
|
+
"user_id" uuid NOT NULL,
|
|
12
12
|
"token" varchar NOT NULL,
|
|
13
13
|
"expires_at" timestamptz NOT NULL,
|
|
14
14
|
"used_at" timestamptz,
|
|
@@ -7,7 +7,7 @@ class CreateSecurityUser1739530000000 {
|
|
|
7
7
|
const userTableRef = getUserTableReference();
|
|
8
8
|
await queryRunner.query(`
|
|
9
9
|
CREATE TABLE IF NOT EXISTS "security_user" (
|
|
10
|
-
"user_id"
|
|
10
|
+
"user_id" uuid PRIMARY KEY NOT NULL,
|
|
11
11
|
"password_hash" varchar NOT NULL,
|
|
12
12
|
"email_verified_at" timestamptz,
|
|
13
13
|
"email_verification_token" varchar,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { CreateAppUser1739490000000 } from "./1739490000000-create-app-user";
|
|
1
2
|
import { AddRefreshTokens1700000000001 } from "./1700000000001-add-refresh-tokens";
|
|
2
3
|
import { CreateSecurityIdentity1739500000000 } from "./1739500000000-create-security-identity";
|
|
3
4
|
import { CreateSecurityRoles1739510000000 } from "./1739510000000-create-security-roles";
|
|
4
5
|
import { CreateSecurityUserRoles1739515000000 } from "./1739515000000-create-security-user-roles";
|
|
5
6
|
import { CreatePasswordResetTokens1739520000000 } from "./1739520000000-create-password-reset-tokens";
|
|
6
7
|
import { CreateSecurityUser1739530000000 } from "./1739530000000-create-security-user";
|
|
7
|
-
export declare const
|
|
8
|
-
export
|
|
8
|
+
export declare const allMigrations: (typeof CreateAppUser1739490000000)[];
|
|
9
|
+
export declare const securityMigrations: (typeof CreateAppUser1739490000000)[];
|
|
10
|
+
export { CreateAppUser1739490000000, AddRefreshTokens1700000000001, CreateSecurityIdentity1739500000000, CreateSecurityRoles1739510000000, CreateSecurityUserRoles1739515000000, CreatePasswordResetTokens1739520000000, CreateSecurityUser1739530000000, };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CreateSecurityUser1739530000000 = exports.CreatePasswordResetTokens1739520000000 = exports.CreateSecurityUserRoles1739515000000 = exports.CreateSecurityRoles1739510000000 = exports.CreateSecurityIdentity1739500000000 = exports.AddRefreshTokens1700000000001 = exports.securityMigrations = void 0;
|
|
3
|
+
exports.CreateSecurityUser1739530000000 = exports.CreatePasswordResetTokens1739520000000 = exports.CreateSecurityUserRoles1739515000000 = exports.CreateSecurityRoles1739510000000 = exports.CreateSecurityIdentity1739500000000 = exports.AddRefreshTokens1700000000001 = exports.CreateAppUser1739490000000 = exports.securityMigrations = exports.allMigrations = void 0;
|
|
4
|
+
const _1739490000000_create_app_user_1 = require("./1739490000000-create-app-user");
|
|
5
|
+
Object.defineProperty(exports, "CreateAppUser1739490000000", { enumerable: true, get: function () { return _1739490000000_create_app_user_1.CreateAppUser1739490000000; } });
|
|
4
6
|
const _1700000000001_add_refresh_tokens_1 = require("./1700000000001-add-refresh-tokens");
|
|
5
7
|
Object.defineProperty(exports, "AddRefreshTokens1700000000001", { enumerable: true, get: function () { return _1700000000001_add_refresh_tokens_1.AddRefreshTokens1700000000001; } });
|
|
6
8
|
const _1739500000000_create_security_identity_1 = require("./1739500000000-create-security-identity");
|
|
@@ -13,7 +15,8 @@ const _1739520000000_create_password_reset_tokens_1 = require("./1739520000000-c
|
|
|
13
15
|
Object.defineProperty(exports, "CreatePasswordResetTokens1739520000000", { enumerable: true, get: function () { return _1739520000000_create_password_reset_tokens_1.CreatePasswordResetTokens1739520000000; } });
|
|
14
16
|
const _1739530000000_create_security_user_1 = require("./1739530000000-create-security-user");
|
|
15
17
|
Object.defineProperty(exports, "CreateSecurityUser1739530000000", { enumerable: true, get: function () { return _1739530000000_create_security_user_1.CreateSecurityUser1739530000000; } });
|
|
16
|
-
exports.
|
|
18
|
+
exports.allMigrations = [
|
|
19
|
+
_1739490000000_create_app_user_1.CreateAppUser1739490000000,
|
|
17
20
|
_1700000000001_add_refresh_tokens_1.AddRefreshTokens1700000000001,
|
|
18
21
|
_1739500000000_create_security_identity_1.CreateSecurityIdentity1739500000000,
|
|
19
22
|
_1739510000000_create_security_roles_1.CreateSecurityRoles1739510000000,
|
|
@@ -21,3 +24,4 @@ exports.securityMigrations = [
|
|
|
21
24
|
_1739520000000_create_password_reset_tokens_1.CreatePasswordResetTokens1739520000000,
|
|
22
25
|
_1739530000000_create_security_user_1.CreateSecurityUser1739530000000,
|
|
23
26
|
];
|
|
27
|
+
exports.securityMigrations = exports.allMigrations.filter((migration) => migration !== _1739490000000_create_app_user_1.CreateAppUser1739490000000);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const vitest_1 = require("vitest");
|
|
4
|
+
const _1739490000000_create_app_user_1 = require("./1739490000000-create-app-user");
|
|
4
5
|
const _1700000000001_add_refresh_tokens_1 = require("./1700000000001-add-refresh-tokens");
|
|
5
6
|
const _1739500000000_create_security_identity_1 = require("./1739500000000-create-security-identity");
|
|
6
7
|
const _1739510000000_create_security_roles_1 = require("./1739510000000-create-security-roles");
|
|
@@ -14,6 +15,18 @@ const originalEnv = { ...process.env };
|
|
|
14
15
|
vitest_1.vi.restoreAllMocks();
|
|
15
16
|
});
|
|
16
17
|
(0, vitest_1.describe)("security migrations", () => {
|
|
18
|
+
(0, vitest_1.it)("exports all migration list for test/dev", () => {
|
|
19
|
+
(0, vitest_1.expect)(index_1.allMigrations.length).toBe(7);
|
|
20
|
+
(0, vitest_1.expect)(index_1.allMigrations).toEqual([
|
|
21
|
+
_1739490000000_create_app_user_1.CreateAppUser1739490000000,
|
|
22
|
+
_1700000000001_add_refresh_tokens_1.AddRefreshTokens1700000000001,
|
|
23
|
+
_1739500000000_create_security_identity_1.CreateSecurityIdentity1739500000000,
|
|
24
|
+
_1739510000000_create_security_roles_1.CreateSecurityRoles1739510000000,
|
|
25
|
+
_1739515000000_create_security_user_roles_1.CreateSecurityUserRoles1739515000000,
|
|
26
|
+
_1739520000000_create_password_reset_tokens_1.CreatePasswordResetTokens1739520000000,
|
|
27
|
+
_1739530000000_create_security_user_1.CreateSecurityUser1739530000000,
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
17
30
|
(0, vitest_1.it)("exports migration list", () => {
|
|
18
31
|
(0, vitest_1.expect)(index_1.securityMigrations.length).toBe(6);
|
|
19
32
|
(0, vitest_1.expect)(index_1.securityMigrations).toEqual([
|
|
@@ -25,6 +38,14 @@ const originalEnv = { ...process.env };
|
|
|
25
38
|
_1739530000000_create_security_user_1.CreateSecurityUser1739530000000,
|
|
26
39
|
]);
|
|
27
40
|
});
|
|
41
|
+
(0, vitest_1.it)("runs app user migration up/down", async () => {
|
|
42
|
+
const query = vitest_1.vi.fn().mockResolvedValue(undefined);
|
|
43
|
+
const migration = new _1739490000000_create_app_user_1.CreateAppUser1739490000000();
|
|
44
|
+
await migration.up({ query });
|
|
45
|
+
await migration.down({ query });
|
|
46
|
+
(0, vitest_1.expect)(query).toHaveBeenCalledWith(vitest_1.expect.stringContaining('CREATE TABLE IF NOT EXISTS "public"."app_user"'));
|
|
47
|
+
(0, vitest_1.expect)(query).toHaveBeenCalledWith(vitest_1.expect.stringContaining('DROP TABLE IF EXISTS "public"."app_user"'));
|
|
48
|
+
});
|
|
28
49
|
(0, vitest_1.it)("runs refresh token migration up/down", async () => {
|
|
29
50
|
const query = vitest_1.vi.fn().mockResolvedValue(undefined);
|
|
30
51
|
const migration = new _1700000000001_add_refresh_tokens_1.AddRefreshTokens1700000000001();
|
|
@@ -95,15 +95,9 @@ const resetSchemaBeforeRun = process.env.SECURITY_TEST_RESET_SCHEMA !== "false";
|
|
|
95
95
|
}
|
|
96
96
|
await client.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`);
|
|
97
97
|
await client.query(`SET search_path TO "${schema}", public`);
|
|
98
|
-
await client.query(`
|
|
99
|
-
CREATE TABLE IF NOT EXISTS "${schema}"."app_user" (
|
|
100
|
-
"id" varchar PRIMARY KEY NOT NULL,
|
|
101
|
-
"email" varchar NOT NULL
|
|
102
|
-
)
|
|
103
|
-
`);
|
|
104
98
|
process.env.USER_TABLE = "app_user";
|
|
105
99
|
process.env.USER_TABLE_SCHEMA = schema;
|
|
106
|
-
for (const Migration of migrations_1.
|
|
100
|
+
for (const Migration of migrations_1.allMigrations) {
|
|
107
101
|
await new Migration().up(runner);
|
|
108
102
|
}
|
|
109
103
|
});
|
|
@@ -112,10 +106,9 @@ const resetSchemaBeforeRun = process.env.SECURITY_TEST_RESET_SCHEMA !== "false";
|
|
|
112
106
|
return;
|
|
113
107
|
}
|
|
114
108
|
if (!keepSchemaForDebug) {
|
|
115
|
-
for (const Migration of [...migrations_1.
|
|
109
|
+
for (const Migration of [...migrations_1.allMigrations].reverse()) {
|
|
116
110
|
await new Migration().down(runner);
|
|
117
111
|
}
|
|
118
|
-
await client.query(`DROP TABLE IF EXISTS "${schema}"."app_user"`);
|
|
119
112
|
await client.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
|
|
120
113
|
}
|
|
121
114
|
await client.end();
|
|
@@ -17,7 +17,7 @@ let AppUserEntity = class AppUserEntity {
|
|
|
17
17
|
};
|
|
18
18
|
exports.AppUserEntity = AppUserEntity;
|
|
19
19
|
__decorate([
|
|
20
|
-
(0, typeorm_1.
|
|
20
|
+
(0, typeorm_1.PrimaryColumn)({ type: "uuid" }),
|
|
21
21
|
__metadata("design:type", String)
|
|
22
22
|
], AppUserEntity.prototype, "id", void 0);
|
|
23
23
|
__decorate([
|
|
@@ -11,6 +11,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.PasswordResetTokenEntity = void 0;
|
|
13
13
|
const typeorm_1 = require("typeorm");
|
|
14
|
+
const uuid_1 = require("uuid");
|
|
14
15
|
let PasswordResetTokenEntity = class PasswordResetTokenEntity {
|
|
15
16
|
id;
|
|
16
17
|
userId;
|
|
@@ -18,14 +19,19 @@ let PasswordResetTokenEntity = class PasswordResetTokenEntity {
|
|
|
18
19
|
expiresAt;
|
|
19
20
|
usedAt;
|
|
20
21
|
createdAt;
|
|
22
|
+
ensureId() {
|
|
23
|
+
if (!this.id) {
|
|
24
|
+
this.id = (0, uuid_1.v7)();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
21
27
|
};
|
|
22
28
|
exports.PasswordResetTokenEntity = PasswordResetTokenEntity;
|
|
23
29
|
__decorate([
|
|
24
|
-
(0, typeorm_1.
|
|
30
|
+
(0, typeorm_1.PrimaryColumn)({ type: "uuid" }),
|
|
25
31
|
__metadata("design:type", String)
|
|
26
32
|
], PasswordResetTokenEntity.prototype, "id", void 0);
|
|
27
33
|
__decorate([
|
|
28
|
-
(0, typeorm_1.Column)({ type: "
|
|
34
|
+
(0, typeorm_1.Column)({ type: "uuid", name: "user_id" }),
|
|
29
35
|
__metadata("design:type", String)
|
|
30
36
|
], PasswordResetTokenEntity.prototype, "userId", void 0);
|
|
31
37
|
__decorate([
|
|
@@ -44,6 +50,12 @@ __decorate([
|
|
|
44
50
|
(0, typeorm_1.CreateDateColumn)({ name: "created_at" }),
|
|
45
51
|
__metadata("design:type", Date)
|
|
46
52
|
], PasswordResetTokenEntity.prototype, "createdAt", void 0);
|
|
53
|
+
__decorate([
|
|
54
|
+
(0, typeorm_1.BeforeInsert)(),
|
|
55
|
+
__metadata("design:type", Function),
|
|
56
|
+
__metadata("design:paramtypes", []),
|
|
57
|
+
__metadata("design:returntype", void 0)
|
|
58
|
+
], PasswordResetTokenEntity.prototype, "ensureId", null);
|
|
47
59
|
exports.PasswordResetTokenEntity = PasswordResetTokenEntity = __decorate([
|
|
48
60
|
(0, typeorm_1.Entity)({ name: "security_password_reset_token" })
|
|
49
61
|
], PasswordResetTokenEntity);
|
|
@@ -21,7 +21,7 @@ let RefreshTokenEntity = class RefreshTokenEntity {
|
|
|
21
21
|
};
|
|
22
22
|
exports.RefreshTokenEntity = RefreshTokenEntity;
|
|
23
23
|
__decorate([
|
|
24
|
-
(0, typeorm_1.PrimaryColumn)({ type: "
|
|
24
|
+
(0, typeorm_1.PrimaryColumn)({ type: "uuid" }),
|
|
25
25
|
__metadata("design:type", String)
|
|
26
26
|
], RefreshTokenEntity.prototype, "id", void 0);
|
|
27
27
|
__decorate([
|
|
@@ -37,7 +37,7 @@ __decorate([
|
|
|
37
37
|
__metadata("design:type", Object)
|
|
38
38
|
], RefreshTokenEntity.prototype, "revokedAt", void 0);
|
|
39
39
|
__decorate([
|
|
40
|
-
(0, typeorm_1.Column)({ type: "
|
|
40
|
+
(0, typeorm_1.Column)({ type: "uuid", name: "userId", nullable: true }),
|
|
41
41
|
__metadata("design:type", Object)
|
|
42
42
|
], RefreshTokenEntity.prototype, "userId", void 0);
|
|
43
43
|
__decorate([
|
|
@@ -10,16 +10,22 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.SecurityRoleEntity = void 0;
|
|
13
|
+
const uuid_1 = require("uuid");
|
|
13
14
|
const typeorm_1 = require("typeorm");
|
|
14
15
|
let SecurityRoleEntity = class SecurityRoleEntity {
|
|
15
16
|
id;
|
|
16
17
|
roleKey;
|
|
17
18
|
description;
|
|
18
19
|
isSystem;
|
|
20
|
+
ensureId() {
|
|
21
|
+
if (!this.id) {
|
|
22
|
+
this.id = (0, uuid_1.v7)();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
19
25
|
};
|
|
20
26
|
exports.SecurityRoleEntity = SecurityRoleEntity;
|
|
21
27
|
__decorate([
|
|
22
|
-
(0, typeorm_1.
|
|
28
|
+
(0, typeorm_1.PrimaryColumn)({ type: "uuid" }),
|
|
23
29
|
__metadata("design:type", String)
|
|
24
30
|
], SecurityRoleEntity.prototype, "id", void 0);
|
|
25
31
|
__decorate([
|
|
@@ -34,6 +40,12 @@ __decorate([
|
|
|
34
40
|
(0, typeorm_1.Column)({ type: "boolean", name: "is_system", default: false }),
|
|
35
41
|
__metadata("design:type", Boolean)
|
|
36
42
|
], SecurityRoleEntity.prototype, "isSystem", void 0);
|
|
43
|
+
__decorate([
|
|
44
|
+
(0, typeorm_1.BeforeInsert)(),
|
|
45
|
+
__metadata("design:type", Function),
|
|
46
|
+
__metadata("design:paramtypes", []),
|
|
47
|
+
__metadata("design:returntype", void 0)
|
|
48
|
+
], SecurityRoleEntity.prototype, "ensureId", null);
|
|
37
49
|
exports.SecurityRoleEntity = SecurityRoleEntity = __decorate([
|
|
38
50
|
(0, typeorm_1.Entity)({ name: "security_role" })
|
|
39
51
|
], SecurityRoleEntity);
|
|
@@ -10,25 +10,37 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.SecurityUserRoleEntity = void 0;
|
|
13
|
+
const uuid_1 = require("uuid");
|
|
13
14
|
const typeorm_1 = require("typeorm");
|
|
14
15
|
let SecurityUserRoleEntity = class SecurityUserRoleEntity {
|
|
15
16
|
id;
|
|
16
17
|
userId;
|
|
17
18
|
roleId;
|
|
19
|
+
ensureId() {
|
|
20
|
+
if (!this.id) {
|
|
21
|
+
this.id = (0, uuid_1.v7)();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
18
24
|
};
|
|
19
25
|
exports.SecurityUserRoleEntity = SecurityUserRoleEntity;
|
|
20
26
|
__decorate([
|
|
21
|
-
(0, typeorm_1.
|
|
27
|
+
(0, typeorm_1.PrimaryColumn)({ type: "uuid" }),
|
|
22
28
|
__metadata("design:type", String)
|
|
23
29
|
], SecurityUserRoleEntity.prototype, "id", void 0);
|
|
24
30
|
__decorate([
|
|
25
|
-
(0, typeorm_1.Column)({ type: "
|
|
31
|
+
(0, typeorm_1.Column)({ type: "uuid", name: "user_id" }),
|
|
26
32
|
__metadata("design:type", String)
|
|
27
33
|
], SecurityUserRoleEntity.prototype, "userId", void 0);
|
|
28
34
|
__decorate([
|
|
29
35
|
(0, typeorm_1.Column)({ type: "uuid", name: "role_id" }),
|
|
30
36
|
__metadata("design:type", String)
|
|
31
37
|
], SecurityUserRoleEntity.prototype, "roleId", void 0);
|
|
38
|
+
__decorate([
|
|
39
|
+
(0, typeorm_1.BeforeInsert)(),
|
|
40
|
+
__metadata("design:type", Function),
|
|
41
|
+
__metadata("design:paramtypes", []),
|
|
42
|
+
__metadata("design:returntype", void 0)
|
|
43
|
+
], SecurityUserRoleEntity.prototype, "ensureId", null);
|
|
32
44
|
exports.SecurityUserRoleEntity = SecurityUserRoleEntity = __decorate([
|
|
33
45
|
(0, typeorm_1.Entity)({ name: "security_user_role" })
|
|
34
46
|
], SecurityUserRoleEntity);
|
|
@@ -22,7 +22,7 @@ let SecurityUserEntity = class SecurityUserEntity {
|
|
|
22
22
|
};
|
|
23
23
|
exports.SecurityUserEntity = SecurityUserEntity;
|
|
24
24
|
__decorate([
|
|
25
|
-
(0, typeorm_1.PrimaryColumn)({ type: "
|
|
25
|
+
(0, typeorm_1.PrimaryColumn)({ type: "uuid", name: "user_id" }),
|
|
26
26
|
__metadata("design:type", String)
|
|
27
27
|
], SecurityUserEntity.prototype, "userId", void 0);
|
|
28
28
|
__decorate([
|
|
@@ -17,6 +17,7 @@ const common_1 = require("@nestjs/common");
|
|
|
17
17
|
const crypto_1 = require("crypto");
|
|
18
18
|
const bcryptjs_1 = require("bcryptjs");
|
|
19
19
|
const jsonwebtoken_1 = require("jsonwebtoken");
|
|
20
|
+
const uuid_1 = require("uuid");
|
|
20
21
|
const typeorm_1 = require("@nestjs/typeorm");
|
|
21
22
|
const typeorm_2 = require("typeorm");
|
|
22
23
|
const roles_1 = require("../api/roles");
|
|
@@ -58,6 +59,7 @@ let SecurityAuthService = class SecurityAuthService {
|
|
|
58
59
|
throw new common_1.BadRequestException("Email already in use");
|
|
59
60
|
}
|
|
60
61
|
const appUser = await this.appUsersRepo.save(this.appUsersRepo.create({
|
|
62
|
+
id: (0, uuid_1.v7)(),
|
|
61
63
|
email,
|
|
62
64
|
}));
|
|
63
65
|
const securityUser = await this.securityUsersRepo.save(this.securityUsersRepo.create({
|
|
@@ -161,6 +163,7 @@ let SecurityAuthService = class SecurityAuthService {
|
|
|
161
163
|
const expiresAt = new Date(Date.now() +
|
|
162
164
|
(this.options.passwordResetTokenExpiresInMinutes ?? 30) * 60_000);
|
|
163
165
|
await this.passwordResetRepo.save(this.passwordResetRepo.create({
|
|
166
|
+
id: (0, uuid_1.v7)(),
|
|
164
167
|
userId: appUser.id,
|
|
165
168
|
token,
|
|
166
169
|
expiresAt,
|
|
@@ -220,7 +223,7 @@ let SecurityAuthService = class SecurityAuthService {
|
|
|
220
223
|
const refreshTokenExpiresAt = new Date(Date.now() +
|
|
221
224
|
(this.options.refreshTokenExpiresInDays ?? 30) * 24 * 60 * 60 * 1000);
|
|
222
225
|
await this.refreshTokenRepo.save(this.refreshTokenRepo.create({
|
|
223
|
-
id: (0,
|
|
226
|
+
id: (0, uuid_1.v7)(),
|
|
224
227
|
userId: appUser.id,
|
|
225
228
|
tokenHash: refreshTokenHash,
|
|
226
229
|
expiresAt: refreshTokenExpiresAt,
|
|
@@ -8,11 +8,13 @@ vitest_1.vi.mock("bcryptjs", () => ({
|
|
|
8
8
|
}));
|
|
9
9
|
vitest_1.vi.mock("crypto", () => ({
|
|
10
10
|
randomBytes: vitest_1.vi.fn(() => ({ toString: () => "token-bytes" })),
|
|
11
|
-
randomUUID: vitest_1.vi.fn(() => "uuid-1"),
|
|
12
11
|
}));
|
|
13
12
|
vitest_1.vi.mock("jsonwebtoken", () => ({
|
|
14
13
|
sign: vitest_1.vi.fn(() => "signed-access-token"),
|
|
15
14
|
}));
|
|
15
|
+
vitest_1.vi.mock("uuid", () => ({
|
|
16
|
+
v7: vitest_1.vi.fn(() => "uuid-1"),
|
|
17
|
+
}));
|
|
16
18
|
const bcryptjs_1 = require("bcryptjs");
|
|
17
19
|
const jsonwebtoken_1 = require("jsonwebtoken");
|
|
18
20
|
const security_auth_service_1 = require("./security-auth.service");
|
|
@@ -49,23 +49,23 @@ export declare class SecurityWorkflowsController {
|
|
|
49
49
|
} | {
|
|
50
50
|
success: true;
|
|
51
51
|
}>;
|
|
52
|
-
getUserRoles(
|
|
52
|
+
getUserRoles(userId: string): Promise<{
|
|
53
53
|
userId: string;
|
|
54
54
|
roles: string[];
|
|
55
55
|
}>;
|
|
56
|
-
setUserRoles(
|
|
56
|
+
setUserRoles(userId: string, body: {
|
|
57
57
|
roles?: string[];
|
|
58
58
|
}): Promise<{
|
|
59
59
|
userId: string;
|
|
60
60
|
roles: string[];
|
|
61
61
|
}>;
|
|
62
|
-
assignUserRole(
|
|
62
|
+
assignUserRole(userId: string, body: {
|
|
63
63
|
role?: string;
|
|
64
64
|
}): Promise<{
|
|
65
65
|
userId: string;
|
|
66
66
|
roles: string[];
|
|
67
67
|
}>;
|
|
68
|
-
removeUserRole(
|
|
68
|
+
removeUserRole(userId: string, role: string): Promise<{
|
|
69
69
|
userId: string;
|
|
70
70
|
roles: string[];
|
|
71
71
|
}>;
|
|
@@ -53,23 +53,23 @@ let SecurityWorkflowsController = class SecurityWorkflowsController {
|
|
|
53
53
|
async removeRole(role) {
|
|
54
54
|
return this.workflowsService.removeRole(role);
|
|
55
55
|
}
|
|
56
|
-
async getUserRoles(
|
|
57
|
-
return this.workflowsService.getUserRoles(
|
|
56
|
+
async getUserRoles(userId) {
|
|
57
|
+
return this.workflowsService.getUserRoles(userId);
|
|
58
58
|
}
|
|
59
|
-
async setUserRoles(
|
|
59
|
+
async setUserRoles(userId, body) {
|
|
60
60
|
if (!Array.isArray(body.roles)) {
|
|
61
61
|
throw new common_1.BadRequestException("roles must be an array");
|
|
62
62
|
}
|
|
63
|
-
return this.workflowsService.setUserRoles(
|
|
63
|
+
return this.workflowsService.setUserRoles(userId, body.roles);
|
|
64
64
|
}
|
|
65
|
-
async assignUserRole(
|
|
65
|
+
async assignUserRole(userId, body) {
|
|
66
66
|
if (!body.role || !body.role.trim()) {
|
|
67
67
|
throw new common_1.BadRequestException("role is required");
|
|
68
68
|
}
|
|
69
|
-
return this.workflowsService.assignRoleToUser(
|
|
69
|
+
return this.workflowsService.assignRoleToUser(userId, body.role);
|
|
70
70
|
}
|
|
71
|
-
async removeUserRole(
|
|
72
|
-
return this.workflowsService.removeRoleFromUser(
|
|
71
|
+
async removeUserRole(userId, role) {
|
|
72
|
+
return this.workflowsService.removeRoleFromUser(userId, role);
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
75
|
exports.SecurityWorkflowsController = SecurityWorkflowsController;
|
|
@@ -136,45 +136,65 @@ __decorate([
|
|
|
136
136
|
__metadata("design:returntype", Promise)
|
|
137
137
|
], SecurityWorkflowsController.prototype, "removeRole", null);
|
|
138
138
|
__decorate([
|
|
139
|
-
(0, common_1.Get)("users/:
|
|
139
|
+
(0, common_1.Get)("users/:userId/roles"),
|
|
140
140
|
(0, common_1.UseGuards)(security_jwt_guard_1.SecurityJwtGuard, security_admin_guard_1.SecurityAdminGuard),
|
|
141
141
|
(0, swagger_1.ApiOperation)({ summary: "Get assigned roles for a user" }),
|
|
142
|
+
(0, swagger_1.ApiParam)({
|
|
143
|
+
name: "userId",
|
|
144
|
+
description: "User id from app_user.id",
|
|
145
|
+
type: String,
|
|
146
|
+
}),
|
|
142
147
|
(0, swagger_1.ApiBearerAuth)(),
|
|
143
|
-
__param(0, (0, common_1.Param)("
|
|
148
|
+
__param(0, (0, common_1.Param)("userId")),
|
|
144
149
|
__metadata("design:type", Function),
|
|
145
150
|
__metadata("design:paramtypes", [String]),
|
|
146
151
|
__metadata("design:returntype", Promise)
|
|
147
152
|
], SecurityWorkflowsController.prototype, "getUserRoles", null);
|
|
148
153
|
__decorate([
|
|
149
|
-
(0, common_1.Put)("users/:
|
|
154
|
+
(0, common_1.Put)("users/:userId/roles"),
|
|
150
155
|
(0, common_1.UseGuards)(security_jwt_guard_1.SecurityJwtGuard, security_admin_guard_1.SecurityAdminGuard),
|
|
151
156
|
(0, swagger_1.ApiOperation)({ summary: "Replace user roles" }),
|
|
157
|
+
(0, swagger_1.ApiParam)({
|
|
158
|
+
name: "userId",
|
|
159
|
+
description: "User id from app_user.id",
|
|
160
|
+
type: String,
|
|
161
|
+
}),
|
|
152
162
|
(0, swagger_1.ApiBearerAuth)(),
|
|
153
163
|
(0, swagger_1.ApiBody)({ type: workflows_dto_1.SetUserRolesDto }),
|
|
154
|
-
__param(0, (0, common_1.Param)("
|
|
164
|
+
__param(0, (0, common_1.Param)("userId")),
|
|
155
165
|
__param(1, (0, common_1.Body)()),
|
|
156
166
|
__metadata("design:type", Function),
|
|
157
167
|
__metadata("design:paramtypes", [String, Object]),
|
|
158
168
|
__metadata("design:returntype", Promise)
|
|
159
169
|
], SecurityWorkflowsController.prototype, "setUserRoles", null);
|
|
160
170
|
__decorate([
|
|
161
|
-
(0, common_1.Post)("users/:
|
|
171
|
+
(0, common_1.Post)("users/:userId/roles"),
|
|
162
172
|
(0, common_1.UseGuards)(security_jwt_guard_1.SecurityJwtGuard, security_admin_guard_1.SecurityAdminGuard),
|
|
163
173
|
(0, swagger_1.ApiOperation)({ summary: "Assign one role to a user" }),
|
|
174
|
+
(0, swagger_1.ApiParam)({
|
|
175
|
+
name: "userId",
|
|
176
|
+
description: "User id from app_user.id",
|
|
177
|
+
type: String,
|
|
178
|
+
}),
|
|
164
179
|
(0, swagger_1.ApiBearerAuth)(),
|
|
165
180
|
(0, swagger_1.ApiBody)({ type: workflows_dto_1.AssignRoleDto }),
|
|
166
|
-
__param(0, (0, common_1.Param)("
|
|
181
|
+
__param(0, (0, common_1.Param)("userId")),
|
|
167
182
|
__param(1, (0, common_1.Body)()),
|
|
168
183
|
__metadata("design:type", Function),
|
|
169
184
|
__metadata("design:paramtypes", [String, Object]),
|
|
170
185
|
__metadata("design:returntype", Promise)
|
|
171
186
|
], SecurityWorkflowsController.prototype, "assignUserRole", null);
|
|
172
187
|
__decorate([
|
|
173
|
-
(0, common_1.Delete)("users/:
|
|
188
|
+
(0, common_1.Delete)("users/:userId/roles/:role"),
|
|
174
189
|
(0, common_1.UseGuards)(security_jwt_guard_1.SecurityJwtGuard, security_admin_guard_1.SecurityAdminGuard),
|
|
175
190
|
(0, swagger_1.ApiOperation)({ summary: "Remove one role from a user" }),
|
|
191
|
+
(0, swagger_1.ApiParam)({
|
|
192
|
+
name: "userId",
|
|
193
|
+
description: "User id from app_user.id",
|
|
194
|
+
type: String,
|
|
195
|
+
}),
|
|
176
196
|
(0, swagger_1.ApiBearerAuth)(),
|
|
177
|
-
__param(0, (0, common_1.Param)("
|
|
197
|
+
__param(0, (0, common_1.Param)("userId")),
|
|
178
198
|
__param(1, (0, common_1.Param)("role")),
|
|
179
199
|
__metadata("design:type", Function),
|
|
180
200
|
__metadata("design:paramtypes", [String, String]),
|
|
@@ -16,6 +16,7 @@ exports.SecurityWorkflowsService = void 0;
|
|
|
16
16
|
const common_1 = require("@nestjs/common");
|
|
17
17
|
const typeorm_1 = require("@nestjs/typeorm");
|
|
18
18
|
const typeorm_2 = require("typeorm");
|
|
19
|
+
const uuid_1 = require("uuid");
|
|
19
20
|
const contracts_1 = require("../api/contracts");
|
|
20
21
|
const roles_1 = require("../api/roles");
|
|
21
22
|
const app_user_entity_1 = require("./entities/app-user.entity");
|
|
@@ -94,6 +95,7 @@ let SecurityWorkflowsService = class SecurityWorkflowsService {
|
|
|
94
95
|
let role = await this.rolesRepo.findOne({ where: { roleKey } });
|
|
95
96
|
if (!role) {
|
|
96
97
|
role = this.rolesRepo.create({
|
|
98
|
+
id: (0, uuid_1.v7)(),
|
|
97
99
|
roleKey,
|
|
98
100
|
description: description?.trim() || null,
|
|
99
101
|
isSystem: roleKey === contracts_1.ADMIN_ROLE,
|
|
@@ -139,6 +141,7 @@ let SecurityWorkflowsService = class SecurityWorkflowsService {
|
|
|
139
141
|
await this.userRolesRepo.delete({ userId });
|
|
140
142
|
if (roles.length > 0) {
|
|
141
143
|
await this.userRolesRepo.save(roles.map((role) => this.userRolesRepo.create({
|
|
144
|
+
id: (0, uuid_1.v7)(),
|
|
142
145
|
userId,
|
|
143
146
|
roleId: role.id,
|
|
144
147
|
})));
|
|
@@ -182,6 +185,7 @@ let SecurityWorkflowsService = class SecurityWorkflowsService {
|
|
|
182
185
|
return;
|
|
183
186
|
}
|
|
184
187
|
await this.rolesRepo.save(missing.map((roleKey) => this.rolesRepo.create({
|
|
188
|
+
id: (0, uuid_1.v7)(),
|
|
185
189
|
roleKey,
|
|
186
190
|
description: null,
|
|
187
191
|
isSystem: roleKey === contracts_1.ADMIN_ROLE,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scryan7371/sdr-security",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Reusable auth/security capability for API and app clients.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@babel/runtime": "7.28.6",
|
|
36
36
|
"bcryptjs": "3.0.3",
|
|
37
|
-
"jsonwebtoken": "9.0.3"
|
|
37
|
+
"jsonwebtoken": "9.0.3",
|
|
38
|
+
"uuid": "11.1.0"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
40
41
|
"@nestjs/common": "^11.0.0",
|
|
@@ -8,11 +8,11 @@ export class AddRefreshTokens1700000000001 {
|
|
|
8
8
|
|
|
9
9
|
await queryRunner.query(`
|
|
10
10
|
CREATE TABLE "refresh_token" (
|
|
11
|
-
"id"
|
|
11
|
+
"id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
|
|
12
12
|
"token_hash" varchar NOT NULL,
|
|
13
13
|
"expires_at" timestamptz NOT NULL,
|
|
14
14
|
"revoked_at" timestamptz,
|
|
15
|
-
"userId"
|
|
15
|
+
"userId" uuid,
|
|
16
16
|
"created_at" timestamptz NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
|
17
17
|
CONSTRAINT "FK_refresh_token_user" FOREIGN KEY ("userId") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
|
18
18
|
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export class CreateAppUser1739490000000 {
|
|
2
|
+
name = "CreateAppUser1739490000000";
|
|
3
|
+
|
|
4
|
+
async up(queryRunner: {
|
|
5
|
+
query: (sql: string) => Promise<unknown>;
|
|
6
|
+
}): Promise<void> {
|
|
7
|
+
const userTableRef = getUserTableReference();
|
|
8
|
+
|
|
9
|
+
await queryRunner.query(`
|
|
10
|
+
CREATE TABLE IF NOT EXISTS ${userTableRef} (
|
|
11
|
+
"id" uuid PRIMARY KEY NOT NULL,
|
|
12
|
+
"email" varchar NOT NULL
|
|
13
|
+
)
|
|
14
|
+
`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async down(queryRunner: {
|
|
18
|
+
query: (sql: string) => Promise<unknown>;
|
|
19
|
+
}): Promise<void> {
|
|
20
|
+
const userTableRef = getUserTableReference();
|
|
21
|
+
await queryRunner.query(`DROP TABLE IF EXISTS ${userTableRef}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const getSafeIdentifier = (value: string | undefined, fallback: string) => {
|
|
26
|
+
const resolved = value?.trim() || fallback;
|
|
27
|
+
if (!resolved || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(resolved)) {
|
|
28
|
+
throw new Error(`Invalid SQL identifier: ${resolved}`);
|
|
29
|
+
}
|
|
30
|
+
return resolved;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const getUserTableReference = () => {
|
|
34
|
+
const table = getSafeIdentifier(process.env.USER_TABLE, "app_user");
|
|
35
|
+
const schema = process.env.USER_TABLE_SCHEMA
|
|
36
|
+
? getSafeIdentifier(process.env.USER_TABLE_SCHEMA, "public")
|
|
37
|
+
: "public";
|
|
38
|
+
return `"${schema}"."${table}"`;
|
|
39
|
+
};
|
|
@@ -9,7 +9,7 @@ export class CreateSecurityIdentity1739500000000 {
|
|
|
9
9
|
await queryRunner.query(`
|
|
10
10
|
CREATE TABLE IF NOT EXISTS "security_identity" (
|
|
11
11
|
"id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
|
|
12
|
-
"user_id"
|
|
12
|
+
"user_id" uuid NOT NULL,
|
|
13
13
|
"provider" varchar NOT NULL,
|
|
14
14
|
"provider_subject" varchar NOT NULL,
|
|
15
15
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
@@ -9,7 +9,7 @@ export class CreateSecurityUserRoles1739515000000 {
|
|
|
9
9
|
await queryRunner.query(`
|
|
10
10
|
CREATE TABLE IF NOT EXISTS "security_user_role" (
|
|
11
11
|
"id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
|
|
12
|
-
"user_id"
|
|
12
|
+
"user_id" uuid NOT NULL,
|
|
13
13
|
"role_id" uuid NOT NULL,
|
|
14
14
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
15
15
|
CONSTRAINT "FK_security_user_role_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE,
|
|
@@ -9,7 +9,7 @@ export class CreatePasswordResetTokens1739520000000 {
|
|
|
9
9
|
await queryRunner.query(`
|
|
10
10
|
CREATE TABLE IF NOT EXISTS "security_password_reset_token" (
|
|
11
11
|
"id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
|
|
12
|
-
"user_id"
|
|
12
|
+
"user_id" uuid NOT NULL,
|
|
13
13
|
"token" varchar NOT NULL,
|
|
14
14
|
"expires_at" timestamptz NOT NULL,
|
|
15
15
|
"used_at" timestamptz,
|
|
@@ -8,7 +8,7 @@ export class CreateSecurityUser1739530000000 {
|
|
|
8
8
|
|
|
9
9
|
await queryRunner.query(`
|
|
10
10
|
CREATE TABLE IF NOT EXISTS "security_user" (
|
|
11
|
-
"user_id"
|
|
11
|
+
"user_id" uuid PRIMARY KEY NOT NULL,
|
|
12
12
|
"password_hash" varchar NOT NULL,
|
|
13
13
|
"email_verified_at" timestamptz,
|
|
14
14
|
"email_verification_token" varchar,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CreateAppUser1739490000000 } from "./1739490000000-create-app-user";
|
|
1
2
|
import { AddRefreshTokens1700000000001 } from "./1700000000001-add-refresh-tokens";
|
|
2
3
|
import { CreateSecurityIdentity1739500000000 } from "./1739500000000-create-security-identity";
|
|
3
4
|
import { CreateSecurityRoles1739510000000 } from "./1739510000000-create-security-roles";
|
|
@@ -5,7 +6,8 @@ import { CreateSecurityUserRoles1739515000000 } from "./1739515000000-create-sec
|
|
|
5
6
|
import { CreatePasswordResetTokens1739520000000 } from "./1739520000000-create-password-reset-tokens";
|
|
6
7
|
import { CreateSecurityUser1739530000000 } from "./1739530000000-create-security-user";
|
|
7
8
|
|
|
8
|
-
export const
|
|
9
|
+
export const allMigrations = [
|
|
10
|
+
CreateAppUser1739490000000,
|
|
9
11
|
AddRefreshTokens1700000000001,
|
|
10
12
|
CreateSecurityIdentity1739500000000,
|
|
11
13
|
CreateSecurityRoles1739510000000,
|
|
@@ -14,7 +16,12 @@ export const securityMigrations = [
|
|
|
14
16
|
CreateSecurityUser1739530000000,
|
|
15
17
|
];
|
|
16
18
|
|
|
19
|
+
export const securityMigrations = allMigrations.filter(
|
|
20
|
+
(migration) => migration !== CreateAppUser1739490000000,
|
|
21
|
+
);
|
|
22
|
+
|
|
17
23
|
export {
|
|
24
|
+
CreateAppUser1739490000000,
|
|
18
25
|
AddRefreshTokens1700000000001,
|
|
19
26
|
CreateSecurityIdentity1739500000000,
|
|
20
27
|
CreateSecurityRoles1739510000000,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { CreateAppUser1739490000000 } from "./1739490000000-create-app-user";
|
|
2
3
|
import { AddRefreshTokens1700000000001 } from "./1700000000001-add-refresh-tokens";
|
|
3
4
|
import { CreateSecurityIdentity1739500000000 } from "./1739500000000-create-security-identity";
|
|
4
5
|
import { CreateSecurityRoles1739510000000 } from "./1739510000000-create-security-roles";
|
|
5
6
|
import { CreateSecurityUserRoles1739515000000 } from "./1739515000000-create-security-user-roles";
|
|
6
7
|
import { CreatePasswordResetTokens1739520000000 } from "./1739520000000-create-password-reset-tokens";
|
|
7
8
|
import { CreateSecurityUser1739530000000 } from "./1739530000000-create-security-user";
|
|
8
|
-
import { securityMigrations } from "./index";
|
|
9
|
+
import { allMigrations, securityMigrations } from "./index";
|
|
9
10
|
|
|
10
11
|
const originalEnv = { ...process.env };
|
|
11
12
|
|
|
@@ -15,6 +16,19 @@ afterEach(() => {
|
|
|
15
16
|
});
|
|
16
17
|
|
|
17
18
|
describe("security migrations", () => {
|
|
19
|
+
it("exports all migration list for test/dev", () => {
|
|
20
|
+
expect(allMigrations.length).toBe(7);
|
|
21
|
+
expect(allMigrations).toEqual([
|
|
22
|
+
CreateAppUser1739490000000,
|
|
23
|
+
AddRefreshTokens1700000000001,
|
|
24
|
+
CreateSecurityIdentity1739500000000,
|
|
25
|
+
CreateSecurityRoles1739510000000,
|
|
26
|
+
CreateSecurityUserRoles1739515000000,
|
|
27
|
+
CreatePasswordResetTokens1739520000000,
|
|
28
|
+
CreateSecurityUser1739530000000,
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
|
|
18
32
|
it("exports migration list", () => {
|
|
19
33
|
expect(securityMigrations.length).toBe(6);
|
|
20
34
|
expect(securityMigrations).toEqual([
|
|
@@ -27,6 +41,21 @@ describe("security migrations", () => {
|
|
|
27
41
|
]);
|
|
28
42
|
});
|
|
29
43
|
|
|
44
|
+
it("runs app user migration up/down", async () => {
|
|
45
|
+
const query = vi.fn().mockResolvedValue(undefined);
|
|
46
|
+
const migration = new CreateAppUser1739490000000();
|
|
47
|
+
|
|
48
|
+
await migration.up({ query });
|
|
49
|
+
await migration.down({ query });
|
|
50
|
+
|
|
51
|
+
expect(query).toHaveBeenCalledWith(
|
|
52
|
+
expect.stringContaining('CREATE TABLE IF NOT EXISTS "public"."app_user"'),
|
|
53
|
+
);
|
|
54
|
+
expect(query).toHaveBeenCalledWith(
|
|
55
|
+
expect.stringContaining('DROP TABLE IF EXISTS "public"."app_user"'),
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
30
59
|
it("runs refresh token migration up/down", async () => {
|
|
31
60
|
const query = vi.fn().mockResolvedValue(undefined);
|
|
32
61
|
const migration = new AddRefreshTokens1700000000001();
|
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
4
4
|
import { Client, ClientConfig } from "pg";
|
|
5
|
-
import {
|
|
5
|
+
import { allMigrations } from "../api/migrations";
|
|
6
6
|
|
|
7
7
|
type QueryRunnerLike = {
|
|
8
8
|
query: (sql: string, params?: unknown[]) => Promise<unknown>;
|
|
@@ -119,17 +119,10 @@ describe("database integration", () => {
|
|
|
119
119
|
}
|
|
120
120
|
await client.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`);
|
|
121
121
|
await client.query(`SET search_path TO "${schema}", public`);
|
|
122
|
-
await client.query(`
|
|
123
|
-
CREATE TABLE IF NOT EXISTS "${schema}"."app_user" (
|
|
124
|
-
"id" varchar PRIMARY KEY NOT NULL,
|
|
125
|
-
"email" varchar NOT NULL
|
|
126
|
-
)
|
|
127
|
-
`);
|
|
128
|
-
|
|
129
122
|
process.env.USER_TABLE = "app_user";
|
|
130
123
|
process.env.USER_TABLE_SCHEMA = schema;
|
|
131
124
|
|
|
132
|
-
for (const Migration of
|
|
125
|
+
for (const Migration of allMigrations) {
|
|
133
126
|
await new Migration().up(runner as never);
|
|
134
127
|
}
|
|
135
128
|
});
|
|
@@ -140,10 +133,9 @@ describe("database integration", () => {
|
|
|
140
133
|
}
|
|
141
134
|
|
|
142
135
|
if (!keepSchemaForDebug) {
|
|
143
|
-
for (const Migration of [...
|
|
136
|
+
for (const Migration of [...allMigrations].reverse()) {
|
|
144
137
|
await new Migration().down(runner as never);
|
|
145
138
|
}
|
|
146
|
-
await client.query(`DROP TABLE IF EXISTS "${schema}"."app_user"`);
|
|
147
139
|
await client.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
|
|
148
140
|
}
|
|
149
141
|
await client.end();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Column, Entity,
|
|
1
|
+
import { Column, Entity, PrimaryColumn } from "typeorm";
|
|
2
2
|
|
|
3
3
|
@Entity({ name: "app_user" })
|
|
4
4
|
export class AppUserEntity {
|
|
5
|
-
@
|
|
5
|
+
@PrimaryColumn({ type: "uuid" })
|
|
6
6
|
id!: string;
|
|
7
7
|
|
|
8
8
|
@Column({ type: "varchar" })
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
+
BeforeInsert,
|
|
2
3
|
Column,
|
|
3
4
|
CreateDateColumn,
|
|
4
5
|
Entity,
|
|
5
|
-
|
|
6
|
+
PrimaryColumn,
|
|
6
7
|
} from "typeorm";
|
|
8
|
+
import { v7 as uuidv7 } from "uuid";
|
|
7
9
|
|
|
8
10
|
@Entity({ name: "security_password_reset_token" })
|
|
9
11
|
export class PasswordResetTokenEntity {
|
|
10
|
-
@
|
|
12
|
+
@PrimaryColumn({ type: "uuid" })
|
|
11
13
|
id!: string;
|
|
12
14
|
|
|
13
|
-
@Column({ type: "
|
|
15
|
+
@Column({ type: "uuid", name: "user_id" })
|
|
14
16
|
userId!: string;
|
|
15
17
|
|
|
16
18
|
@Column({ type: "varchar", unique: true })
|
|
@@ -24,4 +26,11 @@ export class PasswordResetTokenEntity {
|
|
|
24
26
|
|
|
25
27
|
@CreateDateColumn({ name: "created_at" })
|
|
26
28
|
createdAt!: Date;
|
|
29
|
+
|
|
30
|
+
@BeforeInsert()
|
|
31
|
+
ensureId() {
|
|
32
|
+
if (!this.id) {
|
|
33
|
+
this.id = uuidv7();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
27
36
|
}
|
|
@@ -2,7 +2,7 @@ import { Column, CreateDateColumn, Entity, PrimaryColumn } from "typeorm";
|
|
|
2
2
|
|
|
3
3
|
@Entity({ name: "refresh_token" })
|
|
4
4
|
export class RefreshTokenEntity {
|
|
5
|
-
@PrimaryColumn({ type: "
|
|
5
|
+
@PrimaryColumn({ type: "uuid" })
|
|
6
6
|
id!: string;
|
|
7
7
|
|
|
8
8
|
@Column({ type: "varchar", name: "token_hash" })
|
|
@@ -14,7 +14,7 @@ export class RefreshTokenEntity {
|
|
|
14
14
|
@Column({ type: "timestamptz", name: "revoked_at", nullable: true })
|
|
15
15
|
revokedAt!: Date | null;
|
|
16
16
|
|
|
17
|
-
@Column({ type: "
|
|
17
|
+
@Column({ type: "uuid", name: "userId", nullable: true })
|
|
18
18
|
userId!: string | null;
|
|
19
19
|
|
|
20
20
|
@CreateDateColumn({ name: "created_at" })
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { v7 as uuidv7 } from "uuid";
|
|
2
|
+
import { BeforeInsert, Column, Entity, PrimaryColumn } from "typeorm";
|
|
2
3
|
|
|
3
4
|
@Entity({ name: "security_role" })
|
|
4
5
|
export class SecurityRoleEntity {
|
|
5
|
-
@
|
|
6
|
+
@PrimaryColumn({ type: "uuid" })
|
|
6
7
|
id!: string;
|
|
7
8
|
|
|
8
9
|
@Column({ type: "varchar", name: "role_key", unique: true })
|
|
@@ -13,4 +14,11 @@ export class SecurityRoleEntity {
|
|
|
13
14
|
|
|
14
15
|
@Column({ type: "boolean", name: "is_system", default: false })
|
|
15
16
|
isSystem!: boolean;
|
|
17
|
+
|
|
18
|
+
@BeforeInsert()
|
|
19
|
+
ensureId() {
|
|
20
|
+
if (!this.id) {
|
|
21
|
+
this.id = uuidv7();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
16
24
|
}
|
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { v7 as uuidv7 } from "uuid";
|
|
2
|
+
import { BeforeInsert, Column, Entity, PrimaryColumn } from "typeorm";
|
|
2
3
|
|
|
3
4
|
@Entity({ name: "security_user_role" })
|
|
4
5
|
export class SecurityUserRoleEntity {
|
|
5
|
-
@
|
|
6
|
+
@PrimaryColumn({ type: "uuid" })
|
|
6
7
|
id!: string;
|
|
7
8
|
|
|
8
|
-
@Column({ type: "
|
|
9
|
+
@Column({ type: "uuid", name: "user_id" })
|
|
9
10
|
userId!: string;
|
|
10
11
|
|
|
11
12
|
@Column({ type: "uuid", name: "role_id" })
|
|
12
13
|
roleId!: string;
|
|
14
|
+
|
|
15
|
+
@BeforeInsert()
|
|
16
|
+
ensureId() {
|
|
17
|
+
if (!this.id) {
|
|
18
|
+
this.id = uuidv7();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
13
21
|
}
|
|
@@ -2,7 +2,7 @@ import { Column, CreateDateColumn, Entity, PrimaryColumn } from "typeorm";
|
|
|
2
2
|
|
|
3
3
|
@Entity({ name: "security_user" })
|
|
4
4
|
export class SecurityUserEntity {
|
|
5
|
-
@PrimaryColumn({ type: "
|
|
5
|
+
@PrimaryColumn({ type: "uuid", name: "user_id" })
|
|
6
6
|
userId!: string;
|
|
7
7
|
|
|
8
8
|
@Column({ type: "varchar", name: "password_hash" })
|
|
@@ -10,13 +10,16 @@ vi.mock("bcryptjs", () => ({
|
|
|
10
10
|
|
|
11
11
|
vi.mock("crypto", () => ({
|
|
12
12
|
randomBytes: vi.fn(() => ({ toString: () => "token-bytes" })),
|
|
13
|
-
randomUUID: vi.fn(() => "uuid-1"),
|
|
14
13
|
}));
|
|
15
14
|
|
|
16
15
|
vi.mock("jsonwebtoken", () => ({
|
|
17
16
|
sign: vi.fn(() => "signed-access-token"),
|
|
18
17
|
}));
|
|
19
18
|
|
|
19
|
+
vi.mock("uuid", () => ({
|
|
20
|
+
v7: vi.fn(() => "uuid-1"),
|
|
21
|
+
}));
|
|
22
|
+
|
|
20
23
|
import { compare } from "bcryptjs";
|
|
21
24
|
import { sign } from "jsonwebtoken";
|
|
22
25
|
import { SecurityAuthService } from "./security-auth.service";
|
|
@@ -4,9 +4,10 @@ import {
|
|
|
4
4
|
Injectable,
|
|
5
5
|
UnauthorizedException,
|
|
6
6
|
} from "@nestjs/common";
|
|
7
|
-
import { randomBytes
|
|
7
|
+
import { randomBytes } from "crypto";
|
|
8
8
|
import { compare, hash } from "bcryptjs";
|
|
9
9
|
import { sign, type SignOptions } from "jsonwebtoken";
|
|
10
|
+
import { v7 as uuidv7 } from "uuid";
|
|
10
11
|
import { InjectRepository } from "@nestjs/typeorm";
|
|
11
12
|
import { In, IsNull, Repository } from "typeorm";
|
|
12
13
|
import { AuthResponse, RegisterResponse } from "../api/contracts";
|
|
@@ -60,6 +61,7 @@ export class SecurityAuthService {
|
|
|
60
61
|
|
|
61
62
|
const appUser = await this.appUsersRepo.save(
|
|
62
63
|
this.appUsersRepo.create({
|
|
64
|
+
id: uuidv7(),
|
|
63
65
|
email,
|
|
64
66
|
}),
|
|
65
67
|
);
|
|
@@ -195,6 +197,7 @@ export class SecurityAuthService {
|
|
|
195
197
|
);
|
|
196
198
|
await this.passwordResetRepo.save(
|
|
197
199
|
this.passwordResetRepo.create({
|
|
200
|
+
id: uuidv7(),
|
|
198
201
|
userId: appUser.id,
|
|
199
202
|
token,
|
|
200
203
|
expiresAt,
|
|
@@ -283,7 +286,7 @@ export class SecurityAuthService {
|
|
|
283
286
|
|
|
284
287
|
await this.refreshTokenRepo.save(
|
|
285
288
|
this.refreshTokenRepo.create({
|
|
286
|
-
id:
|
|
289
|
+
id: uuidv7(),
|
|
287
290
|
userId: appUser.id,
|
|
288
291
|
tokenHash: refreshTokenHash,
|
|
289
292
|
expiresAt: refreshTokenExpiresAt,
|
|
@@ -10,7 +10,13 @@ import {
|
|
|
10
10
|
Put,
|
|
11
11
|
UseGuards,
|
|
12
12
|
} from "@nestjs/common";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
ApiBearerAuth,
|
|
15
|
+
ApiBody,
|
|
16
|
+
ApiOperation,
|
|
17
|
+
ApiParam,
|
|
18
|
+
ApiTags,
|
|
19
|
+
} from "@nestjs/swagger";
|
|
14
20
|
import { SecurityAdminGuard } from "./security-admin.guard";
|
|
15
21
|
import { SecurityJwtGuard } from "./security-jwt.guard";
|
|
16
22
|
import { SecurityWorkflowsService } from "./security-workflows.service";
|
|
@@ -101,49 +107,72 @@ export class SecurityWorkflowsController {
|
|
|
101
107
|
return this.workflowsService.removeRole(role);
|
|
102
108
|
}
|
|
103
109
|
|
|
104
|
-
@Get("users/:
|
|
110
|
+
@Get("users/:userId/roles")
|
|
105
111
|
@UseGuards(SecurityJwtGuard, SecurityAdminGuard)
|
|
106
112
|
@ApiOperation({ summary: "Get assigned roles for a user" })
|
|
113
|
+
@ApiParam({
|
|
114
|
+
name: "userId",
|
|
115
|
+
description: "User id from app_user.id",
|
|
116
|
+
type: String,
|
|
117
|
+
})
|
|
107
118
|
@ApiBearerAuth()
|
|
108
|
-
async getUserRoles(@Param("
|
|
109
|
-
return this.workflowsService.getUserRoles(
|
|
119
|
+
async getUserRoles(@Param("userId") userId: string) {
|
|
120
|
+
return this.workflowsService.getUserRoles(userId);
|
|
110
121
|
}
|
|
111
122
|
|
|
112
|
-
@Put("users/:
|
|
123
|
+
@Put("users/:userId/roles")
|
|
113
124
|
@UseGuards(SecurityJwtGuard, SecurityAdminGuard)
|
|
114
125
|
@ApiOperation({ summary: "Replace user roles" })
|
|
126
|
+
@ApiParam({
|
|
127
|
+
name: "userId",
|
|
128
|
+
description: "User id from app_user.id",
|
|
129
|
+
type: String,
|
|
130
|
+
})
|
|
115
131
|
@ApiBearerAuth()
|
|
116
132
|
@ApiBody({ type: SetUserRolesDto })
|
|
117
133
|
async setUserRoles(
|
|
118
|
-
@Param("
|
|
134
|
+
@Param("userId") userId: string,
|
|
119
135
|
@Body() body: { roles?: string[] },
|
|
120
136
|
) {
|
|
121
137
|
if (!Array.isArray(body.roles)) {
|
|
122
138
|
throw new BadRequestException("roles must be an array");
|
|
123
139
|
}
|
|
124
|
-
return this.workflowsService.setUserRoles(
|
|
140
|
+
return this.workflowsService.setUserRoles(userId, body.roles);
|
|
125
141
|
}
|
|
126
142
|
|
|
127
|
-
@Post("users/:
|
|
143
|
+
@Post("users/:userId/roles")
|
|
128
144
|
@UseGuards(SecurityJwtGuard, SecurityAdminGuard)
|
|
129
145
|
@ApiOperation({ summary: "Assign one role to a user" })
|
|
146
|
+
@ApiParam({
|
|
147
|
+
name: "userId",
|
|
148
|
+
description: "User id from app_user.id",
|
|
149
|
+
type: String,
|
|
150
|
+
})
|
|
130
151
|
@ApiBearerAuth()
|
|
131
152
|
@ApiBody({ type: AssignRoleDto })
|
|
132
153
|
async assignUserRole(
|
|
133
|
-
@Param("
|
|
154
|
+
@Param("userId") userId: string,
|
|
134
155
|
@Body() body: { role?: string },
|
|
135
156
|
) {
|
|
136
157
|
if (!body.role || !body.role.trim()) {
|
|
137
158
|
throw new BadRequestException("role is required");
|
|
138
159
|
}
|
|
139
|
-
return this.workflowsService.assignRoleToUser(
|
|
160
|
+
return this.workflowsService.assignRoleToUser(userId, body.role);
|
|
140
161
|
}
|
|
141
162
|
|
|
142
|
-
@Delete("users/:
|
|
163
|
+
@Delete("users/:userId/roles/:role")
|
|
143
164
|
@UseGuards(SecurityJwtGuard, SecurityAdminGuard)
|
|
144
165
|
@ApiOperation({ summary: "Remove one role from a user" })
|
|
166
|
+
@ApiParam({
|
|
167
|
+
name: "userId",
|
|
168
|
+
description: "User id from app_user.id",
|
|
169
|
+
type: String,
|
|
170
|
+
})
|
|
145
171
|
@ApiBearerAuth()
|
|
146
|
-
async removeUserRole(
|
|
147
|
-
|
|
172
|
+
async removeUserRole(
|
|
173
|
+
@Param("userId") userId: string,
|
|
174
|
+
@Param("role") role: string,
|
|
175
|
+
) {
|
|
176
|
+
return this.workflowsService.removeRoleFromUser(userId, role);
|
|
148
177
|
}
|
|
149
178
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Inject, Injectable, NotFoundException } from "@nestjs/common";
|
|
2
2
|
import { InjectRepository } from "@nestjs/typeorm";
|
|
3
3
|
import { In, Repository } from "typeorm";
|
|
4
|
+
import { v7 as uuidv7 } from "uuid";
|
|
4
5
|
import { ADMIN_ROLE } from "../api/contracts";
|
|
5
6
|
import { normalizeRoleName } from "../api/roles";
|
|
6
7
|
import { AppUserEntity } from "./entities/app-user.entity";
|
|
@@ -106,6 +107,7 @@ export class SecurityWorkflowsService {
|
|
|
106
107
|
let role = await this.rolesRepo.findOne({ where: { roleKey } });
|
|
107
108
|
if (!role) {
|
|
108
109
|
role = this.rolesRepo.create({
|
|
110
|
+
id: uuidv7(),
|
|
109
111
|
roleKey,
|
|
110
112
|
description: description?.trim() || null,
|
|
111
113
|
isSystem: roleKey === ADMIN_ROLE,
|
|
@@ -158,6 +160,7 @@ export class SecurityWorkflowsService {
|
|
|
158
160
|
await this.userRolesRepo.save(
|
|
159
161
|
roles.map((role) =>
|
|
160
162
|
this.userRolesRepo.create({
|
|
163
|
+
id: uuidv7(),
|
|
161
164
|
userId,
|
|
162
165
|
roleId: role.id,
|
|
163
166
|
}),
|
|
@@ -210,6 +213,7 @@ export class SecurityWorkflowsService {
|
|
|
210
213
|
await this.rolesRepo.save(
|
|
211
214
|
missing.map((roleKey) =>
|
|
212
215
|
this.rolesRepo.create({
|
|
216
|
+
id: uuidv7(),
|
|
213
217
|
roleKey,
|
|
214
218
|
description: null,
|
|
215
219
|
isSystem: roleKey === ADMIN_ROLE,
|