Highly ambitious ATProtocol AppView service and sdks
at main 290 lines 8.1 kB view raw
1import type { SessionStore } from "./store.ts"; 2import type { SessionData } from "./types.ts"; 3import { 4 OAuthClient, 5 type OAuthTokens, 6 type OAuthConfig, 7 type OAuthStorage, 8} from "@slices/oauth"; 9 10export interface OAuthSessionOptions { 11 sessionStore: SessionStore; 12 oauthConfig: OAuthConfig; 13 oauthStorage: OAuthStorage; 14 autoRefresh?: boolean; 15 onTokenRefresh?: (sessionId: string, tokens: OAuthTokens) => Promise<void>; 16 onLogout?: (sessionId: string) => Promise<void>; 17} 18 19export class OAuthSessionManager { 20 private options: OAuthSessionOptions; 21 22 constructor(options: OAuthSessionOptions) { 23 this.options = options; 24 } 25 26 async createOAuthSession(tokens: OAuthTokens): Promise<string | null> { 27 try { 28 // Create temporary OAuth client to fetch user info 29 const tempClient = new OAuthClient( 30 this.options.oauthConfig, 31 this.options.oauthStorage, 32 "temp_" + Date.now() 33 ); 34 35 // Temporarily store tokens to fetch user info 36 await this.options.oauthStorage.setTokens( 37 tokens, 38 tempClient.getSessionId() 39 ); 40 41 // Get user info from OAuth 42 const userInfo = await tempClient.getUserInfo(); 43 if (!userInfo) { 44 await this.options.oauthStorage.clearTokens(tempClient.getSessionId()); 45 return null; 46 } 47 48 // Clean up temp tokens 49 await this.options.oauthStorage.clearTokens(tempClient.getSessionId()); 50 51 // Create session FIRST to get sessionId 52 const sessionId = await this.options.sessionStore.createSession( 53 userInfo.sub, 54 userInfo.name, 55 { 56 userInfo: { 57 ...userInfo, 58 handle: userInfo.name, 59 }, 60 } 61 ); 62 63 // NOW store tokens by sessionId 64 await this.options.oauthStorage.setTokens(tokens, sessionId); 65 66 return sessionId; 67 } catch (error) { 68 console.error("Failed to create OAuth session:", error); 69 return null; 70 } 71 } 72 73 async getOAuthSession(sessionId: string): Promise<SessionData | null> { 74 const session = await this.options.sessionStore.getSession(sessionId); 75 if (!session) { 76 return null; 77 } 78 79 // Auto-refresh is handled by OAuth client, not session storage 80 if (this.options.autoRefresh) { 81 try { 82 // Create session-scoped OAuth client 83 const sessionClient = new OAuthClient( 84 this.options.oauthConfig, 85 this.options.oauthStorage, 86 sessionId 87 ); 88 89 // This ensures tokens are fresh in OAuth storage 90 await sessionClient.ensureValidToken(); 91 92 // Call refresh callback if provided 93 if (this.options.onTokenRefresh) { 94 const tokens = await sessionClient.getTokens(); 95 if (tokens) { 96 await this.options.onTokenRefresh(sessionId, tokens); 97 } 98 } 99 } catch (error) { 100 console.error("Failed to refresh OAuth tokens:", error); 101 // Session is still valid even if token refresh fails 102 } 103 } 104 105 return session; 106 } 107 108 async logout(sessionId: string): Promise<void> { 109 try { 110 // Create session-scoped OAuth client and logout 111 const sessionClient = new OAuthClient( 112 this.options.oauthConfig, 113 this.options.oauthStorage, 114 sessionId 115 ); 116 await sessionClient.logout(); 117 118 // Delete session 119 await this.options.sessionStore.deleteSession(sessionId); 120 121 // Call logout callback if provided 122 if (this.options.onLogout) { 123 await this.options.onLogout(sessionId); 124 } 125 } catch (error) { 126 console.error("Failed to logout OAuth session:", error); 127 // Still delete the session even if OAuth logout fails 128 await this.options.sessionStore.deleteSession(sessionId); 129 } 130 } 131 132 async hasValidOAuthTokens(sessionId: string): Promise<boolean> { 133 const session = await this.getOAuthSession(sessionId); 134 if (!session) return false; 135 136 try { 137 // Create session-scoped OAuth client 138 const sessionClient = new OAuthClient( 139 this.options.oauthConfig, 140 this.options.oauthStorage, 141 sessionId 142 ); 143 // Let OAuth client determine token validity 144 const tokens = await sessionClient.ensureValidToken(); 145 return !!tokens.accessToken; 146 } catch (_error) { 147 return false; 148 } 149 } 150 151 async getAccessToken(sessionId: string): Promise<string | null> { 152 const session = await this.getOAuthSession(sessionId); 153 if (!session) return null; 154 155 try { 156 // Create session-scoped OAuth client 157 const sessionClient = new OAuthClient( 158 this.options.oauthConfig, 159 this.options.oauthStorage, 160 sessionId 161 ); 162 // Get fresh tokens from OAuth client 163 const tokens = await sessionClient.ensureValidToken(); 164 return tokens.accessToken || null; 165 } catch (_error) { 166 return null; 167 } 168 } 169} 170 171// Convenience function to create an OAuth-enabled session store 172export function withOAuthSession( 173 sessionStore: SessionStore, 174 oauthConfig: OAuthConfig, 175 oauthStorage: OAuthStorage, 176 options: Partial< 177 Omit<OAuthSessionOptions, "sessionStore" | "oauthConfig" | "oauthStorage"> 178 > = {} 179): OAuthSessionManager { 180 return new OAuthSessionManager({ 181 sessionStore, 182 oauthConfig, 183 oauthStorage, 184 autoRefresh: true, 185 ...options, 186 }); 187} 188 189// Helper middleware for frameworks 190export interface OAuthSessionMiddlewareOptions extends OAuthSessionOptions { 191 cookieName?: string; 192 loginRedirect?: string; 193 logoutRedirect?: string; 194} 195 196export function createOAuthSessionMiddleware( 197 options: OAuthSessionMiddlewareOptions 198) { 199 const manager = new OAuthSessionManager(options); 200 const cookieName = options.cookieName || "slice-session"; 201 202 return { 203 // Get current user from session 204 async getCurrentUser(request: Request) { 205 const sessionId = getSessionIdFromRequest(request, cookieName); 206 if (!sessionId) { 207 return { isAuthenticated: false }; 208 } 209 210 const session = await manager.getOAuthSession(sessionId); 211 if (!session) { 212 return { isAuthenticated: false }; 213 } 214 215 return { 216 isAuthenticated: true, 217 sub: session.userId, 218 handle: session.handle, 219 // User info from session data (no OAuth tokens) 220 ...(session.data?.userInfo || {}), 221 }; 222 }, 223 224 // Login handler - expects tokens from OAuth callback 225 async login(tokens: OAuthTokens): Promise<Response> { 226 const sessionId = await manager.createOAuthSession(tokens); 227 if (!sessionId) { 228 return new Response("Authentication failed", { status: 401 }); 229 } 230 231 const cookie = options.sessionStore.createSessionCookie(sessionId); 232 const redirectUrl = options.loginRedirect || "/"; 233 234 return new Response("", { 235 status: 302, 236 headers: { 237 Location: redirectUrl, 238 "Set-Cookie": cookie, 239 }, 240 }); 241 }, 242 243 // Logout handler 244 async logout(request: Request): Promise<Response> { 245 const sessionId = getSessionIdFromRequest(request, cookieName); 246 if (sessionId) { 247 await manager.logout(sessionId); 248 } 249 250 const cookie = options.sessionStore.createLogoutCookie(); 251 const redirectUrl = options.logoutRedirect || "/"; 252 253 return new Response("", { 254 status: 302, 255 headers: { 256 Location: redirectUrl, 257 "Set-Cookie": cookie, 258 }, 259 }); 260 }, 261 262 manager, 263 }; 264} 265 266// Helper to extract session ID from request 267function getSessionIdFromRequest( 268 request: Request, 269 cookieName: string 270): string | null { 271 const cookieHeader = request.headers.get("cookie"); 272 if (!cookieHeader) return null; 273 274 const cookies = parseCookies(cookieHeader); 275 return cookies[cookieName] || null; 276} 277 278// Simple cookie parser 279function parseCookies(cookieString: string): Record<string, string> { 280 const cookies: Record<string, string> = {}; 281 282 cookieString.split(";").forEach((cookie) => { 283 const [key, value] = cookie.split("=").map((s) => s.trim()); 284 if (key && value) { 285 cookies[decodeURIComponent(key)] = decodeURIComponent(value); 286 } 287 }); 288 289 return cookies; 290}