@baruk/core 0.1.0

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 (113) hide show
  1. package/README.md +6 -0
  2. package/dist/audit/audit.d.ts +22 -0
  3. package/dist/audit/audit.d.ts.map +1 -0
  4. package/dist/audit/audit.js +28 -0
  5. package/dist/audit/audit.js.map +1 -0
  6. package/dist/audit/repository.d.ts +22 -0
  7. package/dist/audit/repository.d.ts.map +1 -0
  8. package/dist/audit/repository.js +29 -0
  9. package/dist/audit/repository.js.map +1 -0
  10. package/dist/auth/password-reset.d.ts +20 -0
  11. package/dist/auth/password-reset.d.ts.map +1 -0
  12. package/dist/auth/password-reset.js +56 -0
  13. package/dist/auth/password-reset.js.map +1 -0
  14. package/dist/auth/password.d.ts +21 -0
  15. package/dist/auth/password.d.ts.map +1 -0
  16. package/dist/auth/password.js +67 -0
  17. package/dist/auth/password.js.map +1 -0
  18. package/dist/auth/service.d.ts +82 -0
  19. package/dist/auth/service.d.ts.map +1 -0
  20. package/dist/auth/service.js +214 -0
  21. package/dist/auth/service.js.map +1 -0
  22. package/dist/auth/session-token.d.ts +8 -0
  23. package/dist/auth/session-token.d.ts.map +1 -0
  24. package/dist/auth/session-token.js +19 -0
  25. package/dist/auth/session-token.js.map +1 -0
  26. package/dist/clients/repository.d.ts +53 -0
  27. package/dist/clients/repository.d.ts.map +1 -0
  28. package/dist/clients/repository.js +113 -0
  29. package/dist/clients/repository.js.map +1 -0
  30. package/dist/dashboard/repository.d.ts +19 -0
  31. package/dist/dashboard/repository.d.ts.map +1 -0
  32. package/dist/dashboard/repository.js +36 -0
  33. package/dist/dashboard/repository.js.map +1 -0
  34. package/dist/db/client.d.ts +19 -0
  35. package/dist/db/client.d.ts.map +1 -0
  36. package/dist/db/client.js +21 -0
  37. package/dist/db/client.js.map +1 -0
  38. package/dist/db/context.d.ts +27 -0
  39. package/dist/db/context.d.ts.map +1 -0
  40. package/dist/db/context.js +36 -0
  41. package/dist/db/context.js.map +1 -0
  42. package/dist/db/load-env.d.ts +8 -0
  43. package/dist/db/load-env.d.ts.map +1 -0
  44. package/dist/db/load-env.js +44 -0
  45. package/dist/db/load-env.js.map +1 -0
  46. package/dist/db/migrate.d.ts +2 -0
  47. package/dist/db/migrate.d.ts.map +1 -0
  48. package/dist/db/migrate.js +139 -0
  49. package/dist/db/migrate.js.map +1 -0
  50. package/dist/db/seed.d.ts +2 -0
  51. package/dist/db/seed.d.ts.map +1 -0
  52. package/dist/db/seed.js +64 -0
  53. package/dist/db/seed.js.map +1 -0
  54. package/dist/import/clients-import.d.ts +39 -0
  55. package/dist/import/clients-import.d.ts.map +1 -0
  56. package/dist/import/clients-import.js +142 -0
  57. package/dist/import/clients-import.js.map +1 -0
  58. package/dist/index.d.ts +40 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +40 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/mail/resend.d.ts +13 -0
  63. package/dist/mail/resend.d.ts.map +1 -0
  64. package/dist/mail/resend.js +16 -0
  65. package/dist/mail/resend.js.map +1 -0
  66. package/dist/notifications/repository.d.ts +23 -0
  67. package/dist/notifications/repository.d.ts.map +1 -0
  68. package/dist/notifications/repository.js +43 -0
  69. package/dist/notifications/repository.js.map +1 -0
  70. package/dist/portal/repository.d.ts +11 -0
  71. package/dist/portal/repository.d.ts.map +1 -0
  72. package/dist/portal/repository.js +32 -0
  73. package/dist/portal/repository.js.map +1 -0
  74. package/dist/products/catalog.d.ts +14 -0
  75. package/dist/products/catalog.d.ts.map +1 -0
  76. package/dist/products/catalog.js +7 -0
  77. package/dist/products/catalog.js.map +1 -0
  78. package/dist/products/repository.d.ts +22 -0
  79. package/dist/products/repository.d.ts.map +1 -0
  80. package/dist/products/repository.js +56 -0
  81. package/dist/products/repository.js.map +1 -0
  82. package/dist/rbac/authorize.d.ts +18 -0
  83. package/dist/rbac/authorize.d.ts.map +1 -0
  84. package/dist/rbac/authorize.js +35 -0
  85. package/dist/rbac/authorize.js.map +1 -0
  86. package/dist/rbac/catalog.d.ts +21 -0
  87. package/dist/rbac/catalog.d.ts.map +1 -0
  88. package/dist/rbac/catalog.js +59 -0
  89. package/dist/rbac/catalog.js.map +1 -0
  90. package/dist/requests/repository.d.ts +66 -0
  91. package/dist/requests/repository.d.ts.map +1 -0
  92. package/dist/requests/repository.js +106 -0
  93. package/dist/requests/repository.js.map +1 -0
  94. package/dist/tenancy/provisioning.d.ts +25 -0
  95. package/dist/tenancy/provisioning.d.ts.map +1 -0
  96. package/dist/tenancy/provisioning.js +119 -0
  97. package/dist/tenancy/provisioning.js.map +1 -0
  98. package/dist/users/repository.d.ts +19 -0
  99. package/dist/users/repository.d.ts.map +1 -0
  100. package/dist/users/repository.js +78 -0
  101. package/dist/users/repository.js.map +1 -0
  102. package/package.json +41 -0
  103. package/src/db/migrations/0001_extensions.sql +5 -0
  104. package/src/db/migrations/0002_core_identity.sql +78 -0
  105. package/src/db/migrations/0003_rbac.sql +68 -0
  106. package/src/db/migrations/0004_directory.sql +35 -0
  107. package/src/db/migrations/0005_audit.sql +35 -0
  108. package/src/db/migrations/0006_rls_policies.sql +69 -0
  109. package/src/db/migrations/0007_requests_notifications_portal.sql +103 -0
  110. package/src/db/migrations/0008_client_deleted_by.sql +5 -0
  111. package/src/db/migrations/0009_client_scope_rls.sql +50 -0
  112. package/src/db/migrations/0010_product_entitlements.sql +43 -0
  113. package/src/db/migrations/0011_password_reset.sql +18 -0
