This commit fixes the persistent login/redirect loop issue and implements a robust authentication system for the Docker/localhost environment. Key Changes: - Environment-aware cookie configuration in supabase-ssr.ts - New AuthLoader component to prevent content flashing during auth checks - Cleaned up login page client-side auth logic to prevent redirect loops - Updated dashboard to use AuthLoader for smooth authentication experience Technical Details: - Cookies now use environment-appropriate security settings - Server-side auth verification eliminates client-side timing issues - Loading states provide better UX during auth transitions - Unified authentication pattern across all protected pages Fixes: - Dashboard no longer flashes before auth redirect - Login page loads cleanly without auth checking loops - Cookie configuration works correctly in Docker localhost - No more redirect loops between login and dashboard pages 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
import { createServerClient, type CookieOptions } from '@supabase/ssr'
|
|
import type { Database } from './database.types'
|
|
import type { AstroCookies } from 'astro'
|
|
|
|
export function createSupabaseServerClient(
|
|
cookies: AstroCookies,
|
|
cookieOptions?: CookieOptions
|
|
) {
|
|
// Environment-aware cookie configuration
|
|
const isProduction = import.meta.env.PROD || process.env.NODE_ENV === 'production';
|
|
|
|
// For Docker/localhost, always use non-secure cookies
|
|
// In production, this will be overridden to use secure cookies
|
|
const useSecureCookies = isProduction;
|
|
|
|
const defaultCookieOptions: CookieOptions = {
|
|
secure: useSecureCookies, // secure in production, non-secure for localhost
|
|
sameSite: 'lax', // allow cross-site cookie on navigation
|
|
path: '/', // root-wide access
|
|
httpOnly: true, // JS-inaccessible for security
|
|
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
};
|
|
|
|
return createServerClient<Database>(
|
|
import.meta.env.PUBLIC_SUPABASE_URL!,
|
|
import.meta.env.PUBLIC_SUPABASE_ANON_KEY!,
|
|
{
|
|
cookies: {
|
|
get(name: string) {
|
|
if (!cookies) return undefined;
|
|
return cookies.get(name)?.value
|
|
},
|
|
set(name: string, value: string, options: CookieOptions) {
|
|
if (!cookies) return;
|
|
// Merge with default options, allowing overrides
|
|
cookies.set(name, value, {
|
|
...defaultCookieOptions,
|
|
...cookieOptions,
|
|
...options,
|
|
})
|
|
},
|
|
remove(name: string, options: CookieOptions) {
|
|
if (!cookies) return;
|
|
cookies.delete(name, {
|
|
...defaultCookieOptions,
|
|
...options,
|
|
})
|
|
},
|
|
},
|
|
cookieOptions: defaultCookieOptions,
|
|
}
|
|
)
|
|
}
|
|
|
|
// Helper to create a client from Request headers (for API routes)
|
|
export function createSupabaseServerClientFromRequest(request: Request) {
|
|
const cookieHeader = request.headers.get('Cookie') || ''
|
|
|
|
return createServerClient<Database>(
|
|
import.meta.env.PUBLIC_SUPABASE_URL!,
|
|
import.meta.env.PUBLIC_SUPABASE_ANON_KEY!,
|
|
{
|
|
cookies: {
|
|
get(name: string) {
|
|
const cookies = parseCookies(cookieHeader)
|
|
return cookies[name]
|
|
},
|
|
set() {
|
|
// Can't set cookies in request context
|
|
},
|
|
remove() {
|
|
// Can't remove cookies in request context
|
|
},
|
|
},
|
|
}
|
|
)
|
|
}
|
|
|
|
function parseCookies(cookieHeader: string): Record<string, string> {
|
|
return cookieHeader.split(';').reduce((acc, cookie) => {
|
|
const [key, value] = cookie.trim().split('=')
|
|
if (key && value) {
|
|
acc[key] = decodeURIComponent(value)
|
|
}
|
|
return acc
|
|
}, {} as Record<string, string>)
|
|
} |