feat: implement tenant isolation with PostgreSQL RLS
- users + tenant_memberships tables (tenant-scoped)
- RLS policy on tenant_memberships using set_config('app.current_tenant_id', ...)
- withTenantContext() helper for transaction-scoped tenant context
- separate non-superuser app role (b2bcall_app): the default Docker postgres
user is SUPERUSER and always bypasses RLS even with FORCE, so the app must
never connect through the migration/owner role. Documented in
docs/TENANT_ISOLATION.md.
- automated isolation test proving tenant A never sees tenant B's data
This commit is contained in:
@@ -1,2 +1,5 @@
|
|||||||
FREESWITCH_PAT=
|
FREESWITCH_PAT=
|
||||||
DATABASE_URL=postgresql://user:password@localhost:5432/b2bcall?schema=public
|
DATABASE_URL=postgresql://user:password@localhost:5432/b2bcall?schema=public
|
||||||
|
POSTGRES_APP_USER=
|
||||||
|
POSTGRES_APP_PASSWORD=
|
||||||
|
APP_DATABASE_URL=postgresql://user:password@localhost:5432/b2bcall?schema=public
|
||||||
|
|||||||
10
TODO.md
10
TODO.md
@@ -19,8 +19,14 @@
|
|||||||
- [x] Tabela `tenants` criada via migration (seção 29 do agente.md)
|
- [x] Tabela `tenants` criada via migration (seção 29 do agente.md)
|
||||||
|
|
||||||
## PHASE 03 — Tenant Isolation
|
## PHASE 03 — Tenant Isolation
|
||||||
- [ ] Tabela `tenants` + RLS
|
- [x] Tabelas `users` + `tenant_memberships` (tenant-scoped)
|
||||||
- [ ] Tenant context em transação PostgreSQL
|
- [x] RLS (`ENABLE`/`FORCE ROW LEVEL SECURITY` + policy) em `tenant_memberships`
|
||||||
|
- [x] Tenant context via `set_config('app.current_tenant_id', ..., true)` (transaction-local)
|
||||||
|
- [x] Helper `withTenantContext()` em `packages/database`
|
||||||
|
- [x] Role de banco separado para runtime (`b2bcall_app`, sem SUPERUSER/BYPASSRLS) —
|
||||||
|
achado crítico: o role padrão do Docker Postgres é SUPERUSER e SEMPRE ignora RLS,
|
||||||
|
até com FORCE. Ver `docs/TENANT_ISOLATION.md`.
|
||||||
|
- [x] Teste automatizado de isolamento (`pnpm --filter @b2bcall/database run test:isolation`)
|
||||||
|
|
||||||
## PHASE 04 — Authentication / RBAC
|
## PHASE 04 — Authentication / RBAC
|
||||||
- [ ] Login (Argon2id), access/refresh tokens
|
- [ ] Login (Argon2id), access/refresh tokens
|
||||||
|
|||||||
94
docs/TENANT_ISOLATION.md
Normal file
94
docs/TENANT_ISOLATION.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Isolamento Multi-Tenant
|
||||||
|
|
||||||
|
## Mecanismo
|
||||||
|
|
||||||
|
Tabelas tenant-scoped usam PostgreSQL Row Level Security (RLS). A aplicação define,
|
||||||
|
dentro de cada transação, o tenant atual via:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT set_config('app.current_tenant_id', '<uuid>', true)
|
||||||
|
```
|
||||||
|
|
||||||
|
O terceiro argumento (`true` = `is_local`) faz o valor durar só a transação atual —
|
||||||
|
seguro sob connection pooling, já que a mesma conexão física é reaproveitada por
|
||||||
|
requisições de tenants diferentes.
|
||||||
|
|
||||||
|
Toda tabela tenant-scoped repete o mesmo padrão de policy:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE "<tabela>" ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE "<tabela>" FORCE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
CREATE POLICY "tenant_isolation" ON "<tabela>"
|
||||||
|
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
|
||||||
|
```
|
||||||
|
|
||||||
|
Sem `app.current_tenant_id` definido, `current_setting(..., true)` retorna `NULL`, a
|
||||||
|
comparação vira `NULL`/falsa, e a query não retorna nenhuma linha — **deny-by-default**.
|
||||||
|
|
||||||
|
Em código (`packages/database`), use sempre o helper `withTenantContext`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { getPrismaClient, withTenantContext } from "@b2bcall/database";
|
||||||
|
|
||||||
|
const prisma = getPrismaClient();
|
||||||
|
const memberships = await withTenantContext(prisma, tenantId, (tx) =>
|
||||||
|
tx.tenantMembership.findMany(),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Nunca faça `prisma.tenantMembership.findMany()` direto (fora de `withTenantContext`)
|
||||||
|
em código de aplicação — isso corre em cima da conexão sem contexto de tenant e,
|
||||||
|
com a policy acima, simplesmente não retorna nada (falha "segura", mas ainda assim
|
||||||
|
é um bug: use sempre o helper).
|
||||||
|
|
||||||
|
## Achado crítico: role de conexão importa mais que a policy
|
||||||
|
|
||||||
|
A imagem oficial do Postgres no Docker sempre cria o usuário inicial
|
||||||
|
(`POSTGRES_USER`, aqui `b2bcall`) como **SUPERUSER**. Superusers — e qualquer role
|
||||||
|
com o atributo `BYPASSRLS` — **sempre ignoram RLS**, mesmo com
|
||||||
|
`FORCE ROW LEVEL SECURITY` habilitado. Não existe exceção via configuração de
|
||||||
|
policy: é uma regra do Postgres anterior à avaliação de qualquer `USING`/`WITH CHECK`.
|
||||||
|
|
||||||
|
Por isso o projeto usa **dois roles de banco diferentes**:
|
||||||
|
|
||||||
|
| Role | Uso | Privilégios | Variável de ambiente |
|
||||||
|
|----------------|----------------------------------------|----------------------------------|-----------------------|
|
||||||
|
| `b2bcall` | Migrations, DDL, dono das tabelas | SUPERUSER (padrão da imagem) | `DATABASE_URL` |
|
||||||
|
| `b2bcall_app` | Runtime da aplicação (API, workers) | Sem SUPERUSER, sem BYPASSRLS | `APP_DATABASE_URL` |
|
||||||
|
|
||||||
|
`getPrismaClient()` em `packages/database/src/index.ts` só aceita `APP_DATABASE_URL`
|
||||||
|
e lança erro se não estiver definida — propositalmente, para impedir que algum
|
||||||
|
serviço se conecte por engano como o role superuser e sile
|
||||||
|
nciosamente ignore o isolamento entre tenants.
|
||||||
|
|
||||||
|
O role `b2bcall_app` é criado pela migration `app_role_and_grants`
|
||||||
|
(sem senha — senha nunca fica em migration versionada no Git). A senha é
|
||||||
|
aplicada via `scripts/db-setup-app-role.sh`, que lê `POSTGRES_APP_PASSWORD` do
|
||||||
|
`.env` (fora do Git).
|
||||||
|
|
||||||
|
## Teste de verificação
|
||||||
|
|
||||||
|
`packages/database/src/__tests__/tenant-isolation.test.ts` cria dois tenants e
|
||||||
|
confirma que o contexto de um nunca enxerga dados do outro, e que sem contexto
|
||||||
|
nenhuma linha tenant-scoped é visível. Rodar com:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @b2bcall/database run test:isolation
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso corresponde ao critério de aceite da seção 204 do `agente.md`
|
||||||
|
("User Tenant A tentando acessar dado Tenant B: resultado obrigatório 403/404,
|
||||||
|
nunca dado do Tenant B" — aqui verificado na camada de banco, a camada de API/HTTP
|
||||||
|
ainda vai reforçar isso com 403/404 quando a Fase de Autenticação/RBAC existir).
|
||||||
|
|
||||||
|
## Ao criar uma nova tabela tenant-scoped
|
||||||
|
|
||||||
|
1. Adicionar `tenantId String @map("tenant_id") @db.Uuid` + relação com `Tenant`.
|
||||||
|
2. Na migration gerada, adicionar o bloco `ENABLE/FORCE ROW LEVEL SECURITY` +
|
||||||
|
`CREATE POLICY "tenant_isolation" ...` (copiar o padrão acima).
|
||||||
|
3. Não é necessário adicionar `GRANT` manual para `b2bcall_app` — a migration
|
||||||
|
`app_role_and_grants` já configurou `ALTER DEFAULT PRIVILEGES` cobrindo tabelas
|
||||||
|
futuras criadas pelo role de migration.
|
||||||
|
4. Toda leitura/escrita nessa tabela em código de aplicação deve passar por
|
||||||
|
`withTenantContext`.
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
"prisma:generate": "prisma generate",
|
"prisma:generate": "prisma generate",
|
||||||
"prisma:migrate": "prisma migrate dev",
|
"prisma:migrate": "prisma migrate dev",
|
||||||
"prisma:deploy": "prisma migrate deploy",
|
"prisma:deploy": "prisma migrate deploy",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test:isolation": "tsx src/__tests__/tenant-isolation.test.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/adapter-pg": "^7.10.0",
|
"@prisma/adapter-pg": "^7.10.0",
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/pg": "^8.23.1",
|
"@types/pg": "^8.23.1",
|
||||||
"prisma": "7.10.0"
|
"prisma": "7.10.0",
|
||||||
|
"tsx": "^4.23.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "user_status" AS ENUM ('ACTIVE', 'DISABLED');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "users" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"password_hash" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"status" "user_status" NOT NULL DEFAULT 'ACTIVE',
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
"deleted_at" TIMESTAMP(3),
|
||||||
|
|
||||||
|
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "tenant_memberships" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"tenant_id" UUID NOT NULL,
|
||||||
|
"user_id" UUID NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "tenant_memberships_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "tenant_memberships_tenant_id_idx" ON "tenant_memberships"("tenant_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "tenant_memberships_tenant_id_user_id_key" ON "tenant_memberships"("tenant_id", "user_id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "tenant_memberships" ADD CONSTRAINT "tenant_memberships_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "tenant_memberships" ADD CONSTRAINT "tenant_memberships_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Row Level Security: tenant_memberships is the first tenant-scoped table.
|
||||||
|
-- Every future tenant-scoped table must repeat this pattern.
|
||||||
|
--
|
||||||
|
-- Convention: the application sets a session-local Postgres setting
|
||||||
|
-- `app.current_tenant_id` (via set_config(..., true) inside a transaction,
|
||||||
|
-- see packages/database withTenantContext()) before running tenant-scoped
|
||||||
|
-- queries. When unset, current_setting(..., true) returns NULL, and the
|
||||||
|
-- comparison below evaluates to NULL/false — deny-by-default, no data leaks.
|
||||||
|
--
|
||||||
|
-- FORCE ROW LEVEL SECURITY makes the policy apply even to the table owner
|
||||||
|
-- (the same role used for migrations), so app code cannot accidentally
|
||||||
|
-- bypass isolation just because it runs as that role.
|
||||||
|
ALTER TABLE "tenant_memberships" ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE "tenant_memberships" FORCE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
CREATE POLICY "tenant_isolation" ON "tenant_memberships"
|
||||||
|
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
-- The Docker Postgres image always makes the initial user (POSTGRES_USER,
|
||||||
|
-- e.g. "b2bcall") a SUPERUSER. Superusers (and BYPASSRLS roles) always
|
||||||
|
-- bypass Row Level Security, no matter FORCE ROW LEVEL SECURITY — so the
|
||||||
|
-- application must NEVER connect as that role for normal queries.
|
||||||
|
--
|
||||||
|
-- This migration creates a separate, unprivileged role for the running
|
||||||
|
-- application. Only this role should be used for APP_DATABASE_URL. The
|
||||||
|
-- superuser role stays reserved for migrations/schema changes (DATABASE_URL
|
||||||
|
-- used by `prisma migrate`).
|
||||||
|
--
|
||||||
|
-- The role's password is set out-of-band (scripts/db-setup-app-role.sh),
|
||||||
|
-- never embedded in a migration file that gets committed to Git.
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'b2bcall_app') THEN
|
||||||
|
CREATE ROLE b2bcall_app LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
GRANT USAGE ON SCHEMA public TO b2bcall_app;
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO b2bcall_app;
|
||||||
|
|
||||||
|
-- Applies automatically to tables created by future migrations (run as the
|
||||||
|
-- same owner role), so we don't need to repeat these grants every time.
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO b2bcall_app;
|
||||||
@@ -32,5 +32,47 @@ model Tenant {
|
|||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
deletedAt DateTime? @map("deleted_at")
|
deletedAt DateTime? @map("deleted_at")
|
||||||
|
|
||||||
|
memberships TenantMembership[]
|
||||||
|
|
||||||
@@map("tenants")
|
@@map("tenants")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum UserStatus {
|
||||||
|
ACTIVE
|
||||||
|
DISABLED
|
||||||
|
|
||||||
|
@@map("user_status")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identidade global do usuário. NUNCA carrega tenant_id diretamente — o tenant
|
||||||
|
// é sempre resolvido via TenantMembership (agente.md secao 31).
|
||||||
|
model User {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
email String @unique
|
||||||
|
passwordHash String @map("password_hash")
|
||||||
|
name String
|
||||||
|
status UserStatus @default(ACTIVE)
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
deletedAt DateTime? @map("deleted_at")
|
||||||
|
|
||||||
|
memberships TenantMembership[]
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tabela tenant-scoped protegida por Row Level Security (ver migration
|
||||||
|
// 'tenant_isolation' e docs/TENANT_ISOLATION.md).
|
||||||
|
model TenantMembership {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
tenantId String @map("tenant_id") @db.Uuid
|
||||||
|
userId String @map("user_id") @db.Uuid
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
|
@@unique([tenantId, userId])
|
||||||
|
@@index([tenantId])
|
||||||
|
@@map("tenant_memberships")
|
||||||
|
}
|
||||||
|
|||||||
91
packages/database/src/__tests__/tenant-isolation.test.ts
Normal file
91
packages/database/src/__tests__/tenant-isolation.test.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Verificação de isolamento multi-tenant (agente.md secao 204).
|
||||||
|
*
|
||||||
|
* Cria dois tenants + um usuário em cada, e confirma que a RLS de
|
||||||
|
* `tenant_memberships` nunca deixa o contexto do Tenant A enxergar dados
|
||||||
|
* do Tenant B — mesmo usando a mesma conexão/role de banco.
|
||||||
|
*
|
||||||
|
* Roda com: pnpm --filter @b2bcall/database run test:isolation
|
||||||
|
*/
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { getPrismaClient, withTenantContext } from "../index";
|
||||||
|
|
||||||
|
function assert(condition: boolean, message: string): void {
|
||||||
|
if (!condition) {
|
||||||
|
throw new Error(`FALHOU: ${message}`);
|
||||||
|
}
|
||||||
|
console.log(`OK: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const prisma = getPrismaClient();
|
||||||
|
const suffix = randomUUID().slice(0, 8);
|
||||||
|
|
||||||
|
const tenantA = await prisma.tenant.create({
|
||||||
|
data: { code: `test-a-${suffix}`, slug: `test-a-${suffix}`, legalName: "Tenant A LTDA" },
|
||||||
|
});
|
||||||
|
const tenantB = await prisma.tenant.create({
|
||||||
|
data: { code: `test-b-${suffix}`, slug: `test-b-${suffix}`, legalName: "Tenant B LTDA" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const userA = await prisma.user.create({
|
||||||
|
data: { email: `user-a-${suffix}@test.local`, passwordHash: "x", name: "User A" },
|
||||||
|
});
|
||||||
|
const userB = await prisma.user.create({
|
||||||
|
data: { email: `user-b-${suffix}@test.local`, passwordHash: "x", name: "User B" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// As INSERTs abaixo precisam ir através do contexto correto: RLS com
|
||||||
|
// FORCE ROW LEVEL SECURITY também bloqueia INSERT sem app.current_tenant_id
|
||||||
|
// compatível.
|
||||||
|
await withTenantContext(prisma, tenantA.id, (tx) =>
|
||||||
|
tx.tenantMembership.create({ data: { tenantId: tenantA.id, userId: userA.id } }),
|
||||||
|
);
|
||||||
|
await withTenantContext(prisma, tenantB.id, (tx) =>
|
||||||
|
tx.tenantMembership.create({ data: { tenantId: tenantB.id, userId: userB.id } }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Contexto do Tenant A: só pode ver a própria membership.
|
||||||
|
const seenFromA = await withTenantContext(prisma, tenantA.id, (tx) =>
|
||||||
|
tx.tenantMembership.findMany(),
|
||||||
|
);
|
||||||
|
assert(seenFromA.length === 1, "Tenant A vê exatamente 1 membership");
|
||||||
|
assert(seenFromA[0]!.tenantId === tenantA.id, "Tenant A só vê a própria membership");
|
||||||
|
assert(
|
||||||
|
!seenFromA.some((m) => m.tenantId === tenantB.id),
|
||||||
|
"Tenant A NUNCA vê membership do Tenant B",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Contexto do Tenant B: simetricamente, só vê a própria.
|
||||||
|
const seenFromB = await withTenantContext(prisma, tenantB.id, (tx) =>
|
||||||
|
tx.tenantMembership.findMany(),
|
||||||
|
);
|
||||||
|
assert(seenFromB.length === 1, "Tenant B vê exatamente 1 membership");
|
||||||
|
assert(seenFromB[0]!.tenantId === tenantB.id, "Tenant B só vê a própria membership");
|
||||||
|
|
||||||
|
// Sem contexto de tenant nenhum: deny-by-default, zero linhas.
|
||||||
|
const seenWithoutContext = await prisma.tenantMembership.findMany();
|
||||||
|
assert(
|
||||||
|
seenWithoutContext.filter((m) => m.tenantId === tenantA.id || m.tenantId === tenantB.id)
|
||||||
|
.length === 0,
|
||||||
|
"Sem app.current_tenant_id definido, nenhuma membership de teste é visível (deny-by-default)",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Limpeza.
|
||||||
|
await withTenantContext(prisma, tenantA.id, (tx) =>
|
||||||
|
tx.tenantMembership.deleteMany({ where: { tenantId: tenantA.id } }),
|
||||||
|
);
|
||||||
|
await withTenantContext(prisma, tenantB.id, (tx) =>
|
||||||
|
tx.tenantMembership.deleteMany({ where: { tenantId: tenantB.id } }),
|
||||||
|
);
|
||||||
|
await prisma.user.deleteMany({ where: { id: { in: [userA.id, userB.id] } } });
|
||||||
|
await prisma.tenant.deleteMany({ where: { id: { in: [tenantA.id, tenantB.id] } } });
|
||||||
|
|
||||||
|
console.log("\nIsolamento multi-tenant OK: Tenant A e Tenant B nunca se enxergam.");
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(async (err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient, Prisma } from "@prisma/client";
|
||||||
import { PrismaPg } from "@prisma/adapter-pg";
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||||||
|
|
||||||
export * from "@prisma/client";
|
export * from "@prisma/client";
|
||||||
@@ -7,8 +7,37 @@ let prisma: PrismaClient | undefined;
|
|||||||
|
|
||||||
export function getPrismaClient(): PrismaClient {
|
export function getPrismaClient(): PrismaClient {
|
||||||
if (!prisma) {
|
if (!prisma) {
|
||||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
// APP_DATABASE_URL must point to a non-superuser, non-BYPASSRLS role
|
||||||
|
// (b2bcall_app) — superusers always bypass Row Level Security, so the
|
||||||
|
// running application can never connect as the migration/owner role
|
||||||
|
// (DATABASE_URL) without silently defeating tenant isolation.
|
||||||
|
const connectionString = process.env.APP_DATABASE_URL;
|
||||||
|
if (!connectionString) {
|
||||||
|
throw new Error(
|
||||||
|
"APP_DATABASE_URL is not set. The application must connect through the " +
|
||||||
|
"restricted b2bcall_app role, not the migration superuser (DATABASE_URL).",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const adapter = new PrismaPg({ connectionString });
|
||||||
prisma = new PrismaClient({ adapter });
|
prisma = new PrismaClient({ adapter });
|
||||||
}
|
}
|
||||||
return prisma;
|
return prisma;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs `fn` inside a transaction with the Postgres RLS tenant context set via
|
||||||
|
* `set_config('app.current_tenant_id', tenantId, true)` — transaction-local,
|
||||||
|
* safe under connection pooling. Every tenant-scoped query must go through
|
||||||
|
* this helper; never query tenant-scoped tables outside of it (see
|
||||||
|
* docs/TENANT_ISOLATION.md).
|
||||||
|
*/
|
||||||
|
export async function withTenantContext<T>(
|
||||||
|
client: PrismaClient,
|
||||||
|
tenantId: string,
|
||||||
|
fn: (tx: Prisma.TransactionClient) => Promise<T>,
|
||||||
|
): Promise<T> {
|
||||||
|
return client.$transaction(async (tx) => {
|
||||||
|
await tx.$executeRaw`SELECT set_config('app.current_tenant_id', ${tenantId}, true)`;
|
||||||
|
return fn(tx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
290
pnpm-lock.yaml
generated
290
pnpm-lock.yaml
generated
@@ -30,6 +30,9 @@ importers:
|
|||||||
prisma:
|
prisma:
|
||||||
specifier: 7.10.0
|
specifier: 7.10.0
|
||||||
version: 7.10.0(@types/react@19.2.18)(magicast@0.5.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3)
|
version: 7.10.0(@types/react@19.2.18)(magicast@0.5.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3)
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.23.12
|
||||||
|
version: 4.23.12
|
||||||
|
|
||||||
packages/shared:
|
packages/shared:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -80,6 +83,162 @@ packages:
|
|||||||
'@electric-sql/pglite@0.4.3':
|
'@electric-sql/pglite@0.4.3':
|
||||||
resolution: {integrity: sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ==}
|
resolution: {integrity: sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ==}
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [aix]
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.28.2':
|
||||||
|
resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.28.2':
|
||||||
|
resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.28.2':
|
||||||
|
resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.28.2':
|
||||||
|
resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.28.2':
|
||||||
|
resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openharmony]
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.28.2':
|
||||||
|
resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.28.2':
|
||||||
|
resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@prisma/adapter-pg@7.10.0':
|
'@prisma/adapter-pg@7.10.0':
|
||||||
resolution: {integrity: sha512-N7nwSor0HO1Kz6xBv0TPAjAPysKK0fac6p4fVN3ensLOuzc/83Fgmln5k92eK/cvzqdkSR/2kkAqlbcdwVrwpw==}
|
resolution: {integrity: sha512-N7nwSor0HO1Kz6xBv0TPAjAPysKK0fac6p4fVN3ensLOuzc/83Fgmln5k92eK/cvzqdkSR/2kkAqlbcdwVrwpw==}
|
||||||
|
|
||||||
@@ -421,6 +580,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==}
|
resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
|
esbuild@0.28.2:
|
||||||
|
resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
exsolve@1.1.1:
|
exsolve@1.1.1:
|
||||||
resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==}
|
resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==}
|
||||||
|
|
||||||
@@ -448,6 +612,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
generate-function@2.3.1:
|
generate-function@2.3.1:
|
||||||
resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
|
resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
|
||||||
|
|
||||||
@@ -679,6 +848,11 @@ packages:
|
|||||||
std-env@3.10.0:
|
std-env@3.10.0:
|
||||||
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
||||||
|
|
||||||
|
tsx@4.23.12:
|
||||||
|
resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
typescript@5.9.3:
|
typescript@5.9.3:
|
||||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
@@ -736,6 +910,84 @@ snapshots:
|
|||||||
|
|
||||||
'@electric-sql/pglite@0.4.3': {}
|
'@electric-sql/pglite@0.4.3': {}
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.28.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@prisma/adapter-pg@7.10.0':
|
'@prisma/adapter-pg@7.10.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@prisma/driver-adapter-utils': 7.10.0
|
'@prisma/driver-adapter-utils': 7.10.0
|
||||||
@@ -1138,6 +1390,35 @@ snapshots:
|
|||||||
|
|
||||||
env-paths@3.0.0: {}
|
env-paths@3.0.0: {}
|
||||||
|
|
||||||
|
esbuild@0.28.2:
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/aix-ppc64': 0.28.2
|
||||||
|
'@esbuild/android-arm': 0.28.2
|
||||||
|
'@esbuild/android-arm64': 0.28.2
|
||||||
|
'@esbuild/android-x64': 0.28.2
|
||||||
|
'@esbuild/darwin-arm64': 0.28.2
|
||||||
|
'@esbuild/darwin-x64': 0.28.2
|
||||||
|
'@esbuild/freebsd-arm64': 0.28.2
|
||||||
|
'@esbuild/freebsd-x64': 0.28.2
|
||||||
|
'@esbuild/linux-arm': 0.28.2
|
||||||
|
'@esbuild/linux-arm64': 0.28.2
|
||||||
|
'@esbuild/linux-ia32': 0.28.2
|
||||||
|
'@esbuild/linux-loong64': 0.28.2
|
||||||
|
'@esbuild/linux-mips64el': 0.28.2
|
||||||
|
'@esbuild/linux-ppc64': 0.28.2
|
||||||
|
'@esbuild/linux-riscv64': 0.28.2
|
||||||
|
'@esbuild/linux-s390x': 0.28.2
|
||||||
|
'@esbuild/linux-x64': 0.28.2
|
||||||
|
'@esbuild/netbsd-arm64': 0.28.2
|
||||||
|
'@esbuild/netbsd-x64': 0.28.2
|
||||||
|
'@esbuild/openbsd-arm64': 0.28.2
|
||||||
|
'@esbuild/openbsd-x64': 0.28.2
|
||||||
|
'@esbuild/openharmony-arm64': 0.28.2
|
||||||
|
'@esbuild/sunos-x64': 0.28.2
|
||||||
|
'@esbuild/win32-arm64': 0.28.2
|
||||||
|
'@esbuild/win32-ia32': 0.28.2
|
||||||
|
'@esbuild/win32-x64': 0.28.2
|
||||||
|
|
||||||
exsolve@1.1.1: {}
|
exsolve@1.1.1: {}
|
||||||
|
|
||||||
fast-check@3.23.2:
|
fast-check@3.23.2:
|
||||||
@@ -1165,6 +1446,9 @@ snapshots:
|
|||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
signal-exit: 4.1.0
|
signal-exit: 4.1.0
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
generate-function@2.3.1:
|
generate-function@2.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-property: 1.0.2
|
is-property: 1.0.2
|
||||||
@@ -1363,6 +1647,12 @@ snapshots:
|
|||||||
|
|
||||||
std-env@3.10.0: {}
|
std-env@3.10.0: {}
|
||||||
|
|
||||||
|
tsx@4.23.12:
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.28.2
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
typescript@5.9.3: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
undici-types@8.3.0: {}
|
undici-types@8.3.0: {}
|
||||||
|
|||||||
28
scripts/db-setup-app-role.sh
Executable file
28
scripts/db-setup-app-role.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Sets/rotates the password of the restricted, non-superuser Postgres role
|
||||||
|
# used by the running application (b2bcall_app). The role itself is created
|
||||||
|
# by the "app_role_and_grants" Prisma migration; this script only sets the
|
||||||
|
# secret, which must never be embedded in a committed migration file.
|
||||||
|
#
|
||||||
|
# Reads POSTGRES_USER/PASSWORD (superuser, to run the ALTER ROLE) and
|
||||||
|
# POSTGRES_APP_USER/POSTGRES_APP_PASSWORD from .env.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$ROOT_DIR/.env"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
: "${POSTGRES_APP_USER:?POSTGRES_APP_USER not set in .env}"
|
||||||
|
: "${POSTGRES_APP_PASSWORD:?POSTGRES_APP_PASSWORD not set in .env}"
|
||||||
|
|
||||||
|
docker exec -i b2bcall-postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
|
||||||
|
-v ON_ERROR_STOP=1 \
|
||||||
|
-v app_user="$POSTGRES_APP_USER" \
|
||||||
|
-v app_password="$POSTGRES_APP_PASSWORD" \
|
||||||
|
<<'SQL'
|
||||||
|
ALTER ROLE :"app_user" WITH PASSWORD :'app_password';
|
||||||
|
SQL
|
||||||
|
|
||||||
|
echo "Senha do role '${POSTGRES_APP_USER}' aplicada."
|
||||||
Reference in New Issue
Block a user