Skip to main content

Permission Service

The permission system provides granular access control using resource-based permissions, role management, plan-based feature guards, and authentication utilities. It spans definitions, grouping, state management, database storage, and plan-gated feature access.

Architecture Overview

ModulePathPurpose
Definitionslib/permissions/definitions.tsPermission constants, types, default roles
Groupslib/permissions/groups.tsUI grouping for permission management
Utilslib/permissions/utils.tsState management and diff utilities
Role DB Servicelib/services/role-db.service.tsDatabase CRUD for roles and permissions
Plan Guardlib/guards/plan-features.guard.tsSubscription plan feature access
Client Authlib/utils/client-auth.tsAPI route authentication helpers

Permission Definitions

All permissions follow a resource:action naming pattern:

// lib/permissions/definitions.ts
export const PERMISSIONS = {
items: {
read: 'items:read',
create: 'items:create',
update: 'items:update',
delete: 'items:delete',
review: 'items:review',
approve: 'items:approve',
reject: 'items:reject',
},
categories: {
read: 'categories:read',
create: 'categories:create',
update: 'categories:update',
delete: 'categories:delete',
},
tags: {
read: 'tags:read',
create: 'tags:create',
update: 'tags:update',
delete: 'tags:delete',
},
roles: {
read: 'roles:read',
create: 'roles:create',
update: 'roles:update',
delete: 'roles:delete',
},
users: {
read: 'users:read',
create: 'users:create',
update: 'users:update',
delete: 'users:delete',
assignRoles: 'users:assignRoles',
},
analytics: {
read: 'analytics:read',
export: 'analytics:export',
},
system: {
settings: 'system:settings',
},
} as const;

Helper Functions

// Get all permissions as a flat array
getAllPermissions(): Permission[]

// Get permissions for a specific resource
getPermissionsForResource(resource: keyof typeof PERMISSIONS): Permission[]

// Type guard for permission validation
isValidPermission(permission: string): permission is Permission

Default Roles

Two built-in role templates are provided:

export const DEFAULT_ROLES = {
SUPER_ADMIN: {
id: 'super-admin',
name: 'Super Administrator',
description: 'Full system access with all permissions',
permissions: getAllPermissions(),
},
CONTENT_MANAGER: {
id: 'content-manager',
name: 'Content Manager',
description: 'Manage content including items, categories, and tags',
permissions: [
...getPermissionsForResource('items'),
...getPermissionsForResource('categories'),
...getPermissionsForResource('tags'),
],
},
};

Permission Groups

For the admin UI, permissions are organized into logical groups:

// lib/permissions/groups.ts
export const PERMISSION_GROUPS: PermissionGroup[] = [
{
id: 'content',
name: 'Content Management',
description: 'Manage items, categories, and tags',
icon: 'FileText',
permissions: [
...getPermissionsForResource('items'),
...getPermissionsForResource('categories'),
...getPermissionsForResource('tags'),
],
},
{
id: 'users',
name: 'User Management',
description: 'Manage users and their roles',
icon: 'Users',
permissions: [
...getPermissionsForResource('users'),
...getPermissionsForResource('roles'),
],
},
{
id: 'system',
name: 'System & Analytics',
description: 'System settings and analytics access',
icon: 'Settings',
permissions: [
...getPermissionsForResource('analytics'),
...getPermissionsForResource('system'),
],
},
];

Utility functions for permission display:

formatPermissionName('items:create')    // "Create Items"
formatPermissionDescription('items:create') // "Create new items and submissions"

Permission State Management

The lib/permissions/utils.ts module provides helpers for the role editing UI:

// Create a boolean map from a permission array
createPermissionState(currentPermissions: Permission[]): PermissionState

// Extract selected permissions from a state map
getSelectedPermissions(permissionState: PermissionState): Permission[]

// Calculate what changed between two permission sets
calculatePermissionChanges(
originalPermissions: Permission[],
newPermissions: Permission[]
): PermissionChanges // { added: Permission[], removed: Permission[] }

// Compare two permission sets for equality
arePermissionsEqual(permissions1: Permission[], permissions2: Permission[]): boolean

