@scryan7371/sdr-security 0.1.2 → 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 (68) hide show
  1. package/README.md +48 -7
  2. package/dist/api/contracts.d.ts +0 -2
  3. package/dist/api/migrations/1739500000000-create-security-identity.d.ts +1 -1
  4. package/dist/api/migrations/1739500000000-create-security-identity.js +9 -35
  5. package/dist/api/migrations/1739510000000-create-security-roles.d.ts +1 -1
  6. package/dist/api/migrations/1739510000000-create-security-roles.js +1 -67
  7. package/dist/api/migrations/1739515000000-create-security-user-roles.d.ts +9 -0
  8. package/dist/api/migrations/1739515000000-create-security-user-roles.js +39 -0
  9. package/dist/api/migrations/1739520000000-create-password-reset-tokens.js +1 -1
  10. package/dist/api/migrations/1739530000000-create-security-user.d.ts +9 -0
  11. package/dist/api/migrations/1739530000000-create-security-user.js +41 -0
  12. package/dist/api/migrations/index.d.ts +3 -2
  13. package/dist/api/migrations/index.js +7 -4
  14. package/dist/api/migrations/migrations.test.js +37 -83
  15. package/dist/api/notification-workflows.d.ts +0 -4
  16. package/dist/api/notification-workflows.js +0 -1
  17. package/dist/api/notification-workflows.test.js +1 -4
  18. package/dist/app/client.d.ts +0 -2
  19. package/dist/app/client.test.js +0 -2
  20. package/dist/nest/contracts.d.ts +0 -3
  21. package/dist/nest/dto/auth.dto.d.ts +0 -2
  22. package/dist/nest/dto/auth.dto.js +0 -10
  23. package/dist/nest/entities/app-user.entity.d.ts +0 -7
  24. package/dist/nest/entities/app-user.entity.js +0 -35
  25. package/dist/nest/entities/security-user.entity.d.ts +9 -0
  26. package/dist/nest/entities/security-user.entity.js +54 -0
  27. package/dist/nest/index.d.ts +1 -0
  28. package/dist/nest/index.js +1 -0
  29. package/dist/nest/security-auth.controller.d.ts +0 -2
  30. package/dist/nest/security-auth.controller.js +0 -2
  31. package/dist/nest/security-auth.controller.test.js +0 -4
  32. package/dist/nest/security-auth.module.js +2 -0
  33. package/dist/nest/security-auth.service.d.ts +5 -4
  34. package/dist/nest/security-auth.service.js +81 -51
  35. package/dist/nest/security-auth.service.test.js +45 -41
  36. package/dist/nest/security-workflows.module.js +2 -0
  37. package/dist/nest/security-workflows.service.d.ts +4 -2
  38. package/dist/nest/security-workflows.service.js +19 -16
  39. package/dist/nest/security-workflows.service.test.js +29 -24
  40. package/package.json +3 -3
  41. package/src/api/contracts.ts +0 -2
  42. package/src/api/migrations/1739500000000-create-security-identity.ts +11 -50
  43. package/src/api/migrations/1739510000000-create-security-roles.ts +2 -89
  44. package/src/api/migrations/1739515000000-create-security-user-roles.ts +49 -0
  45. package/src/api/migrations/1739520000000-create-password-reset-tokens.ts +1 -1
  46. package/src/api/migrations/1739530000000-create-security-user.ts +51 -0
  47. package/src/api/migrations/index.ts +6 -3
  48. package/src/api/migrations/migrations.test.ts +48 -111
  49. package/src/api/notification-workflows.test.ts +1 -4
  50. package/src/api/notification-workflows.ts +1 -8
  51. package/src/app/client.test.ts +0 -2
  52. package/src/app/client.ts +1 -6
  53. package/src/nest/contracts.ts +1 -6
  54. package/src/nest/dto/auth.dto.ts +0 -6
  55. package/src/nest/entities/app-user.entity.ts +0 -21
  56. package/src/nest/entities/security-user.entity.ts +25 -0
  57. package/src/nest/index.ts +1 -0
  58. package/src/nest/security-auth.controller.test.ts +0 -4
  59. package/src/nest/security-auth.controller.ts +0 -4
  60. package/src/nest/security-auth.module.ts +2 -0
  61. package/src/nest/security-auth.service.test.ts +74 -43
  62. package/src/nest/security-auth.service.ts +88 -51
  63. package/src/nest/security-workflows.module.ts +2 -0
  64. package/src/nest/security-workflows.service.test.ts +31 -25
  65. package/src/nest/security-workflows.service.ts +18 -13
  66. package/dist/api/migrations/1739490000000-add-google-subject-to-user.d.ts +0 -5
  67. package/dist/api/migrations/1739490000000-add-google-subject-to-user.js +0 -14
  68. package/src/api/migrations/1739490000000-add-google-subject-to-user.ts +0 -12
