@scryan7371/sdr-security 0.1.1 → 0.1.3

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 (145) hide show
  1. package/README.md +216 -13
  2. package/dist/api/contracts.d.ts +12 -2
  3. package/dist/api/index.d.ts +1 -0
  4. package/dist/api/index.js +1 -0
  5. package/dist/api/migrations/1739500000000-create-security-identity.d.ts +1 -1
  6. package/dist/api/migrations/1739500000000-create-security-identity.js +9 -35
  7. package/dist/api/migrations/1739510000000-create-security-roles.d.ts +1 -1
  8. package/dist/api/migrations/1739510000000-create-security-roles.js +1 -67
  9. package/dist/api/migrations/1739515000000-create-security-user-roles.d.ts +9 -0
  10. package/dist/api/migrations/1739515000000-create-security-user-roles.js +39 -0
  11. package/dist/api/migrations/1739520000000-create-password-reset-tokens.d.ts +9 -0
  12. package/dist/api/migrations/1739520000000-create-password-reset-tokens.js +42 -0
  13. package/dist/api/migrations/1739530000000-create-security-user.d.ts +9 -0
  14. package/dist/api/migrations/1739530000000-create-security-user.js +41 -0
  15. package/dist/api/migrations/index.d.ts +4 -2
  16. package/dist/api/migrations/index.js +10 -4
  17. package/dist/api/migrations/migrations.test.d.ts +1 -0
  18. package/dist/api/migrations/migrations.test.js +88 -0
  19. package/dist/api/notification-workflows.d.ts +31 -0
  20. package/dist/api/notification-workflows.js +22 -0
  21. package/dist/api/notification-workflows.test.d.ts +1 -0
  22. package/dist/api/notification-workflows.test.js +63 -0
  23. package/dist/api/validation.test.d.ts +1 -0
  24. package/dist/api/validation.test.js +20 -0
  25. package/dist/app/client.d.ts +17 -4
  26. package/dist/app/client.js +38 -11
  27. package/dist/app/client.test.d.ts +1 -0
  28. package/dist/app/client.test.js +130 -0
  29. package/dist/index.test.d.ts +1 -0
  30. package/dist/index.test.js +10 -0
  31. package/dist/integration/database.integration.test.d.ts +1 -0
  32. package/dist/integration/database.integration.test.js +158 -0
  33. package/dist/nest/contracts.d.ts +21 -0
  34. package/dist/nest/contracts.js +2 -0
  35. package/dist/nest/dto/auth.dto.d.ts +25 -0
  36. package/dist/nest/dto/auth.dto.js +89 -0
  37. package/dist/nest/dto/workflows.dto.d.ts +16 -0
  38. package/dist/nest/dto/workflows.dto.js +58 -0
  39. package/dist/nest/entities/app-user.entity.d.ts +4 -0
  40. package/dist/nest/entities/app-user.entity.js +29 -0
  41. package/dist/nest/entities/password-reset-token.entity.d.ts +8 -0
  42. package/dist/nest/entities/password-reset-token.entity.js +49 -0
  43. package/dist/nest/entities/refresh-token.entity.d.ts +8 -0
  44. package/dist/nest/entities/refresh-token.entity.js +49 -0
  45. package/dist/nest/entities/security-role.entity.d.ts +6 -0
  46. package/dist/nest/entities/security-role.entity.js +39 -0
  47. package/dist/nest/entities/security-user-role.entity.d.ts +5 -0
  48. package/dist/nest/entities/security-user-role.entity.js +34 -0
  49. package/dist/nest/entities/security-user.entity.d.ts +9 -0
  50. package/dist/nest/entities/security-user.entity.js +54 -0
  51. package/dist/nest/index.d.ts +19 -0
  52. package/dist/nest/index.js +35 -0
  53. package/dist/nest/index.test.d.ts +1 -0
  54. package/dist/nest/index.test.js +14 -0
  55. package/dist/nest/security-admin.guard.d.ts +4 -0
  56. package/dist/nest/security-admin.guard.js +25 -0
  57. package/dist/nest/security-admin.guard.test.d.ts +1 -0
  58. package/dist/nest/security-admin.guard.test.js +24 -0
  59. package/dist/nest/security-auth.constants.d.ts +1 -0
  60. package/dist/nest/security-auth.constants.js +4 -0
  61. package/dist/nest/security-auth.controller.d.ts +51 -0
  62. package/dist/nest/security-auth.controller.js +177 -0
  63. package/dist/nest/security-auth.controller.test.d.ts +1 -0
  64. package/dist/nest/security-auth.controller.test.js +87 -0
  65. package/dist/nest/security-auth.module.d.ts +9 -0
  66. package/dist/nest/security-auth.module.js +70 -0
  67. package/dist/nest/security-auth.options.d.ts +8 -0
  68. package/dist/nest/security-auth.options.js +2 -0
  69. package/dist/nest/security-auth.service.d.ts +60 -0
  70. package/dist/nest/security-auth.service.js +299 -0
  71. package/dist/nest/security-auth.service.test.d.ts +1 -0
  72. package/dist/nest/security-auth.service.test.js +249 -0
  73. package/dist/nest/security-jwt.guard.d.ts +7 -0
  74. package/dist/nest/security-jwt.guard.js +46 -0
  75. package/dist/nest/security-jwt.guard.test.d.ts +1 -0
  76. package/dist/nest/security-jwt.guard.test.js +51 -0
  77. package/dist/nest/security-modules.test.d.ts +1 -0
  78. package/dist/nest/security-modules.test.js +61 -0
  79. package/dist/nest/security-workflows.controller.d.ts +72 -0
  80. package/dist/nest/security-workflows.controller.js +187 -0
  81. package/dist/nest/security-workflows.controller.test.d.ts +1 -0
  82. package/dist/nest/security-workflows.controller.test.js +87 -0
  83. package/dist/nest/security-workflows.module.d.ts +9 -0
  84. package/dist/nest/security-workflows.module.js +61 -0
  85. package/dist/nest/security-workflows.service.d.ts +69 -0
  86. package/dist/nest/security-workflows.service.js +203 -0
  87. package/dist/nest/security-workflows.service.test.d.ts +1 -0
  88. package/dist/nest/security-workflows.service.test.js +178 -0
  89. package/dist/nest/swagger.d.ts +2 -0
  90. package/dist/nest/swagger.js +16 -0
  91. package/dist/nest/swagger.test.d.ts +1 -0
  92. package/dist/nest/swagger.test.js +21 -0
  93. package/dist/nest/tokens.d.ts +1 -0
  94. package/dist/nest/tokens.js +4 -0
  95. package/package.json +45 -4
  96. package/src/api/contracts.ts +11 -2
  97. package/src/api/index.ts +1 -0
  98. package/src/api/migrations/1739500000000-create-security-identity.ts +11 -50
  99. package/src/api/migrations/1739510000000-create-security-roles.ts +2 -89
  100. package/src/api/migrations/1739515000000-create-security-user-roles.ts +49 -0
  101. package/src/api/migrations/1739520000000-create-password-reset-tokens.ts +57 -0
  102. package/src/api/migrations/1739530000000-create-security-user.ts +51 -0
  103. package/src/api/migrations/index.ts +9 -3
  104. package/src/api/migrations/migrations.test.ts +145 -0
  105. package/src/api/notification-workflows.test.ts +78 -0
  106. package/src/api/notification-workflows.ts +38 -0
  107. package/src/api/validation.test.ts +21 -0
  108. package/src/app/client.test.ts +157 -0
  109. package/src/app/client.ts +74 -18
  110. package/src/index.test.ts +9 -0
  111. package/src/integration/database.integration.test.ts +205 -0
  112. package/src/nest/contracts.ts +20 -0
  113. package/src/nest/dto/auth.dto.ts +48 -0
  114. package/src/nest/dto/workflows.dto.ts +29 -0
  115. package/src/nest/entities/app-user.entity.ts +10 -0
  116. package/src/nest/entities/password-reset-token.entity.ts +27 -0
  117. package/src/nest/entities/refresh-token.entity.ts +22 -0
  118. package/src/nest/entities/security-role.entity.ts +16 -0
  119. package/src/nest/entities/security-user-role.entity.ts +13 -0
  120. package/src/nest/entities/security-user.entity.ts +25 -0
  121. package/src/nest/index.test.ts +20 -0
  122. package/src/nest/index.ts +19 -0
  123. package/src/nest/security-admin.guard.test.ts +31 -0
  124. package/src/nest/security-admin.guard.ts +21 -0
  125. package/src/nest/security-auth.constants.ts +1 -0
  126. package/src/nest/security-auth.controller.test.ts +128 -0
  127. package/src/nest/security-auth.controller.ts +148 -0
  128. package/src/nest/security-auth.module.ts +65 -0
  129. package/src/nest/security-auth.options.ts +8 -0
  130. package/src/nest/security-auth.service.test.ts +368 -0
  131. package/src/nest/security-auth.service.ts +356 -0
  132. package/src/nest/security-jwt.guard.test.ts +65 -0
  133. package/src/nest/security-jwt.guard.ts +47 -0
  134. package/src/nest/security-modules.test.ts +79 -0
  135. package/src/nest/security-workflows.controller.test.ts +119 -0
  136. package/src/nest/security-workflows.controller.ts +149 -0
  137. package/src/nest/security-workflows.module.ts +56 -0
  138. package/src/nest/security-workflows.service.test.ts +238 -0
  139. package/src/nest/security-workflows.service.ts +220 -0
  140. package/src/nest/swagger.test.ts +27 -0
  141. package/src/nest/swagger.ts +18 -0
  142. package/src/nest/tokens.ts +1 -0
  143. package/dist/api/migrations/1739490000000-add-google-subject-to-user.d.ts +0 -5
  144. package/dist/api/migrations/1739490000000-add-google-subject-to-user.js +0 -14
  145. package/src/api/migrations/1739490000000-add-google-subject-to-user.ts +0 -12
