@scryan7371/sdr-security 0.1.2 → 0.1.4

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 (83) hide show
  1. package/README.md +48 -7
  2. package/dist/api/contracts.d.ts +0 -2
  3. package/dist/api/migrations/1700000000001-add-refresh-tokens.js +4 -2
  4. package/dist/api/migrations/1739500000000-create-security-identity.d.ts +1 -1
  5. package/dist/api/migrations/1739500000000-create-security-identity.js +12 -36
  6. package/dist/api/migrations/1739510000000-create-security-roles.d.ts +1 -1
  7. package/dist/api/migrations/1739510000000-create-security-roles.js +3 -68
  8. package/dist/api/migrations/1739515000000-create-security-user-roles.d.ts +9 -0
  9. package/dist/api/migrations/1739515000000-create-security-user-roles.js +42 -0
  10. package/dist/api/migrations/1739520000000-create-password-reset-tokens.js +4 -2
  11. package/dist/api/migrations/1739530000000-create-security-user.d.ts +9 -0
  12. package/dist/api/migrations/1739530000000-create-security-user.js +42 -0
  13. package/dist/api/migrations/index.d.ts +3 -2
  14. package/dist/api/migrations/index.js +7 -4
  15. package/dist/api/migrations/migrations.test.js +37 -83
  16. package/dist/api/notification-workflows.d.ts +0 -4
  17. package/dist/api/notification-workflows.js +0 -1
  18. package/dist/api/notification-workflows.test.js +1 -4
  19. package/dist/app/client.d.ts +0 -2
  20. package/dist/app/client.test.js +0 -2
  21. package/dist/integration/database.integration.test.js +1 -1
  22. package/dist/nest/contracts.d.ts +0 -3
  23. package/dist/nest/dto/auth.dto.d.ts +0 -2
  24. package/dist/nest/dto/auth.dto.js +0 -10
  25. package/dist/nest/entities/app-user.entity.d.ts +0 -7
  26. package/dist/nest/entities/app-user.entity.js +1 -36
  27. package/dist/nest/entities/password-reset-token.entity.d.ts +1 -0
  28. package/dist/nest/entities/password-reset-token.entity.js +14 -2
  29. package/dist/nest/entities/refresh-token.entity.js +2 -2
  30. package/dist/nest/entities/security-role.entity.d.ts +1 -0
  31. package/dist/nest/entities/security-role.entity.js +13 -1
  32. package/dist/nest/entities/security-user-role.entity.d.ts +1 -0
  33. package/dist/nest/entities/security-user-role.entity.js +14 -2
  34. package/dist/nest/entities/security-user.entity.d.ts +9 -0
  35. package/dist/nest/entities/security-user.entity.js +54 -0
  36. package/dist/nest/index.d.ts +1 -0
  37. package/dist/nest/index.js +1 -0
  38. package/dist/nest/security-auth.controller.d.ts +0 -2
  39. package/dist/nest/security-auth.controller.js +0 -2
  40. package/dist/nest/security-auth.controller.test.js +0 -4
  41. package/dist/nest/security-auth.module.js +2 -0
  42. package/dist/nest/security-auth.service.d.ts +5 -4
  43. package/dist/nest/security-auth.service.js +85 -52
  44. package/dist/nest/security-auth.service.test.js +48 -42
  45. package/dist/nest/security-workflows.module.js +2 -0
  46. package/dist/nest/security-workflows.service.d.ts +4 -2
  47. package/dist/nest/security-workflows.service.js +23 -16
  48. package/dist/nest/security-workflows.service.test.js +29 -24
  49. package/package.json +5 -4
  50. package/src/api/contracts.ts +0 -2
  51. package/src/api/migrations/1700000000001-add-refresh-tokens.ts +4 -2
  52. package/src/api/migrations/1739500000000-create-security-identity.ts +14 -51
  53. package/src/api/migrations/1739510000000-create-security-roles.ts +4 -90
  54. package/src/api/migrations/1739515000000-create-security-user-roles.ts +52 -0
  55. package/src/api/migrations/1739520000000-create-password-reset-tokens.ts +4 -2
  56. package/src/api/migrations/1739530000000-create-security-user.ts +52 -0
  57. package/src/api/migrations/index.ts +6 -3
  58. package/src/api/migrations/migrations.test.ts +48 -111
  59. package/src/api/notification-workflows.test.ts +1 -4
  60. package/src/api/notification-workflows.ts +1 -8
  61. package/src/app/client.test.ts +0 -2
  62. package/src/app/client.ts +1 -6
  63. package/src/integration/database.integration.test.ts +1 -1
  64. package/src/nest/contracts.ts +1 -6
  65. package/src/nest/dto/auth.dto.ts +0 -6
  66. package/src/nest/entities/app-user.entity.ts +2 -23
  67. package/src/nest/entities/password-reset-token.entity.ts +12 -3
  68. package/src/nest/entities/refresh-token.entity.ts +2 -2
  69. package/src/nest/entities/security-role.entity.ts +10 -2
  70. package/src/nest/entities/security-user-role.entity.ts +11 -3
  71. package/src/nest/entities/security-user.entity.ts +25 -0
  72. package/src/nest/index.ts +1 -0
  73. package/src/nest/security-auth.controller.test.ts +0 -4
  74. package/src/nest/security-auth.controller.ts +0 -4
  75. package/src/nest/security-auth.module.ts +2 -0
  76. package/src/nest/security-auth.service.test.ts +78 -44
  77. package/src/nest/security-auth.service.ts +93 -53
  78. package/src/nest/security-workflows.module.ts +2 -0
  79. package/src/nest/security-workflows.service.test.ts +31 -25
  80. package/src/nest/security-workflows.service.ts +22 -13
  81. package/dist/api/migrations/1739490000000-add-google-subject-to-user.d.ts +0 -5
  82. package/dist/api/migrations/1739490000000-add-google-subject-to-user.js +0 -14
  83. package/src/api/migrations/1739490000000-add-google-subject-to-user.ts +0 -12