@@ -2,20 +2,11 @@ export class CreateSecurityRoles1739510000000 {
2
2
  name = "CreateSecurityRoles1739510000000";
3
3
 
4
4
  async up(queryRunner: {
5
- query: (sql: string, params?: unknown[]) => Promise<unknown>;
5
+ query: (sql: string) => Promise<unknown>;
6
6
  }): Promise<void> {
7
- const userTable = getSafeIdentifier(process.env.USER_TABLE, "app_user");
8
- const userSchema = getSafeIdentifier(
9
- process.env.USER_TABLE_SCHEMA,
10
- "public",
11
- );
12
- const userTableRef = `"${userSchema}"."${userTable}"`;
13
-
14
- await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
15
-
16
7
  await queryRunner.query(`
17
8
  CREATE TABLE IF NOT EXISTS "security_role" (
18
- "id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
9
+ "id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
19
10
  "role_key" varchar NOT NULL,
20
11
  "description" text,
21
12
  "is_system" boolean NOT NULL DEFAULT false,
@@ -28,95 +19,17 @@ export class CreateSecurityRoles1739510000000 {
28
19
  `CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_role_key" ON "security_role" ("role_key")`,
29
20
  );
30
21
 
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
22
  await queryRunner.query(`
47
23
  INSERT INTO "security_role" ("role_key", "description", "is_system", "created_at", "updated_at")
48
24
  VALUES ('ADMIN', 'Administrative access', true, now(), now())
49
25
  ON CONFLICT ("role_key") DO NOTHING
50
26
  `);
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
27
  }
103
28
 
104
29
  async down(queryRunner: {
105
30
  query: (sql: string) => Promise<unknown>;
106
31
  }): 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
32
  await queryRunner.query(`DROP INDEX IF EXISTS "IDX_security_role_key"`);
112
33
  await queryRunner.query(`DROP TABLE IF EXISTS "security_role"`);
113
34
  }