package/README.md CHANGED
@@ -15,13 +15,153 @@ Use shared helpers/types in your API controllers/services where useful:
15
15
  - `isValidEmail`
16
16
  - `isStrongPassword`
17
17
  - `AuthResponse`, `RegisterResponse`, `SafeUser`
18
+ - `notifyAdminsOnEmailVerified`
19
+ - `notifyUserOnAdminApproval`
20
+
21
+ ## Nest Integration
22
+
23
+ Import the Nest surface from `@scryan7371/sdr-security/nest`.
24
+
25
+ ```ts
26
+ import { Module } from "@nestjs/common";
27
+ import {
28
+ SecurityWorkflowsModule,
29
+ SECURITY_WORKFLOW_NOTIFIER,
30
+ } from "@scryan7371/sdr-security/nest";
31
+ import { EmailService } from "./notifications/email.service";
32
+
33
+ @Module({
34
+ imports: [
35
+ SecurityWorkflowsModule.forRoot({
36
+ notifierProvider: {
37
+ provide: SECURITY_WORKFLOW_NOTIFIER,
38
+ useFactory: (emailService: EmailService) => ({
39
+ sendAdminsUserEmailVerified: ({ adminEmails, user }) =>
40
+ emailService.sendEmailVerifiedNotificationToAdmins(
41
+ adminEmails,
42
+ user,
43
+ ),
44
+ sendUserAccountApproved: ({ email }) =>
45
+ emailService.sendAccountApproved(email),
46
+ }),
47
+ inject: [EmailService],
48
+ },
49
+ }),
50
+ ],
51
+ })
52
+ export class AppModule {}
53
+ ```
54
+
55
+ ### User Table Ownership Model
56
+
57
+ Consuming apps keep ownership of their own `app_user` table. `sdr-security`
58
+ stores security/auth state in its own tables and links them by user id.
59
+
60
+ - App-owned table:
61
+ - `app_user` (at minimum: `id`, `email`, plus any app-specific columns)
62
+ - `sdr-security` tables:
63
+ - `security_user` (password hash, verified/approved/active flags)
64
+ - `security_identity` (provider links such as Google subject)
65
+ - `security_role`, `security_user_role`
66
+ - `refresh_token`
67
+ - `security_password_reset_token`
68
+
69
+ Link key:
70
+
71
+ - `security_* .user_id` -> `app_user.id`
72
+
73
+ This lets each app evolve its user schema independently while reusing the same
74
+ security workflows, guards, controllers, and migrations.
75
+
76
+ Typical app query pattern is a join when you need security state:
77
+
78
+ ```sql
79
+ SELECT u.id, u.email, su.is_active, su.admin_approved_at, su.email_verified_at
80
+ FROM app_user u
81
+ LEFT JOIN security_user su ON su.user_id = u.id
82
+ WHERE u.id = $1;
83
+ ```
84
+
85
+ Nest/TypeORM equivalent:
86
+
87
+ ```ts
88
+ const row = await usersRepo
89
+ .createQueryBuilder("user")
90
+ .leftJoin("security_user", "securityUser", "securityUser.user_id = user.id")
91
+ .select("user.id", "id")
92
+ .addSelect("user.email", "email")
93
+ .addSelect("securityUser.is_active", "isActive")
94
+ .addSelect("securityUser.admin_approved_at", "adminApprovedAt")
95
+ .addSelect("securityUser.email_verified_at", "emailVerifiedAt")
96
+ .where("user.id = :id", { id: userId })
97
+ .getRawOne();
98
+ ```
99
+
100
+ Optional Swagger setup in consuming app:
101
+
102
+ ```ts
103
+ import { setupSecuritySwagger } from "@scryan7371/sdr-security/nest";
104
+
105
+ setupSecuritySwagger(app); // default path: /docs/security
106
+ ```
107
+
108
+ Routes exposed by the shared controller:
109
+
110
+ - `POST /security/auth/register`
111
+ - `POST /security/auth/login`
112
+ - `POST /security/auth/forgot-password`
113
+ - `POST /security/auth/reset-password`
114
+ - `GET /security/auth/verify-email?token=...`
115
+ - `POST /security/auth/change-password` (JWT required)
116
+ - `POST /security/auth/logout` (JWT required)
117
+ - `POST /security/auth/refresh`
118
+ - `GET /security/auth/me/roles` (JWT required)
119
+ - `POST /security/workflows/users/:id/email-verified`
120
+ - marks `email_verified_at` and notifies admins.
121
+ - `PATCH /security/workflows/users/:id/admin-approval` with `{ approved: boolean }`
122
+ - updates `admin_approved_at` and notifies user when approved (admin JWT required).
123
+ - `PATCH /security/workflows/users/:id/active` with `{ active: boolean }` (admin JWT required)
124
+ - `GET /security/workflows/roles` (admin JWT required)
125
+ - `POST /security/workflows/roles` (admin JWT required)
126
+ - `DELETE /security/workflows/roles/:role` (admin JWT required)
127
+ - `GET /security/workflows/users/:id/roles` (admin JWT required)
128
+ - `PUT /security/workflows/users/:id/roles` (admin JWT required)
129
+ - `POST /security/workflows/users/:id/roles` with `{ role: string }` (admin JWT required)
130
+ - `DELETE /security/workflows/users/:id/roles/:role` (admin JWT required)
131
+
132
+ ### Shared notification workflows
133
+
134
+ Use these helpers to standardize notification behavior across apps while still
135
+ keeping app-specific email sending in your own services.
136
+
137
+ ```ts
138
+ import { api as sdrSecurity } from "@scryan7371/sdr-security";
139
+
140
+ await sdrSecurity.notifyAdminsOnEmailVerified({
141
+ user: {
142
+ id: user.id,
143
+ email: user.email,
144
+ },
145
+ listAdminEmails: () => usersService.listAdminEmails(),
146
+ notifyAdmins: ({ adminEmails, user }) =>
147
+ emailService.sendEmailVerifiedNotificationToAdmins(adminEmails, user),
148
+ });
149
+
150
+ await sdrSecurity.notifyUserOnAdminApproval({
151
+ approved: body.approved,
152
+ user: {
153
+ email: user.email,
154
+ },
155
+ notifyUser: ({ email }) => emailService.sendAccountApproved(email),
156
+ });
157
+ ```
18
158
 
