This commit is contained in:
phaichayon
2026-06-11 12:46:57 +07:00
parent 1993d88306
commit 7a390cf0df
720 changed files with 100919 additions and 0 deletions

114
src/lib/auth/rbac.ts Normal file
View File

@@ -0,0 +1,114 @@
export const SYSTEM_ROLES = ['super_admin', 'user'] as const;
export const MEMBERSHIP_ROLES = ['admin', 'user'] as const;
export const BUSINESS_ROLES = [
'it_admin',
'helpdesk',
'infrastructure',
'application',
'auditor',
'viewer'
] as const;
export type SystemRole = (typeof SYSTEM_ROLES)[number];
export type MembershipRole = (typeof MEMBERSHIP_ROLES)[number];
export type BusinessRole = (typeof BUSINESS_ROLES)[number];
export const PERMISSIONS = {
productsRead: 'products:read',
productsWrite: 'products:write',
organizationManage: 'organization:manage',
usersManage: 'users:manage',
masterDataManage: 'master_data:manage',
assetRead: 'asset:read',
assetCreate: 'asset:create',
assetUpdate: 'asset:update',
assetAssign: 'asset:assign',
assetTransfer: 'asset:transfer',
assetReturn: 'asset:return',
assetRepair: 'asset:repair',
assetAudit: 'asset:audit',
reportRead: 'report:read'
} as const;
export type Permission = (typeof PERMISSIONS)[keyof typeof PERMISSIONS];
function getMembershipBasePermissions(role: MembershipRole): Permission[] {
if (role === 'admin') {
return [
PERMISSIONS.productsRead,
PERMISSIONS.productsWrite,
PERMISSIONS.organizationManage,
PERMISSIONS.usersManage
];
}
return [PERMISSIONS.productsRead];
}
export function getBusinessRolePermissions(role: BusinessRole): Permission[] {
switch (role) {
case 'it_admin':
return [
PERMISSIONS.masterDataManage,
PERMISSIONS.assetRead,
PERMISSIONS.assetCreate,
PERMISSIONS.assetUpdate,
PERMISSIONS.assetAssign,
PERMISSIONS.assetTransfer,
PERMISSIONS.assetReturn,
PERMISSIONS.assetRepair,
PERMISSIONS.assetAudit,
PERMISSIONS.reportRead
];
case 'helpdesk':
return [
PERMISSIONS.assetRead,
PERMISSIONS.assetAssign,
PERMISSIONS.assetTransfer,
PERMISSIONS.assetReturn
];
case 'infrastructure':
return [
PERMISSIONS.assetRead,
PERMISSIONS.assetCreate,
PERMISSIONS.assetUpdate,
PERMISSIONS.assetRepair,
PERMISSIONS.reportRead
];
case 'application':
return [
PERMISSIONS.assetRead,
PERMISSIONS.assetCreate,
PERMISSIONS.assetUpdate,
PERMISSIONS.reportRead
];
case 'auditor':
return [PERMISSIONS.assetRead, PERMISSIONS.assetAudit, PERMISSIONS.reportRead];
case 'viewer':
default:
return [PERMISSIONS.assetRead];
}
}
export function getDefaultBusinessRole(role: MembershipRole): BusinessRole {
return role === 'admin' ? 'it_admin' : 'viewer';
}
export function getDefaultPermissions(
role: MembershipRole,
businessRole: BusinessRole = getDefaultBusinessRole(role)
): Permission[] {
return [...new Set([...getMembershipBasePermissions(role), ...getBusinessRolePermissions(businessRole)])];
}
export function isSystemRole(value: string): value is SystemRole {
return SYSTEM_ROLES.includes(value as SystemRole);
}
export function isMembershipRole(value: string): value is MembershipRole {
return MEMBERSHIP_ROLES.includes(value as MembershipRole);
}
export function isBusinessRole(value: string): value is BusinessRole {
return BUSINESS_ROLES.includes(value as BusinessRole);
}

115
src/lib/auth/session.ts Normal file
View File

@@ -0,0 +1,115 @@
import { and, eq } from 'drizzle-orm';
import { auth } from '@/auth';
import { memberships, organizations } from '@/db/schema';
import { db } from '@/lib/db';
export class AuthError extends Error {
status: number;
constructor(message: string, status: number) {
super(message);
this.name = 'AuthError';
this.status = status;
}
}
export async function requireSession() {
const session = await auth();
if (!session?.user?.id) {
throw new AuthError('Unauthorized', 401);
}
return session;
}
export async function requireSystemRole(role: 'super_admin') {
const session = await requireSession();
if (session.user.systemRole !== role) {
throw new AuthError('Forbidden', 403);
}
return session;
}
export async function requireOrganizationAccess(options?: {
role?: string;
roles?: string[];
permission?: string;
permissions?: string[];
allowSuperAdminBypass?: boolean;
}) {
const session = await requireSession();
if (options?.allowSuperAdminBypass !== false && session.user.systemRole === 'super_admin') {
const activeOrganization = session.user.organizations.find(
(organization) => organization.id === session.user.activeOrganizationId
);
if (!activeOrganization) {
throw new AuthError('Organization membership required', 403);
}
return {
session,
membership: {
userId: session.user.id,
organizationId: activeOrganization.id,
role: activeOrganization.role,
businessRole: activeOrganization.businessRole,
permissions: session.user.activePermissions
},
organization: {
id: activeOrganization.id,
name: activeOrganization.name,
slug: activeOrganization.slug,
plan: activeOrganization.plan,
imageUrl: activeOrganization.imageUrl
}
};
}
if (!session.user.activeOrganizationId) {
throw new AuthError('Organization membership required', 403);
}
const membership = await db.query.memberships.findFirst({
where: and(
eq(memberships.userId, session.user.id),
eq(memberships.organizationId, session.user.activeOrganizationId)
)
});
if (!membership) {
throw new AuthError('Forbidden', 403);
}
const allowedRoles = options?.roles ?? (options?.role ? [options.role] : []);
const allowedPermissions = options?.permissions ?? (options?.permission ? [options.permission] : []);
if (allowedRoles.length > 0 && !allowedRoles.includes(membership.role)) {
throw new AuthError('Forbidden', 403);
}
if (
allowedPermissions.length > 0 &&
!allowedPermissions.every((permission) => membership.permissions.includes(permission))
) {
throw new AuthError('Forbidden', 403);
}
const organization = await db.query.organizations.findFirst({
where: eq(organizations.id, membership.organizationId)
});
if (!organization) {
throw new AuthError('Organization not found', 404);
}
return {
session,
membership,
organization
};
}