chore: initial commit for phase03

This commit is contained in:
hibna
2026-02-21 13:37:46 +03:00
parent 8eb7c90958
commit d0c20581b6
12 changed files with 1175 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
import type { FastifyRequest } from 'fastify';
import { auditLogs } from '@source/database';
import type { Database } from '@source/database';
export async function createAuditLog(
db: Database,
request: FastifyRequest,
data: {
organizationId: string;
action: string;
serverId?: string;
metadata?: Record<string, unknown>;
},
) {
await db.insert(auditLogs).values({
organizationId: data.organizationId,
userId: request.user.sub,
serverId: data.serverId,
action: data.action,
metadata: data.metadata ?? {},
ipAddress: request.ip,
});
}
+25
View File
@@ -0,0 +1,25 @@
import { Type } from '@sinclair/typebox';
export const PaginationQuerySchema = Type.Object({
page: Type.Optional(Type.Number({ minimum: 1, default: 1 })),
perPage: Type.Optional(Type.Number({ minimum: 1, maximum: 100, default: 20 })),
});
export function paginate(query: { page?: number; perPage?: number }) {
const page = query.page ?? 1;
const perPage = query.perPage ?? 20;
const offset = (page - 1) * perPage;
return { page, perPage, offset, limit: perPage };
}
export function paginatedResponse<T>(data: T[], total: number, page: number, perPage: number) {
return {
data,
meta: {
page,
perPage,
total,
totalPages: Math.ceil(total / perPage),
},
};
}
+82
View File
@@ -0,0 +1,82 @@
import type { FastifyRequest } from 'fastify';
import { eq, and } from 'drizzle-orm';
import { organizationMembers } from '@source/database';
import { ROLES } from '@source/shared';
import type { Permission, Role } from '@source/shared';
import { AppError } from './errors.js';
interface OrgMember {
role: Role;
customPermissions: Record<string, boolean>;
}
/**
* Get the requesting user's membership in an organization.
* Super admins bypass membership checks.
*/
export async function getOrgMembership(
request: FastifyRequest,
orgId: string,
): Promise<OrgMember | 'super_admin'> {
const user = request.user;
if (user.isSuperAdmin) {
return 'super_admin';
}
const member = await (request.server as any).db.query.organizationMembers.findFirst({
where: and(
eq(organizationMembers.organizationId, orgId),
eq(organizationMembers.userId, user.sub),
),
});
if (!member) {
throw AppError.forbidden('You are not a member of this organization');
}
return {
role: member.role as Role,
customPermissions: (member.customPermissions ?? {}) as Record<string, boolean>,
};
}
/**
* Check if the user has a specific permission in the organization.
* Super admins always have all permissions.
*/
export function hasPermission(membership: OrgMember | 'super_admin', permission: Permission): boolean {
if (membership === 'super_admin') return true;
// Check custom permission overrides first
if (permission in membership.customPermissions) {
return membership.customPermissions[permission]!;
}
// Fall back to role defaults
const rolePerms = ROLES[membership.role]?.permissions ?? [];
return (rolePerms as readonly string[]).includes(permission);
}
/**
* Require a specific permission, throw 403 if not allowed.
*/
export async function requirePermission(
request: FastifyRequest,
orgId: string,
permission: Permission,
): Promise<void> {
const membership = await getOrgMembership(request, orgId);
if (!hasPermission(membership, permission)) {
throw AppError.forbidden(`Missing permission: ${permission}`);
}
}
/**
* Require super admin role.
*/
export function requireSuperAdmin(request: FastifyRequest): void {
if (!request.user.isSuperAdmin) {
throw AppError.forbidden('Super admin access required');
}
}