@kontrolia/db 2.0.0 → 2.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.
- package/README.md +1 -1
- package/dist/grant-platform-admin.d.ts +21 -0
- package/dist/grant-platform-admin.d.ts.map +1 -0
- package/dist/grant-platform-admin.js +21 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/migrations/0021_org_owner_delete.sql +31 -0
- package/migrations/0022_application_homepage_url.sql +17 -0
- package/migrations/0023_fix_audit_triggers_on_org_delete.sql +60 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,4 +15,4 @@ DATABASE_URL="postgres://postgres:postgres@localhost:5432/postgres" pnpm --filte
|
|
|
15
15
|
## Activar el hook
|
|
16
16
|
|
|
17
17
|
- **Self-hosted**: variable de entorno de GoTrue `GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI=pg-functions://postgres/kontrolia_auth/custom_access_token_hook` (ver `docker/docker-compose.yml`).
|
|
18
|
-
- **Supabase Cloud**:
|
|
18
|
+
- **Supabase Cloud**: `npx create-kontrolia-auth` (o `deploy`) lo automatiza vía la Management API de Supabase, pidiendo un Personal Access Token de la cuenta. A mano: Dashboard → Authentication → Hooks → Custom Access Token → seleccionar `kontrolia_auth.custom_access_token_hook`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface GrantPlatformAdminOptions {
|
|
2
|
+
/** Postgres connection string — same trust model as migrate(): whoever has
|
|
3
|
+
* this can already do anything in the database, so this bypasses the
|
|
4
|
+
* app's own is_platform_admin authorization check entirely. That's the
|
|
5
|
+
* point — it's the recovery path for when there are zero platform admins
|
|
6
|
+
* (e.g. bootstrap_first_platform_admin() didn't fire because auth.users
|
|
7
|
+
* already had rows from something else sharing the same Supabase
|
|
8
|
+
* project), which the admin-panel UI can never self-serve since its API
|
|
9
|
+
* requires an existing platform admin to call it. */
|
|
10
|
+
connectionString: string;
|
|
11
|
+
email: string;
|
|
12
|
+
}
|
|
13
|
+
export interface GrantPlatformAdminResult {
|
|
14
|
+
userId: string;
|
|
15
|
+
email: string;
|
|
16
|
+
/** True if this user already was a platform admin — grant is idempotent. */
|
|
17
|
+
alreadyGranted: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** Grants platform admin directly via SQL — no HTTP layer, no JWT required. */
|
|
20
|
+
export declare function grantPlatformAdmin({ connectionString, email }: GrantPlatformAdminOptions): Promise<GrantPlatformAdminResult>;
|
|
21
|
+
//# sourceMappingURL=grant-platform-admin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grant-platform-admin.d.ts","sourceRoot":"","sources":["../src/grant-platform-admin.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,yBAAyB;IACxC;;;;;;;yDAOqD;IACrD,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,+EAA+E;AAC/E,wBAAsB,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,KAAK,EAAE,EAAE,yBAAyB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CA0BlI"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Client } from "pg";
|
|
2
|
+
/** Grants platform admin directly via SQL — no HTTP layer, no JWT required. */
|
|
3
|
+
export async function grantPlatformAdmin({ connectionString, email }) {
|
|
4
|
+
const client = new Client({ connectionString });
|
|
5
|
+
await client.connect();
|
|
6
|
+
try {
|
|
7
|
+
const { rows: users } = await client.query("select id, email from auth.users where lower(email) = lower($1) limit 1", [email]);
|
|
8
|
+
const user = users[0];
|
|
9
|
+
if (!user) {
|
|
10
|
+
throw new Error(`No hay ningún usuario registrado con el correo "${email}". Regístrate primero desde auth-server.`);
|
|
11
|
+
}
|
|
12
|
+
const { rows: inserted } = await client.query(`insert into kontrolia_auth.platform_admins (user_id)
|
|
13
|
+
values ($1)
|
|
14
|
+
on conflict (user_id) do nothing
|
|
15
|
+
returning user_id`, [user.id]);
|
|
16
|
+
return { userId: user.id, email: user.email, alreadyGranted: inserted.length === 0 };
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
await client.end();
|
|
20
|
+
}
|
|
21
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { migrate, type MigrateOptions } from "./migrate.js";
|
|
2
|
+
export { grantPlatformAdmin, type GrantPlatformAdminOptions, type GrantPlatformAdminResult } from "./grant-platform-admin.js";
|
|
2
3
|
export { registerApplication, type RegisterApplicationOptions, type RegisteredApplication, type PermissionInput, } from "./register-application.js";
|
|
3
4
|
export { generateApplicationApiKey, hashApplicationApiKey } from "./api-key.js";
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EACL,mBAAmB,EACnB,KAAK,0BAA0B,EAC/B,KAAK,qBAAqB,EAC1B,KAAK,eAAe,GACrB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,KAAK,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC9H,OAAO,EACL,mBAAmB,EACnB,KAAK,0BAA0B,EAC/B,KAAK,qBAAqB,EAC1B,KAAK,eAAe,GACrB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- Deleting an organization is irreversible and cascades through every
|
|
2
|
+
-- membership/role/invitation/audit-log row for it (see the FKs in
|
|
3
|
+
-- 0002/0004/0005/0006_*.sql) — permission_denied by default today, since no
|
|
4
|
+
-- DELETE policy exists on kontrolia_auth.organizations at all. Reserved for
|
|
5
|
+
-- the org's Owner specifically, not any Admin: is_org_admin() (0009/0020)
|
|
6
|
+
-- deliberately treats Owner and Admin the same for day-to-day management,
|
|
7
|
+
-- but wiping the whole organization (including its audit trail) is a
|
|
8
|
+
-- different level of consequence.
|
|
9
|
+
create or replace function kontrolia_auth.is_org_owner(org_id uuid)
|
|
10
|
+
returns boolean
|
|
11
|
+
language sql
|
|
12
|
+
stable
|
|
13
|
+
security definer
|
|
14
|
+
set search_path = ''
|
|
15
|
+
as $$
|
|
16
|
+
select exists (
|
|
17
|
+
select 1
|
|
18
|
+
from kontrolia_auth.memberships m
|
|
19
|
+
join kontrolia_auth.membership_roles mr on mr.membership_id = m.id
|
|
20
|
+
join kontrolia_auth.roles r on r.id = mr.role_id
|
|
21
|
+
where m.organization_id = org_id
|
|
22
|
+
and m.user_id = auth.uid()
|
|
23
|
+
and m.status = 'active'
|
|
24
|
+
and r.slug = 'owner'
|
|
25
|
+
);
|
|
26
|
+
$$;
|
|
27
|
+
|
|
28
|
+
grant execute on function kontrolia_auth.is_org_owner(uuid) to authenticated;
|
|
29
|
+
|
|
30
|
+
create policy "org owners can delete their organization" on kontrolia_auth.organizations
|
|
31
|
+
for delete using (kontrolia_auth.is_org_owner(id));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- Nothing in the schema records where a registered application is actually
|
|
2
|
+
-- reachable for end users — redirect_urls is OAuth callback URIs, not a
|
|
3
|
+
-- home page. Needed for an "apps you have access to" launcher (auth-server)
|
|
4
|
+
-- to have somewhere to link. Nullable: not always known at registration
|
|
5
|
+
-- time via the CLI wizard.
|
|
6
|
+
alter table kontrolia_auth.applications add column homepage_url text;
|
|
7
|
+
|
|
8
|
+
comment on column kontrolia_auth.applications.homepage_url is
|
|
9
|
+
'Where this application is reachable for end users (its actual URL, not an OAuth redirect URI) — what an "apps you can access" launcher links to.';
|
|
10
|
+
|
|
11
|
+
-- No UPDATE policy existed on applications at all before this (0018 only
|
|
12
|
+
-- added a SELECT policy widening catalog browsing). Scoped to the
|
|
13
|
+
-- registering org's admins, not every org that merely enabled the app.
|
|
14
|
+
create policy "owning org admins can update their application" on kontrolia_auth.applications
|
|
15
|
+
for update
|
|
16
|
+
using (owner_organization_id is not null and kontrolia_auth.is_org_admin(owner_organization_id))
|
|
17
|
+
with check (owner_organization_id is not null and kontrolia_auth.is_org_admin(owner_organization_id));
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
-- Deleting an organization (0021) cascades to memberships, which cascades
|
|
2
|
+
-- to membership_roles — and the AFTER DELETE audit triggers on both
|
|
3
|
+
-- (0013_audit_log_triggers.sql) tried to INSERT a new audit_logs row
|
|
4
|
+
-- referencing that same organization_id mid-cascade, violating
|
|
5
|
+
-- audit_logs_organization_id_fkey (audit_logs itself also cascades on
|
|
6
|
+
-- organization delete, so by the time these triggers fire the parent
|
|
7
|
+
-- organization row is gone). There's no point logging "membership removed"
|
|
8
|
+
-- for a membership that's disappearing because its whole organization —
|
|
9
|
+
-- audit trail included — is being wiped a moment later anyway. Guard both
|
|
10
|
+
-- triggers: only log when the organization is still around, i.e. this is a
|
|
11
|
+
-- real standalone removal, not a side effect of deleting the org itself.
|
|
12
|
+
create or replace function kontrolia_auth.log_membership_change()
|
|
13
|
+
returns trigger
|
|
14
|
+
language plpgsql
|
|
15
|
+
security definer
|
|
16
|
+
set search_path = ''
|
|
17
|
+
as $$
|
|
18
|
+
begin
|
|
19
|
+
if tg_op = 'INSERT' then
|
|
20
|
+
insert into kontrolia_auth.audit_logs (organization_id, actor_user_id, action, target_type, target_id, metadata)
|
|
21
|
+
values (new.organization_id, coalesce(auth.uid(), new.user_id), 'membership.created', 'membership', new.id::text, jsonb_build_object('user_id', new.user_id, 'status', new.status));
|
|
22
|
+
return new;
|
|
23
|
+
else
|
|
24
|
+
if exists (select 1 from kontrolia_auth.organizations where id = old.organization_id) then
|
|
25
|
+
insert into kontrolia_auth.audit_logs (organization_id, actor_user_id, action, target_type, target_id, metadata)
|
|
26
|
+
values (old.organization_id, auth.uid(), 'membership.removed', 'membership', old.id::text, jsonb_build_object('user_id', old.user_id));
|
|
27
|
+
end if;
|
|
28
|
+
return old;
|
|
29
|
+
end if;
|
|
30
|
+
end;
|
|
31
|
+
$$;
|
|
32
|
+
|
|
33
|
+
create or replace function kontrolia_auth.log_role_assignment_change()
|
|
34
|
+
returns trigger
|
|
35
|
+
language plpgsql
|
|
36
|
+
security definer
|
|
37
|
+
set search_path = ''
|
|
38
|
+
as $$
|
|
39
|
+
declare
|
|
40
|
+
target_membership record;
|
|
41
|
+
begin
|
|
42
|
+
select organization_id, user_id into target_membership
|
|
43
|
+
from kontrolia_auth.memberships
|
|
44
|
+
where id = coalesce(new.membership_id, old.membership_id);
|
|
45
|
+
|
|
46
|
+
if not exists (select 1 from kontrolia_auth.organizations where id = target_membership.organization_id) then
|
|
47
|
+
if tg_op = 'INSERT' then return new; else return old; end if;
|
|
48
|
+
end if;
|
|
49
|
+
|
|
50
|
+
if tg_op = 'INSERT' then
|
|
51
|
+
insert into kontrolia_auth.audit_logs (organization_id, actor_user_id, action, target_type, target_id, metadata)
|
|
52
|
+
values (target_membership.organization_id, coalesce(auth.uid(), target_membership.user_id), 'role.assigned', 'membership_role', new.membership_id::text, jsonb_build_object('role_id', new.role_id));
|
|
53
|
+
return new;
|
|
54
|
+
else
|
|
55
|
+
insert into kontrolia_auth.audit_logs (organization_id, actor_user_id, action, target_type, target_id, metadata)
|
|
56
|
+
values (target_membership.organization_id, auth.uid(), 'role.unassigned', 'membership_role', old.membership_id::text, jsonb_build_object('role_id', old.role_id));
|
|
57
|
+
return old;
|
|
58
|
+
end if;
|
|
59
|
+
end;
|
|
60
|
+
$$;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontrolia/db",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "KontrolIA Auth database layer: SQL migrations for the `kontrolia` schema (organizations, RBAC, Custom Access Token Hook) and a connection-string-agnostic migration runner.",
|
|
6
6
|
"keywords": [
|