@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.
- package/README.md +216 -13
- package/dist/api/contracts.d.ts +12 -2
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/migrations/1739500000000-create-security-identity.d.ts +1 -1
- package/dist/api/migrations/1739500000000-create-security-identity.js +9 -35
- package/dist/api/migrations/1739510000000-create-security-roles.d.ts +1 -1
- package/dist/api/migrations/1739510000000-create-security-roles.js +1 -67
- package/dist/api/migrations/1739515000000-create-security-user-roles.d.ts +9 -0
- package/dist/api/migrations/1739515000000-create-security-user-roles.js +39 -0
- package/dist/api/migrations/1739520000000-create-password-reset-tokens.d.ts +9 -0
- package/dist/api/migrations/1739520000000-create-password-reset-tokens.js +42 -0
- package/dist/api/migrations/1739530000000-create-security-user.d.ts +9 -0
- package/dist/api/migrations/1739530000000-create-security-user.js +41 -0
- package/dist/api/migrations/index.d.ts +4 -2
- package/dist/api/migrations/index.js +10 -4
- package/dist/api/migrations/migrations.test.d.ts +1 -0
- package/dist/api/migrations/migrations.test.js +88 -0
- package/dist/api/notification-workflows.d.ts +31 -0
- package/dist/api/notification-workflows.js +22 -0
- package/dist/api/notification-workflows.test.d.ts +1 -0
- package/dist/api/notification-workflows.test.js +63 -0
- package/dist/api/validation.test.d.ts +1 -0
- package/dist/api/validation.test.js +20 -0
- package/dist/app/client.d.ts +17 -4
- package/dist/app/client.js +38 -11
- package/dist/app/client.test.d.ts +1 -0
- package/dist/app/client.test.js +130 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +10 -0
- package/dist/integration/database.integration.test.d.ts +1 -0
- package/dist/integration/database.integration.test.js +158 -0
- package/dist/nest/contracts.d.ts +21 -0
- package/dist/nest/contracts.js +2 -0
- package/dist/nest/dto/auth.dto.d.ts +25 -0
- package/dist/nest/dto/auth.dto.js +89 -0
- package/dist/nest/dto/workflows.dto.d.ts +16 -0
- package/dist/nest/dto/workflows.dto.js +58 -0
- package/dist/nest/entities/app-user.entity.d.ts +4 -0
- package/dist/nest/entities/app-user.entity.js +29 -0
- package/dist/nest/entities/password-reset-token.entity.d.ts +8 -0
- package/dist/nest/entities/password-reset-token.entity.js +49 -0
- package/dist/nest/entities/refresh-token.entity.d.ts +8 -0
- package/dist/nest/entities/refresh-token.entity.js +49 -0
- package/dist/nest/entities/security-role.entity.d.ts +6 -0
- package/dist/nest/entities/security-role.entity.js +39 -0
- package/dist/nest/entities/security-user-role.entity.d.ts +5 -0
- package/dist/nest/entities/security-user-role.entity.js +34 -0
- package/dist/nest/entities/security-user.entity.d.ts +9 -0
- package/dist/nest/entities/security-user.entity.js +54 -0
- package/dist/nest/index.d.ts +19 -0
- package/dist/nest/index.js +35 -0
- package/dist/nest/index.test.d.ts +1 -0
- package/dist/nest/index.test.js +14 -0
- package/dist/nest/security-admin.guard.d.ts +4 -0
- package/dist/nest/security-admin.guard.js +25 -0
- package/dist/nest/security-admin.guard.test.d.ts +1 -0
- package/dist/nest/security-admin.guard.test.js +24 -0
- package/dist/nest/security-auth.constants.d.ts +1 -0
- package/dist/nest/security-auth.constants.js +4 -0
- package/dist/nest/security-auth.controller.d.ts +51 -0
- package/dist/nest/security-auth.controller.js +177 -0
- package/dist/nest/security-auth.controller.test.d.ts +1 -0
- package/dist/nest/security-auth.controller.test.js +87 -0
- package/dist/nest/security-auth.module.d.ts +9 -0
- package/dist/nest/security-auth.module.js +70 -0
- package/dist/nest/security-auth.options.d.ts +8 -0
- package/dist/nest/security-auth.options.js +2 -0
- package/dist/nest/security-auth.service.d.ts +60 -0
- package/dist/nest/security-auth.service.js +299 -0
- package/dist/nest/security-auth.service.test.d.ts +1 -0
- package/dist/nest/security-auth.service.test.js +249 -0
- package/dist/nest/security-jwt.guard.d.ts +7 -0
- package/dist/nest/security-jwt.guard.js +46 -0
- package/dist/nest/security-jwt.guard.test.d.ts +1 -0
- package/dist/nest/security-jwt.guard.test.js +51 -0
- package/dist/nest/security-modules.test.d.ts +1 -0
- package/dist/nest/security-modules.test.js +61 -0
- package/dist/nest/security-workflows.controller.d.ts +72 -0
- package/dist/nest/security-workflows.controller.js +187 -0
- package/dist/nest/security-workflows.controller.test.d.ts +1 -0
- package/dist/nest/security-workflows.controller.test.js +87 -0
- package/dist/nest/security-workflows.module.d.ts +9 -0
- package/dist/nest/security-workflows.module.js +61 -0
- package/dist/nest/security-workflows.service.d.ts +69 -0
- package/dist/nest/security-workflows.service.js +203 -0
- package/dist/nest/security-workflows.service.test.d.ts +1 -0
- package/dist/nest/security-workflows.service.test.js +178 -0
- package/dist/nest/swagger.d.ts +2 -0
- package/dist/nest/swagger.js +16 -0
- package/dist/nest/swagger.test.d.ts +1 -0
- package/dist/nest/swagger.test.js +21 -0
- package/dist/nest/tokens.d.ts +1 -0
- package/dist/nest/tokens.js +4 -0
- package/package.json +45 -4
- package/src/api/contracts.ts +11 -2
- package/src/api/index.ts +1 -0
- package/src/api/migrations/1739500000000-create-security-identity.ts +11 -50
- package/src/api/migrations/1739510000000-create-security-roles.ts +2 -89
- package/src/api/migrations/1739515000000-create-security-user-roles.ts +49 -0
- package/src/api/migrations/1739520000000-create-password-reset-tokens.ts +57 -0
- package/src/api/migrations/1739530000000-create-security-user.ts +51 -0
- package/src/api/migrations/index.ts +9 -3
- package/src/api/migrations/migrations.test.ts +145 -0
- package/src/api/notification-workflows.test.ts +78 -0
- package/src/api/notification-workflows.ts +38 -0
- package/src/api/validation.test.ts +21 -0
- package/src/app/client.test.ts +157 -0
- package/src/app/client.ts +74 -18
- package/src/index.test.ts +9 -0
- package/src/integration/database.integration.test.ts +205 -0
- package/src/nest/contracts.ts +20 -0
- package/src/nest/dto/auth.dto.ts +48 -0
- package/src/nest/dto/workflows.dto.ts +29 -0
- package/src/nest/entities/app-user.entity.ts +10 -0
- package/src/nest/entities/password-reset-token.entity.ts +27 -0
- package/src/nest/entities/refresh-token.entity.ts +22 -0
- package/src/nest/entities/security-role.entity.ts +16 -0
- package/src/nest/entities/security-user-role.entity.ts +13 -0
- package/src/nest/entities/security-user.entity.ts +25 -0
- package/src/nest/index.test.ts +20 -0
- package/src/nest/index.ts +19 -0
- package/src/nest/security-admin.guard.test.ts +31 -0
- package/src/nest/security-admin.guard.ts +21 -0
- package/src/nest/security-auth.constants.ts +1 -0
- package/src/nest/security-auth.controller.test.ts +128 -0
- package/src/nest/security-auth.controller.ts +148 -0
- package/src/nest/security-auth.module.ts +65 -0
- package/src/nest/security-auth.options.ts +8 -0
- package/src/nest/security-auth.service.test.ts +368 -0
- package/src/nest/security-auth.service.ts +356 -0
- package/src/nest/security-jwt.guard.test.ts +65 -0
- package/src/nest/security-jwt.guard.ts +47 -0
- package/src/nest/security-modules.test.ts +79 -0
- package/src/nest/security-workflows.controller.test.ts +119 -0
- package/src/nest/security-workflows.controller.ts +149 -0
- package/src/nest/security-workflows.module.ts +56 -0
- package/src/nest/security-workflows.service.test.ts +238 -0
- package/src/nest/security-workflows.service.ts +220 -0
- package/src/nest/swagger.test.ts +27 -0
- package/src/nest/swagger.ts +18 -0
- package/src/nest/tokens.ts +1 -0
- package/dist/api/migrations/1739490000000-add-google-subject-to-user.d.ts +0 -5
- package/dist/api/migrations/1739490000000-add-google-subject-to-user.js +0 -14
- 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
|
|
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
|
-
##
|
|
185
|
+
## Publish (npmjs)
|
|
186
|
+
|
|
187
|
+
1. Configure project-local npm auth (`.npmrc`):
|
|
46
188
|
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
203
|
+
3. Push commit and tags:
|
|
54
204
|
|
|
55
205
|
```bash
|
|
56
|
-
|
|
206
|
+
git push
|
|
207
|
+
git push --tags
|
|
57
208
|
```
|
|
58
209
|
|
|
59
|
-
##
|
|
210
|
+
## CI Publish (GitHub Actions)
|
|
60
211
|
|
|
61
|
-
|
|
212
|
+
Tag pushes like `sdr-security-v*` trigger `.github/workflows/publish.yml`.
|
|
62
213
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
```
|
|
214
|
+
Required repo secret:
|
|
215
|
+
|
|
216
|
+
- `NPM_TOKEN` (npm granular token with read/write + bypass 2FA for automation).
|
|
67
217
|
|
|
68
|
-
|
|
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`.
|
package/dist/api/contracts.d.ts
CHANGED
|
@@ -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
|
+
};
|
package/dist/api/index.d.ts
CHANGED
package/dist/api/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare class CreateSecurityIdentity1739500000000 {
|
|
2
2
|
name: string;
|
|
3
3
|
up(queryRunner: {
|
|
4
|
-
query: (sql: string
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,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,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,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,
|
|
8
|
+
export { AddRefreshTokens1700000000001, CreateSecurityIdentity1739500000000, CreateSecurityRoles1739510000000, CreateSecurityUserRoles1739515000000, CreatePasswordResetTokens1739520000000, CreateSecurityUser1739530000000, };
|