CRUD de Usuários e Papéis (role weight, permissões, gitignore de worktrees)
- apps/api/src/users/: invite (senha temporária, mesma entropia do legado), PATCH (role/isActive, com checagem de peso), reset de senha administrativo, troca de senha self-service, grant/revoke de acesso a aplicação — cada ação sensível auditada. - apps/api/src/roles/: CRUD de papéis e gestão de role_permissions, exclusivo super_admin (SuperAdminGuard), mirando o requireSuperAdmin do legado — deliberadamente não generalizado para o modelo de permissão resource+action+scope, por ser a raiz do próprio sistema de autorização (documentado em roles.service.ts). - apps/api/src/auth/role-weight.ts: assertCanAssignRole/assertCanActOnUser (eden.md §1.6) — um ator nunca atribui papel nem mexe em usuário com peso maior que o seu; super_admin sempre passa. - .gitignore: .claude/worktrees/ (worktrees temporários de subagentes paralelos nunca devem virar submódulo do repo principal). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { query } from "../pool.js";
|
||||
import { query, withTransaction } from "../pool.js";
|
||||
import type { Role, RolePermission } from "./types.js";
|
||||
|
||||
export async function getRoleByKey(key: string): Promise<Role | null> {
|
||||
@@ -18,3 +18,71 @@ export async function getRolePermissions(roleKey: string): Promise<RolePermissio
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export interface CreateRoleInput {
|
||||
key: string;
|
||||
label: string;
|
||||
weight: number;
|
||||
createdBy?: string | null;
|
||||
}
|
||||
|
||||
/** New roles always start with zero permissions (empty row set) — no implicit inheritance, mirrors eden.md §7 item 5. */
|
||||
export async function createRole(input: CreateRoleInput): Promise<Role> {
|
||||
const { rows } = await query<Role>(
|
||||
`INSERT INTO roles (key, label, weight, is_system, created_by)
|
||||
VALUES ($1, $2, $3, false, $4)
|
||||
RETURNING *`,
|
||||
[input.key, input.label, input.weight, input.createdBy ?? null],
|
||||
);
|
||||
const role = rows[0];
|
||||
if (!role) throw new Error("failed to create role");
|
||||
return role;
|
||||
}
|
||||
|
||||
export interface UpdateRoleInput {
|
||||
label?: string;
|
||||
weight?: number;
|
||||
}
|
||||
|
||||
export async function updateRole(key: string, input: UpdateRoleInput): Promise<Role | null> {
|
||||
const { rows } = await query<Role>(
|
||||
`UPDATE roles SET label = COALESCE($2, label), weight = COALESCE($3, weight) WHERE key = $1 RETURNING *`,
|
||||
[key, input.label ?? null, input.weight ?? null],
|
||||
);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
export async function countUsersWithRole(roleKey: string): Promise<number> {
|
||||
const { rows } = await query<{ count: string }>("SELECT count(*) FROM users WHERE role_key = $1", [
|
||||
roleKey,
|
||||
]);
|
||||
return Number(rows[0]?.count ?? 0);
|
||||
}
|
||||
|
||||
export async function deleteRole(key: string): Promise<void> {
|
||||
await query("DELETE FROM roles WHERE key = $1", [key]);
|
||||
}
|
||||
|
||||
export interface SetRolePermissionInput {
|
||||
resource: string;
|
||||
action: RolePermission["action"];
|
||||
scope: RolePermission["scope"];
|
||||
}
|
||||
|
||||
/** Replaces the entire permission set for a role in one transaction — never a partial write. */
|
||||
export async function setRolePermissions(
|
||||
roleKey: string,
|
||||
permissions: SetRolePermissionInput[],
|
||||
actorId?: string | null,
|
||||
): Promise<void> {
|
||||
await withTransaction(async (client) => {
|
||||
await client.query("DELETE FROM role_permissions WHERE role_key = $1", [roleKey]);
|
||||
for (const p of permissions) {
|
||||
await client.query(
|
||||
`INSERT INTO role_permissions (role_key, resource, action, scope, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[roleKey, p.resource, p.action, p.scope, actorId ?? null],
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,3 +78,49 @@ export async function getUserApplications(userId: string): Promise<string[]> {
|
||||
);
|
||||
return rows.map((r) => r.application_key);
|
||||
}
|
||||
|
||||
export async function revokeUserApplication(
|
||||
userId: string,
|
||||
applicationKey: "core" | "reseller" | "subscriber",
|
||||
): Promise<void> {
|
||||
await query("DELETE FROM user_applications WHERE user_id = $1 AND application_key = $2", [
|
||||
userId,
|
||||
applicationKey,
|
||||
]);
|
||||
}
|
||||
|
||||
export interface ListUsersFilter {
|
||||
activeOnly?: boolean;
|
||||
}
|
||||
|
||||
/** Includes inactive users by default (admin screens need to see them to reactivate) — pass activeOnly to filter. */
|
||||
export async function listUsers(filter: ListUsersFilter = {}): Promise<User[]> {
|
||||
const { rows } = await query<User>(
|
||||
filter.activeOnly
|
||||
? "SELECT * FROM users WHERE is_active = true ORDER BY created_at DESC"
|
||||
: "SELECT * FROM users ORDER BY created_at DESC",
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export interface UpdateUserProfileInput {
|
||||
fullName?: string | null;
|
||||
roleKey?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export async function updateUserProfile(
|
||||
userId: string,
|
||||
input: UpdateUserProfileInput,
|
||||
): Promise<User | null> {
|
||||
const { rows } = await query<User>(
|
||||
`UPDATE users SET
|
||||
full_name = COALESCE($2, full_name),
|
||||
role_key = COALESCE($3, role_key),
|
||||
is_active = COALESCE($4, is_active)
|
||||
WHERE id = $1
|
||||
RETURNING *`,
|
||||
[userId, input.fullName ?? null, input.roleKey ?? null, input.isActive ?? null],
|
||||
);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user