114
35
  }
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,49 @@
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" varchar NOT NULL,
13
+ "role_id" uuid NOT NULL,
14
+ "created_at" timestamptz NOT NULL DEFAULT now(),
15
+ CONSTRAINT "FK_security_user_role_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE,
16
+ CONSTRAINT "FK_security_user_role_role_id" FOREIGN KEY ("role_id") REFERENCES "security_role" ("id") ON DELETE CASCADE
17
+ )
18
+ `);
19
+
20
+ await queryRunner.query(
21
+ `CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_user_role_user_role" ON "security_user_role" ("user_id", "role_id")`,
22
+ );
23
+ }
24
+
25
+ async down(queryRunner: {
26
+ query: (sql: string) => Promise<unknown>;
27
+ }): Promise<void> {
28
+ await queryRunner.query(
29
+ `DROP INDEX IF EXISTS "IDX_security_user_role_user_role"`,
30
+ );
31
+ await queryRunner.query(`DROP TABLE IF EXISTS "security_user_role"`);
32
+ }
33
+ }
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
+ };
42
+
43
+ const getSafeIdentifier = (value: string | undefined, fallback: string) => {
44
+ const resolved = value?.trim() || fallback;
45
+ if (!resolved || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(resolved)) {
46
+ throw new Error(`Invalid SQL identifier: ${resolved}`);
47
+ }
48
+ return resolved;
49
+ };
@@ -8,7 +8,7 @@ export class CreatePasswordResetTokens1739520000000 {
8
8
 
9
9
  await queryRunner.query(`
10
10
  CREATE TABLE IF NOT EXISTS "security_password_reset_token" (
11
- "id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
11
+ "id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
12
12
  "user_id" varchar NOT NULL,
13
13
  "token" varchar NOT NULL,
14
14
  "expires_at" timestamptz NOT NULL,
@@ -0,0 +1,51 @@
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" varchar 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 "FK_security_user_user_id" FOREIGN KEY ("user_id") REFERENCES ${userTableRef} ("id") ON DELETE CASCADE
19
+ )
20
+ `);
21
+
22
+ await queryRunner.query(
23
+ `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`,
24
+ );
25
+ }
26
+
27
+ async down(queryRunner: {
28
+ query: (sql: string) => Promise<unknown>;
29
+ }): Promise<void> {
30
+ await queryRunner.query(
31
+ `DROP INDEX IF EXISTS "IDX_security_user_email_verification_token"`,
32
+ );
33
+ await queryRunner.query(`DROP TABLE IF EXISTS "security_user"`);
34
+ }
35
+ }
36
+
37
+ const getSafeIdentifier = (value: string | undefined, fallback: string) => {
38
+ const resolved = value?.trim() || fallback;
39
+ if (!resolved || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(resolved)) {
40
+ throw new Error(`Invalid SQL identifier: ${resolved}`);
41
+ }
42
+ return resolved;
43
+ };
44
+
45
+ const getUserTableReference = () => {
46
+ const table = getSafeIdentifier(process.env.USER_TABLE, "app_user");
47
+ const schema = process.env.USER_TABLE_SCHEMA
48
+ ? getSafeIdentifier(process.env.USER_TABLE_SCHEMA, "public")
49
+ : "public";
50
+ return `"${schema}"."${table}"`;
51
+ };
@@ -1,21 +1,24 @@
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
5
  import { CreatePasswordResetTokens1739520000000 } from "./1739520000000-create-password-reset-tokens";
6
+ import { CreateSecurityUser1739530000000 } from "./1739530000000-create-security-user";
6
7
 
7
8
  export const securityMigrations = [
8
9
  AddRefreshTokens1700000000001,
9
- AddGoogleSubjectToUser1739490000000,
10
10
  CreateSecurityIdentity1739500000000,
11
11
  CreateSecurityRoles1739510000000,
12
+ CreateSecurityUserRoles1739515000000,
12
13
  CreatePasswordResetTokens1739520000000,
14
+ CreateSecurityUser1739530000000,
13
15
  ];
14
16
 
15
17
  export {
16
18
  AddRefreshTokens1700000000001,
17
- AddGoogleSubjectToUser1739490000000,
18
19
  CreateSecurityIdentity1739500000000,
19
20
  CreateSecurityRoles1739510000000,
21
+ CreateSecurityUserRoles1739515000000,
20
22
  CreatePasswordResetTokens1739520000000,
23
+ CreateSecurityUser1739530000000,
21
24
  };
@@ -1,9 +1,10 @@
1
1
  import { afterEach, describe, expect, it, vi } from "vitest";
2
2
  import { AddRefreshTokens1700000000001 } from "./1700000000001-add-refresh-tokens";
3
- import { AddGoogleSubjectToUser1739490000000 } from "./1739490000000-add-google-subject-to-user";
4
3
  import { CreateSecurityIdentity1739500000000 } from "./1739500000000-create-security-identity";
5
4
  import { CreateSecurityRoles1739510000000 } from "./1739510000000-create-security-roles";
5
+ import { CreateSecurityUserRoles1739515000000 } from "./1739515000000-create-security-user-roles";
6
6
  import { CreatePasswordResetTokens1739520000000 } from "./1739520000000-create-password-reset-tokens";
7
+ import { CreateSecurityUser1739530000000 } from "./1739530000000-create-security-user";
7
8
  import { securityMigrations } from "./index";
8
9
 
9
10
  const originalEnv = { ...process.env };
