@scryan7371/sdr-security 0.1.4 → 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 (26) hide show
  1. package/dist/api/migrations/1700000000001-add-refresh-tokens.js +0 -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 +0 -2
  5. package/dist/api/migrations/1739510000000-create-security-roles.js +1 -2
  6. package/dist/api/migrations/1739515000000-create-security-user-roles.js +0 -3
  7. package/dist/api/migrations/1739520000000-create-password-reset-tokens.js +0 -2
  8. package/dist/api/migrations/1739530000000-create-security-user.js +0 -1
  9. package/dist/api/migrations/index.d.ts +4 -2
  10. package/dist/api/migrations/index.js +6 -2
  11. package/dist/api/migrations/migrations.test.js +21 -0
  12. package/dist/integration/database.integration.test.js +2 -9
  13. package/dist/nest/security-workflows.controller.d.ts +4 -4
  14. package/dist/nest/security-workflows.controller.js +36 -16
  15. package/package.json +1 -1
  16. package/src/api/migrations/1700000000001-add-refresh-tokens.ts +0 -2
  17. package/src/api/migrations/1739490000000-create-app-user.ts +39 -0
  18. package/src/api/migrations/1739500000000-create-security-identity.ts +0 -2
  19. package/src/api/migrations/1739510000000-create-security-roles.ts +1 -2
  20. package/src/api/migrations/1739515000000-create-security-user-roles.ts +0 -3
  21. package/src/api/migrations/1739520000000-create-password-reset-tokens.ts +0 -2
  22. package/src/api/migrations/1739530000000-create-security-user.ts +0 -1
  23. package/src/api/migrations/index.ts +8 -1
  24. package/src/api/migrations/migrations.test.ts +30 -1
  25. package/src/integration/database.integration.test.ts +3 -11
  26. package/src/nest/security-workflows.controller.ts +42 -13
