diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index b5b1963..5cf3f33 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -40,9 +40,7 @@ import CookieConsent from '../components/CookieConsent.astro'; })(); - - - + @@ -110,4 +108,5 @@ import CookieConsent from '../components/CookieConsent.astro'; \ No newline at end of file diff --git a/src/layouts/LoginLayout.astro b/src/layouts/LoginLayout.astro index 720cac0..51dbed7 100644 --- a/src/layouts/LoginLayout.astro +++ b/src/layouts/LoginLayout.astro @@ -50,9 +50,7 @@ import CookieConsent from '../components/CookieConsent.astro'; })(); - - - + @@ -99,4 +97,5 @@ import CookieConsent from '../components/CookieConsent.astro'; \ No newline at end of file diff --git a/src/lib/auth-unified.ts b/src/lib/auth-unified.ts new file mode 100644 index 0000000..b59d4ad --- /dev/null +++ b/src/lib/auth-unified.ts @@ -0,0 +1,383 @@ +/** + * Unified Authentication Module + * This is the SINGLE source of truth for all authentication in the application. + * All auth-related functions should be imported from this file. + */ + +import { createSupabaseServerClient, createSupabaseServerClientFromRequest } from './supabase-ssr'; +import { logSecurityEvent, logUserActivity } from './logger'; +import type { AstroCookies } from 'astro'; +import type { User, Session } from '@supabase/supabase-js'; + +// Custom error class for auth-related errors +export class AuthError extends Error { + constructor( + message: string, + public code: 'NO_SESSION' | 'INVALID_TOKEN' | 'NO_PERMISSION' | 'EXPIRED' | 'NO_ORGANIZATION', + public statusCode: number = 401 + ) { + super(message); + this.name = 'AuthError'; + } +} + +// Auth context interface +export interface AuthContext { + user: User; + session: Session; + isAdmin: boolean; + isSuperAdmin: boolean; + organizationId: string | null; +} + +// Type guard for authentication +export function isAuthenticated(auth: AuthContext | null): auth is AuthContext { + return auth !== null; +} + +/** + * Universal auth verification function + * Works with both Request objects (API routes) and AstroCookies (Astro pages) + */ +export async function verifyAuth(requestOrCookies: Request | AstroCookies): Promise { + try { + // Create appropriate Supabase client based on input type + const supabase = requestOrCookies instanceof Request + ? createSupabaseServerClientFromRequest(requestOrCookies) + : createSupabaseServerClient(requestOrCookies); + + // Get session from cookies + const { data: { session }, error } = await supabase.auth.getSession(); + + if (error || !session) { + // Try Authorization header as fallback (only for Request objects) + if (requestOrCookies instanceof Request) { + const authHeader = requestOrCookies.headers.get('Authorization'); + if (authHeader && authHeader.startsWith('Bearer ')) { + const accessToken = authHeader.substring(7); + const { data: { user }, error: tokenError } = await supabase.auth.getUser(accessToken); + + if (tokenError || !user) { + logSecurityEvent({ + type: 'auth_failure', + ipAddress: getClientIP(requestOrCookies), + userAgent: requestOrCookies.headers.get('User-Agent') || undefined, + severity: 'medium', + details: { error: tokenError?.message, reason: 'invalid_bearer_token' } + }); + return null; + } + + // Create a minimal session for bearer token auth + return await buildAuthContext(user, accessToken, supabase, requestOrCookies); + } + } + + // No valid session or token found + return null; + } + + // Build and return auth context + return await buildAuthContext(session.user, session.access_token, supabase, requestOrCookies); + } catch (error) { + console.error('[Auth] Verification error:', error); + return null; + } +} + +/** + * Build auth context with user data from database + */ +async function buildAuthContext( + user: User, + accessToken: string, + supabaseClient: any, + requestOrCookies: Request | AstroCookies +): Promise { + // Get additional user data from database + const { data: userRecord, error: dbError } = await supabaseClient + .from('users') + .select('role, organization_id, is_super_admin') + .eq('id', user.id) + .single(); + + if (dbError) { + console.error('[Auth] Database user lookup failed:', dbError.message); + } + + // Log successful authentication + if (requestOrCookies instanceof Request) { + logUserActivity({ + action: 'auth_success', + userId: user.id, + ipAddress: getClientIP(requestOrCookies), + userAgent: requestOrCookies.headers.get('User-Agent') || undefined, + details: { organizationId: userRecord?.organization_id, role: userRecord?.role } + }); + } + + return { + user, + session: { + access_token: accessToken, + refresh_token: '', + expires_in: 3600, + expires_at: Date.now() / 1000 + 3600, + token_type: 'bearer', + user + } as Session, + isAdmin: userRecord?.role === 'admin' || false, + isSuperAdmin: userRecord?.role === 'admin' && userRecord?.is_super_admin === true, + organizationId: userRecord?.organization_id || null + }; +} + +/** + * Require authentication - throws if not authenticated + */ +export async function requireAuth(requestOrCookies: Request | AstroCookies): Promise { + const auth = await verifyAuth(requestOrCookies); + + if (!auth) { + if (requestOrCookies instanceof Request) { + logSecurityEvent({ + type: 'access_denied', + ipAddress: getClientIP(requestOrCookies), + userAgent: requestOrCookies.headers.get('User-Agent') || undefined, + severity: 'low', + details: { reason: 'no_authentication' } + }); + } + throw new AuthError('Authentication required', 'NO_SESSION'); + } + + return auth; +} + +/** + * Require admin access - throws if not admin + */ +export async function requireAdmin(requestOrCookies: Request | AstroCookies): Promise { + const auth = await requireAuth(requestOrCookies); + + if (!auth.isAdmin) { + if (requestOrCookies instanceof Request) { + logSecurityEvent({ + type: 'access_denied', + userId: auth.user.id, + ipAddress: getClientIP(requestOrCookies), + userAgent: requestOrCookies.headers.get('User-Agent') || undefined, + severity: 'medium', + details: { reason: 'insufficient_privileges', requiredRole: 'admin' } + }); + } + throw new AuthError('Admin access required', 'NO_PERMISSION', 403); + } + + return auth; +} + +/** + * Require super admin access - throws if not super admin + */ +export async function requireSuperAdmin(requestOrCookies: Request | AstroCookies): Promise { + const auth = await requireAuth(requestOrCookies); + + if (!auth.isSuperAdmin) { + if (requestOrCookies instanceof Request) { + logSecurityEvent({ + type: 'access_denied', + userId: auth.user.id, + ipAddress: getClientIP(requestOrCookies), + userAgent: requestOrCookies.headers.get('User-Agent') || undefined, + severity: 'high', + details: { reason: 'insufficient_privileges', requiredRole: 'super_admin' } + }); + } + throw new AuthError('Super admin access required', 'NO_PERMISSION', 403); + } + + return auth; +} + +/** + * Check if user has access to a specific organization + */ +export async function requireOrganizationAccess( + requestOrCookies: Request | AstroCookies, + organizationId: string +): Promise { + const auth = await requireAuth(requestOrCookies); + + if (auth.organizationId !== organizationId && !auth.isAdmin) { + if (requestOrCookies instanceof Request) { + logSecurityEvent({ + type: 'access_denied', + userId: auth.user.id, + ipAddress: getClientIP(requestOrCookies), + userAgent: requestOrCookies.headers.get('User-Agent') || undefined, + severity: 'high', + details: { + reason: 'organization_access_violation', + userOrganization: auth.organizationId, + requestedOrganization: organizationId + } + }); + } + throw new AuthError('Access denied to this organization', 'NO_PERMISSION', 403); + } + + return auth; +} + +/** + * Middleware wrapper for auth-protected routes + */ +export async function withAuth( + requestOrCookies: Request | AstroCookies, + handler: (auth: AuthContext) => T | Promise +): Promise { + const auth = await verifyAuth(requestOrCookies); + if (!auth) { + throw new AuthError('Not authenticated', 'NO_SESSION'); + } + return handler(auth); +} + +/** + * Generate CSRF token for forms + */ +export function generateCSRFToken(): string { + return crypto.randomUUID(); +} + +/** + * Verify CSRF token + */ +export function verifyCSRFToken(request: Request, sessionToken: string): boolean { + const submittedToken = request.headers.get('X-CSRF-Token') || + request.headers.get('X-Requested-With'); + + return submittedToken === sessionToken; +} + +/** + * Rate limiting - simple in-memory implementation + * For production, use Redis or a proper rate limiting service + */ +const rateLimitStore = new Map(); + +export function checkRateLimit( + identifier: string, + maxRequests: number = 10, + windowMs: number = 60000 +): boolean { + const now = Date.now(); + const windowStart = now - windowMs; + + let entry = rateLimitStore.get(identifier); + + if (!entry || entry.lastReset < windowStart) { + entry = { count: 0, lastReset: now }; + rateLimitStore.set(identifier, entry); + } + + entry.count++; + + // Clean up old entries periodically + if (Math.random() < 0.01) { // 1% chance + cleanupRateLimit(windowStart); + } + + const isAllowed = entry.count <= maxRequests; + + // Log rate limit violations + if (!isAllowed) { + logSecurityEvent({ + type: 'rate_limit', + ipAddress: identifier.includes(':') ? identifier.split(':')[1] : identifier, + severity: 'medium', + details: { + maxRequests, + windowMs, + currentCount: entry.count, + identifier + } + }); + } + + return isAllowed; +} + +function cleanupRateLimit(cutoff: number) { + for (const [key, entry] of rateLimitStore.entries()) { + if (entry.lastReset < cutoff) { + rateLimitStore.delete(key); + } + } +} + +/** + * Get client IP address from request + */ +export function getClientIP(request: Request): string { + // Try various headers that might contain the real IP + const forwardedFor = request.headers.get('X-Forwarded-For'); + const realIP = request.headers.get('X-Real-IP'); + const cfConnectingIP = request.headers.get('CF-Connecting-IP'); + + if (cfConnectingIP) return cfConnectingIP; + if (realIP) return realIP; + if (forwardedFor) return forwardedFor.split(',')[0].trim(); + + // Fallback + return request.headers.get('X-Client-IP') || 'unknown'; +} + +/** + * Create secure response with auth headers + */ +export function createAuthResponse( + body: string | object, + status: number = 200, + additionalHeaders: Record = {} +): Response { + const headers = { + 'Content-Type': typeof body === 'string' ? 'text/plain' : 'application/json', + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'X-XSS-Protection': '1; mode=block', + ...additionalHeaders + }; + + return new Response( + typeof body === 'string' ? body : JSON.stringify(body), + { status, headers } + ); +} + +/** + * Debug utilities (only in development) + */ +export const authDebug = { + logCookies: (request: Request) => { + if (process.env.NODE_ENV !== 'development') return; + const cookieHeader = request.headers.get('Cookie'); + console.log('[Auth Debug] Cookies:', cookieHeader); + }, + + logSession: (session: Session | null) => { + if (process.env.NODE_ENV !== 'development') return; + console.log('[Auth Debug] Session:', session ? { + user_id: session.user.id, + expires_at: new Date(session.expires_at! * 1000).toISOString() + } : 'No session'); + } +}; + +// Re-export frequently used functions with simpler names (backwards compatibility) +export { + verifyAuth as verifyAuthSimple, + requireAdmin as requireAdminSimple, + requireSuperAdmin as requireSuperAdminSimple +}; \ No newline at end of file diff --git a/src/lib/auth.ts b/src/lib/auth.ts index ba34f7c..3148642 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,316 +1,7 @@ -import { supabase } from './supabase'; -import { logSecurityEvent, logUserActivity } from './logger'; -import type { User, Session } from '@supabase/supabase-js'; - -// Get the Supabase URL for cookie name generation -const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL || import.meta.env.SUPABASE_URL; - -export interface AuthContext { - user: User; - session: Session; - isAdmin?: boolean; - organizationId?: string; -} - /** - * Server-side authentication verification - * Validates the auth token from cookies or headers + * DEPRECATED: This file is deprecated. Use auth-unified.ts instead. + * This file now proxies all exports to the unified auth module for backwards compatibility. */ -export async function verifyAuth(request: Request): Promise { - try { - // Get auth token from Authorization header or cookies - const authHeader = request.headers.get('Authorization'); - const cookieHeader = request.headers.get('Cookie'); - - let accessToken: string | null = null; - - // Try Authorization header first - if (authHeader && authHeader.startsWith('Bearer ')) { - accessToken = authHeader.substring(7); - } - - // Try cookies if no auth header - if (!accessToken && cookieHeader) { - const cookies = parseCookies(cookieHeader); - - // Extract Supabase project ref for cookie names - const projectRef = supabaseUrl.split('//')[1].split('.')[0]; - - // Check for various Supabase cookie patterns - accessToken = cookies[`sb-${projectRef}-auth-token`] || - cookies[`sb-${projectRef}-auth-token.0`] || - cookies[`sb-${projectRef}-auth-token.1`] || - cookies['sb-access-token'] || - cookies['supabase-auth-token'] || - cookies['access_token']; - - // Log for debugging (only if no token found) - if (!accessToken) { - console.log('Auth debug - no token found:', { - projectRef, - cookieKeys: Object.keys(cookies).filter(k => k.includes('sb') || k.includes('supabase')), - tokenSource: 'none' - }); - } - } - - if (!accessToken) { - return null; - } - - // Verify the token with Supabase - const { data: { user }, error } = await supabase.auth.getUser(accessToken); - - if (error || !user) { - // Log failed authentication attempt - logSecurityEvent({ - type: 'auth_failure', - ipAddress: getClientIPFromHeaders(request), - userAgent: request.headers.get('User-Agent') || undefined, - severity: 'medium', - details: { error: error?.message, reason: 'invalid_token' } - }); - return null; - } - - // Get user's organization - const { data: userRecord } = await supabase - .from('users') - .select('organization_id, role') - .eq('id', user.id) - .single(); - - // Mock session object (since we're doing server-side verification) - const session: Session = { - access_token: accessToken, - refresh_token: '', // Not needed for verification - expires_in: 3600, - expires_at: Date.now() / 1000 + 3600, - token_type: 'bearer', - user - }; - - // Log successful authentication - logUserActivity({ - action: 'auth_success', - userId: user.id, - ipAddress: getClientIPFromHeaders(request), - userAgent: request.headers.get('User-Agent') || undefined, - details: { organizationId: userRecord?.organization_id, role: userRecord?.role } - }); - return { - user, - session, - isAdmin: userRecord?.role === 'admin', - organizationId: userRecord?.organization_id - }; - } catch (error) { - console.error('Error verifying auth:', error); - return null; - } -} - -/** - * Middleware function to protect routes - */ -export async function requireAuth(request: Request): Promise { - const auth = await verifyAuth(request); - - if (!auth) { - logSecurityEvent({ - type: 'access_denied', - ipAddress: getClientIPFromHeaders(request), - userAgent: request.headers.get('User-Agent') || undefined, - severity: 'low', - details: { reason: 'no_authentication' } - }); - throw new Error('Authentication required'); - } - - return auth; -} - -/** - * Middleware function to require admin access - */ -export async function requireAdmin(request: Request): Promise { - const auth = await requireAuth(request); - - if (!auth.isAdmin) { - logSecurityEvent({ - type: 'access_denied', - userId: auth.user.id, - ipAddress: getClientIPFromHeaders(request), - userAgent: request.headers.get('User-Agent') || undefined, - severity: 'medium', - details: { reason: 'insufficient_privileges', requiredRole: 'admin' } - }); - throw new Error('Admin access required'); - } - - return auth; -} - -/** - * Check if user has access to a specific organization - */ -export async function requireOrganizationAccess( - request: Request, - organizationId: string -): Promise { - const auth = await requireAuth(request); - - if (auth.organizationId !== organizationId && !auth.isAdmin) { - logSecurityEvent({ - type: 'access_denied', - userId: auth.user.id, - ipAddress: getClientIPFromHeaders(request), - userAgent: request.headers.get('User-Agent') || undefined, - severity: 'high', - details: { - reason: 'organization_access_violation', - userOrganization: auth.organizationId, - requestedOrganization: organizationId - } - }); - throw new Error('Access denied to this organization'); - } - - return auth; -} - -/** - * Generate CSRF token - */ -export function generateCSRFToken(): string { - return crypto.randomUUID(); -} - -/** - * Verify CSRF token - */ -export function verifyCSRFToken(request: Request, sessionToken: string): boolean { - const submittedToken = request.headers.get('X-CSRF-Token') || - request.headers.get('X-Requested-With'); - - return submittedToken === sessionToken; -} - -/** - * Rate limiting - simple in-memory implementation - * For production, use Redis or a proper rate limiting service - */ -const rateLimitStore = new Map(); - -export function checkRateLimit( - identifier: string, - maxRequests: number = 10, - windowMs: number = 60000 -): boolean { - const now = Date.now(); - const windowStart = now - windowMs; - - let entry = rateLimitStore.get(identifier); - - if (!entry || entry.lastReset < windowStart) { - entry = { count: 0, lastReset: now }; - rateLimitStore.set(identifier, entry); - } - - entry.count++; - - // Clean up old entries periodically - if (Math.random() < 0.01) { // 1% chance - cleanupRateLimit(windowStart); - } - - const isAllowed = entry.count <= maxRequests; - - // Log rate limit violations - if (!isAllowed) { - logSecurityEvent({ - type: 'rate_limit', - ipAddress: identifier.includes(':') ? identifier.split(':')[1] : identifier, - severity: 'medium', - details: { - maxRequests, - windowMs, - currentCount: entry.count, - identifier - } - }); - } - - return isAllowed; -} - -function cleanupRateLimit(cutoff: number) { - for (const [key, entry] of rateLimitStore.entries()) { - if (entry.lastReset < cutoff) { - rateLimitStore.delete(key); - } - } -} - -/** - * Parse cookies from cookie header - */ -function parseCookies(cookieHeader: string): Record { - const cookies: Record = {}; - - cookieHeader.split(';').forEach(cookie => { - const [name, ...rest] = cookie.trim().split('='); - if (name && rest.length > 0) { - cookies[name] = rest.join('='); - } - }); - - return cookies; -} - -/** - * Create secure response with auth headers - */ -export function createAuthResponse( - body: string | object, - status: number = 200, - additionalHeaders: Record = {} -): Response { - const headers = { - 'Content-Type': typeof body === 'string' ? 'text/plain' : 'application/json', - 'X-Content-Type-Options': 'nosniff', - 'X-Frame-Options': 'DENY', - 'X-XSS-Protection': '1; mode=block', - ...additionalHeaders - }; - - return new Response( - typeof body === 'string' ? body : JSON.stringify(body), - { status, headers } - ); -} - -/** - * Get client IP address for rate limiting - */ -export function getClientIP(request: Request): string { - return getClientIPFromHeaders(request); -} - -/** - * Helper function to extract IP from headers - */ -function getClientIPFromHeaders(request: Request): string { - // Try various headers that might contain the real IP - const forwardedFor = request.headers.get('X-Forwarded-For'); - const realIP = request.headers.get('X-Real-IP'); - const cfConnectingIP = request.headers.get('CF-Connecting-IP'); - - if (cfConnectingIP) return cfConnectingIP; - if (realIP) return realIP; - if (forwardedFor) return forwardedFor.split(',')[0].trim(); - - // Fallback to connection IP (may not be available in all environments) - return request.headers.get('X-Client-IP') || 'unknown'; -} \ No newline at end of file +// Re-export everything from the unified auth module +export * from './auth-unified'; \ No newline at end of file diff --git a/src/lib/simple-auth.ts b/src/lib/simple-auth.ts index 2b95f9f..3148642 100644 --- a/src/lib/simple-auth.ts +++ b/src/lib/simple-auth.ts @@ -1,126 +1,7 @@ -import { supabase } from './supabase'; -import { createSupabaseServerClientFromRequest } from './supabase-ssr'; -import type { User, Session } from '@supabase/supabase-js'; - /** - * Parse cookie header into key-value pairs + * DEPRECATED: This file is deprecated. Use auth-unified.ts instead. + * This file now proxies all exports to the unified auth module for backwards compatibility. */ -function parseCookies(cookieHeader: string): Record { - return cookieHeader.split(';').reduce((acc, cookie) => { - const [key, value] = cookie.trim().split('='); - if (key && value) { - acc[key] = decodeURIComponent(value); - } - return acc; - }, {} as Record); -} -export interface AuthContext { - user: User; - session: Session; - isAdmin?: boolean; - isSuperAdmin?: boolean; - organizationId?: string; -} - -/** - * Simplified authentication verification without logging - * Uses Supabase's built-in server-side session parsing - */ -export async function verifyAuthSimple(request: Request): Promise { - try { - // Create SSR Supabase client - const supabaseSSR = createSupabaseServerClientFromRequest(request); - - // Get the session from cookies - const { data: { session }, error } = await supabaseSSR.auth.getSession(); - - console.log('Session check:', session ? 'Session found' : 'No session', error?.message); - - if (error || !session) { - // Try Authorization header as fallback - const authHeader = request.headers.get('Authorization'); - console.log('Auth header:', authHeader ? 'Present' : 'Not present'); - - if (authHeader && authHeader.startsWith('Bearer ')) { - const accessToken = authHeader.substring(7); - - // Verify the token with Supabase - const { data, error } = await supabase.auth.getUser(accessToken); - - if (error || !data.user) { - console.log('Bearer token verification failed:', error?.message); - return null; - } - - return await buildAuthContext(data.user, accessToken, supabaseSSR); - } - - return null; - } - - return await buildAuthContext(session.user, session.access_token, supabaseSSR); - } catch (error) { - console.error('Auth verification failed:', error); - return null; - } -} - -/** - * Build auth context with user data - */ -async function buildAuthContext(user: any, accessToken: string, supabaseClient: any): Promise { - // Get user role from database - const { data: userRecord, error: dbError } = await supabaseClient - .from('users') - .select('role, organization_id, is_super_admin') - .eq('id', user.id) - .single(); - - if (dbError) { - console.log('Database user lookup failed:', dbError.message); - // Continue without role data - better than failing entirely - } - - return { - user, - session: { access_token: accessToken } as Session, - isAdmin: userRecord?.role === 'admin' || false, - isSuperAdmin: userRecord?.role === 'admin' && userRecord?.is_super_admin === true, - organizationId: userRecord?.organization_id - }; -} - -/** - * Simplified admin requirement check - */ -export async function requireAdminSimple(request: Request): Promise { - const auth = await verifyAuthSimple(request); - - if (!auth) { - throw new Error('Authentication required'); - } - - if (!auth.isAdmin) { - throw new Error('Admin access required'); - } - - return auth; -} - -/** - * Super admin requirement check - */ -export async function requireSuperAdminSimple(request: Request): Promise { - const auth = await verifyAuthSimple(request); - - if (!auth) { - throw new Error('Authentication required'); - } - - if (!auth.isSuperAdmin) { - throw new Error('Super admin access required'); - } - - return auth; -} \ No newline at end of file +// Re-export everything from the unified auth module +export * from './auth-unified'; \ No newline at end of file diff --git a/src/pages/auth-test-unified.astro b/src/pages/auth-test-unified.astro new file mode 100644 index 0000000..d6f8594 --- /dev/null +++ b/src/pages/auth-test-unified.astro @@ -0,0 +1,202 @@ +--- +import Layout from '../layouts/Layout.astro'; +import { verifyAuth, authDebug } from '../lib/auth-unified'; + +export const prerender = false; + +// Test authentication +let auth = null; +let authError = null; +let cookies = null; +let headers = null; + +try { + // Test unified auth + auth = await verifyAuth(Astro.request); + + // Capture debug info + cookies = Astro.request.headers.get('Cookie') || 'No cookies'; + headers = { + 'Authorization': Astro.request.headers.get('Authorization') || 'Not set', + 'User-Agent': Astro.request.headers.get('User-Agent') || 'Not set', + 'X-Forwarded-For': Astro.request.headers.get('X-Forwarded-For') || 'Not set' + }; + + // Use debug utilities in development + if (import.meta.env.DEV) { + authDebug.logCookies(Astro.request); + authDebug.logSession(auth?.session || null); + } +} catch (error) { + authError = error instanceof Error ? error.message : 'Unknown error'; +} +--- + + +
+
+