19
159
  ## App Integration
20
160
 
21
161
  Create one client per app session and reuse it across screens:
22
162
 
23
163
  ```ts
24
- import { app as sdrSecurity } from '@scryan7371/sdr-security';
164
+ import { app as sdrSecurity } from "@scryan7371/sdr-security";
25
165
 
26
166
  const securityClient = sdrSecurity.createSecurityClient({
27
167
  baseUrl,
@@ -42,31 +182,94 @@ Methods:
42
182
  - `requestPhoneVerification`
43
183
  - `verifyPhone`
44
184
 
45
- ## Private Registry Publish (GitHub Packages)
185
+ ## Publish (npmjs)
186
+
187
+ 1. Configure project-local npm auth (`.npmrc`):
46
188
 
47
- 1. Set your token:
189
+ ```ini
190
+ registry=https://registry.npmjs.org/
191
+ @scryan7371:registry=https://registry.npmjs.org/
192
+ //registry.npmjs.org/:_authToken=${NPM_TOKEN}
193
+ ```
194
+
195
+ 2. Set token, bump version, and publish:
48
196
 
49
197
  ```bash
50
- export GITHUB_PACKAGES_TOKEN=ghp_xxx
198
+ export NPM_TOKEN=xxxx
199
+ npm version patch
200
+ npm publish --access public --registry=https://registry.npmjs.org --userconfig .npmrc
51
201
  ```
52
202
 
53
- 2. Publish:
203
+ 3. Push commit and tags:
54
204
 
55
205
  ```bash
