init
This commit is contained in:
114
src/lib/auth/rbac.ts
Normal file
114
src/lib/auth/rbac.ts
Normal 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
115
src/lib/auth/session.ts
Normal 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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user