Unified Authentication System Test

+ + +
+

Authentication Status

+ + {auth ? ( +
+
+

✅ Authenticated

+
+

User ID: {auth.user.id}

+

Email: {auth.user.email}

+

Is Admin: {auth.isAdmin ? 'Yes' : 'No'}

+

Is Super Admin: {auth.isSuperAdmin ? 'Yes' : 'No'}

+

Organization ID: {auth.organizationId || 'None'}

+
+
+ + +
+

Session Information

+
+

Token Type: {auth.session.token_type}

+

Expires At: {new Date((auth.session.expires_at || 0) * 1000).toLocaleString()}

+

Access Token: {auth.session.access_token.substring(0, 20)}...

+
+
+ + +
+ + User Dashboard + + {auth.isAdmin && ( + + Admin Dashboard + + )} + {auth.isSuperAdmin && ( + + Super Dashboard + + )} + + Check Session API + +
+
+ ) : ( +
+
+

❌ Not Authenticated

+

You are not logged in or your session has expired.

+ {authError && ( +

Error: {authError}

+ )} +
+ + +
+ )} +
+ + +
+

Request Debug Information

+ +
+ +
+

Cookies

+
{cookies}
+
+ + +
+

Relevant Headers

+
{JSON.stringify(headers, null, 2)}
+
+
+
+ + + + + +
+