56
- npm publish
206
+ git push
207
+ git push --tags
57
208
  ```
58
209
 
59
- ## Install From Any Environment
210
+ ## CI Publish (GitHub Actions)
60
211
 
61
- 1. Add auth to your consuming project's `.npmrc`:
212
+ Tag pushes like `sdr-security-v*` trigger `.github/workflows/publish.yml`.
62
213
 
63
- ```ini
64
- @scryan7371:registry=https://npm.pkg.github.com
65
- //npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}
66
- ```
214
+ Required repo secret:
215
+
216
+ - `NPM_TOKEN` (npm granular token with read/write + bypass 2FA for automation).
67
217
 
68
- 2. Install a pinned version:
218
+ ## Install
219
+
220
+ Install a pinned version:
69
221
 
70
222
  ```bash
71
223
  npm install @scryan7371/sdr-security@0.1.0
72
224
  ```
225
+
226
+ ## Database Integration Test
227
+
228
+ A sample Postgres integration test is included at:
229
+
230
+ - `src/integration/database.integration.test.ts`
231
+
232
+ Run it with:
233
+
234
+ ```bash
235
+ npm run test:db
236
+ ```
237
+
238
+ Configuration resolution order:
239
+
240
+ 1. `.env.test` (if present)
241
+ 2. `.env.dev` (if present)
242
+ 3. existing process env
243
+
244
+ Supported env vars:
245
+
246
+ - `SECURITY_TEST_DATABASE_URL` (preferred)
247
+ - or `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME`
248
+ - optional fallback: `DATABASE_URL`
249
+ - optional debug:
250
+ - `SECURITY_TEST_KEEP_SCHEMA=true` (do not drop schema after test run)
251
+ - `SECURITY_TEST_SCHEMA=your_schema_name` (use fixed schema name)
252
+
253
+ See `.env.test.example` for a template.
254
+
255
+ ## Release Script
256
+
257
+ You can automate version bump + tag + push with:
258
+
259
+ ```bash
260
+ npm run release:patch
261
+ npm run release:minor
262
+ npm run release:major
263
+ ```
264
+
265
+ What it does:
266
+
267
+ 1. Verifies clean git working tree
268
+ 2. Runs `npm test`
269
+ 3. Runs `npm run build`
270
+ 4. Bumps `package.json` + `package-lock.json`
271
+ 5. Commits as `chore(release): vX.Y.Z`
272
+ 6. Tags as `sdr-security-vX.Y.Z`
273
+ 7. Pushes commit and tag
274
+
275
+ This tag format triggers `.github/workflows/publish.yml`.
@@ -3,8 +3,6 @@ export type UserRole = string;
3
3
  export type SafeUser = {
4
4
  id: string;
5
5
  email: string;
6
- firstName: string | null;
7
- lastName: string | null;
8
6
  phone: string | null;
9
7
  roles: UserRole[];
10
8
  emailVerifiedAt: string | Date | null;
@@ -44,3 +42,15 @@ export type DebugCodeResponse = {
44
42
  success: true;
45
43
  debugCode?: string;
46
44
  };
45
+ export type GenericSuccessResponse = {
46
+ success: true;
47
+ };
48
+ export type UserActiveResponse = {
49
+ success: true;
50
+ userId: string;
51
+ active: boolean;
52
+ };
53
+ export type AdminNotificationResponse = {
54
+ success: true;
55
+ notified: boolean;
56
+ };
@@ -3,3 +3,4 @@ export * from "./migrations";
3
3
  export * from "./access-policy";
4
4
  export * from "./validation";
5
5
  export * from "./roles";
6
+ export * from "./notification-workflows";
package/dist/api/index.js CHANGED
@@ -19,3 +19,4 @@ __exportStar(require("./migrations"), exports);
19
19
  __exportStar(require("./access-policy"), exports);
20
20
  __exportStar(require("./validation"), exports);
21
21
  __exportStar(require("./roles"), exports);
22
+ __exportStar(require("./notification-workflows"), exports);
@@ -1,7 +1,7 @@
1
1
  export declare class CreateSecurityIdentity1739500000000 {
2
2
  name: string;
3
3
  up(queryRunner: {
4
- query: (sql: string, params?: unknown[]) => Promise<unknown>;
4
+ query: (sql: string) => Promise<unknown>;
5
5
  }): Promise<void>;
6
6
  down(queryRunner: {
7
7
  query: (sql: string) => Promise<unknown>;
@@ -4,13 +4,10 @@ exports.CreateSecurityIdentity1739500000000 = void 0;
4
4
  class CreateSecurityIdentity1739500000000 {
5
5
  name = "CreateSecurityIdentity1739500000000";
6
6
  async up(queryRunner) {
7
- const userTable = getSafeIdentifier(process.env.USER_TABLE, "app_user");
8
- const userSchema = getSafeIdentifier(process.env.USER_TABLE_SCHEMA, "public");
9
- const userTableRef = `"${userSchema}"."${userTable}"`;
10
- await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
7
+ const userTableRef = getUserTableReference();
11
8
  await queryRunner.query(`
