@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.
Files changed (45) hide show
  1. package/dist/api/migrations/1700000000001-add-refresh-tokens.js +2 -2
  2. package/dist/api/migrations/1739490000000-create-app-user.d.ts +9 -0
  3. package/dist/api/migrations/1739490000000-create-app-user.js +34 -0
  4. package/dist/api/migrations/1739500000000-create-security-identity.js +1 -1
  5. package/dist/api/migrations/1739515000000-create-security-user-roles.js +1 -1
  6. package/dist/api/migrations/1739520000000-create-password-reset-tokens.js +1 -1
  7. package/dist/api/migrations/1739530000000-create-security-user.js +1 -1
  8. package/dist/api/migrations/index.d.ts +4 -2
  9. package/dist/api/migrations/index.js +6 -2
  10. package/dist/api/migrations/migrations.test.js +21 -0
  11. package/dist/integration/database.integration.test.js +2 -9
  12. package/dist/nest/entities/app-user.entity.js +1 -1
  13. package/dist/nest/entities/password-reset-token.entity.d.ts +1 -0
  14. package/dist/nest/entities/password-reset-token.entity.js +14 -2
  15. package/dist/nest/entities/refresh-token.entity.js +2 -2
  16. package/dist/nest/entities/security-role.entity.d.ts +1 -0
  17. package/dist/nest/entities/security-role.entity.js +13 -1
  18. package/dist/nest/entities/security-user-role.entity.d.ts +1 -0
  19. package/dist/nest/entities/security-user-role.entity.js +14 -2
  20. package/dist/nest/entities/security-user.entity.js +1 -1
  21. package/dist/nest/security-auth.service.js +4 -1
  22. package/dist/nest/security-auth.service.test.js +3 -1
  23. package/dist/nest/security-workflows.controller.d.ts +4 -4
  24. package/dist/nest/security-workflows.controller.js +36 -16
  25. package/dist/nest/security-workflows.service.js +4 -0
  26. package/package.json +3 -2
  27. package/src/api/migrations/1700000000001-add-refresh-tokens.ts +2 -2
  28. package/src/api/migrations/1739490000000-create-app-user.ts +39 -0
  29. package/src/api/migrations/1739500000000-create-security-identity.ts +1 -1
  30. package/src/api/migrations/1739515000000-create-security-user-roles.ts +1 -1
  31. package/src/api/migrations/1739520000000-create-password-reset-tokens.ts +1 -1
  32. package/src/api/migrations/1739530000000-create-security-user.ts +1 -1
  33. package/src/api/migrations/index.ts +8 -1
  34. package/src/api/migrations/migrations.test.ts +30 -1
  35. package/src/integration/database.integration.test.ts +3 -11
  36. package/src/nest/entities/app-user.entity.ts +2 -2
  37. package/src/nest/entities/password-reset-token.entity.ts +12 -3
  38. package/src/nest/entities/refresh-token.entity.ts +2 -2
  39. package/src/nest/entities/security-role.entity.ts +10 -2
  40. package/src/nest/entities/security-user-role.entity.ts +11 -3
  41. package/src/nest/entities/security-user.entity.ts +1 -1
  42. package/src/nest/security-auth.service.test.ts +4 -1
  43. package/src/nest/security-auth.service.ts +5 -2
  44. package/src/nest/security-workflows.controller.ts +42 -13
  45. 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" varchar PRIMARY KEY NOT NULL,
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" varchar,
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,9 @@
1
+ export declare class CreateAppUser1739490000000 {
2
+ name: string;
3
+ up(queryRunner: {
4
+ query: (sql: string) => Promise<unknown>;
5
+ }): Promise<void>;
6
+ down(queryRunner: {
7
+ query: (sql: string) => Promise<unknown>;
8
+ }): Promise<void>;
9
+ }
@@ -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" varchar NOT NULL,
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" varchar NOT NULL,
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" varchar NOT NULL,
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" varchar PRIMARY KEY NOT NULL,
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 securityMigrations: (typeof AddRefreshTokens1700000000001)[];
8
- export { AddRefreshTokens1700000000001, CreateSecurityIdentity1739500000000, CreateSecurityRoles1739510000000, CreateSecurityUserRoles1739515000000, CreatePasswordResetTokens1739520000000, CreateSecurityUser1739530000000, };
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.securityMigrations = [
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.securityMigrations) {
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.securityMigrations].reverse()) {
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.PrimaryGeneratedColumn)("uuid"),
20
+ (0, typeorm_1.PrimaryColumn)({ type: "uuid" }),
21
21
  __metadata("design:type", String)
22
22
  ], AppUserEntity.prototype, "id", void 0);