Unified Auth System Information

+ +
+

Features

+
    +
  • ✅ Single source of truth for authentication
  • +
  • ✅ Works with both Request objects and AstroCookies
  • +
  • ✅ Supabase SSR integration
  • +
  • ✅ Bearer token fallback support
  • +
  • ✅ Role-based access control (User/Admin/Super Admin)
  • +
  • ✅ Organization-based access control
  • +
  • ✅ Security logging and rate limiting
  • +
  • ✅ Type-safe auth context
  • +
+ +

Usage

+
+// In Astro pages
+import { verifyAuth, requireAuth } from '../lib/auth-unified';
+
+// Check auth (returns null if not authenticated)
+const auth = await verifyAuth(Astro.request);
+
+// Require auth (throws if not authenticated)
+const auth = await requireAuth(Astro.request);
+
+// Require admin
+const auth = await requireAdmin(Astro.request);
+          
+
+
+
+
+
+ + \ No newline at end of file diff --git a/src/pages/dashboard.astro b/src/pages/dashboard.astro index 76808d3..f329b3b 100644 --- a/src/pages/dashboard.astro +++ b/src/pages/dashboard.astro @@ -6,12 +6,11 @@ import { verifyAuth } from '../lib/auth'; // Enable server-side rendering for auth checks export const prerender = false; -// Disable server-side auth check temporarily to fix redirect loop -// We'll handle auth check on the client side in the script section -// const auth = await verifyAuth(Astro.request); -// if (!auth) { -// return Astro.redirect('/login'); -// } +// Server-side auth check using cookies for better SSR compatibility +const auth = await verifyAuth(Astro.cookies); +if (!auth) { + return Astro.redirect('/login'); +} --- diff --git a/src/styles/glassmorphism.css b/src/styles/glassmorphism.css index bbb8314..98a460d 100644 --- a/src/styles/glassmorphism.css +++ b/src/styles/glassmorphism.css @@ -25,6 +25,8 @@ --bg-orb-1: rgba(147, 51, 234, 0.2); --bg-orb-2: rgba(59, 130, 246, 0.2); --bg-orb-3: rgba(99, 102, 241, 0.1); + --bg-orb-4: rgba(147, 51, 234, 0.15); + --bg-orb-5: rgba(59, 130, 246, 0.12); /* Premium Color Palette */ --success-color: #34d399; diff --git a/src/styles/global.css b/src/styles/global.css index 9ee6536..3a847e4 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1,7 +1,6 @@ @tailwind base; @tailwind components; @tailwind utilities; -@import "./glassmorphism.css"; /* Accessibility Styles */