12
9
  CREATE TABLE IF NOT EXISTS "security_identity" (
13
- "id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
10
+ "id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
14
11
  "user_id" varchar NOT NULL,
15
12
  "provider" varchar NOT NULL,
16
13
  "provider_subject" varchar NOT NULL,
@@ -21,36 +18,6 @@ class CreateSecurityIdentity1739500000000 {
21
18
  `);
22
19
  await queryRunner.query(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_identity_provider_subject" ON "security_identity" ("provider", "provider_subject")`);
23
20
  await queryRunner.query(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_identity_user_provider" ON "security_identity" ("user_id", "provider")`);
24
- const hasGoogleSubjectColumn = (await queryRunner.query(`
25
- SELECT 1
26
- FROM information_schema.columns
27
- WHERE table_schema = $1
28
- AND table_name = $2
29
- AND column_name = 'google_subject'
30
- LIMIT 1
31
- `, [userSchema, userTable]));
32
- if (hasGoogleSubjectColumn.length > 0) {
33
- await queryRunner.query(`
34
- INSERT INTO "security_identity" (
35
- "user_id",
36
- "provider",
37
- "provider_subject",
38
- "created_at",
39
- "updated_at"
40
- )
41
- SELECT
42
- "id",
43
- 'google',
44
- "google_subject",
45
- now(),
46
- now()
47
- FROM ${userTableRef}
48
- WHERE "google_subject" IS NOT NULL
49
- ON CONFLICT ("provider", "provider_subject") DO NOTHING
50
- `);
51
- await queryRunner.query(`DROP INDEX IF EXISTS "IDX_app_user_google_subject"`);
52
- await queryRunner.query(`ALTER TABLE ${userTableRef} DROP COLUMN IF EXISTS "google_subject"`);
53
- }
54
21
  }
55
22
  async down(queryRunner) {
56
23
  await queryRunner.query(`DROP INDEX IF EXISTS "IDX_security_identity_user_provider"`);
@@ -66,3 +33,10 @@ const getSafeIdentifier = (value, fallback) => {
66
33
  }
67
34
  return resolved;
68
35
  };
36
+ const getUserTableReference = () => {
37
+ const table = getSafeIdentifier(process.env.USER_TABLE, "app_user");
38
+ const schema = process.env.USER_TABLE_SCHEMA
39
+ ? getSafeIdentifier(process.env.USER_TABLE_SCHEMA, "public")
40
+ : "public";
41
+ return `"${schema}"."${table}"`;
42
+ };
@@ -1,7 +1,7 @@
1
1
  export declare class CreateSecurityRoles1739510000000 {
2
2
  name: string;
3
3
  up(queryRunner: {
4
- query: (sql: string, params?: unknown[]) => Promise<unknown>;
4
+ query: (sql: string) => Promise<unknown>;
5
5
  }): Promise<void>;
6
6
  down(queryRunner: {
7
7
  query: (sql: string) => Promise<unknown>;
@@ -4,13 +4,9 @@ exports.CreateSecurityRoles1739510000000 = void 0;
4
4
  class CreateSecurityRoles1739510000000 {
5
5
  name = "CreateSecurityRoles1739510000000";
6
6
  async up(queryRunner) {
7
- const userTable = getSafeIdentifier(process.env.USER_TABLE, "app_user");
8
- const userSchema = getSafeIdentifier(process.env.USER_TABLE_SCHEMA, "public");
9
- const userTableRef = `"${userSchema}"."${userTable}"`;
10
- await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
11
7
  await queryRunner.query(`
12
8
  CREATE TABLE IF NOT EXISTS "security_role" (
13
- "id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
9
+ "id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
14
10
  "role_key" varchar NOT NULL,
15
11
  "description" text,
16
12
  "is_system" boolean NOT NULL DEFAULT false,
@@ -20,76 +16,14 @@ class CreateSecurityRoles1739510000000 {
20
16
  `);
21
17
  await queryRunner.query(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_role_key" ON "security_role" ("role_key")`);
22
18
  await queryRunner.query(`
23
- CREATE TABLE IF NOT EXISTS "security_user_role" (
24
- "id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
25
- "user_id" varchar NOT NULL,
26
- "role_id" uuid NOT NULL,
27
- "created_at" timestamptz NOT NULL DEFAULT now(),
28
- CONSTRAINT "FK_security_user_role_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE,
29
- CONSTRAINT "FK_security_user_role_role_id" FOREIGN KEY ("role_id") REFERENCES "security_role" ("id") ON DELETE CASCADE
30
- )
31
- `);
32
- await queryRunner.query(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_user_role_user_role" ON "security_user_role" ("user_id", "role_id")`);
33
- await queryRunner.query(`
34
19
  INSERT INTO "security_role" ("role_key", "description", "is_system", "created_at", "updated_at")
35
20
  VALUES ('ADMIN', 'Administrative access', true, now(), now())
36
21
  ON CONFLICT ("role_key") DO NOTHING
37
22
  `);
38
- const hasRoleColumn = (await queryRunner.query(`
39
- SELECT 1
40
- FROM information_schema.columns
41
- WHERE table_schema = $1
42
- AND table_name = $2
43
- AND column_name = 'role'
44
- LIMIT 1
45
- `, [userSchema, userTable]));
46
- if (hasRoleColumn.length > 0) {
47
- await queryRunner.query(`
48
- INSERT INTO "security_role" ("role_key", "description", "is_system", "created_at", "updated_at")
49
- SELECT DISTINCT
50
- CASE
51
- WHEN UPPER(TRIM("role")) = 'ADMINISTRATOR' THEN 'ADMIN'
52
- ELSE UPPER(TRIM("role"))
53
- END AS "role_key",
54
- NULL,
55
- false,
56
- now(),
57
- now()
58
- FROM ${userTableRef}
59
- WHERE "role" IS NOT NULL
60
- AND LENGTH(TRIM("role")) > 0
61
- ON CONFLICT ("role_key") DO NOTHING
62
- `);
63
- await queryRunner.query(`
64
- INSERT INTO "security_user_role" ("user_id", "role_id", "created_at")
65
- SELECT
66
- u."id" AS "user_id",
67
- r."id" AS "role_id",
68
- now()
69
- FROM ${userTableRef} u
70
- INNER JOIN "security_role" r ON r."role_key" = CASE
71
- WHEN UPPER(TRIM(u."role")) = 'ADMINISTRATOR' THEN 'ADMIN'
72
- ELSE UPPER(TRIM(u."role"))
73
- END
74
- WHERE u."role" IS NOT NULL
75
- AND LENGTH(TRIM(u."role")) > 0
76
- ON CONFLICT ("user_id", "role_id") DO NOTHING
77
- `);
78
- await queryRunner.query(`ALTER TABLE ${userTableRef} DROP COLUMN IF EXISTS "role"`);
79
- }
80
23
  }
81
24
  async down(queryRunner) {
82
- await queryRunner.query(`DROP INDEX IF EXISTS "IDX_security_user_role_user_role"`);
83
- await queryRunner.query(`DROP TABLE IF EXISTS "security_user_role"`);
84
25
  await queryRunner.query(`DROP INDEX IF EXISTS "IDX_security_role_key"`);
85
26
  await queryRunner.query(`DROP TABLE IF EXISTS "security_role"`);
86
27
  }
87
28
  }
88
29
  exports.CreateSecurityRoles1739510000000 = CreateSecurityRoles1739510000000;
89
- const getSafeIdentifier = (value, fallback) => {
90
- const resolved = value?.trim() || fallback;
91
- if (!resolved || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(resolved)) {
92
- throw new Error(`Invalid SQL identifier: ${resolved}`);
93
- }
94
- return resolved;
95
- };
@@ -0,0 +1,9 @@
1
+ export declare class CreateSecurityUserRoles1739515000000 {
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,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CreateSecurityUserRoles1739515000000 = void 0;
4
+ class CreateSecurityUserRoles1739515000000 {
5
+ name = "CreateSecurityUserRoles1739515000000";
6
+ async up(queryRunner) {
7
+ const userTableRef = getUserTableReference();
8
+ await queryRunner.query(`
9
+ CREATE TABLE IF NOT EXISTS "security_user_role" (
10
+ "id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
11
+ "user_id" varchar NOT NULL,
12
+ "role_id" uuid NOT NULL,
13
+ "created_at" timestamptz NOT NULL DEFAULT now(),
14
+ CONSTRAINT "FK_security_user_role_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE,
15
+ CONSTRAINT "FK_security_user_role_role_id" FOREIGN KEY ("role_id") REFERENCES "security_role" ("id") ON DELETE CASCADE
16
+ )
17
+ `);
18
+ await queryRunner.query(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_user_role_user_role" ON "security_user_role" ("user_id", "role_id")`);
19
+ }
20
+ async down(queryRunner) {
21
+ await queryRunner.query(`DROP INDEX IF EXISTS "IDX_security_user_role_user_role"`);
22
+ await queryRunner.query(`DROP TABLE IF EXISTS "security_user_role"`);
23
+ }
24
+ }
25
+ exports.CreateSecurityUserRoles1739515000000 = CreateSecurityUserRoles1739515000000;
26
+ const getUserTableReference = () => {
27
+ const table = getSafeIdentifier(process.env.USER_TABLE, "app_user");
28
+ const schema = process.env.USER_TABLE_SCHEMA
29
+ ? getSafeIdentifier(process.env.USER_TABLE_SCHEMA, "public")
30
+ : "public";
31
+ return `"${schema}"."${table}"`;
32
+ };
33
+ const getSafeIdentifier = (value, fallback) => {
34
+ const resolved = value?.trim() || fallback;
35
+ if (!resolved || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(resolved)) {
36
+ throw new Error(`Invalid SQL identifier: ${resolved}`);
37
+ }
38
+ return resolved;
39
+ };
@@ -0,0 +1,9 @@
1
+ export declare class CreatePasswordResetTokens1739520000000 {
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,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CreatePasswordResetTokens1739520000000 = void 0;
4
+ class CreatePasswordResetTokens1739520000000 {
5
+ name = "CreatePasswordResetTokens1739520000000";
6
+ async up(queryRunner) {
7
+ const userTableRef = getUserTableReference();
8
+ await queryRunner.query(`
9
+ CREATE TABLE IF NOT EXISTS "security_password_reset_token" (
10
+ "id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
11
+ "user_id" varchar NOT NULL,
12
+ "token" varchar NOT NULL,
13
+ "expires_at" timestamptz NOT NULL,
14
+ "used_at" timestamptz,
15
+ "created_at" timestamptz NOT NULL DEFAULT now(),
16
+ CONSTRAINT "FK_security_password_reset_token_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE
17
+ )
18
+ `);
19
+ await queryRunner.query(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_password_reset_token_token" ON "security_password_reset_token" ("token")`);
20
+ await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_security_password_reset_token_user_id" ON "security_password_reset_token" ("user_id")`);
21
+ }
22
+ async down(queryRunner) {
23
+ await queryRunner.query(`DROP INDEX IF EXISTS "IDX_security_password_reset_token_user_id"`);
24
+ await queryRunner.query(`DROP INDEX IF EXISTS "IDX_security_password_reset_token_token"`);
25
+ await queryRunner.query(`DROP TABLE IF EXISTS "security_password_reset_token"`);
26
+ }
27
+ }
28
+ exports.CreatePasswordResetTokens1739520000000 = CreatePasswordResetTokens1739520000000;
29
+ const getUserTableReference = () => {
30
+ const table = getSafeIdentifier(process.env.USER_TABLE, "app_user");
31
+ const schema = process.env.USER_TABLE_SCHEMA
32
+ ? getSafeIdentifier(process.env.USER_TABLE_SCHEMA, "public")
33
+ : "public";
34
+ return `"${schema}"."${table}"`;
35
+ };
36
+ const getSafeIdentifier = (value, fallback) => {
37
+ const resolved = value?.trim() || fallback;
38
+ if (!resolved || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(resolved)) {
39
+ throw new Error(`Invalid SQL identifier: ${resolved}`);
40
+ }
41
+ return resolved;
42
+ };
@@ -0,0 +1,9 @@
1
+ export declare class CreateSecurityUser1739530000000 {
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,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CreateSecurityUser1739530000000 = void 0;
4
+ class CreateSecurityUser1739530000000 {
5
+ name = "CreateSecurityUser1739530000000";
6
+ async up(queryRunner) {
7
+ const userTableRef = getUserTableReference();
8
+ await queryRunner.query(`
9
+ CREATE TABLE IF NOT EXISTS "security_user" (
10
+ "user_id" varchar PRIMARY KEY NOT NULL,
11
+ "password_hash" varchar NOT NULL,
12
+ "email_verified_at" timestamptz,
13
+ "email_verification_token" varchar,
14
+ "admin_approved_at" timestamptz,
15
+ "is_active" boolean NOT NULL DEFAULT true,
16
+ "created_at" timestamptz NOT NULL DEFAULT now(),
17
+ CONSTRAINT "FK_security_user_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE
18
+ )
19
+ `);
20
+ await queryRunner.query(`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`);
21
+ }
22
+ async down(queryRunner) {
23
+ await queryRunner.query(`DROP INDEX IF EXISTS "IDX_security_user_email_verification_token"`);
24
+ await queryRunner.query(`DROP TABLE IF EXISTS "security_user"`);
25
+ }
26
+ }
27
+ exports.CreateSecurityUser1739530000000 = CreateSecurityUser1739530000000;
28
+ const getSafeIdentifier = (value, fallback) => {
29
+ const resolved = value?.trim() || fallback;
30
+ if (!resolved || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(resolved)) {
31
+ throw new Error(`Invalid SQL identifier: ${resolved}`);
32
+ }
33
+ return resolved;
34
+ };
35
+ const getUserTableReference = () => {
36
+ const table = getSafeIdentifier(process.env.USER_TABLE, "app_user");
37
+ const schema = process.env.USER_TABLE_SCHEMA
38
+ ? getSafeIdentifier(process.env.USER_TABLE_SCHEMA, "public")
39
+ : "public";
40
+ return `"${schema}"."${table}"`;
41
+ };
@@ -1,6 +1,8 @@
1
1
  import { AddRefreshTokens1700000000001 } from "./1700000000001-add-refresh-tokens";
2
- import { AddGoogleSubjectToUser1739490000000 } from "./1739490000000-add-google-subject-to-user";
3
2
  import { CreateSecurityIdentity1739500000000 } from "./1739500000000-create-security-identity";
4
3
  import { CreateSecurityRoles1739510000000 } from "./1739510000000-create-security-roles";
4
+ import { CreateSecurityUserRoles1739515000000 } from "./1739515000000-create-security-user-roles";
5
+ import { CreatePasswordResetTokens1739520000000 } from "./1739520000000-create-password-reset-tokens";
6
+ import { CreateSecurityUser1739530000000 } from "./1739530000000-create-security-user";
5
7
  export declare const securityMigrations: (typeof AddRefreshTokens1700000000001)[];
6
- export { AddRefreshTokens1700000000001, AddGoogleSubjectToUser1739490000000, CreateSecurityIdentity1739500000000, CreateSecurityRoles1739510000000, };
8
+ export { AddRefreshTokens1700000000001, CreateSecurityIdentity1739500000000, CreateSecurityRoles1739510000000, CreateSecurityUserRoles1739515000000, CreatePasswordResetTokens1739520000000, CreateSecurityUser1739530000000, };