@taukala/xs-ctrl
Version:
A flexible and powerful access control library for JavaScript applications with dynamic validation support
135 lines (127 loc) • 4.34 kB
JavaScript
import { validateClaim } from './validateClaim';
/**
* Creates a permission validator that combines authentication and authorization checks.
* This factory function generates a validator that can be used to protect routes or resources.
* Supports both static claim-based and dynamic resource-based authorization rules.
*
* @param {Object} options - Configuration options
* @param {Function} options.getSession - Async function to retrieve the current session
* Should return null/undefined if no session exists
*
* @param {Function} options.getClaims - Async function to retrieve user claims
* Receives the session object and should return claims object
* Structure: async (session) => ({
* role: ['business'],
* businessOwner: ['business-1', 'business-2']
* })
*
* @param {Function} options.onUnauthenticated - Callback for handling unauthenticated users
* Usually redirects to login page or returns 401 response
*
* @param {Function} options.onUnauthorized - Callback for handling unauthorized access
* Usually redirects to home page or returns 403 response
*
* @returns {Function} An async validator function that accepts access rules and context
*
* @example
* // Basic usage with Next.js and static rules
* const validatePermission = createPermissionValidator({
* getSession: auth,
* getClaims: async (session) => ({
* role: ['admin'],
* department: ['IT']
* }),
* onUnauthenticated: () => redirect('/auth/signin'),
* onUnauthorized: () => redirect('/')
* });
*
* @example
* // Usage with dynamic business rules
* const validatePermission = createPermissionValidator({
* getSession: auth,
* getClaims: async (session) => ({
* role: ['business'],
* businessOwner: ['business-1', 'business-2'],
* businessOperator: ['business-3']
* }),
* onUnauthenticated: () => redirect('/auth/signin'),
* onUnauthorized: () => redirect('/')
* });
*
* // Using with dynamic validation
* const businessRule = createAccessRule()
* .addCondition('role', 'business')
* .addDynamicCondition(({ claims, resources }) => {
* const businessIds = claims.businessOwner || [];
* return businessIds.includes(resources.business?.id);
* })
* .build();
*
* const { session, claims } = await validatePermission(
* [businessRule],
* { resources: { business: { id: 'business-1' } } }
* );
*/
export function createPermissionValidator({
getSession,
getClaims,
onUnauthenticated,
onUnauthorized
}) {
if (!getSession || !getClaims || !onUnauthenticated || !onUnauthorized) {
throw new Error('All parameters are required');
}
if (typeof getSession !== 'function'
|| typeof getClaims !== 'function'
|| typeof onUnauthenticated !== 'function'
|| typeof onUnauthorized !== 'function') {
throw new Error('All parameters must be functions');
}
/**
* Validates user permission against given access rules
*
* @param {Array<Object>} criteria - Array of access rules
* @param {Object} [context] - Optional context for dynamic validation
* @param {Object} [context.resources] - Resources for dynamic validation
* @returns {Promise<Object>} Object containing session, claims, and status
*
* @example
* // Static validation
* const { session, claims } = await validatePermission([adminRule]);
*
* @example
* // Dynamic validation with resources
* const { session, claims } = await validatePermission(
* [businessRule],
* {
* resources: {
* business: await getBusiness(businessId)
* }
* }
* );
*/
return async function validatePermission(criteria, context = {}) {
// Check authentication
const session = await getSession();
if (!session) {
onUnauthenticated();
return { session, claims: {}, status: 401 };
}
// Get claims
const claims = await getClaims(session);
// Prepare validation context
const validationContext = {
...context,
session,
claims
};
// Validate claims
try {
await validateClaim(criteria, claims, validationContext);
} catch (error) {
onUnauthorized();
return { session, claims, status: 403 };
}
return { session, claims, status: 200 };
};
}