@@ -0,0 +1,78 @@
1
+ import { ConflictError, uuidv7 } from '@baruk/shared';
2
+ const ADMIN_ROLE_KEYS = ['owner', 'admin'];
3
+ export async function listTenantMembers(tx) {
4
+ const rows = await tx `
5
+ SELECT
6
+ m.user_id, u.email, u.full_name, u.user_type, m.status,
7
+ array_remove(array_agg(DISTINCT r.key), NULL) AS role_keys
8
+ FROM membership m
9
+ JOIN "user" u ON u.id = m.user_id
10
+ LEFT JOIN user_role ur ON ur.user_id = m.user_id AND ur.deleted_at IS NULL
11
+ LEFT JOIN role r ON r.id = ur.role_id
12
+ WHERE m.deleted_at IS NULL
13
+ GROUP BY m.user_id, u.email, u.full_name, u.user_type, m.status
14
+ ORDER BY u.full_name
15
+ `;
16
+ return rows.map((r) => ({
17
+ userId: r.user_id,
18
+ email: r.email,
19
+ fullName: r.full_name,
20
+ userType: r.user_type,
21
+ membershipStatus: r.status,
22
+ roles: r.role_keys ?? [],
23
+ }));
24
+ }
25
+ async function countActiveAdmins(tx, excludeUserId) {
26
+ const rows = await tx `
27
+ SELECT count(DISTINCT m.user_id)::int AS n
28
+ FROM membership m
29
+ JOIN user_role ur ON ur.user_id = m.user_id AND ur.deleted_at IS NULL
30
+ JOIN role r ON r.id = ur.role_id
31
+ WHERE m.status = 'active' AND m.deleted_at IS NULL
32
+ AND r.key = ANY(${ADMIN_ROLE_KEYS})
33
+ AND (${excludeUserId ?? null}::uuid IS NULL OR m.user_id <> ${excludeUserId ?? null})
34
+ `;
35
+ return rows[0]?.n ?? 0;
36
+ }
37
+ /** Substitui o(s) papel(is) do usuário no tenant por um único papel (por key). */
38
+ export async function setUserRole(tx, userId, roleKey) {
39
+ // Guard: não permitir rebaixar o último admin.
40
+ const isAdminTarget = ADMIN_ROLE_KEYS.includes(roleKey);
41
+ if (!isAdminTarget && (await isUserAdmin(tx, userId)) && (await countActiveAdmins(tx, userId)) === 0) {
42
+ throw new ConflictError('Não é possível rebaixar o último administrador do tenant');
43
+ }
44
+ const role = await tx `SELECT id FROM role WHERE key = ${roleKey} AND tenant_id IS NULL`;
45
+ if (!role[0])
46
+ throw new ConflictError(`Papel desconhecido: ${roleKey}`);
47
+ await tx `
48
+ UPDATE user_role SET deleted_at = now()
49
+ WHERE user_id = ${userId} AND deleted_at IS NULL
50
+ AND tenant_id = current_setting('app.current_tenant')::uuid
51
+ `;
52
+ await tx `
53
+ INSERT INTO user_role (id, tenant_id, user_id, role_id)
54
+ VALUES (${uuidv7()}, current_setting('app.current_tenant')::uuid, ${userId}, ${role[0].id})
55
+ `;
56
+ }
57
+ async function isUserAdmin(tx, userId) {
58
+ const rows = await tx `
59
+ SELECT count(*)::int AS n
60
+ FROM user_role ur JOIN role r ON r.id = ur.role_id
61
+ WHERE ur.user_id = ${userId} AND ur.deleted_at IS NULL AND r.key = ANY(${ADMIN_ROLE_KEYS})
62
+ `;
63
+ return (rows[0]?.n ?? 0) > 0;
64
+ }
65
+ /** Ativa/desativa (ou revoga) a membership. Bloqueia desativar o último admin. */
66
+ export async function setMembershipStatus(tx, userId, status) {
67
+ if (status !== 'active' && (await isUserAdmin(tx, userId)) && (await countActiveAdmins(tx, userId)) === 0) {
68
+ throw new ConflictError('Não é possível desativar o último administrador do tenant');
69
+ }
70
+ const rows = await tx `
71
+ UPDATE membership SET status = ${status}, updated_at = now()
72
+ WHERE user_id = ${userId} AND deleted_at IS NULL
73
+ AND tenant_id = current_setting('app.current_tenant')::uuid
74
+ RETURNING id
75
+ `;
76
+ return rows.count;
77
+ }
78
+ //# sourceMappingURL=repository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.js","sourceRoot":"","sources":["../../src/users/repository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAgBtD,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAE3C,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,EAAM;IAC5C,MAAM,IAAI,GAAG,MAAM,EAAE,CAEpB;;;;;;;;;;;GAWA,CAAC;IACF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACtB,MAAM,EAAE,CAAC,CAAC,OAAO;QACjB,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,QAAQ,EAAE,CAAC,CAAC,SAAS;QACrB,QAAQ,EAAE,CAAC,CAAC,SAAS;QACrB,gBAAgB,EAAE,CAAC,CAAC,MAAM;QAC1B,KAAK,EAAE,CAAC,CAAC,SAAS,IAAI,EAAE;KACzB,CAAC,CAAC,CAAC;AACN,CAAC;AAED,KAAK,UAAU,iBAAiB,CAAC,EAAM,EAAE,aAAsB;IAC7D,MAAM,IAAI,GAAG,MAAM,EAAE,CAAiB;;;;;;wBAMhB,eAAe;aAC1B,aAAa,IAAI,IAAI,kCAAkC,aAAa,IAAI,IAAI;GACtF,CAAC;IACF,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC;AAED,kFAAkF;AAClF,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAAM,EAAE,MAAc,EAAE,OAAe;IACvE,+CAA+C;IAC/C,MAAM,aAAa,GAAG,eAAe,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACxD,IAAI,CAAC,aAAa,IAAI,CAAC,MAAM,WAAW,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,iBAAiB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;QACrG,MAAM,IAAI,aAAa,CAAC,0DAA0D,CAAC,CAAC;IACtF,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAkB,mCAAmC,OAAO,wBAAwB,CAAC;IAC1G,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,aAAa,CAAC,uBAAuB,OAAO,EAAE,CAAC,CAAC;IACxE,MAAM,EAAE,CAAA;;sBAEY,MAAM;;GAEzB,CAAC;IACF,MAAM,EAAE,CAAA;;cAEI,MAAM,EAAE,kDAAkD,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE;GAC1F,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,EAAM,EAAE,MAAc;IAC/C,MAAM,IAAI,GAAG,MAAM,EAAE,CAAiB;;;yBAGf,MAAM,8CAA8C,eAAe;GACzF,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;AAC/B,CAAC;AAED,kFAAkF;AAClF,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,EAAM,EACN,MAAc,EACd,MAAwC;IAExC,IAAI,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,WAAW,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,iBAAiB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1G,MAAM,IAAI,aAAa,CAAC,2DAA2D,CAAC,CAAC;IACvF,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAA;qCACc,MAAM;sBACrB,MAAM;;;GAGzB,CAAC;IACF,OAAO,IAAI,CAAC,KAAK,CAAC;AACpB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@baruk/core",
3
+ "version": "0.1.0",
4
+ "license": "UNLICENSED",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "registry": "https://registry.npmjs.org",
8
+ "access": "public"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "!dist/__tests__",
21
+ "src/db/migrations"
22
+ ],
23
+ "dependencies": {
24
+ "@baruk/identity": "^0.2.0",
25
+ "postgres": "^3.4.5",
26
+ "zod": "^3.24.1",
27
+ "@baruk/shared": "^0.1.0"
28
+ },
29
+ "devDependencies": {
30
+ "tsx": "^4.19.2"
31
+ },
32
+ "scripts": {
33
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
34
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
35
+ "typecheck": "tsc -p tsconfig.json --noEmit",
36
+ "lint": "eslint src",
37
+ "test": "vitest run --passWithNoTests",
38
+ "migrate": "tsx src/db/migrate.ts",
39
+ "seed": "tsx src/db/seed.ts"
40
+ }
41
+ }
@@ -0,0 +1,5 @@
1
+ -- 0001 — Extensões necessárias.
2
+ -- citext: e-mails case-insensitive (03-data-model §6.1). UUIDs são gerados na
3
+ -- aplicação (uuidv7), portanto não dependemos de pgcrypto/uuid-ossp para PKs.
4
+
5
+ CREATE EXTENSION IF NOT EXISTS citext;
@@ -0,0 +1,78 @@
1
+ -- 0002 — Identidade & Acesso (entidades globais de SSO) + organização.
2
+ -- Entidades GLOBAIS (sem tenant_id): tenant, "user", user_credential,
3
+ -- user_session, permission. Justificativa: SSO central, um login por staff
4
+ -- acessando vários tenants (ADR 0002 / 03-data-model §2.2). O vínculo ao tenant
5
+ -- vive em `membership` (migration 0003), que é tenant-scoped + RLS.
6
+
7
+ -- ── tenant (raiz da árvore de tenancy) ──────────────────────────────────────
8
+ CREATE TABLE tenant (
9
+ id uuid PRIMARY KEY,
10
+ code text NOT NULL,
11
+ legal_name text,
12
+ display_name text NOT NULL,
13
+ status text NOT NULL DEFAULT 'trial'
14
+ CHECK (status IN ('active', 'suspended', 'trial', 'churned')),
15
+ primary_product text NOT NULL DEFAULT 'contabil',
16
+ settings jsonb NOT NULL DEFAULT '{}'::jsonb,
17
+ created_at timestamptz NOT NULL DEFAULT now(),
18
+ updated_at timestamptz NOT NULL DEFAULT now(),
19
+ deleted_at timestamptz
20
+ );
21
+ CREATE UNIQUE INDEX tenant_code_uq ON tenant (lower(code)) WHERE deleted_at IS NULL;
22
+
23
+ -- ── user (identidade única de SSO — global) ─────────────────────────────────
24
+ CREATE TABLE "user" (
25
+ id uuid PRIMARY KEY,
26
+ email citext NOT NULL,
27
+ full_name text NOT NULL,
28
+ user_type text NOT NULL DEFAULT 'staff'
29
+ CHECK (user_type IN ('staff', 'client_portal', 'service')),
30
+ status text NOT NULL DEFAULT 'active'
31
+ CHECK (status IN ('active', 'invited', 'disabled')),
32
+ auth_provider text NOT NULL DEFAULT 'password'
33
+ CHECK (auth_provider IN ('password', 'oidc', 'magic_link')),
34
+ last_login_at timestamptz,
35
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
36
+ created_at timestamptz NOT NULL DEFAULT now(),
37
+ updated_at timestamptz NOT NULL DEFAULT now(),
38
+ deleted_at timestamptz
39
+ );
40
+ CREATE UNIQUE INDEX user_email_uq ON "user" (email) WHERE deleted_at IS NULL;
41
+
42
+ -- ── user_credential (hash de senha p/ auth local — fundação/dev) ────────────
43
+ -- Concessão da fundação: em produção a autenticação vai para o IdP central
44
+ -- (OIDC); aqui guardamos apenas o hash scrypt (nunca a senha) para o walking
45
+ -- skeleton local. Global (segue `user`). NUNCA guarda segredo em claro.
46
+ CREATE TABLE user_credential (
47
+ user_id uuid PRIMARY KEY REFERENCES "user"(id) ON DELETE CASCADE,
48
+ password_hash text NOT NULL,
49
+ updated_at timestamptz NOT NULL DEFAULT now()
50
+ );
51
+
52
+ -- ── user_session (sessão opaca — global, resolvida por hash do token) ───────
53
+ CREATE TABLE user_session (
54
+ id uuid PRIMARY KEY,
55
+ user_id uuid NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
56
+ token_hash text NOT NULL,
57
+ active_tenant_id uuid REFERENCES tenant(id),
58
+ created_at timestamptz NOT NULL DEFAULT now(),
59
+ expires_at timestamptz NOT NULL,
60
+ revoked_at timestamptz
61
+ );
62
+ CREATE UNIQUE INDEX user_session_token_uq ON user_session (token_hash);
63
+ CREATE INDEX user_session_user_idx ON user_session (user_id);
64
+
65
+ -- ── organization (unidade dentro de um tenant — tenant-scoped) ──────────────
66
+ CREATE TABLE organization (
67
+ id uuid PRIMARY KEY,
68
+ tenant_id uuid NOT NULL REFERENCES tenant(id),
69
+ name text NOT NULL,
70
+ type text NOT NULL DEFAULT 'headquarters'
71
+ CHECK (type IN ('headquarters', 'branch', 'department')),
72
+ parent_org_id uuid REFERENCES organization(id),
73
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
74
+ created_at timestamptz NOT NULL DEFAULT now(),
75
+ updated_at timestamptz NOT NULL DEFAULT now(),
76
+ deleted_at timestamptz
77
+ );
78
+ CREATE INDEX organization_tenant_idx ON organization (tenant_id);
@@ -0,0 +1,68 @@
1
+ -- 0003 — RBAC: permission (catálogo global), role (templates + por tenant),
2
+ -- role_permission, membership (vínculo user×tenant), user_role.
3
+ -- Detalhes em 04-rbac-permissions.md.
4
+
5
+ -- ── permission (catálogo global, read-only) ─────────────────────────────────
6
+ CREATE TABLE permission (
7
+ id uuid PRIMARY KEY,
8
+ key text NOT NULL,
9
+ resource text NOT NULL,
10
+ action text NOT NULL,
11
+ description text,
12
+ created_at timestamptz NOT NULL DEFAULT now()
13
+ );
14
+ CREATE UNIQUE INDEX permission_key_uq ON permission (key);
15
+
16
+ -- ── role (tenant_id NULL = template de sistema; preenchido = papel do tenant) ─
17
+ CREATE TABLE role (
18
+ id uuid PRIMARY KEY,
19
+ tenant_id uuid REFERENCES tenant(id),
20
+ key text NOT NULL,
21
+ name text NOT NULL,
22
+ is_system boolean NOT NULL DEFAULT false,
23
+ created_at timestamptz NOT NULL DEFAULT now()
24
+ );
25
+ -- Um template por key (tenant_id NULL) e um por (tenant, key).
26
+ CREATE UNIQUE INDEX role_system_key_uq ON role (key) WHERE tenant_id IS NULL;
27
+ CREATE UNIQUE INDEX role_tenant_key_uq ON role (tenant_id, key) WHERE tenant_id IS NOT NULL;
28
+
29
+ -- ── role_permission (N:N role↔permission) ───────────────────────────────────
30
+ CREATE TABLE role_permission (
31
+ role_id uuid NOT NULL REFERENCES role(id) ON DELETE CASCADE,
32
+ permission_id uuid NOT NULL REFERENCES permission(id) ON DELETE CASCADE,
33
+ tenant_id uuid REFERENCES tenant(id),
34
+ PRIMARY KEY (role_id, permission_id)
35
+ );
36
+
37
+ -- ── membership (vínculo user×tenant — porta de entrada do isolamento staff) ──
38
+ CREATE TABLE membership (
39
+ id uuid PRIMARY KEY,
40
+ tenant_id uuid NOT NULL REFERENCES tenant(id),
41
+ user_id uuid NOT NULL REFERENCES "user"(id),
42
+ organization_id uuid REFERENCES organization(id),
43
+ status text NOT NULL DEFAULT 'active'
44
+ CHECK (status IN ('active', 'invited', 'revoked')),
45
+ invited_by uuid REFERENCES "user"(id),
46
+ created_at timestamptz NOT NULL DEFAULT now(),
47
+ updated_at timestamptz NOT NULL DEFAULT now(),
48
+ deleted_at timestamptz
49
+ );
50
+ CREATE UNIQUE INDEX membership_tenant_user_uq
51
+ ON membership (tenant_id, user_id) WHERE deleted_at IS NULL;
52
+ CREATE INDEX membership_user_idx ON membership (user_id);
53
+
54
+ -- ── user_role (atribuição de papel a um usuário dentro de um tenant) ─────────
55
+ CREATE TABLE user_role (
56
+ id uuid PRIMARY KEY,
57
+ tenant_id uuid NOT NULL REFERENCES tenant(id),
58
+ user_id uuid NOT NULL REFERENCES "user"(id),
59
+ role_id uuid NOT NULL REFERENCES role(id),
60
+ organization_id uuid REFERENCES organization(id),
61
+ client_id uuid,
62
+ created_at timestamptz NOT NULL DEFAULT now(),
63
+ deleted_at timestamptz
64
+ );
65
+ CREATE UNIQUE INDEX user_role_uq
66
+ ON user_role (tenant_id, user_id, role_id)
67
+ WHERE deleted_at IS NULL AND organization_id IS NULL AND client_id IS NULL;
68
+ CREATE INDEX user_role_tenant_user_idx ON user_role (tenant_id, user_id);
@@ -0,0 +1,35 @@
1
+ -- 0004 — Directory/CRM: `client` (o cliente do escritório, PF/PJ).
2
+ -- Entidade de NEGÓCIO tenant-scoped — é a "estrela" dos testes de isolamento RLS.
3
+ -- (03-data-model §6.2). Detalhes PF/PJ ficam para fases seguintes.
4
+
5
+ CREATE TABLE client (
6
+ id uuid PRIMARY KEY,
7
+ tenant_id uuid NOT NULL REFERENCES tenant(id),
8
+ client_type text NOT NULL DEFAULT 'pj' CHECK (client_type IN ('pf', 'pj')),
9
+ code text,
10
+ display_name text NOT NULL,
11
+ legal_name text,
12
+ document_number text, -- CPF/CNPJ normalizado (só dígitos)
13
+ status text NOT NULL DEFAULT 'active'
14
+ CHECK (status IN ('active', 'inactive', 'prospect', 'archived')),
15
+ email citext,
16
+ phone text,
17
+ onboarded_at date,
18
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
19
+ created_at timestamptz NOT NULL DEFAULT now(),
20
+ updated_at timestamptz NOT NULL DEFAULT now(),
21
+ deleted_at timestamptz,
22
+ created_by uuid REFERENCES "user"(id),
23
+ updated_by uuid REFERENCES "user"(id),
24
+ version int NOT NULL DEFAULT 1,
25
+ -- Permite FK composta (tenant_id, id) a partir das verticais: garante, no
26
+ -- nível referencial, que um registro de outro tenant NUNCA seja referenciado
27
+ -- (FKs ignoram RLS, então a integridade same-tenant precisa ser estrutural —
28
+ -- 03-data-model §5).
29
+ CONSTRAINT client_tenant_id_uq UNIQUE (tenant_id, id)
30
+ );
31
+ -- Documento único por tenant (índice único parcial permite reusar após soft-delete).
32
+ CREATE UNIQUE INDEX client_tenant_document_uq
33
+ ON client (tenant_id, document_number)
34
+ WHERE deleted_at IS NULL AND document_number IS NOT NULL;
35
+ CREATE INDEX client_tenant_status_idx ON client (tenant_id, status);
@@ -0,0 +1,35 @@
1
+ -- 0005 — Auditoria append-only (03-data-model §6.9 / 06-audit.md).
2
+ -- Nunca sofre UPDATE/DELETE: um trigger bloqueia mutações e o papel de app
3
+ -- não recebe privilégio de UPDATE/DELETE (defesa em profundidade).
4
+
5
+ CREATE TABLE audit_log (
6
+ id uuid PRIMARY KEY, -- v7: ordenável no tempo
7
+ tenant_id uuid NOT NULL REFERENCES tenant(id),
8
+ actor_type text NOT NULL DEFAULT 'user'
9
+ CHECK (actor_type IN ('user', 'system', 'automation', 'ai_agent', 'api_key')),
10
+ actor_id uuid REFERENCES "user"(id),
11
+ action text NOT NULL, -- create|update|delete|login|select_tenant|export…
12
+ resource_type text NOT NULL,
13
+ resource_id uuid,
14
+ result text NOT NULL DEFAULT 'success'
15
+ CHECK (result IN ('success', 'failure', 'denied')),
16
+ diff jsonb, -- antes/depois (segredos redigidos)
17
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
18
+ ip inet,
19
+ user_agent text,
20
+ occurred_at timestamptz NOT NULL DEFAULT now()
21
+ );
22
+ CREATE INDEX audit_log_tenant_time_idx ON audit_log (tenant_id, occurred_at DESC);
23
+ CREATE INDEX audit_log_resource_idx ON audit_log (tenant_id, resource_type, resource_id);
24
+
25
+ -- Guarda de imutabilidade: proíbe UPDATE/DELETE em qualquer papel.
26
+ CREATE OR REPLACE FUNCTION audit_log_block_mutation()
27
+ RETURNS trigger LANGUAGE plpgsql AS $$
28
+ BEGIN
29
+ RAISE EXCEPTION 'audit_log é append-only: % não é permitido', TG_OP;
30
+ END;
31
+ $$;
32
+
33
+ CREATE TRIGGER audit_log_no_update
34
+ BEFORE UPDATE OR DELETE ON audit_log
35
+ FOR EACH ROW EXECUTE FUNCTION audit_log_block_mutation();
@@ -0,0 +1,69 @@
1
+ -- 0006 — Row-Level Security (RLS) em todas as tabelas tenant-scoped.
2
+ --
3
+ -- Mecânica (01-multi-tenancy §2 / 05-security §7.1):
4
+ -- • A GUC `app.current_tenant` é setada por transação via set_config(...,true)
5
+ -- (equivale a SET LOCAL) — ver src/db/context.ts. Nunca vem do cliente.
6
+ -- • `NULLIF(current_setting('app.current_tenant', true), '')::uuid` devolve
7
+ -- NULL quando a GUC não está setada OU voltou a '' após um SET LOCAL numa
8
+ -- conexão reusada do pool (GUCs customizadas revertem para '' , não p/ unset).
9
+ -- Com NULL → `tenant_id = NULL` é falso → NENHUMA linha retorna nem pode ser
10
+ -- inserida: comportamento FAIL-CLOSED por construção (sem erro de cast '').
11
+ -- • ENABLE + FORCE: FORCE sujeita ATÉ O DONO da tabela à RLS (o papel de
12
+ -- migração/seed também precisa do contexto correto para escrever).
13
+ -- • O papel de aplicação (baruk_app) é NOSUPERUSER e NOBYPASSRLS (ver runner).
14
+ --
15
+ -- Entidades globais (tenant, "user", user_credential, user_session, permission)
16
+ -- NÃO recebem RLS por design (SSO/catálogo — 03-data-model §2.2). O escopo de
17
+ -- quais tenants um usuário enxerga é aplicado por JOIN em `membership` (que TEM RLS).
18
+
19
+ -- ── organization ────────────────────────────────────────────────────────────
20
+ ALTER TABLE organization ENABLE ROW LEVEL SECURITY;
21
+ ALTER TABLE organization FORCE ROW LEVEL SECURITY;
22
+ CREATE POLICY organization_tenant_isolation ON organization
23
+ USING (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid)
24
+ WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid);
25
+
26
+ -- ── membership (com cláusula "self": o usuário vê os próprios vínculos para o
27
+ -- seletor de tenant do SSO, antes de escolher um tenant ativo) ─────────────
28
+ ALTER TABLE membership ENABLE ROW LEVEL SECURITY;
29
+ ALTER TABLE membership FORCE ROW LEVEL SECURITY;
30
+ CREATE POLICY membership_isolation ON membership
31
+ USING (
32
+ tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid
33
+ OR user_id = NULLIF(current_setting('app.current_user', true), '')::uuid
34
+ )
35
+ WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid);
36
+
37
+ -- ── user_role ───────────────────────────────────────────────────────────────
38
+ ALTER TABLE user_role ENABLE ROW LEVEL SECURITY;
39
+ ALTER TABLE user_role FORCE ROW LEVEL SECURITY;
40
+ CREATE POLICY user_role_tenant_isolation ON user_role
41
+ USING (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid)
42
+ WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid);
43
+
44
+ -- ── client (entidade de negócio — foco dos testes de isolamento) ────────────
45
+ ALTER TABLE client ENABLE ROW LEVEL SECURITY;
46
+ ALTER TABLE client FORCE ROW LEVEL SECURITY;
47
+ CREATE POLICY client_tenant_isolation ON client
48
+ USING (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid)
49
+ WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid);
50
+
51
+ -- ── audit_log (tenant-scoped; append-only garantido pelo trigger de 0005) ────
52
+ ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY;
53
+ ALTER TABLE audit_log FORCE ROW LEVEL SECURITY;
54
+ CREATE POLICY audit_log_tenant_isolation ON audit_log
55
+ USING (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid)
56
+ WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid);
57
+
58
+ -- ── role / role_permission (template global tenant_id IS NULL, ou do tenant) ─
59
+ ALTER TABLE role ENABLE ROW LEVEL SECURITY;
60
+ ALTER TABLE role FORCE ROW LEVEL SECURITY;
61
+ CREATE POLICY role_visibility ON role
62
+ USING (tenant_id IS NULL OR tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid)
63
+ WITH CHECK (tenant_id IS NULL OR tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid);
64
+
65
+ ALTER TABLE role_permission ENABLE ROW LEVEL SECURITY;
66
+ ALTER TABLE role_permission FORCE ROW LEVEL SECURITY;
67
+ CREATE POLICY role_permission_visibility ON role_permission
68
+ USING (tenant_id IS NULL OR tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid)
69
+ WITH CHECK (tenant_id IS NULL OR tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid);
@@ -0,0 +1,103 @@
1
+ -- 0007 — Ciclo operacional escritório↔cliente: solicitações, comentários,
2
+ -- notificações in-app e acesso de portal do cliente. Todas tenant-scoped com
3
+ -- RLS FORCE + FK composta (tenant_id, id) para impedir vínculo cross-tenant.
4
+
5
+ -- ── service_request (solicitação) ───────────────────────────────────────────
6
+ CREATE TABLE service_request (
7
+ id uuid PRIMARY KEY,
8
+ tenant_id uuid NOT NULL REFERENCES tenant(id),
9
+ client_id uuid NOT NULL,
10
+ title text NOT NULL,
11
+ description text,
12
+ category text NOT NULL DEFAULT 'geral',
13
+ competence_label text, -- ex.: '2026-07' (competência simplificada nesta fase)
14
+ priority text NOT NULL DEFAULT 'medium'
15
+ CHECK (priority IN ('low', 'medium', 'high', 'urgent')),
16
+ status text NOT NULL DEFAULT 'draft'
17
+ CHECK (status IN ('draft','sent','viewed','awaiting_client',
18
+ 'answered','in_review','done','canceled','overdue')),
19
+ due_date date,
20
+ responsible_user_id uuid REFERENCES "user"(id),
21
+ created_by uuid REFERENCES "user"(id),
22
+ created_at timestamptz NOT NULL DEFAULT now(),
23
+ updated_at timestamptz NOT NULL DEFAULT now(),
24
+ deleted_at timestamptz,
25
+ version int NOT NULL DEFAULT 1,
26
+ CONSTRAINT service_request_tenant_id_uq UNIQUE (tenant_id, id),
27
+ FOREIGN KEY (tenant_id, client_id) REFERENCES client (tenant_id, id)
28
+ );
29
+ CREATE INDEX service_request_tenant_status_idx ON service_request (tenant_id, status, due_date);
30
+ CREATE INDEX service_request_tenant_client_idx ON service_request (tenant_id, client_id);
31
+
32
+ -- ── request_comment (thread da solicitação) ─────────────────────────────────
33
+ CREATE TABLE request_comment (
34
+ id uuid PRIMARY KEY,
35
+ tenant_id uuid NOT NULL REFERENCES tenant(id),
36
+ request_id uuid NOT NULL,
37
+ author_user_id uuid REFERENCES "user"(id),
38
+ author_kind text NOT NULL DEFAULT 'staff'
39
+ CHECK (author_kind IN ('staff', 'client', 'system')),
40
+ body text NOT NULL,
41
+ created_at timestamptz NOT NULL DEFAULT now(),
42
+ FOREIGN KEY (tenant_id, request_id) REFERENCES service_request (tenant_id, id)
43
+ );
44
+ CREATE INDEX request_comment_thread_idx ON request_comment (tenant_id, request_id, created_at);
45
+
46
+ -- ── notification (in-app) ───────────────────────────────────────────────────
47
+ CREATE TABLE notification (
48
+ id uuid PRIMARY KEY,
49
+ tenant_id uuid NOT NULL REFERENCES tenant(id),
50
+ user_id uuid NOT NULL REFERENCES "user"(id),
51
+ type text NOT NULL DEFAULT 'info'
52
+ CHECK (type IN ('info', 'warning', 'action_required')),
53
+ title text NOT NULL,
54
+ body text,
55
+ link_ref jsonb,
56
+ read_at timestamptz,
57
+ delivered_channels jsonb NOT NULL DEFAULT '["in_app"]'::jsonb,
58
+ created_at timestamptz NOT NULL DEFAULT now()
59
+ );
60
+ CREATE INDEX notification_user_idx ON notification (tenant_id, user_id, created_at DESC);
61
+
62
+ -- ── client_portal_access (liga um usuário de portal a um client específico) ──
63
+ CREATE TABLE client_portal_access (
64
+ id uuid PRIMARY KEY,
65
+ tenant_id uuid NOT NULL REFERENCES tenant(id),
66
+ user_id uuid NOT NULL REFERENCES "user"(id),
67
+ client_id uuid NOT NULL,
68
+ status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked')),
69
+ created_at timestamptz NOT NULL DEFAULT now(),
70
+ FOREIGN KEY (tenant_id, client_id) REFERENCES client (tenant_id, id)
71
+ );
72
+ CREATE UNIQUE INDEX client_portal_access_uq ON client_portal_access (tenant_id, user_id, client_id);
73
+ CREATE INDEX client_portal_access_user_idx ON client_portal_access (user_id);
74
+
75
+ -- ── RLS ─────────────────────────────────────────────────────────────────────
76
+ ALTER TABLE service_request ENABLE ROW LEVEL SECURITY;
77
+ ALTER TABLE service_request FORCE ROW LEVEL SECURITY;
78
+ CREATE POLICY service_request_tenant_isolation ON service_request
79
+ USING (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid)
80
+ WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid);
81
+
82
+ ALTER TABLE request_comment ENABLE ROW LEVEL SECURITY;
83
+ ALTER TABLE request_comment FORCE ROW LEVEL SECURITY;
84
+ CREATE POLICY request_comment_tenant_isolation ON request_comment
85
+ USING (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid)
86
+ WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid);
87
+
88
+ ALTER TABLE notification ENABLE ROW LEVEL SECURITY;
89
+ ALTER TABLE notification FORCE ROW LEVEL SECURITY;
90
+ CREATE POLICY notification_tenant_isolation ON notification
91
+ USING (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid)
92
+ WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid);
93
+
94
+ -- Portal access: cláusula "self" (como membership) para o usuário de portal
95
+ -- resolver o próprio vínculo antes de selecionar tenant.
96
+ ALTER TABLE client_portal_access ENABLE ROW LEVEL SECURITY;
97
+ ALTER TABLE client_portal_access FORCE ROW LEVEL SECURITY;
98
+ CREATE POLICY client_portal_access_isolation ON client_portal_access
99
+ USING (
100
+ tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid
101
+ OR user_id = NULLIF(current_setting('app.current_user', true), '')::uuid
102
+ )
103
+ WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid);
@@ -0,0 +1,5 @@
1
+ -- 0008 — Adiciona `client.deleted_by` (coluna de autoria de soft-delete prevista
2
+ -- em base_columns/03-data-model §2.1, omitida na 0004). Forward-only: migrations
3
+ -- aplicadas são imutáveis, então corrigimos com uma nova migração.
4
+
5
+ ALTER TABLE client ADD COLUMN IF NOT EXISTS deleted_by uuid REFERENCES "user"(id);
@@ -0,0 +1,50 @@
1
+ -- 0009 — RLS por CLIENTE (defesa no banco para o portal do cliente).
2
+ --
3
+ -- Além do isolamento por tenant, entidades client-scoped passam a respeitar a
4
+ -- GUC `app.current_client`: quando setada (sessão de portal), o banco só revela
5
+ -- linhas daquele client — mesmo que a aplicação esquecesse o filtro. Quando NÃO
6
+ -- setada (staff), o comportamento é o de antes (todos os clientes do tenant).
7
+ --
8
+ -- Fail-closed preservado: NULLIF(...,'') → NULL quando não setada.
9
+
10
+ -- service_request: escopo por client quando app.current_client presente.
11
+ DROP POLICY IF EXISTS service_request_tenant_isolation ON service_request;
12
+ CREATE POLICY service_request_tenant_isolation ON service_request
13
+ USING (
14
+ tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid
15
+ AND (
16
+ NULLIF(current_setting('app.current_client', true), '') IS NULL
17
+ OR client_id = NULLIF(current_setting('app.current_client', true), '')::uuid
18
+ )
19
+ )
20
+ WITH CHECK (
21
+ tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid
22
+ AND (
23
+ NULLIF(current_setting('app.current_client', true), '') IS NULL
24
+ OR client_id = NULLIF(current_setting('app.current_client', true), '')::uuid
25
+ )
26
+ );
27
+
28
+ -- request_comment: escopo por client via subconsulta ao request dono.
29
+ DROP POLICY IF EXISTS request_comment_tenant_isolation ON request_comment;
30
+ CREATE POLICY request_comment_tenant_isolation ON request_comment
31
+ USING (
32
+ tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid
33
+ AND (
34
+ NULLIF(current_setting('app.current_client', true), '') IS NULL
35
+ OR request_id IN (
36
+ SELECT id FROM service_request
37
+ WHERE client_id = NULLIF(current_setting('app.current_client', true), '')::uuid
38
+ )
39
+ )
40
+ )
41
+ WITH CHECK (
42
+ tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid
43
+ AND (
44
+ NULLIF(current_setting('app.current_client', true), '') IS NULL
45
+ OR request_id IN (
46
+ SELECT id FROM service_request
47
+ WHERE client_id = NULLIF(current_setting('app.current_client', true), '')::uuid
48
+ )
49
+ )
50
+ );
@@ -0,0 +1,43 @@
1
+ -- 0010 — Catálogo de produtos + entitlements por tenant (IAM multiproduto).
2
+ --
3
+ -- Princípio (ADR 0005): membership no tenant NÃO concede acesso a todos os
4
+ -- produtos Baruk. Acesso = identidade + membership + ENTITLEMENT do produto no
5
+ -- tenant + papel. O Hub bloqueia produto sem entitlement.
6
+
7
+ -- Catálogo global de produtos (como `permission`: global, read-only p/ app).
8
+ CREATE TABLE product (
9
+ id uuid PRIMARY KEY,
10
+ key text NOT NULL, -- contabil | rep | ind | varejo
11
+ name text NOT NULL,
12
+ kind text NOT NULL DEFAULT 'internal' CHECK (kind IN ('internal', 'external')),
13
+ base_url text, -- destino p/ produtos externos (Rep/Ind)
14
+ is_active boolean NOT NULL DEFAULT true,
15
+ created_at timestamptz NOT NULL DEFAULT now()
16
+ );
17
+ CREATE UNIQUE INDEX product_key_uq ON product (key);
18
+
19
+ -- Entitlement: quais produtos o tenant contratou/acessa.
20
+ CREATE TABLE tenant_product (
21
+ id uuid PRIMARY KEY,
22
+ tenant_id uuid NOT NULL REFERENCES tenant(id),
23
+ product_id uuid NOT NULL REFERENCES product(id),
24
+ status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'suspended')),
25
+ enabled_at timestamptz NOT NULL DEFAULT now()
26
+ );
27
+ CREATE UNIQUE INDEX tenant_product_uq ON tenant_product (tenant_id, product_id);
28
+ CREATE INDEX tenant_product_tenant_idx ON tenant_product (tenant_id);
29
+
30
+ -- RLS: como membership, com cláusula "self" para o Hub resolver os produtos do
31
+ -- usuário ANTES de escolher tenant/produto ativo.
32
+ ALTER TABLE tenant_product ENABLE ROW LEVEL SECURITY;
33
+ ALTER TABLE tenant_product FORCE ROW LEVEL SECURITY;
34
+ CREATE POLICY tenant_product_isolation ON tenant_product
35
+ USING (
36
+ tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid
37
+ OR tenant_id IN (
38
+ SELECT tenant_id FROM membership
39
+ WHERE user_id = NULLIF(current_setting('app.current_user', true), '')::uuid
40
+ AND deleted_at IS NULL
41
+ )
42
+ )
43
+ WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid);
@@ -0,0 +1,18 @@
1
+ -- 0011 — Recuperação de senha (token de uso único). Global (segue "user").
2
+ -- Guardamos apenas o HASH do token (nunca o token em claro), com expiração e
3
+ -- uso único. Proteção contra enumeração é feita na camada de aplicação
4
+ -- (resposta genérica).
5
+
6
+ CREATE TABLE password_reset (
7
+ id uuid PRIMARY KEY,
8
+ user_id uuid NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
9
+ token_hash text NOT NULL,
10
+ expires_at timestamptz NOT NULL,
11
+ used_at timestamptz,
12
+ created_at timestamptz NOT NULL DEFAULT now()
13
+ );
14
+ CREATE UNIQUE INDEX password_reset_token_uq ON password_reset (token_hash);
15
+ CREATE INDEX password_reset_user_idx ON password_reset (user_id);
16
+
17
+ -- Marca de "trocar senha no primeiro acesso" para contas provisionadas.
18
+ ALTER TABLE user_credential ADD COLUMN IF NOT EXISTS must_change_password boolean NOT NULL DEFAULT false;