// Filter permissions by search term
filterPermissions(permissions: Permission[], searchTerm: string): Permission[]

Role Database Service

The RoleDbService class in lib/services/role-db.service.ts manages roles with their associated permissions in the database:

export class RoleDbService {
async readRoles(): Promise<RoleData[]>
async findById(id: string): Promise<RoleData | null>
async findBy(key: keyof Role, value: string): Promise<RoleData | null>
async createRole(data: CreateRoleRequest): Promise<RoleData>
async updateRole(id: string, data: UpdateRoleRequest): Promise<RoleData>
async deleteRole(id: string): Promise<void> // soft delete
async hardDeleteRole(id: string): Promise<void> // permanent delete
async findRoles(options?: RoleListOptions): Promise<PaginatedResult>
async exists(id: string, options?): Promise<boolean>
}

Role creation uses transactions to atomically insert the role and its permissions:

async createRole(data: CreateRoleRequest): Promise<RoleData> {
return await db.transaction(async (tx) => {
const result = await tx.insert(roles).values(roleData).returning();
const newRole = result[0];

if (data.permissions && data.permissions.length > 0) {
const permissionRecords = await tx
.select({ id: permissions.id, key: permissions.key })
.from(permissions)
.where(inArray(permissions.key, data.permissions));

await tx.insert(rolePermissions).values(
permissionRecords.map(perm => ({
roleId: newRole.id,
permissionId: perm.id,
}))
);
}
return this.mapDbToRoleData(newRole, data.permissions || []);
});
}

Plan Feature Guards

The lib/guards/plan-features.guard.ts provides subscription-plan-based access control:

Plan Hierarchy

export const PLAN_LEVELS: Record<string, number> = {
free: 1,
standard: 2,
premium: 3,
};

Feature Access Matrix

Features are gated by plan level. Access can be configured as 'all', a specific plan, an array of plans, or a minimum plan requirement:

export const FEATURE_ACCESS: Record<Feature, FeatureAccess> = {
submit_product: 'all',
extended_description: { minPlan: PaymentPlan.STANDARD },
upload_video: PaymentPlan.PREMIUM,
verified_badge: { minPlan: PaymentPlan.STANDARD },
// ... more features
};

Plan Limits

Numeric limits vary by plan:

export const PLAN_LIMITS: Record<PaymentPlan, FeatureLimits> = {
free: { max_images: 1, max_submissions: 1, review_days: 7 },
standard: { max_images: 5, max_submissions: 10, review_days: 3 },
premium: { max_images: null, max_submissions: null, review_days: 1 },
};

Guard Factory

const guard = createPlanGuard(userPlan);

guard.canAccess('upload_video'); // boolean
guard.requireFeature('verified_badge'); // throws PlanGuardError if denied
guard.getLimit('max_images'); // number or null
guard.isWithinLimit('max_submissions', 5); // boolean
guard.getAccessibleFeatures(); // Feature[]

Client Authentication

The lib/utils/client-auth.ts module provides API route helpers:

export async function requireClientAuth():
Promise<ClientAuthResult | ClientAuthError>

// Usage in API routes:
const authResult = await requireClientAuth();
if (!authResult.success) {
return authResult.response; // 401 Unauthorized
}
const { session, userId } = authResult;

Additional response helpers:

unauthorizedResponse(message?)   // 401
forbiddenResponse(message?) // 403
notFoundResponse(message?) // 404
badRequestResponse(message?) // 400
conflictResponse(message?) // 409
serverErrorResponse(error, msg?) // 500

Database Schema

roles

ColumnTypeDescription
idtextPrimary key
nametextUnique role name
descriptiontextRole description
is_adminbooleanAdmin flag
statustextactive or inactive
deleted_attimestampSoft delete

role_permissions (junction)

ColumnTypeDescription
role_idtextFK to roles.id (cascade delete)
permission_idtextFK to permissions.id (cascade delete)

user_roles (junction)

ColumnTypeDescription
user_idtextFK to users.id (cascade delete)
role_idtextFK to roles.id (cascade delete)