@@ -13,8 +13,6 @@ class AddRefreshTokens1700000000001 {
13
13
  "revoked_at" timestamptz,
14
14
  "userId" uuid,
15
15
  "created_at" timestamptz NOT NULL DEFAULT (CURRENT_TIMESTAMP),
16
- CONSTRAINT "CHK_refresh_token_id_uuidv7" CHECK ("id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
17
- CONSTRAINT "CHK_refresh_token_userId_uuidv7" CHECK ("userId" IS NULL OR "userId"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
18
16
  CONSTRAINT "FK_refresh_token_user" FOREIGN KEY ("userId") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE ON UPDATE NO ACTION
19
17
  )
20
18
  `);
@@ -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
+ };
@@ -13,8 +13,6 @@ class CreateSecurityIdentity1739500000000 {
13
13
  "provider_subject" varchar NOT NULL,
14
14
  "created_at" timestamptz NOT NULL DEFAULT now(),
15
15
  "updated_at" timestamptz NOT NULL DEFAULT now(),
16
- CONSTRAINT "CHK_security_identity_id_uuidv7" CHECK ("id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
17
- CONSTRAINT "CHK_security_identity_user_id_uuidv7" CHECK ("user_id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
18
16
  CONSTRAINT "FK_security_identity_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE
19
17
  )
20
18
  `);
@@ -11,8 +11,7 @@ class CreateSecurityRoles1739510000000 {
11
11
  "description" text,
12
12
  "is_system" boolean NOT NULL DEFAULT false,
13
13
  "created_at" timestamptz NOT NULL DEFAULT now(),
14
- "updated_at" timestamptz NOT NULL DEFAULT now(),
15
- CONSTRAINT "CHK_security_role_id_uuidv7" CHECK ("id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$')
14
+ "updated_at" timestamptz NOT NULL DEFAULT now()
16
15
  )
17
16
  `);
18
17
  await queryRunner.query(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_role_key" ON "security_role" ("role_key")`);
@@ -11,9 +11,6 @@ class CreateSecurityUserRoles1739515000000 {
11
11
  "user_id" uuid NOT NULL,
12
12
  "role_id" uuid NOT NULL,
13
13
  "created_at" timestamptz NOT NULL DEFAULT now(),
14
- CONSTRAINT "CHK_security_user_role_id_uuidv7" CHECK ("id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
15
- CONSTRAINT "CHK_security_user_role_user_id_uuidv7" CHECK ("user_id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
16
- CONSTRAINT "CHK_security_user_role_role_id_uuidv7" CHECK ("role_id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
17
14
  CONSTRAINT "FK_security_user_role_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE,
18
15
  CONSTRAINT "FK_security_user_role_role_id" FOREIGN KEY ("role_id") REFERENCES "security_role" ("id") ON DELETE CASCADE
19
16
  )
@@ -13,8 +13,6 @@ class CreatePasswordResetTokens1739520000000 {
13
13
  "expires_at" timestamptz NOT NULL,
14
14
  "used_at" timestamptz,
15
15
  "created_at" timestamptz NOT NULL DEFAULT now(),
16
- CONSTRAINT "CHK_security_password_reset_token_id_uuidv7" CHECK ("id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
17
- CONSTRAINT "CHK_security_password_reset_token_user_id_uuidv7" CHECK ("user_id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
18
16
  CONSTRAINT "FK_security_password_reset_token_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE
19
17
  )
20
18
  `);
@@ -14,7 +14,6 @@ class CreateSecurityUser1739530000000 {
14
14
  "admin_approved_at" timestamptz,
15
15
  "is_active" boolean NOT NULL DEFAULT true,
16
16
  "created_at" timestamptz NOT NULL DEFAULT now(),
17
- CONSTRAINT "CHK_security_user_user_id_uuidv7" CHECK ("user_id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
18
17
  CONSTRAINT "FK_security_user_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE
19
18
  )
20
19
  `);
@@ -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" uuid 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();
@@ -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]),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scryan7371/sdr-security",
3
- "version": "0.1.4",
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": {
@@ -14,8 +14,6 @@ export class AddRefreshTokens1700000000001 {
14
14
  "revoked_at" timestamptz,
15
15
  "userId" uuid,
16
16
  "created_at" timestamptz NOT NULL DEFAULT (CURRENT_TIMESTAMP),
17
- CONSTRAINT "CHK_refresh_token_id_uuidv7" CHECK ("id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
18
- CONSTRAINT "CHK_refresh_token_userId_uuidv7" CHECK ("userId" IS NULL OR "userId"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
19
17
  CONSTRAINT "FK_refresh_token_user" FOREIGN KEY ("userId") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE ON UPDATE NO ACTION
20
18
  )
21
19
  `);
@@ -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
+ };
@@ -14,8 +14,6 @@ export class CreateSecurityIdentity1739500000000 {
14
14
  "provider_subject" varchar NOT NULL,
15
15
  "created_at" timestamptz NOT NULL DEFAULT now(),
16
16
  "updated_at" timestamptz NOT NULL DEFAULT now(),
17
- CONSTRAINT "CHK_security_identity_id_uuidv7" CHECK ("id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
18
- CONSTRAINT "CHK_security_identity_user_id_uuidv7" CHECK ("user_id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
19
17
  CONSTRAINT "FK_security_identity_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE
20
18
  )
21
19
  `);
@@ -11,8 +11,7 @@ export class CreateSecurityRoles1739510000000 {
11
11
  "description" text,
12
12
  "is_system" boolean NOT NULL DEFAULT false,
13
13
  "created_at" timestamptz NOT NULL DEFAULT now(),
14
- "updated_at" timestamptz NOT NULL DEFAULT now(),
15
- CONSTRAINT "CHK_security_role_id_uuidv7" CHECK ("id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$')
14
+ "updated_at" timestamptz NOT NULL DEFAULT now()
16
15
  )
17
16
  `);
18
17
 
@@ -12,9 +12,6 @@ export class CreateSecurityUserRoles1739515000000 {
12
12
  "user_id" uuid NOT NULL,
13
13
  "role_id" uuid NOT NULL,
14
14
  "created_at" timestamptz NOT NULL DEFAULT now(),
15
- CONSTRAINT "CHK_security_user_role_id_uuidv7" CHECK ("id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
16
- CONSTRAINT "CHK_security_user_role_user_id_uuidv7" CHECK ("user_id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
17
- CONSTRAINT "CHK_security_user_role_role_id_uuidv7" CHECK ("role_id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
18
15
  CONSTRAINT "FK_security_user_role_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE,
19
16
  CONSTRAINT "FK_security_user_role_role_id" FOREIGN KEY ("role_id") REFERENCES "security_role" ("id") ON DELETE CASCADE
20
17
  )
@@ -14,8 +14,6 @@ export class CreatePasswordResetTokens1739520000000 {
14
14
  "expires_at" timestamptz NOT NULL,
15
15
  "used_at" timestamptz,
16
16
  "created_at" timestamptz NOT NULL DEFAULT now(),
17
- CONSTRAINT "CHK_security_password_reset_token_id_uuidv7" CHECK ("id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
18
- CONSTRAINT "CHK_security_password_reset_token_user_id_uuidv7" CHECK ("user_id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
19
17
  CONSTRAINT "FK_security_password_reset_token_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE
20
18
  )
21
19
  `);
@@ -15,7 +15,6 @@ export class CreateSecurityUser1739530000000 {
15
15
  "admin_approved_at" timestamptz,
16
16
  "is_active" boolean NOT NULL DEFAULT true,
17
17
  "created_at" timestamptz NOT NULL DEFAULT now(),
18
- CONSTRAINT "CHK_security_user_user_id_uuidv7" CHECK ("user_id"::text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
19
18
  CONSTRAINT "FK_security_user_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE
20
19
  )
21
20
  `);
@@ -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" uuid 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();
@@ -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
  }