Authorization & Access Control
Role-based access control, permissions, and Row Level Security implementation.
Authorization & Access Control
Smart Shelf implements a comprehensive authorization system using Role-Based Access Control (RBAC) combined with Row Level Security (RLS) to ensure users can only access data and perform actions they're explicitly authorized for.
Role-Based Access Control (RBAC)
User Roles Hierarchy
// types/auth.ts
export type UserRole = 'admin' | 'manager' | 'employee' | 'viewer'
export interface UserPermissions {
products: {
create: boolean
read: boolean
update: boolean
delete: boolean
export: boolean
import: boolean
}
inventory: {
view: boolean
adjust: boolean
transfer: boolean
audit: boolean
reports: boolean
}
orders: {
create: boolean
view: boolean
approve: boolean
cancel: boolean
fulfill: boolean
}
analytics: {
basic_reports: boolean
advanced_analytics: boolean
custom_queries: boolean
export_data: boolean
}
administration: {
user_management: boolean
system_settings: boolean
audit_logs: boolean
security_settings: boolean
backup_restore: boolean
}
warehouse: {
manage_locations: boolean
configure_zones: boolean
assign_users: boolean
view_operations: boolean
}
}
export const ROLE_PERMISSIONS: Record<UserRole, UserPermissions> = {
admin: {
products: { create: true, read: true, update: true, delete: true, export: true, import: true },
inventory: { view: true, adjust: true, transfer: true, audit: true, reports: true },
orders: { create: true, view: true, approve: true, cancel: true, fulfill: true },
analytics: { basic_reports: true, advanced_analytics: true, custom_queries: true, export_data: true },
administration: { user_management: true, system_settings: true, audit_logs: true, security_settings: true, backup_restore: true },
warehouse: { manage_locations: true, configure_zones: true, assign_users: true, view_operations: true }
},
manager: {
products: { create: true, read: true, update: true, delete: false, export: true, import: true },
inventory: { view: true, adjust: true, transfer: true, audit: true, reports: true },
orders: { create: true, view: true, approve: true, cancel: true, fulfill: true },
analytics: { basic_reports: true, advanced_analytics: true, custom_queries: false, export_data: true },
administration: { user_management: false, system_settings: false, audit_logs: true, security_settings: false, backup_restore: false },
warehouse: { manage_locations: true, configure_zones: false, assign_users: true, view_operations: true }
},
employee: {
products: { create: true, read: true, update: true, delete: false, export: false, import: false },
inventory: { view: true, adjust: true, transfer: true, audit: false, reports: false },
orders: { create: true, view: true, approve: false, cancel: false, fulfill: true },
analytics: { basic_reports: true, advanced_analytics: false, custom_queries: false, export_data: false },
administration: { user_management: false, system_settings: false, audit_logs: false, security_settings: false, backup_restore: false },
warehouse: { manage_locations: false, configure_zones: false, assign_users: false, view_operations: true }
},
viewer: {
products: { create: false, read: true, update: false, delete: false, export: false, import: false },
inventory: { view: true, adjust: false, transfer: false, audit: false, reports: false },
orders: { create: false, view: true, approve: false, cancel: false, fulfill: false },
analytics: { basic_reports: true, advanced_analytics: false, custom_queries: false, export_data: false },
administration: { user_management: false, system_settings: false, audit_logs: false, security_settings: false, backup_restore: false },
warehouse: { manage_locations: false, configure_zones: false, assign_users: false, view_operations: true }
}
}
Permission Matrix
| Feature | Admin | Manager | Employee | Viewer |
|---|---|---|---|---|
| Products | ||||
| Create Products | ✅ | ✅ | ✅ | ❌ |
| View Products | ✅ | ✅ | ✅ | ✅ |
| Edit Products | ✅ | ✅ | ✅ | ❌ |
| Delete Products | ✅ | ❌ | ❌ | ❌ |
| Export Products | ✅ | ✅ | ❌ | ❌ |
| Import Products | ✅ | ✅ | ❌ | ❌ |
| Inventory | ||||
| View Inventory | ✅ | ✅ | ✅ | ✅ |
| Adjust Stock | ✅ | ✅ | ✅ | ❌ |
| Transfer Stock | ✅ | ✅ | ✅ | ❌ |
| Inventory Audit | ✅ | ✅ | ❌ | ❌ |
| Inventory Reports | ✅ | ✅ | ❌ | ❌ |
| Orders | ||||
| View Orders | ✅ | ✅ | ✅ | ✅ |
| Create Orders | ✅ | ✅ | ✅ | ❌ |
| Approve Orders | ✅ | ✅ | ❌ | ❌ |
| Cancel Orders | ✅ | ✅ | ❌ | ❌ |
| Fulfill Orders | ✅ | ✅ | ✅ | ❌ |
| Analytics | ||||
| Basic Reports | ✅ | ✅ | ✅ | ✅ |
| Advanced Analytics | ✅ | ✅ | ❌ | ❌ |
| Custom Queries | ✅ | ❌ | ❌ | ❌ |
| Export Data | ✅ | ✅ | ❌ | ❌ |
| Administration | ||||
| User Management | ✅ | ❌ | ❌ | ❌ |
| System Settings | ✅ | ❌ | ❌ | ❌ |
| Audit Logs | ✅ | ✅ | ❌ | ❌ |
| Security Settings | ✅ | ❌ | ❌ | ❌ |
| Backup/Restore | ✅ | ❌ | ❌ | ❌ |
Permission Enforcement
Server-Side Permission Checks
// lib/auth/permissions.ts
import { User } from '@supabase/supabase-js'
import { createClient } from '@/lib/supabase/server'
export async function requirePermission(
permission: string,
user?: User
): Promise<User> {
if (!user) {
const supabase = createClient()
const { data: { user: currentUser }, error } = await supabase.auth.getUser()
if (!currentUser || error) {
throw new Error('Authentication required')
}
user = currentUser
}
const userRole = user.user_metadata?.role as UserRole
if (!userRole || !hasPermission(userRole, permission)) {
throw new Error(`Insufficient permissions. Required: ${permission}`)
}
// Log permission check
await logPermissionCheck({
user_id: user.id,
permission,
granted: true,
timestamp: new Date().toISOString()
})
return user
}
export function hasPermission(role: UserRole, permission: string): boolean {
const permissions = ROLE_PERMISSIONS[role]
// Parse permission string (e.g., "products.create")
const [resource, action] = permission.split('.')
if (!permissions[resource as keyof UserPermissions]) {
return false
}
const resourcePermissions = permissions[resource as keyof UserPermissions] as any
return resourcePermissions[action] === true
}
export async function requireRole(requiredRole: UserRole, user?: User): Promise<User> {
if (!user) {
const supabase = createClient()
const { data: { user: currentUser }, error } = await supabase.auth.getUser()
if (!currentUser || error) {
throw new Error('Authentication required')
}
user = currentUser
}
const userRole = user.user_metadata?.role as UserRole
const roleHierarchy: Record<UserRole, number> = {
viewer: 1,
employee: 2,
manager: 3,
admin: 4
}
if (!userRole || roleHierarchy[userRole] < roleHierarchy[requiredRole]) {
throw new Error(`Insufficient role. Required: ${requiredRole}, Current: ${userRole}`)
}
return user
}
API Route Protection
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { requirePermission } from '@/lib/auth/permissions'
export async function GET(request: NextRequest) {
try {
const user = await requirePermission('products.read')
// User is authorized, proceed with request
const products = await getProducts(user)
return NextResponse.json({ data: products })
} catch (error) {
if (error.message === 'Authentication required') {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 }
)
} else if (error.message.includes('Insufficient permissions')) {
return NextResponse.json(
{ error: 'Access denied' },
{ status: 403 }
)
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const user = await requirePermission('products.create')
const body = await request.json()
const product = await createProduct(body, user)
return NextResponse.json({ data: product })
} catch (error) {
// Handle authorization errors
return handleAuthorizationError(error)
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await requirePermission('products.delete')
await deleteProduct(params.id, user)
return NextResponse.json({ success: true })
} catch (error) {
return handleAuthorizationError(error)
}
}
function handleAuthorizationError(error: any): NextResponse {
if (error.message === 'Authentication required') {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 }
)
} else if (error.message.includes('Insufficient')) {
return NextResponse.json(
{ error: 'Access denied' },
{ status: 403 }
)
}
console.error('API Error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
Component-Level Authorization
// components/auth/with-permission.tsx
import { useAuth } from '@/hooks/use-auth'
import { hasPermission } from '@/lib/auth/permissions'
interface WithPermissionProps {
permission: string
fallback?: React.ReactNode
children: React.ReactNode
}
export function WithPermission({ permission, fallback, children }: WithPermissionProps) {
const { user } = useAuth()
if (!user) {
return fallback || <div>Authentication required</div>
}
const userRole = user.user_metadata?.role
if (!hasPermission(userRole, permission)) {
return fallback || <div>Access denied</div>
}
return <>{children}</>
}
// Usage example
export function ProductActions({ product }: { product: Product }) {
return (
<div className="flex gap-2">
<WithPermission permission="products.update">
<EditProductButton product={product} />
</WithPermission>
<WithPermission permission="products.delete">
<DeleteProductButton product={product} />
</WithPermission>
<WithPermission permission="products.export">
<ExportProductButton product={product} />
</WithPermission>
</div>
)
}
Higher-Order Component for Protection
// components/auth/protected-component.tsx
import { ComponentType } from 'react'
import { useAuth } from '@/hooks/use-auth'
import { hasPermission } from '@/lib/auth/permissions'
export function withPermission<P extends object>(
Component: ComponentType<P>,
requiredPermission: string
) {
return function PermissionWrapper(props: P) {
const { user, loading } = useAuth()
if (loading) {
return <div>Loading...</div>
}
if (!user) {
return <div>Authentication required</div>
}
const userRole = user.user_metadata?.role
if (!hasPermission(userRole, requiredPermission)) {
return (
<div className="text-center p-8">
<h2 className="text-xl font-semibold mb-2">Access Denied</h2>
<p>You don't have permission to access this resource.</p>
</div>
)
}
return <Component {...props} />
}
}
// Usage
const ProtectedProductManager = withPermission(ProductManager, 'products.create')
const ProtectedUserSettings = withPermission(UserSettings, 'administration.user_management')
Row Level Security (RLS)
Database Policies
Row Level Security ensures that users can only access data they're authorized to see based on their role and organizational context.
-- Enable RLS on all tables
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
ALTER TABLE inventory ENABLE ROW LEVEL SECURITY;
ALTER TABLE purchase_orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE sales_orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE warehouses ENABLE ROW LEVEL SECURITY;
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
-- Admin access - full access to all data
CREATE POLICY admin_full_access ON products
FOR ALL TO authenticated
USING (
EXISTS (
SELECT 1 FROM users
WHERE users.id = auth.uid()
AND users.role = 'admin'
)
);
-- Manager access - department/warehouse specific
CREATE POLICY manager_department_access ON products
FOR ALL TO authenticated
USING (
category_id IN (
SELECT category_id
FROM manager_category_assignments
WHERE user_id = auth.uid()
)
OR EXISTS (
SELECT 1 FROM users
WHERE users.id = auth.uid()
AND users.role IN ('admin', 'manager')
)
);
-- Warehouse-specific inventory access
CREATE POLICY warehouse_inventory_access ON inventory
FOR ALL TO authenticated
USING (
warehouse_id IN (
SELECT warehouse_id
FROM user_warehouse_assignments
WHERE user_id = auth.uid()
)
OR EXISTS (
SELECT 1 FROM users
WHERE users.id = auth.uid()
AND users.role IN ('admin', 'manager')
)
);
-- Employee access - limited write permissions
CREATE POLICY employee_products_read ON products
FOR SELECT TO authenticated
USING (
EXISTS (
SELECT 1 FROM users
WHERE users.id = auth.uid()
AND users.role IN ('admin', 'manager', 'employee', 'viewer')
)
);
CREATE POLICY employee_products_write ON products
FOR INSERT TO authenticated
WITH CHECK (
EXISTS (
SELECT 1 FROM users
WHERE users.id = auth.uid()
AND users.role IN ('admin', 'manager', 'employee')
)
);
CREATE POLICY employee_products_update ON products
FOR UPDATE TO authenticated
USING (
EXISTS (
SELECT 1 FROM users
WHERE users.id = auth.uid()
AND users.role IN ('admin', 'manager', 'employee')
)
)
WITH CHECK (
-- Prevent employees from updating critical fields
CASE
WHEN EXISTS (
SELECT 1 FROM users
WHERE users.id = auth.uid()
AND users.role = 'employee'
) THEN (
OLD.cost_price = NEW.cost_price
AND OLD.category_id = NEW.category_id
AND OLD.supplier_id = NEW.supplier_id
)
ELSE true
END
);
-- Viewer access - read only
CREATE POLICY viewer_read_only ON products
FOR SELECT TO authenticated
USING (
EXISTS (
SELECT 1 FROM users
WHERE users.id = auth.uid()
)
);
-- Delete permissions - only admin and managers
CREATE POLICY delete_products ON products
FOR DELETE TO authenticated
USING (
EXISTS (
SELECT 1 FROM users
WHERE users.id = auth.uid()
AND users.role IN ('admin', 'manager')
)
);
Multi-Tenant Security
-- Tenant-based access control for multi-tenant deployments
CREATE POLICY tenant_isolation ON products
FOR ALL TO authenticated
USING (
tenant_id = (
SELECT tenant_id
FROM users
WHERE users.id = auth.uid()
)
);
-- Warehouse-based isolation
CREATE POLICY warehouse_isolation ON inventory
FOR ALL TO authenticated
USING (
warehouse_id IN (
SELECT warehouse_id
FROM user_warehouse_access
WHERE user_id = auth.uid()
)
);
-- Order access based on creator or assigned warehouse
CREATE POLICY order_access ON sales_orders
FOR ALL TO authenticated
USING (
created_by = auth.uid()
OR warehouse_id IN (
SELECT warehouse_id
FROM user_warehouse_access
WHERE user_id = auth.uid()
)
OR EXISTS (
SELECT 1 FROM users
WHERE users.id = auth.uid()
AND users.role IN ('admin', 'manager')
)
);
Dynamic Policy Functions
-- Function to check user permissions dynamically
CREATE OR REPLACE FUNCTION check_user_permission(required_permission text)
RETURNS boolean AS $$
DECLARE
user_role text;
has_permission boolean := false;
BEGIN
-- Get user role
SELECT role INTO user_role
FROM users
WHERE id = auth.uid();
-- Check permission based on role
CASE user_role
WHEN 'admin' THEN
has_permission := true;
WHEN 'manager' THEN
has_permission := required_permission NOT IN ('administration.user_management', 'administration.system_settings');
WHEN 'employee' THEN
has_permission := required_permission IN ('products.read', 'products.create', 'products.update', 'inventory.view', 'inventory.adjust');
WHEN 'viewer' THEN
has_permission := required_permission LIKE '%.read' OR required_permission LIKE '%.view';
ELSE
has_permission := false;
END CASE;
RETURN has_permission;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Use the function in policies
CREATE POLICY dynamic_permission_check ON sensitive_operations
FOR ALL TO authenticated
USING (check_user_permission('sensitive_operations.execute'));
Context-Based Access Control
Warehouse-Based Access
// lib/auth/context-based-access.ts
export async function getWarehouseAccess(userId: string): Promise<string[]> {
const { data: assignments } = await supabase
.from('user_warehouse_assignments')
.select('warehouse_id')
.eq('user_id', userId)
.eq('is_active', true)
return assignments?.map(a => a.warehouse_id) || []
}
export async function hasWarehouseAccess(userId: string, warehouseId: string): Promise<boolean> {
const accessibleWarehouses = await getWarehouseAccess(userId)
return accessibleWarehouses.includes(warehouseId)
}
export async function filterByWarehouseAccess<T extends { warehouse_id: string }>(
userId: string,
items: T[]
): Promise<T[]> {
const accessibleWarehouses = await getWarehouseAccess(userId)
return items.filter(item => accessibleWarehouses.includes(item.warehouse_id))
}
Time-Based Access Control
// lib/auth/time-based-access.ts
export interface AccessSchedule {
user_id: string
day_of_week: number // 0-6 (Sunday-Saturday)
start_time: string // HH:MM format
end_time: string // HH:MM format
timezone: string
}
export async function isWithinAccessHours(userId: string): Promise<boolean> {
const now = new Date()
const dayOfWeek = now.getDay()
const currentTime = now.toTimeString().slice(0, 5) // HH:MM
const { data: schedules } = await supabase
.from('user_access_schedules')
.select('*')
.eq('user_id', userId)
.eq('day_of_week', dayOfWeek)
if (!schedules || schedules.length === 0) {
// No schedule defined, allow access
return true
}
return schedules.some(schedule => {
return currentTime >= schedule.start_time && currentTime <= schedule.end_time
})
}
export async function requireTimeBasedAccess(userId: string): Promise<void> {
const hasAccess = await isWithinAccessHours(userId)
if (!hasAccess) {
throw new Error('Access denied: Outside of permitted hours')
}
}
Permission Audit & Monitoring
Permission Usage Tracking
// lib/auth/audit.ts
export async function logPermissionCheck(details: {
user_id: string
permission: string
granted: boolean
resource_id?: string
context?: any
timestamp: string
}) {
await supabase
.from('permission_audit_log')
.insert({
...details,
session_id: await getCurrentSessionId(),
ip_address: await getClientIP(),
user_agent: await getUserAgent()
})
}
export async function generatePermissionReport(userId: string, startDate: Date, endDate: Date) {
const { data: auditLogs } = await supabase
.from('permission_audit_log')
.select('*')
.eq('user_id', userId)
.gte('timestamp', startDate.toISOString())
.lte('timestamp', endDate.toISOString())
.order('timestamp', { ascending: false })
const permissionUsage = auditLogs?.reduce((acc, log) => {
acc[log.permission] = (acc[log.permission] || 0) + 1
return acc
}, {} as Record<string, number>)
return {
total_checks: auditLogs?.length || 0,
permission_usage: permissionUsage,
denied_attempts: auditLogs?.filter(log => !log.granted).length || 0,
most_used_permissions: Object.entries(permissionUsage || {})
.sort(([,a], [,b]) => b - a)
.slice(0, 10)
}
}
This comprehensive authorization system ensures that Smart Shelf maintains strict access controls while providing the flexibility needed for different organizational roles and contexts.