Add role-based authorization system with admin functionality
- Implement authorization middleware and composables for role checking - Add groups/roles support to authentication and session management - Create admin dashboard pages and API endpoints - Add audit logging utility for tracking user actions - Enhance expense page with role-based access control - Improve session caching with authorization state management
This commit is contained in:
62
server/api/admin/audit-logs/list.ts
Normal file
62
server/api/admin/audit-logs/list.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { requireAdmin } from '~/server/utils/auth';
|
||||
import { getAuditLogs } from '~/server/utils/audit-logger';
|
||||
import type { AuditLogFilters } from '~/server/utils/audit-logger';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
console.log('[ADMIN] Audit logs list request');
|
||||
|
||||
try {
|
||||
// Require admin authentication
|
||||
await requireAdmin(event);
|
||||
|
||||
// Get query parameters
|
||||
const query = getQuery(event);
|
||||
|
||||
const filters: AuditLogFilters = {
|
||||
startDate: query.startDate as string,
|
||||
endDate: query.endDate as string,
|
||||
userId: query.userId as string,
|
||||
userEmail: query.userEmail as string,
|
||||
action: query.action as string,
|
||||
resourceType: query.resourceType as string,
|
||||
status: query.status as 'success' | 'failure',
|
||||
limit: query.limit ? parseInt(query.limit as string) : 50,
|
||||
offset: query.offset ? parseInt(query.offset as string) : 0
|
||||
};
|
||||
|
||||
// Get audit logs
|
||||
const result = await getAuditLogs(filters);
|
||||
|
||||
console.log('[ADMIN] Returning audit logs:', {
|
||||
count: result.list.length,
|
||||
total: result.totalCount,
|
||||
filters
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.list,
|
||||
pagination: {
|
||||
total: result.totalCount,
|
||||
limit: filters.limit || 50,
|
||||
offset: filters.offset || 0,
|
||||
hasMore: (filters.offset || 0) + result.list.length < result.totalCount
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[ADMIN] Failed to get audit logs:', error);
|
||||
|
||||
if (error.statusCode === 403) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Insufficient permissions. Admin access required.'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to retrieve audit logs'
|
||||
};
|
||||
}
|
||||
});
|
||||
43
server/api/admin/audit-logs/stats.ts
Normal file
43
server/api/admin/audit-logs/stats.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { requireAdmin } from '~/server/utils/auth';
|
||||
import { getAuditStats } from '~/server/utils/audit-logger';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
console.log('[ADMIN] Audit stats request');
|
||||
|
||||
try {
|
||||
// Require admin authentication
|
||||
await requireAdmin(event);
|
||||
|
||||
// Get query parameters
|
||||
const query = getQuery(event);
|
||||
const days = query.days ? parseInt(query.days as string) : 30;
|
||||
|
||||
// Get audit statistics
|
||||
const stats = await getAuditStats(days);
|
||||
|
||||
console.log('[ADMIN] Returning audit stats:', {
|
||||
totalEvents: stats.totalEvents,
|
||||
days
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: stats
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[ADMIN] Failed to get audit stats:', error);
|
||||
|
||||
if (error.statusCode === 403) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Insufficient permissions. Admin access required.'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to retrieve audit statistics'
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -7,7 +7,7 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
if (!oidcSessionCookie) {
|
||||
console.log('[SESSION] No OIDC session cookie found')
|
||||
return { user: null, authenticated: false }
|
||||
return { user: null, authenticated: false, groups: [] }
|
||||
}
|
||||
|
||||
console.log('[SESSION] OIDC session cookie found, parsing...')
|
||||
@@ -19,6 +19,7 @@ export default defineEventHandler(async (event) => {
|
||||
console.log('[SESSION] Session data parsed successfully:', {
|
||||
hasUser: !!sessionData.user,
|
||||
hasAccessToken: !!sessionData.accessToken,
|
||||
hasIdToken: !!sessionData.idToken,
|
||||
expiresAt: sessionData.expiresAt,
|
||||
createdAt: sessionData.createdAt,
|
||||
timeUntilExpiry: sessionData.expiresAt ? sessionData.expiresAt - Date.now() : 'unknown'
|
||||
@@ -31,7 +32,7 @@ export default defineEventHandler(async (event) => {
|
||||
domain: cookieDomain,
|
||||
path: '/'
|
||||
})
|
||||
return { user: null, authenticated: false }
|
||||
return { user: null, authenticated: false, groups: [] }
|
||||
}
|
||||
|
||||
// Validate session structure
|
||||
@@ -45,7 +46,7 @@ export default defineEventHandler(async (event) => {
|
||||
domain: cookieDomain,
|
||||
path: '/'
|
||||
})
|
||||
return { user: null, authenticated: false }
|
||||
return { user: null, authenticated: false, groups: [] }
|
||||
}
|
||||
|
||||
// Check if session is still valid
|
||||
@@ -61,13 +62,51 @@ export default defineEventHandler(async (event) => {
|
||||
domain: cookieDomain,
|
||||
path: '/'
|
||||
})
|
||||
return { user: null, authenticated: false }
|
||||
return { user: null, authenticated: false, groups: [] }
|
||||
}
|
||||
|
||||
// Extract groups from ID token
|
||||
let userGroups: string[] = [];
|
||||
if (sessionData.idToken) {
|
||||
try {
|
||||
// Parse JWT payload (base64 decode the middle section)
|
||||
const tokenParts = sessionData.idToken.split('.');
|
||||
if (tokenParts.length >= 2) {
|
||||
const payload = JSON.parse(atob(tokenParts[1]));
|
||||
userGroups = payload.groups || [];
|
||||
console.log('[SESSION] Groups extracted from token:', userGroups);
|
||||
}
|
||||
} catch (tokenError) {
|
||||
console.error('[SESSION] Failed to parse ID token:', tokenError);
|
||||
// Continue without groups - not a fatal error
|
||||
}
|
||||
}
|
||||
|
||||
// Also check access token for groups as fallback
|
||||
if (userGroups.length === 0 && sessionData.accessToken) {
|
||||
try {
|
||||
const tokenParts = sessionData.accessToken.split('.');
|
||||
if (tokenParts.length >= 2) {
|
||||
const payload = JSON.parse(atob(tokenParts[1]));
|
||||
userGroups = payload.groups || [];
|
||||
console.log('[SESSION] Groups extracted from access token:', userGroups);
|
||||
}
|
||||
} catch (tokenError) {
|
||||
console.error('[SESSION] Failed to parse access token:', tokenError);
|
||||
}
|
||||
}
|
||||
|
||||
// Default group assignment if no groups found
|
||||
if (userGroups.length === 0) {
|
||||
console.log('[SESSION] No groups found in token, assigning default "user" group');
|
||||
userGroups = ['user'];
|
||||
}
|
||||
|
||||
console.log('[SESSION] Valid session found for user:', {
|
||||
id: sessionData.user.id,
|
||||
email: sessionData.user.email,
|
||||
username: sessionData.user.username
|
||||
username: sessionData.user.username,
|
||||
groups: userGroups
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -76,9 +115,11 @@ export default defineEventHandler(async (event) => {
|
||||
email: sessionData.user.email,
|
||||
username: sessionData.user.username,
|
||||
name: sessionData.user.name,
|
||||
authMethod: sessionData.user.authMethod || 'keycloak'
|
||||
authMethod: sessionData.user.authMethod || 'keycloak',
|
||||
groups: userGroups
|
||||
},
|
||||
authenticated: true
|
||||
authenticated: true,
|
||||
groups: userGroups
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SESSION] OIDC session check error:', error)
|
||||
@@ -88,6 +129,6 @@ export default defineEventHandler(async (event) => {
|
||||
domain: cookieDomain,
|
||||
path: '/'
|
||||
})
|
||||
return { user: null, authenticated: false }
|
||||
return { user: null, authenticated: false, groups: [] }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { requireAuth } from '@/server/utils/auth';
|
||||
import { requireSalesOrAdmin } from '@/server/utils/auth';
|
||||
import { logAuditEvent } from '@/server/utils/audit-logger';
|
||||
import { getExpenseById } from '@/server/utils/nocodb';
|
||||
import { processExpenseWithCurrency } from '@/server/utils/currency';
|
||||
import { uploadBuffer } from '@/server/utils/minio';
|
||||
import type { Expense } from '@/utils/types';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuth(event);
|
||||
await requireSalesOrAdmin(event);
|
||||
|
||||
const body = await readBody(event);
|
||||
const { expenseIds } = body;
|
||||
@@ -141,7 +142,7 @@ export default defineEventHandler(async (event) => {
|
||||
// Return CSV for direct download
|
||||
setHeader(event, 'Content-Type', 'text/csv');
|
||||
setHeader(event, 'Content-Disposition', `attachment; filename="${filename}"`);
|
||||
setHeader(event, 'Content-Length', csvBuffer.length.toString());
|
||||
setHeader(event, 'Content-Length', csvBuffer.length);
|
||||
|
||||
return csvContent;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user