@@ -15,11 +16,18 @@ afterEach(() => {
15
16
 
16
17
  describe("security migrations", () => {
17
18
  it("exports migration list", () => {
18
- expect(securityMigrations.length).toBe(5);
19
- expect(securityMigrations[0]).toBe(AddRefreshTokens1700000000001);
19
+ expect(securityMigrations.length).toBe(6);
20
+ expect(securityMigrations).toEqual([
21
+ AddRefreshTokens1700000000001,
22
+ CreateSecurityIdentity1739500000000,
23
+ CreateSecurityRoles1739510000000,
24
+ CreateSecurityUserRoles1739515000000,
25
+ CreatePasswordResetTokens1739520000000,
26
+ CreateSecurityUser1739530000000,
27
+ ]);
20
28
  });
21
29
 
22
- it("runs add refresh tokens migration up/down", async () => {
30
+ it("runs refresh token migration up/down", async () => {
23
31
  const query = vi.fn().mockResolvedValue(undefined);
24
32
  const migration = new AddRefreshTokens1700000000001();
25
33
 
@@ -34,46 +42,10 @@ describe("security migrations", () => {
34
42
  );
35
43
  });
36
44
 
37
- it("uses schema/table env in refresh token migration", async () => {
38
- process.env.USER_TABLE = "users";
39
- process.env.USER_TABLE_SCHEMA = "security";
45
+ it("runs security identity migration up/down", async () => {
40
46
  const query = vi.fn().mockResolvedValue(undefined);
41
-
42
- await new AddRefreshTokens1700000000001().up({ query });
43
-
44
- expect(query).toHaveBeenCalledWith(
45
- expect.stringContaining('REFERENCES "security"."users" ("id")'),
46
- );
47
- });
48
-
49
- it("throws for invalid identifiers in refresh token migration", async () => {
50
- process.env.USER_TABLE = "bad-name;drop";
51
- const query = vi.fn().mockResolvedValue(undefined);
52
-
53
- await expect(
54
- new AddRefreshTokens1700000000001().up({ query }),
55
- ).rejects.toThrow("Invalid SQL identifier");
56
- });
57
-
58
- it("keeps legacy google subject migration as no-op", async () => {
59
- const migration = new AddGoogleSubjectToUser1739490000000();
60
- await expect(migration.up()).resolves.toBeUndefined();
61
- await expect(migration.down()).resolves.toBeUndefined();
62
- });
63
-
64
- it("runs security identity migration path with google_subject present", async () => {
65
- const query = vi
66
- .fn()
67
- .mockResolvedValueOnce(undefined)
68
- .mockResolvedValueOnce(undefined)
69
- .mockResolvedValueOnce(undefined)
70
- .mockResolvedValueOnce(undefined)
71
- .mockResolvedValueOnce([{ "?column?": 1 }])
72
- .mockResolvedValueOnce(undefined)
73
- .mockResolvedValueOnce(undefined)
74
- .mockResolvedValueOnce(undefined);
75
-
76
47
  const migration = new CreateSecurityIdentity1739500000000();
48
+
77
49
  await migration.up({ query });
78
50
  await migration.down({ query });
79
51
 
@@ -85,52 +57,28 @@ describe("security migrations", () => {
85
57
  );
86
58
  });
87
59
 
88
- it("skips google_subject migration block when column absent", async () => {
89
- const query = vi
90
- .fn()
91
- .mockResolvedValueOnce(undefined)
92
- .mockResolvedValueOnce(undefined)
93
- .mockResolvedValueOnce(undefined)
94
- .mockResolvedValueOnce(undefined)
95
- .mockResolvedValueOnce([]);
60
+ it("runs security role migration up/down", async () => {
61
+ const query = vi.fn().mockResolvedValue(undefined);
62
+ const migration = new CreateSecurityRoles1739510000000();
96
63
 
97
- await new CreateSecurityIdentity1739500000000().up({ query });
64
+ await migration.up({ query });
65
+ await migration.down({ query });
98
66
 
99
- expect(query).not.toHaveBeenCalledWith(
100
- expect.stringContaining('DROP COLUMN IF EXISTS "google_subject"'),
67
+ expect(query).toHaveBeenCalledWith(
68
+ expect.stringContaining('CREATE TABLE IF NOT EXISTS "security_role"'),
69
+ );
70
+ expect(query).toHaveBeenCalledWith(
71
+ expect.stringContaining('DROP TABLE IF EXISTS "security_role"'),
101
72
  );
102
73
  });
103
74
 
104
- it("throws for invalid identifiers in identity migration", async () => {
105
- process.env.USER_TABLE_SCHEMA = "bad-schema!";
75
+ it("runs security user role migration up/down", async () => {
106
76
  const query = vi.fn().mockResolvedValue(undefined);
77
+ const migration = new CreateSecurityUserRoles1739515000000();
107
78
 
108
- await expect(
109
- new CreateSecurityIdentity1739500000000().up({ query }),
110
- ).rejects.toThrow("Invalid SQL identifier");
111
- });
112
-
113
- it("runs security roles migration path with legacy role column", async () => {
114
- const query = vi
115
- .fn()
116
- .mockResolvedValueOnce(undefined)
117
- .mockResolvedValueOnce(undefined)
118
- .mockResolvedValueOnce(undefined)
119
- .mockResolvedValueOnce(undefined)
120
- .mockResolvedValueOnce(undefined)
121
- .mockResolvedValueOnce(undefined)
122
- .mockResolvedValueOnce([{ "?column?": 1 }])
123
- .mockResolvedValueOnce(undefined)
124
- .mockResolvedValueOnce(undefined)
125
- .mockResolvedValueOnce(undefined);
126
-
127
- const migration = new CreateSecurityRoles1739510000000();
128
79
  await migration.up({ query });
129
80
  await migration.down({ query });
130
81
 
131
- expect(query).toHaveBeenCalledWith(
132
- expect.stringContaining('CREATE TABLE IF NOT EXISTS "security_role"'),
133
- );
134
82
  expect(query).toHaveBeenCalledWith(
135
83
  expect.stringContaining(
136
84
  'CREATE TABLE IF NOT EXISTS "security_user_role"',
@@ -141,33 +89,6 @@ describe("security migrations", () => {
141
89
  );
142
90
  });
143
91
 
144
- it("skips legacy role backfill when role column absent", async () => {
145
- const query = vi
146
- .fn()
147
- .mockResolvedValueOnce(undefined)
148
- .mockResolvedValueOnce(undefined)
149
- .mockResolvedValueOnce(undefined)
150
- .mockResolvedValueOnce(undefined)
151
- .mockResolvedValueOnce(undefined)
152
- .mockResolvedValueOnce(undefined)
153
- .mockResolvedValueOnce([]);
154
-
155
- await new CreateSecurityRoles1739510000000().up({ query });
156
-
157
- expect(query).not.toHaveBeenCalledWith(
158
- expect.stringContaining('DROP COLUMN IF EXISTS "role"'),
159
- );
160
- });
161
-
162
- it("throws for invalid identifiers in roles migration", async () => {
163
- process.env.USER_TABLE = "bad-name*";
164
- const query = vi.fn().mockResolvedValue(undefined);
165
-
166
- await expect(
167
- new CreateSecurityRoles1739510000000().up({ query }),
168
- ).rejects.toThrow("Invalid SQL identifier");
169
- });
170
-
171
92
  it("runs password reset token migration up/down", async () => {
172
93
  const query = vi.fn().mockResolvedValue(undefined);
173
94
  const migration = new CreatePasswordResetTokens1739520000000();
@@ -187,22 +108,38 @@ describe("security migrations", () => {
187
108
  );
188
109
  });
189
110
 
190
- it("uses default public schema in password reset migration", async () => {
111
+ it("runs security user migration up/down", async () => {
191
112
  const query = vi.fn().mockResolvedValue(undefined);
113
+ const migration = new CreateSecurityUser1739530000000();
114
+
115
+ await migration.up({ query });
116
+ await migration.down({ query });
117
+
118
+ expect(query).toHaveBeenCalledWith(
119
+ expect.stringContaining('CREATE TABLE IF NOT EXISTS "security_user"'),
120
+ );
121
+ expect(query).toHaveBeenCalledWith(
122
+ expect.stringContaining('DROP TABLE IF EXISTS "security_user"'),
123
+ );
124
+ });
192
125
 
193
- await new CreatePasswordResetTokens1739520000000().up({ query });
126
+ it("uses user schema/table env safely", async () => {
127
+ process.env.USER_TABLE = "users";
128
+ process.env.USER_TABLE_SCHEMA = "security";
129
+ const query = vi.fn().mockResolvedValue(undefined);
194
130
 
131
+ await new CreateSecurityUser1739530000000().up({ query });
195
132
  expect(query).toHaveBeenCalledWith(
196
- expect.stringContaining('REFERENCES "public"."app_user" ("id")'),
133
+ expect.stringContaining('REFERENCES "security"."users" ("id")'),
197
134
  );
198
135
  });
199
136
 
200
- it("throws for invalid identifiers in password reset migration", async () => {
201
- process.env.USER_TABLE_SCHEMA = "bad.schema";
137
+ it("throws for invalid identifiers", async () => {
138
+ process.env.USER_TABLE = "bad-name!";
202
139
  const query = vi.fn().mockResolvedValue(undefined);
203
140
 
204
141
  await expect(
205
- new CreatePasswordResetTokens1739520000000().up({ query }),
142
+ new CreateSecurityUserRoles1739515000000().up({ query }),
206
143
  ).rejects.toThrow("Invalid SQL identifier");
207
144
  });
208
145
  });
@@ -15,8 +15,6 @@ describe("notification-workflows", () => {
15
15
  user: {
16
16
  id: "user-1",
17
17
  email: "user@example.com",
18
- firstName: "User",
19
- lastName: "One",
20
18
  },
21
19
  listAdminEmails,
22
20
  notifyAdmins,
@@ -55,14 +53,13 @@ describe("notification-workflows", () => {
55
53
 
56
54
  const result = await notifyUserOnAdminApproval({
57
55
  approved: true,
58
- user: { email: "user@example.com", firstName: "User" },
56
+ user: { email: "user@example.com" },
59
57
  notifyUser,
60
58
  });
61
59
 
62
60
  expect(result).toEqual({ notified: true });
63
61
  expect(notifyUser).toHaveBeenCalledWith({
64
62
  email: "user@example.com",
65
- firstName: "User",
66
63
  });
67
64
  });
68
65
 
@@ -1,8 +1,6 @@
1
1
  export type VerificationNotificationUser = {
2
2
  id: string;
3
3
  email: string;
4
- firstName?: string | null;
5
- lastName?: string | null;
6
4
  };
7
5
 
8
6
  export const notifyAdminsOnEmailVerified = async (params: {
@@ -26,12 +24,8 @@ export const notifyUserOnAdminApproval = async (params: {
26
24
  approved: boolean;
27
25
  user: {
28
26
  email: string;
29
- firstName?: string | null;
30
27
  };
31
- notifyUser: (payload: {
32
- email: string;
33
- firstName?: string | null;
34
- }) => Promise<void>;
28
+ notifyUser: (payload: { email: string }) => Promise<void>;
35
29
  }) => {
36
30
  if (!params.approved) {
37
31
  return { notified: false as const };
@@ -39,7 +33,6 @@ export const notifyUserOnAdminApproval = async (params: {
39
33
 
40
34
  await params.notifyUser({
41
35
  email: params.user.email,
42
- firstName: params.user.firstName,
43
36
  });
44
37
  return { notified: true as const };
45
38
  };
@@ -30,8 +30,6 @@ describe("createSecurityClient", () => {
30
30
  await client.register({
31
31
  email: "user@example.com",
32
32
  password: "Secret123",
33
- firstName: "A",
34
- lastName: "B",
35
33
  });
36
34
 
37
35
  expect(fetchImpl).toHaveBeenCalledWith(
package/src/app/client.ts CHANGED
@@ -52,12 +52,7 @@ export const createSecurityClient = (options: SecurityClientOptions) => {
52
52
  };
53
53
 
54
54
  return {
55
- register: (payload: {
56
- email: string;
57
- password: string;
58
- firstName?: string;
59
- lastName?: string;
60
- }) =>
55
+ register: (payload: { email: string; password: string }) =>
61
56
  request<RegisterResponse>("/security/auth/register", {
62
57
  method: "POST",
63
58
  body: JSON.stringify(payload),
@@ -1,8 +1,6 @@
1
1
  export type SecurityWorkflowUser = {
2
2
  id: string;
3
3
  email: string;
4
- firstName: string | null;
5
- lastName: string | null;
6
4
  };
7
5
 
8
6
  export type SecurityWorkflowNotifier = {
@@ -18,8 +16,5 @@ export type SecurityWorkflowNotifier = {
18
16
  adminEmails: string[];
19
17
  user: SecurityWorkflowUser;
20
18
  }) => Promise<void>;
21
- sendUserAccountApproved: (params: {
22
- email: string;
23
- firstName: string | null;
24
- }) => Promise<void>;
19
+ sendUserAccountApproved: (params: { email: string }) => Promise<void>;
25
20
  };
@@ -6,12 +6,6 @@ export class RegisterDto {
6
6
 
7
7
  @ApiProperty({ example: "StrongPass1" })
8
8
  password!: string;
9
-
10
- @ApiProperty({ required: false, nullable: true, example: "John" })
11
- firstName?: string | null;
12
-
13
- @ApiProperty({ required: false, nullable: true, example: "Doe" })
14
- lastName?: string | null;
15
9
  }
16
10
 
17
11
  export class LoginDto {
@@ -7,25 +7,4 @@ export class AppUserEntity {
7
7
 
8
8
  @Column({ type: "varchar" })
9
9
  email!: string;
10
-
11
- @Column({ type: "varchar", name: "password_hash" })
12
- passwordHash!: string;
13
-
14
- @Column({ type: "varchar", name: "first_name", nullable: true })
15
- firstName!: string | null;
16
-
17
- @Column({ type: "varchar", name: "last_name", nullable: true })
18
- lastName!: string | null;
19
-
20
- @Column({ type: "timestamptz", name: "email_verified_at", nullable: true })
21
- emailVerifiedAt!: Date | null;
22
-
23
- @Column({ type: "varchar", name: "email_verification_token", nullable: true })
24
- emailVerificationToken!: string | null;
25
-
26
- @Column({ type: "timestamptz", name: "admin_approved_at", nullable: true })
27
- adminApprovedAt!: Date | null;
28
-
29
- @Column({ type: "boolean", name: "is_active", default: true })
30
- isActive!: boolean;
31
10
  }
@@ -0,0 +1,25 @@
1
+ import { Column, CreateDateColumn, Entity, PrimaryColumn } from "typeorm";
2
+
3
+ @Entity({ name: "security_user" })
4
+ export class SecurityUserEntity {
5
+ @PrimaryColumn({ type: "varchar", name: "user_id" })
6
+ userId!: string;
7
+
8
+ @Column({ type: "varchar", name: "password_hash" })
9
+ passwordHash!: string;
10
+
11
+ @Column({ type: "timestamptz", name: "email_verified_at", nullable: true })
12
+ emailVerifiedAt!: Date | null;
13
+
14
+ @Column({ type: "varchar", name: "email_verification_token", nullable: true })
15
+ emailVerificationToken!: string | null;
16
+
17
+ @Column({ type: "timestamptz", name: "admin_approved_at", nullable: true })
18
+ adminApprovedAt!: Date | null;
19
+
20
+ @Column({ type: "boolean", name: "is_active", default: true })
21
+ isActive!: boolean;
22
+
23
+ @CreateDateColumn({ name: "created_at" })
24
+ createdAt!: Date;
25
+ }
package/src/nest/index.ts CHANGED
@@ -15,4 +15,5 @@ export * from "./entities/app-user.entity";
15
15
  export * from "./entities/refresh-token.entity";
16
16
  export * from "./entities/password-reset-token.entity";
17
17
  export * from "./entities/security-role.entity";
18
+ export * from "./entities/security-user.entity";
18
19
  export * from "./entities/security-user-role.entity";
@@ -34,15 +34,11 @@ describe("SecurityAuthController", () => {
34
34
  const result = await controller.register({
35
35
  email: "user@example.com",
36
36
  password: "Secret123",
37
- firstName: "A",
38
- lastName: "B",
39
37
  });
40
38
  expect(result).toEqual({ success: true });
41
39
  expect(service.register).toHaveBeenCalledWith({
42
40
  email: "user@example.com",
43
41
  password: "Secret123",
44
- firstName: "A",
45
- lastName: "B",
46
42
  });
47
43
  });
48
44