@@ -16,26 +16,30 @@ 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");
22
23
  const security_role_entity_1 = require("./entities/security-role.entity");
24
+ const security_user_entity_1 = require("./entities/security-user.entity");
23
25
  const security_user_role_entity_1 = require("./entities/security-user-role.entity");
24
26
  const tokens_1 = require("./tokens");
25
27
  let SecurityWorkflowsService = class SecurityWorkflowsService {
26
- usersRepo;
28
+ appUsersRepo;
29
+ securityUsersRepo;
27
30
  rolesRepo;
28
31
  userRolesRepo;
29
32
  notifier;
30
- constructor(usersRepo, rolesRepo, userRolesRepo, notifier) {
31
- this.usersRepo = usersRepo;
33
+ constructor(appUsersRepo, securityUsersRepo, rolesRepo, userRolesRepo, notifier) {
34
+ this.appUsersRepo = appUsersRepo;
35
+ this.securityUsersRepo = securityUsersRepo;
32
36
  this.rolesRepo = rolesRepo;
33
37
  this.userRolesRepo = userRolesRepo;
34
38
  this.notifier = notifier;
35
39
  }
36
40
  async markEmailVerifiedAndNotifyAdmins(userId) {
37
- await this.usersRepo.update({ id: userId }, { emailVerifiedAt: new Date(), emailVerificationToken: null });
38
- const user = await this.usersRepo.findOne({ where: { id: userId } });
41
+ await this.securityUsersRepo.update({ userId }, { emailVerifiedAt: new Date(), emailVerificationToken: null });
42
+ const user = await this.appUsersRepo.findOne({ where: { id: userId } });
39
43
  if (!user) {
40
44
  throw new common_1.NotFoundException("User not found");
41
45
  }
@@ -48,15 +52,13 @@ let SecurityWorkflowsService = class SecurityWorkflowsService {
48
52
  user: {
49
53
  id: user.id,
50
54
  email: user.email,
51
- firstName: user.firstName,
52
- lastName: user.lastName,
53
55
  },
54
56
  });
55
57
  return { success: true, notified: true, adminEmails };
56
58
  }
57
59
  async setAdminApprovalAndNotifyUser(userId, approved) {
58
- await this.usersRepo.update({ id: userId }, { adminApprovedAt: approved ? new Date() : null });
59
- const user = await this.usersRepo.findOne({ where: { id: userId } });
60
+ await this.securityUsersRepo.update({ userId }, { adminApprovedAt: approved ? new Date() : null });
61
+ const user = await this.appUsersRepo.findOne({ where: { id: userId } });
60
62
  if (!user) {
61
63
  throw new common_1.NotFoundException("User not found");
62
64
  }
@@ -65,7 +67,6 @@ let SecurityWorkflowsService = class SecurityWorkflowsService {
65
67
  }
66
68
  await this.notifier.sendUserAccountApproved({
67
69
  email: user.email,
68
- firstName: user.firstName,
69
70
  });
70
71
  return { success: true, notified: true };
71
72
  }
@@ -74,8 +75,9 @@ let SecurityWorkflowsService = class SecurityWorkflowsService {
74
75
  .createQueryBuilder("userRole")
75
76
  .innerJoin("security_role", "role", "role.id = userRole.role_id")
76
77
  .innerJoin("app_user", "user", "user.id = userRole.user_id")
78
+ .innerJoin("security_user", "securityUser", "securityUser.user_id = userRole.user_id")
77
79
  .where("role.role_key = :roleKey", { roleKey: contracts_1.ADMIN_ROLE })
78
- .andWhere("user.is_active = :isActive", { isActive: true })
80
+ .andWhere("securityUser.is_active = :isActive", { isActive: true })
79
81
  .select("DISTINCT user.email", "email")
80
82
  .getRawMany();
81
83
  return rows.map((row) => row.email).filter(Boolean);
@@ -93,6 +95,7 @@ let SecurityWorkflowsService = class SecurityWorkflowsService {
93
95
  let role = await this.rolesRepo.findOne({ where: { roleKey } });
94
96
  if (!role) {
95
97
  role = this.rolesRepo.create({
98
+ id: (0, uuid_1.v7)(),
96
99
  roleKey,
97
100
  description: description?.trim() || null,
98
101
  isSystem: roleKey === contracts_1.ADMIN_ROLE,
@@ -138,6 +141,7 @@ let SecurityWorkflowsService = class SecurityWorkflowsService {
138
141
  await this.userRolesRepo.delete({ userId });
139
142
  if (roles.length > 0) {
140
143
  await this.userRolesRepo.save(roles.map((role) => this.userRolesRepo.create({
144
+ id: (0, uuid_1.v7)(),
141
145
  userId,
142
146
  roleId: role.id,
143
147
  })));
@@ -158,11 +162,11 @@ let SecurityWorkflowsService = class SecurityWorkflowsService {
158
162
  return this.setUserRoles(userId, nextRoles);
159
163
  }
160
164
  async setUserActive(userId, active) {
161
- await this.usersRepo.update({ id: userId }, { isActive: active });
165
+ await this.securityUsersRepo.update({ userId }, { isActive: active });
162
166
  return { success: true, userId, active };
163
167
  }
164
168
  async assertUserExists(userId) {
165
- const user = await this.usersRepo.findOne({ where: { id: userId } });
169
+ const user = await this.appUsersRepo.findOne({ where: { id: userId } });
166
170
  if (!user) {
167
171
  throw new common_1.NotFoundException("User not found");
168
172
  }
@@ -181,6 +185,7 @@ let SecurityWorkflowsService = class SecurityWorkflowsService {
181
185
  return;
182
186
  }
183
187
  await this.rolesRepo.save(missing.map((roleKey) => this.rolesRepo.create({
188
+ id: (0, uuid_1.v7)(),
184
189
  roleKey,
185
190
  description: null,
186
191
  isSystem: roleKey === contracts_1.ADMIN_ROLE,
@@ -191,10 +196,12 @@ exports.SecurityWorkflowsService = SecurityWorkflowsService;
191
196
  exports.SecurityWorkflowsService = SecurityWorkflowsService = __decorate([
192
197
  (0, common_1.Injectable)(),
193
198
  __param(0, (0, typeorm_1.InjectRepository)(app_user_entity_1.AppUserEntity)),
194
- __param(1, (0, typeorm_1.InjectRepository)(security_role_entity_1.SecurityRoleEntity)),
195
- __param(2, (0, typeorm_1.InjectRepository)(security_user_role_entity_1.SecurityUserRoleEntity)),
196
- __param(3, (0, common_1.Inject)(tokens_1.SECURITY_WORKFLOW_NOTIFIER)),
199
+ __param(1, (0, typeorm_1.InjectRepository)(security_user_entity_1.SecurityUserEntity)),
200
+ __param(2, (0, typeorm_1.InjectRepository)(security_role_entity_1.SecurityRoleEntity)),
201
+ __param(3, (0, typeorm_1.InjectRepository)(security_user_role_entity_1.SecurityUserRoleEntity)),
202
+ __param(4, (0, common_1.Inject)(tokens_1.SECURITY_WORKFLOW_NOTIFIER)),
197
203
  __metadata("design:paramtypes", [typeorm_2.Repository,
204
+ typeorm_2.Repository,
198
205
  typeorm_2.Repository,
199
206
  typeorm_2.Repository, Object])
200
207
  ], SecurityWorkflowsService);
@@ -19,25 +19,31 @@ const makeNotifier = () => ({
19
19
  const makeUser = () => ({
20
20
  id: "user-1",
21
21
  email: "user@example.com",
22
- firstName: "A",
23
- lastName: "B",
24
22
  isActive: true,
25
23
  });
26
24
  const setup = () => {
27
- const usersRepo = makeRepo();
25
+ const appUsersRepo = makeRepo();
26
+ const securityUsersRepo = makeRepo();
28
27
  const rolesRepo = makeRepo();
29
28
  const userRolesRepo = makeRepo();
30
29
  const notifier = makeNotifier();
31
- const service = new security_workflows_service_1.SecurityWorkflowsService(usersRepo, rolesRepo, userRolesRepo, notifier);
32
- return { service, usersRepo, rolesRepo, userRolesRepo, notifier };
30
+ const service = new security_workflows_service_1.SecurityWorkflowsService(appUsersRepo, securityUsersRepo, rolesRepo, userRolesRepo, notifier);
31
+ return {
32
+ service,
33
+ appUsersRepo,
34
+ securityUsersRepo,
35
+ rolesRepo,
36
+ userRolesRepo,
37
+ notifier,
38
+ };
33
39
  };
34
40
  (0, vitest_1.describe)("SecurityWorkflowsService", () => {
35
41
  (0, vitest_1.beforeEach)(() => {
36
42
  vitest_1.vi.clearAllMocks();
37
43
  });
38
44
  (0, vitest_1.it)("marks email verified and notifies admins", async () => {
39
- const { service, usersRepo, userRolesRepo, notifier } = setup();
40
- usersRepo.findOne.mockResolvedValue(makeUser());
45
+ const { service, appUsersRepo, userRolesRepo, notifier } = setup();
46
+ appUsersRepo.findOne.mockResolvedValue(makeUser());
41
47
  const getRawMany = vitest_1.vi
42
48
  .fn()
43
49
  .mockResolvedValue([{ email: "admin@example.com" }]);
@@ -58,8 +64,8 @@ const setup = () => {
58
64
  (0, vitest_1.expect)(notifier.sendAdminsUserEmailVerified).toHaveBeenCalled();
59
65
  });
60
66
  (0, vitest_1.it)("returns not-notified when no admins are present", async () => {
61
- const { service, usersRepo, userRolesRepo } = setup();
62
- usersRepo.findOne.mockResolvedValue(makeUser());
67
+ const { service, appUsersRepo, userRolesRepo } = setup();
68
+ appUsersRepo.findOne.mockResolvedValue(makeUser());
63
69
  const qb = {
64
70
  innerJoin: vitest_1.vi.fn().mockReturnThis(),
65
71
  where: vitest_1.vi.fn().mockReturnThis(),
@@ -71,23 +77,22 @@ const setup = () => {
71
77
  await (0, vitest_1.expect)(service.markEmailVerifiedAndNotifyAdmins("user-1")).resolves.toEqual({ success: true, notified: false, adminEmails: [] });
72
78
  });
73
79
  (0, vitest_1.it)("throws when user is missing during verification flow", async () => {
74
- const { service, usersRepo } = setup();
75
- usersRepo.findOne.mockResolvedValue(null);
80
+ const { service, appUsersRepo } = setup();
81
+ appUsersRepo.findOne.mockResolvedValue(null);
76
82
  await (0, vitest_1.expect)(service.markEmailVerifiedAndNotifyAdmins("missing")).rejects.toBeInstanceOf(common_1.NotFoundException);
77
83
  });
78
84
  (0, vitest_1.it)("handles admin approval notifications", async () => {
79
- const { service, usersRepo, notifier } = setup();
80
- usersRepo.findOne.mockResolvedValue(makeUser());
85
+ const { service, appUsersRepo, notifier } = setup();
86
+ appUsersRepo.findOne.mockResolvedValue(makeUser());
81
87
  await (0, vitest_1.expect)(service.setAdminApprovalAndNotifyUser("user-1", false)).resolves.toEqual({ success: true, notified: false });
82
88
  await (0, vitest_1.expect)(service.setAdminApprovalAndNotifyUser("user-1", true)).resolves.toEqual({ success: true, notified: true });
83
89
  (0, vitest_1.expect)(notifier.sendUserAccountApproved).toHaveBeenCalledWith({
84
90
  email: "user@example.com",
85
- firstName: "A",
86
91
  });
87
92
  });
88
93
  (0, vitest_1.it)("throws when approval target user is missing", async () => {
89
- const { service, usersRepo } = setup();
90
- usersRepo.findOne.mockResolvedValue(null);
94
+ const { service, appUsersRepo } = setup();
95
+ appUsersRepo.findOne.mockResolvedValue(null);
91
96
  await (0, vitest_1.expect)(service.setAdminApprovalAndNotifyUser("missing", true)).rejects.toBeInstanceOf(common_1.NotFoundException);
92
97
  });
93
98
  (0, vitest_1.it)("manages role catalog and protected role removal", async () => {
@@ -125,8 +130,8 @@ const setup = () => {
125
130
  (0, vitest_1.expect)(userRolesRepo.delete).toHaveBeenCalledWith({ roleId: "r2" });
126
131
  });
127
132
  (0, vitest_1.it)("gets and sets user roles", async () => {
128
- const { service, usersRepo, userRolesRepo, rolesRepo } = setup();
129
- usersRepo.findOne.mockResolvedValue(makeUser());
133
+ const { service, appUsersRepo, userRolesRepo, rolesRepo } = setup();
134
+ appUsersRepo.findOne.mockResolvedValue(makeUser());
130
135
  userRolesRepo.find.mockResolvedValue([{ roleId: "r1" }]);
131
136
  rolesRepo.find.mockResolvedValue([{ id: "r1", roleKey: "ADMIN" }]);
132
137
  await (0, vitest_1.expect)(service.getUserRoles("user-1")).resolves.toEqual({
@@ -143,8 +148,8 @@ const setup = () => {
143
148
  (0, vitest_1.expect)(userRolesRepo.save).toHaveBeenCalled();
144
149
  });
145
150
  (0, vitest_1.it)("assigns and removes role from user", async () => {
146
- const { service, usersRepo, userRolesRepo, rolesRepo } = setup();
147
- usersRepo.findOne.mockResolvedValue(makeUser());
151
+ const { service, appUsersRepo, userRolesRepo, rolesRepo } = setup();
152
+ appUsersRepo.findOne.mockResolvedValue(makeUser());
148
153
  userRolesRepo.find.mockResolvedValue([]);
149
154
  rolesRepo.find.mockResolvedValue([]);
150
155
  await service.assignRoleToUser("user-1", "coach");
@@ -156,17 +161,17 @@ const setup = () => {
156
161
  (0, vitest_1.expect)(userRolesRepo.delete).toHaveBeenCalledWith({ userId: "user-1" });
157
162
  });
158
163
  (0, vitest_1.it)("sets user active state", async () => {
159
- const { service, usersRepo } = setup();
164
+ const { service, securityUsersRepo } = setup();
160
165
  await (0, vitest_1.expect)(service.setUserActive("user-1", false)).resolves.toEqual({
161
166
  success: true,
162
167
  userId: "user-1",
163
168
  active: false,
164
169
  });
165
- (0, vitest_1.expect)(usersRepo.update).toHaveBeenCalledWith({ id: "user-1" }, { isActive: false });
170
+ (0, vitest_1.expect)(securityUsersRepo.update).toHaveBeenCalledWith({ userId: "user-1" }, { isActive: false });
166
171
  });
167
172
  (0, vitest_1.it)("throws when role operations target missing user", async () => {
168
- const { service, usersRepo } = setup();
169
- usersRepo.findOne.mockResolvedValue(null);
173
+ const { service, appUsersRepo } = setup();
174
+ appUsersRepo.findOne.mockResolvedValue(null);
170
175
  await (0, vitest_1.expect)(service.getUserRoles("missing")).rejects.toBeInstanceOf(common_1.NotFoundException);
171
176
  await (0, vitest_1.expect)(service.setUserRoles("missing", ["ADMIN"])).rejects.toBeInstanceOf(common_1.NotFoundException);
172
177
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scryan7371/sdr-security",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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",
@@ -62,10 +63,10 @@
62
63
  "@nestjs/typeorm": "11.0.0",
63
64
  "@types/jsonwebtoken": "9.0.10",
64
65
  "@types/node": "^25.2.3",
65
- "@types/pg": "8.15.5",
66
+ "@types/pg": "8.16.0",
66
67
  "@vitest/coverage-v8": "^4.0.18",
67
68
  "eslint": "9.18.0",
68
- "pg": "8.16.3",
69
+ "pg": "8.18.0",
69
70
  "prettier": "3.8.1",
70
71
  "typeorm": "0.3.28",
71
72
  "typescript": "5.9.3",
@@ -4,8 +4,6 @@ export type UserRole = string;
4
4
  export type SafeUser = {
5
5
  id: string;
6
6
  email: string;
7
- firstName: string | null;
8
- lastName: string | null;
9
7
  phone: string | null;
10
8
  roles: UserRole[];
11
9
  emailVerifiedAt: string | Date | null;
@@ -8,12 +8,14 @@ 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
+ 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}$'),
17
19
  CONSTRAINT "FK_refresh_token_user" FOREIGN KEY ("userId") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE ON UPDATE NO ACTION
18
20
  )
19
21
  `);
@@ -2,25 +2,20 @@ export class CreateSecurityIdentity1739500000000 {
2
2
  name = "CreateSecurityIdentity1739500000000";
3
3
 
4
4
  async up(queryRunner: {
5
- query: (sql: string, params?: unknown[]) => Promise<unknown>;
5
+ query: (sql: string) => Promise<unknown>;
6
6
  }): Promise<void> {
7
- const userTable = getSafeIdentifier(process.env.USER_TABLE, "app_user");
8
- const userSchema = getSafeIdentifier(
9
- process.env.USER_TABLE_SCHEMA,
10
- "public",
11
- );
12
- const userTableRef = `"${userSchema}"."${userTable}"`;
13
-
14
- await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
7
+ const userTableRef = getUserTableReference();
15
8
 
16
9
  await queryRunner.query(`
17
10
  CREATE TABLE IF NOT EXISTS "security_identity" (
18
- "id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
19
- "user_id" varchar NOT NULL,
11
+ "id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
12
+ "user_id" uuid NOT NULL,
20
13
  "provider" varchar NOT NULL,
21
14
  "provider_subject" varchar NOT NULL,
22
15
  "created_at" timestamptz NOT NULL DEFAULT now(),
23
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}$'),
24
19
  CONSTRAINT "FK_security_identity_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE
25
20
  )
26
21
  `);
@@ -31,46 +26,6 @@ export class CreateSecurityIdentity1739500000000 {
31
26
  await queryRunner.query(
32
27
  `CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_identity_user_provider" ON "security_identity" ("user_id", "provider")`,
33
28
  );
34
-
35
- const hasGoogleSubjectColumn = (await queryRunner.query(
36
- `
37
- SELECT 1
38
- FROM information_schema.columns
39
- WHERE table_schema = $1
40
- AND table_name = $2
41
- AND column_name = 'google_subject'
42
- LIMIT 1
43
- `,
44
- [userSchema, userTable],
45
- )) as Array<{ "?column?": number }>;
46
-
47
- if (hasGoogleSubjectColumn.length > 0) {
48
- await queryRunner.query(`
49
- INSERT INTO "security_identity" (
50
- "user_id",
51
- "provider",
52
- "provider_subject",
53
- "created_at",
54
- "updated_at"
55
- )
56
- SELECT
57
- "id",
58
- 'google',
59
- "google_subject",
60
- now(),
61
- now()
62
- FROM ${userTableRef}
63
- WHERE "google_subject" IS NOT NULL
64
- ON CONFLICT ("provider", "provider_subject") DO NOTHING
65
- `);
66
-
67
- await queryRunner.query(
68
- `DROP INDEX IF EXISTS "IDX_app_user_google_subject"`,
69
- );
70
- await queryRunner.query(
71
- `ALTER TABLE ${userTableRef} DROP COLUMN IF EXISTS "google_subject"`,
72
- );
73
- }
74
29
  }
75
30
 
76
31
  async down(queryRunner: {
@@ -93,3 +48,11 @@ const getSafeIdentifier = (value: string | undefined, fallback: string) => {
93
48
  }
94
49
  return resolved;
95
50
  };
51
+
52
+ const getUserTableReference = () => {
53
+ const table = getSafeIdentifier(process.env.USER_TABLE, "app_user");
54
+ const schema = process.env.USER_TABLE_SCHEMA
55
+ ? getSafeIdentifier(process.env.USER_TABLE_SCHEMA, "public")
56
+ : "public";
57
+ return `"${schema}"."${table}"`;
58
+ };
@@ -2,25 +2,17 @@ export class CreateSecurityRoles1739510000000 {
2
2
  name = "CreateSecurityRoles1739510000000";
3
3
 
4
4
  async up(queryRunner: {
5
- query: (sql: string, params?: unknown[]) => Promise<unknown>;
5
+ query: (sql: string) => Promise<unknown>;
6
6
  }): Promise<void> {
7
- const userTable = getSafeIdentifier(process.env.USER_TABLE, "app_user");
8
- const userSchema = getSafeIdentifier(
9
- process.env.USER_TABLE_SCHEMA,
10
- "public",
11
- );
12
- const userTableRef = `"${userSchema}"."${userTable}"`;
13
-
14
- await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
15
-
16
7
  await queryRunner.query(`
17
8
  CREATE TABLE IF NOT EXISTS "security_role" (
18
- "id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
9
+ "id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
19
10
  "role_key" varchar NOT NULL,
20
11
  "description" text,
21
12
  "is_system" boolean NOT NULL DEFAULT false,
22
13
  "created_at" timestamptz NOT NULL DEFAULT now(),
23
- "updated_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}$')
24
16
  )
25
17
  `);
26
18
 
@@ -28,95 +20,17 @@ export class CreateSecurityRoles1739510000000 {
28
20
  `CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_role_key" ON "security_role" ("role_key")`,
29
21
  );
30
22
 
31
- await queryRunner.query(`
32
- CREATE TABLE IF NOT EXISTS "security_user_role" (
33
- "id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
34
- "user_id" varchar NOT NULL,
35
- "role_id" uuid NOT NULL,
36
- "created_at" timestamptz NOT NULL DEFAULT now(),
37
- CONSTRAINT "FK_security_user_role_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE,
38
- CONSTRAINT "FK_security_user_role_role_id" FOREIGN KEY ("role_id") REFERENCES "security_role" ("id") ON DELETE CASCADE
39
- )
40
- `);
41
-
42
- await queryRunner.query(
43
- `CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_user_role_user_role" ON "security_user_role" ("user_id", "role_id")`,
44
- );
45
-
46
23
  await queryRunner.query(`
47
24
  INSERT INTO "security_role" ("role_key", "description", "is_system", "created_at", "updated_at")
48
25
  VALUES ('ADMIN', 'Administrative access', true, now(), now())
49
26
  ON CONFLICT ("role_key") DO NOTHING
50
27
  `);
51
-
52
- const hasRoleColumn = (await queryRunner.query(
53
- `
54
- SELECT 1
55
- FROM information_schema.columns
56
- WHERE table_schema = $1
57
- AND table_name = $2
58
- AND column_name = 'role'
59
- LIMIT 1
60
- `,
61
- [userSchema, userTable],
62
- )) as Array<{ "?column?": number }>;
63
-
64
- if (hasRoleColumn.length > 0) {
65
- await queryRunner.query(`
66
- INSERT INTO "security_role" ("role_key", "description", "is_system", "created_at", "updated_at")
67
- SELECT DISTINCT
68
- CASE
69
- WHEN UPPER(TRIM("role")) = 'ADMINISTRATOR' THEN 'ADMIN'
70
- ELSE UPPER(TRIM("role"))
71
- END AS "role_key",
72
- NULL,
73
- false,
74
- now(),
75
- now()
76
- FROM ${userTableRef}
77
- WHERE "role" IS NOT NULL
78
- AND LENGTH(TRIM("role")) > 0
79
- ON CONFLICT ("role_key") DO NOTHING
80
- `);
81
-
82
- await queryRunner.query(`
83
- INSERT INTO "security_user_role" ("user_id", "role_id", "created_at")
84
- SELECT
85
- u."id" AS "user_id",
86
- r."id" AS "role_id",
87
- now()
88
- FROM ${userTableRef} u
89
- INNER JOIN "security_role" r ON r."role_key" = CASE
90
- WHEN UPPER(TRIM(u."role")) = 'ADMINISTRATOR' THEN 'ADMIN'
91
- ELSE UPPER(TRIM(u."role"))
92
- END
93
- WHERE u."role" IS NOT NULL
94
- AND LENGTH(TRIM(u."role")) > 0
95
- ON CONFLICT ("user_id", "role_id") DO NOTHING
96
- `);
97
-
98
- await queryRunner.query(
99
- `ALTER TABLE ${userTableRef} DROP COLUMN IF EXISTS "role"`,
100
- );
101
- }
102
28
  }
103
29
 
104
30
  async down(queryRunner: {
105
31
  query: (sql: string) => Promise<unknown>;
106
32
  }): Promise<void> {
107
- await queryRunner.query(
108
- `DROP INDEX IF EXISTS "IDX_security_user_role_user_role"`,
109
- );
110
- await queryRunner.query(`DROP TABLE IF EXISTS "security_user_role"`);
111
33
  await queryRunner.query(`DROP INDEX IF EXISTS "IDX_security_role_key"`);
112
34
  await queryRunner.query(`DROP TABLE IF EXISTS "security_role"`);
113
35
  }
114
36
  }
115
-
116
- const getSafeIdentifier = (value: string | undefined, fallback: string) => {
117
- const resolved = value?.trim() || fallback;
118
- if (!resolved || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(resolved)) {
119
- throw new Error(`Invalid SQL identifier: ${resolved}`);
120
- }
121
- return resolved;
122
- };
@@ -0,0 +1,52 @@
1
+ export class CreateSecurityUserRoles1739515000000 {
2
+ name = "CreateSecurityUserRoles1739515000000";
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 "security_user_role" (
11
+ "id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
12
+ "user_id" uuid NOT NULL,
13
+ "role_id" uuid NOT NULL,
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
+ CONSTRAINT "FK_security_user_role_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE,
19
+ CONSTRAINT "FK_security_user_role_role_id" FOREIGN KEY ("role_id") REFERENCES "security_role" ("id") ON DELETE CASCADE
20
+ )
21
+ `);
22
+
23
+ await queryRunner.query(
24
+ `CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_user_role_user_role" ON "security_user_role" ("user_id", "role_id")`,
25
+ );
26
+ }
27
+
28
+ async down(queryRunner: {
29
+ query: (sql: string) => Promise<unknown>;
30
+ }): Promise<void> {
31
+ await queryRunner.query(
32
+ `DROP INDEX IF EXISTS "IDX_security_user_role_user_role"`,
33
+ );
34
+ await queryRunner.query(`DROP TABLE IF EXISTS "security_user_role"`);
35
+ }
36
+ }
37
+
38
+ const getUserTableReference = () => {
39
+ const table = getSafeIdentifier(process.env.USER_TABLE, "app_user");
40
+ const schema = process.env.USER_TABLE_SCHEMA
41
+ ? getSafeIdentifier(process.env.USER_TABLE_SCHEMA, "public")
42
+ : "public";
43
+ return `"${schema}"."${table}"`;
44
+ };
45
+
46
+ const getSafeIdentifier = (value: string | undefined, fallback: string) => {
47
+ const resolved = value?.trim() || fallback;
48
+ if (!resolved || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(resolved)) {
49
+ throw new Error(`Invalid SQL identifier: ${resolved}`);
50
+ }
51
+ return resolved;
52
+ };
@@ -8,12 +8,14 @@ export class CreatePasswordResetTokens1739520000000 {
8
8
 
9
9
  await queryRunner.query(`
10
10
  CREATE TABLE IF NOT EXISTS "security_password_reset_token" (
11
- "id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
12
- "user_id" varchar NOT NULL,
11
+ "id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
12
+ "user_id" uuid NOT NULL,
13
13
  "token" varchar NOT NULL,
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}$'),
17
19
  CONSTRAINT "FK_security_password_reset_token_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE
18
20
  )
19
21
  `);
@@ -0,0 +1,52 @@
1
+ export class CreateSecurityUser1739530000000 {
2
+ name = "CreateSecurityUser1739530000000";
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 "security_user" (
11
+ "user_id" uuid PRIMARY KEY NOT NULL,
12
+ "password_hash" varchar NOT NULL,
13
+ "email_verified_at" timestamptz,
14
+ "email_verification_token" varchar,
15
+ "admin_approved_at" timestamptz,
16
+ "is_active" boolean NOT NULL DEFAULT true,
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
+ CONSTRAINT "FK_security_user_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE
20
+ )
21
+ `);
22
+
23
+ await queryRunner.query(
24
+ `CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_user_email_verification_token" ON "security_user" ("email_verification_token") WHERE "email_verification_token" IS NOT NULL`,
25
+ );
26
+ }
27
+
28
+ async down(queryRunner: {
29
+ query: (sql: string) => Promise<unknown>;
30
+ }): Promise<void> {
31
+ await queryRunner.query(
32
+ `DROP INDEX IF EXISTS "IDX_security_user_email_verification_token"`,
33
+ );
34
+ await queryRunner.query(`DROP TABLE IF EXISTS "security_user"`);
35
+ }
36
+ }
37
+
38
+ const getSafeIdentifier = (value: string | undefined, fallback: string) => {
39
+ const resolved = value?.trim() || fallback;
40
+ if (!resolved || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(resolved)) {
41
+ throw new Error(`Invalid SQL identifier: ${resolved}`);
42
+ }
43
+ return resolved;
44
+ };
45
+
46
+ const getUserTableReference = () => {
47
+ const table = getSafeIdentifier(process.env.USER_TABLE, "app_user");
48
+ const schema = process.env.USER_TABLE_SCHEMA
49
+ ? getSafeIdentifier(process.env.USER_TABLE_SCHEMA, "public")
50
+ : "public";
51
+ return `"${schema}"."${table}"`;
52
+ };