chore: initial commit for phase03
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user