@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.
- package/README.md +48 -7
- package/dist/api/contracts.d.ts +0 -2
- package/dist/api/migrations/1700000000001-add-refresh-tokens.js +4 -2
- package/dist/api/migrations/1739500000000-create-security-identity.d.ts +1 -1
- package/dist/api/migrations/1739500000000-create-security-identity.js +12 -36
- package/dist/api/migrations/1739510000000-create-security-roles.d.ts +1 -1
- package/dist/api/migrations/1739510000000-create-security-roles.js +3 -68
- package/dist/api/migrations/1739515000000-create-security-user-roles.d.ts +9 -0
- package/dist/api/migrations/1739515000000-create-security-user-roles.js +42 -0
- package/dist/api/migrations/1739520000000-create-password-reset-tokens.js +4 -2
- package/dist/api/migrations/1739530000000-create-security-user.d.ts +9 -0
- package/dist/api/migrations/1739530000000-create-security-user.js +42 -0
- package/dist/api/migrations/index.d.ts +3 -2
- package/dist/api/migrations/index.js +7 -4
- package/dist/api/migrations/migrations.test.js +37 -83
- package/dist/api/notification-workflows.d.ts +0 -4
- package/dist/api/notification-workflows.js +0 -1
- package/dist/api/notification-workflows.test.js +1 -4
- package/dist/app/client.d.ts +0 -2
- package/dist/app/client.test.js +0 -2
- package/dist/integration/database.integration.test.js +1 -1
- package/dist/nest/contracts.d.ts +0 -3
- package/dist/nest/dto/auth.dto.d.ts +0 -2
- package/dist/nest/dto/auth.dto.js +0 -10
- package/dist/nest/entities/app-user.entity.d.ts +0 -7
- package/dist/nest/entities/app-user.entity.js +1 -36
- package/dist/nest/entities/password-reset-token.entity.d.ts +1 -0
- package/dist/nest/entities/password-reset-token.entity.js +14 -2
- package/dist/nest/entities/refresh-token.entity.js +2 -2
- package/dist/nest/entities/security-role.entity.d.ts +1 -0
- package/dist/nest/entities/security-role.entity.js +13 -1
- package/dist/nest/entities/security-user-role.entity.d.ts +1 -0
- package/dist/nest/entities/security-user-role.entity.js +14 -2
- 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 +1 -0
- package/dist/nest/index.js +1 -0
- package/dist/nest/security-auth.controller.d.ts +0 -2
- package/dist/nest/security-auth.controller.js +0 -2
- package/dist/nest/security-auth.controller.test.js +0 -4
- package/dist/nest/security-auth.module.js +2 -0
- package/dist/nest/security-auth.service.d.ts +5 -4
- package/dist/nest/security-auth.service.js +85 -52
- package/dist/nest/security-auth.service.test.js +48 -42
- package/dist/nest/security-workflows.module.js +2 -0
- package/dist/nest/security-workflows.service.d.ts +4 -2
- package/dist/nest/security-workflows.service.js +23 -16
- package/dist/nest/security-workflows.service.test.js +29 -24
- package/package.json +5 -4
- package/src/api/contracts.ts +0 -2
- package/src/api/migrations/1700000000001-add-refresh-tokens.ts +4 -2
- package/src/api/migrations/1739500000000-create-security-identity.ts +14 -51
- package/src/api/migrations/1739510000000-create-security-roles.ts +4 -90
- package/src/api/migrations/1739515000000-create-security-user-roles.ts +52 -0
- package/src/api/migrations/1739520000000-create-password-reset-tokens.ts +4 -2
- package/src/api/migrations/1739530000000-create-security-user.ts +52 -0
- package/src/api/migrations/index.ts +6 -3
- package/src/api/migrations/migrations.test.ts +48 -111
- package/src/api/notification-workflows.test.ts +1 -4
- package/src/api/notification-workflows.ts +1 -8
- package/src/app/client.test.ts +0 -2
- package/src/app/client.ts +1 -6
- package/src/integration/database.integration.test.ts +1 -1
- package/src/nest/contracts.ts +1 -6
- package/src/nest/dto/auth.dto.ts +0 -6
- package/src/nest/entities/app-user.entity.ts +2 -23
- package/src/nest/entities/password-reset-token.entity.ts +12 -3
- package/src/nest/entities/refresh-token.entity.ts +2 -2
- package/src/nest/entities/security-role.entity.ts +10 -2
- package/src/nest/entities/security-user-role.entity.ts +11 -3
- package/src/nest/entities/security-user.entity.ts +25 -0
- package/src/nest/index.ts +1 -0
- package/src/nest/security-auth.controller.test.ts +0 -4
- package/src/nest/security-auth.controller.ts +0 -4
- package/src/nest/security-auth.module.ts +2 -0
- package/src/nest/security-auth.service.test.ts +78 -44
- package/src/nest/security-auth.service.ts +93 -53
- package/src/nest/security-workflows.module.ts +2 -0
- package/src/nest/security-workflows.service.test.ts +31 -25
- package/src/nest/security-workflows.service.ts +22 -13
- 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
|
@@ -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
|
-
|
|
28
|
+
appUsersRepo;
|
|
29
|
+
securityUsersRepo;
|
|
27
30
|
rolesRepo;
|
|
28
31
|
userRolesRepo;
|
|
29
32
|
notifier;
|
|
30
|
-
constructor(
|
|
31
|
-
this.
|
|
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.
|
|
38
|
-
const user = await this.
|
|
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.
|
|
59
|
-
const user = await this.
|
|
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("
|
|
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.
|
|
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.
|
|
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)(
|
|
195
|
-
__param(2, (0, typeorm_1.InjectRepository)(
|
|
196
|
-
__param(3, (0,
|
|
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
|
|
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(
|
|
32
|
-
return {
|
|
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,
|
|
40
|
-
|
|
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,
|
|
62
|
-
|
|
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,
|
|
75
|
-
|
|
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,
|
|
80
|
-
|
|
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,
|
|
90
|
-
|
|
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,
|
|
129
|
-
|
|
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,
|
|
147
|
-
|
|
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,
|
|
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)(
|
|
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,
|
|
169
|
-
|
|
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.
|
|
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.
|
|
66
|
+
"@types/pg": "8.16.0",
|
|
66
67
|
"@vitest/coverage-v8": "^4.0.18",
|
|
67
68
|
"eslint": "9.18.0",
|
|
68
|
-
"pg": "8.
|
|
69
|
+
"pg": "8.18.0",
|
|
69
70
|
"prettier": "3.8.1",
|
|
70
71
|
"typeorm": "0.3.28",
|
|
71
72
|
"typescript": "5.9.3",
|
package/src/api/contracts.ts
CHANGED
|
@@ -8,12 +8,14 @@ export class AddRefreshTokens1700000000001 {
|
|
|
8
8
|
|
|
9
9
|
await queryRunner.query(`
|
|
10
10
|
CREATE TABLE "refresh_token" (
|
|
11
|
-
"id"
|
|
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"
|
|
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
|
|
5
|
+
query: (sql: string) => Promise<unknown>;
|
|
6
6
|
}): Promise<void> {
|
|
7
|
-
const
|
|
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
|
|
19
|
-
"user_id"
|
|
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
|
|
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
|
|
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
|
|
12
|
-
"user_id"
|
|
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
|
+
};
|