(c: Context, next: Next)
| 57 | * Caches verified user identity for 30s to reduce round-trips. |
| 58 | */ |
| 59 | export async function authMiddleware(c: Context, next: Next): Promise<Response | void> { |
| 60 | const authHeader = c.req.header('Authorization'); |
| 61 | |
| 62 | if (!authHeader || !authHeader.startsWith('Bearer ')) { |
| 63 | return c.json({ error: 'Missing or invalid Authorization header' }, 401); |
| 64 | } |
| 65 | |
| 66 | const token = authHeader.slice(7); |
| 67 | |
| 68 | let userId: string; |
| 69 | let userEmail: string; |
| 70 | |
| 71 | // Check if this token was recently verified |
| 72 | const cached = verifiedUserCache.get(token); |
| 73 | if (cached && Date.now() - cached.verifiedAt < VERIFY_TTL) { |
| 74 | userId = cached.id; |
| 75 | userEmail = cached.email; |
| 76 | } else { |
| 77 | // Verify JWT signature + expiry via Supabase |
| 78 | try { |
| 79 | const { data: { user }, error } = await supabase.auth.getUser(token); |
| 80 | if (error || !user) { |
| 81 | return c.json({ error: 'Invalid or expired token' }, 401); |
| 82 | } |
| 83 | userId = user.id; |
| 84 | userEmail = user.email ?? ''; |
| 85 | verifiedUserCache.set(token, { id: userId, email: userEmail, verifiedAt: Date.now() }); |
| 86 | } catch (err) { |
| 87 | console.error('[auth] Supabase verification failed:', err instanceof Error ? err.message : err); |
| 88 | return c.json({ error: 'Authentication service unavailable' }, 503); |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | // Cached profile lookup |
| 93 | const profile = await getProfile(userId); |
| 94 | |
| 95 | const authUser: AuthUser = { |
| 96 | id: userId, |
| 97 | email: userEmail, |
| 98 | role: (profile?.role as 'user' | 'admin') ?? 'user', |
| 99 | displayName: profile?.display_name ?? '', |
| 100 | avatarUrl: profile?.avatar_url ?? '', |
| 101 | }; |
| 102 | |
| 103 | c.set('user', authUser); |
| 104 | c.set('userId', userId); |
| 105 | await next(); |
| 106 | } |
| 107 | |
| 108 | /** |
| 109 | * Requires the authenticated user to have admin role. |
nothing calls this directly
no test coverage detected