23
23
  __decorate([
@@ -5,4 +5,5 @@ export declare class PasswordResetTokenEntity {
5
5
  expiresAt: Date;
6
6
  usedAt: Date | null;
7
7
  createdAt: Date;
8
+ ensureId(): void;
8
9
  }
@@ -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.PrimaryGeneratedColumn)("uuid"),
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: "varchar", name: "user_id" }),
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: "varchar" }),
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: "varchar", name: "userId", nullable: true }),
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([
@@ -3,4 +3,5 @@ export declare class SecurityRoleEntity {
3
3
  roleKey: string;
4
4
  description: string | null;
5
5
  isSystem: boolean;
6
+ ensureId(): void;
6
7
  }
@@ -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.PrimaryGeneratedColumn)("uuid"),
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);
@@ -2,4 +2,5 @@ export declare class SecurityUserRoleEntity {
2
2
  id: string;
3
3
  userId: string;
4
4
  roleId: string;
5
+ ensureId(): void;
5
6
  }
@@ -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.PrimaryGeneratedColumn)("uuid"),
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: "varchar", name: "user_id" }),
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: "varchar", name: "user_id" }),
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, crypto_1.randomUUID)(),
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(id: string): Promise<{
52
+ getUserRoles(userId: string): Promise<{
53
53
  userId: string;
54
54
  roles: string[];
55
55
  }>;
56
- setUserRoles(id: string, body: {
56
+ setUserRoles(userId: string, body: {
57
57
  roles?: string[];
58
58
  }): Promise<{
59
59
  userId: string;
60
60
  roles: string[];
61
61
  }>;
62
- assignUserRole(id: string, body: {
62
+ assignUserRole(userId: string, body: {
63
63
  role?: string;
64
64
  }): Promise<{
65
65
  userId: string;
66
66
  roles: string[];
67
67
  }>;
68
- removeUserRole(id: string, role: string): Promise<{
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(id) {
57
- return this.workflowsService.getUserRoles(id);
56
+ async getUserRoles(userId) {
57
+ return this.workflowsService.getUserRoles(userId);
58
58
  }
59
- async setUserRoles(id, body) {
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(id, body.roles);
63
+ return this.workflowsService.setUserRoles(userId, body.roles);
64
64
  }
65
- async assignUserRole(id, body) {
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(id, body.role);
69
+ return this.workflowsService.assignRoleToUser(userId, body.role);
70
70
  }
71
- async removeUserRole(id, role) {
72
- return this.workflowsService.removeRoleFromUser(id, role);
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/:id/roles"),
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)("id")),
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/:id/roles"),
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)("id")),
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/:id/roles"),
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)("id")),
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/:id/roles/:role"),
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)("id")),
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",
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" varchar PRIMARY KEY NOT NULL,
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" varchar,
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" varchar NOT NULL,
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" varchar NOT NULL,
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" varchar NOT NULL,
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" varchar PRIMARY KEY NOT NULL,
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 securityMigrations = [
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 { securityMigrations } from "../api/migrations";
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 securityMigrations) {
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 [...securityMigrations].reverse()) {
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, PrimaryGeneratedColumn } from "typeorm";
1
+ import { Column, Entity, PrimaryColumn } from "typeorm";
2
2
 
3
3
  @Entity({ name: "app_user" })
4
4
  export class AppUserEntity {
5
- @PrimaryGeneratedColumn("uuid")
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
- PrimaryGeneratedColumn,
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
- @PrimaryGeneratedColumn("uuid")
12
+ @PrimaryColumn({ type: "uuid" })
11
13
  id!: string;
12
14
 
13
- @Column({ type: "varchar", name: "user_id" })
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: "varchar" })
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: "varchar", name: "userId", nullable: true })
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 { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
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
- @PrimaryGeneratedColumn("uuid")
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 { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
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
- @PrimaryGeneratedColumn("uuid")
6
+ @PrimaryColumn({ type: "uuid" })
6
7
  id!: string;
7
8
 
8
- @Column({ type: "varchar", name: "user_id" })
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: "varchar", name: "user_id" })
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, randomUUID } from "crypto";
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: randomUUID(),
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 { ApiBearerAuth, ApiBody, ApiOperation, ApiTags } from "@nestjs/swagger";
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/:id/roles")
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("id") id: string) {
109
- return this.workflowsService.getUserRoles(id);
119
+ async getUserRoles(@Param("userId") userId: string) {
120
+ return this.workflowsService.getUserRoles(userId);
110
121
  }
111
122
 
112
- @Put("users/:id/roles")
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("id") id: string,
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(id, body.roles);
140
+ return this.workflowsService.setUserRoles(userId, body.roles);
125
141
  }
126
142
 
127
- @Post("users/:id/roles")
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("id") id: string,
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(id, body.role);
160
+ return this.workflowsService.assignRoleToUser(userId, body.role);
140
161
  }
141
162
 
142
- @Delete("users/:id/roles/:role")
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(@Param("id") id: string, @Param("role") role: string) {
147
- return this.workflowsService.removeRoleFromUser(id, role);
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,