Highly ambitious ATProtocol AppView service and sdks
at main 159 lines 4.4 kB view raw
1import type { SessionAdapter, SessionData, SessionOptions, SessionUser } from "./types.ts"; 2import { parseCookie, serializeCookie } from "./cookie.ts"; 3 4const DEFAULT_SESSION_TTL = 30 * 24 * 60 * 60 * 1000; // 30 days 5const DEFAULT_CLEANUP_INTERVAL = 60 * 60 * 1000; // 1 hour 6const DEFAULT_COOKIE_NAME = "slice-session"; 7 8export class SessionStore { 9 private adapter: SessionAdapter; 10 private options: Required<SessionOptions>; 11 private cleanupTimer?: number; 12 13 constructor(options: SessionOptions) { 14 this.adapter = options.adapter; 15 this.options = { 16 adapter: options.adapter, 17 cookieName: options.cookieName ?? DEFAULT_COOKIE_NAME, 18 cookieOptions: { 19 httpOnly: true, 20 secure: true, 21 sameSite: "lax", 22 path: "/", 23 ...options.cookieOptions, 24 }, 25 sessionTTL: options.sessionTTL ?? DEFAULT_SESSION_TTL, 26 cleanupInterval: options.cleanupInterval ?? DEFAULT_CLEANUP_INTERVAL, 27 generateId: options.generateId ?? (() => crypto.randomUUID()), 28 }; 29 30 // Start cleanup timer 31 this.startCleanupTimer(); 32 } 33 34 private startCleanupTimer() { 35 if (this.cleanupTimer) { 36 clearInterval(this.cleanupTimer); 37 } 38 39 this.cleanupTimer = setInterval(() => { 40 this.cleanup().catch(console.error); 41 }, this.options.cleanupInterval); 42 } 43 44 async createSession(userId: string, handle?: string, data?: Record<string, unknown>): Promise<string> { 45 const sessionId = this.options.generateId(); 46 const now = Date.now(); 47 48 const sessionData: SessionData = { 49 sessionId, 50 userId, 51 handle, 52 isAuthenticated: true, 53 data, 54 createdAt: now, 55 expiresAt: now + this.options.sessionTTL, 56 lastAccessedAt: now, 57 }; 58 59 await this.adapter.set(sessionId, sessionData); 60 return sessionId; 61 } 62 63 async getSession(sessionId: string): Promise<SessionData | null> { 64 if (!sessionId) return null; 65 66 const session = await this.adapter.get(sessionId); 67 if (!session) return null; 68 69 // Check if session is expired 70 if (session.expiresAt < Date.now()) { 71 await this.adapter.delete(sessionId); 72 return null; 73 } 74 75 // Update last accessed time 76 await this.adapter.update(sessionId, { 77 lastAccessedAt: Date.now(), 78 }); 79 80 return session; 81 } 82 83 async updateSession(sessionId: string, updates: Partial<SessionData>): Promise<boolean> { 84 const session = await this.getSession(sessionId); 85 if (!session) return false; 86 87 // Extend expiration on update 88 const extendedExpiration = Date.now() + this.options.sessionTTL; 89 90 return await this.adapter.update(sessionId, { 91 ...updates, 92 expiresAt: extendedExpiration, 93 lastAccessedAt: Date.now(), 94 }); 95 } 96 97 async deleteSession(sessionId: string): Promise<void> { 98 await this.adapter.delete(sessionId); 99 } 100 101 async cleanup(): Promise<number> { 102 return await this.adapter.cleanup(Date.now()); 103 } 104 105 // Get session from request cookies 106 async getSessionFromRequest(request: Request): Promise<SessionData | null> { 107 const cookieHeader = request.headers.get("cookie"); 108 if (!cookieHeader) return null; 109 110 const cookies = parseCookie(cookieHeader); 111 const sessionId = cookies[this.options.cookieName]; 112 if (!sessionId) return null; 113 114 return await this.getSession(sessionId); 115 } 116 117 // Get user info from session 118 async getCurrentUser(request: Request): Promise<SessionUser> { 119 const session = await this.getSessionFromRequest(request); 120 121 if (!session) { 122 return { 123 isAuthenticated: false, 124 }; 125 } 126 127 return { 128 sessionId: session.sessionId, 129 sub: session.userId, 130 handle: session.handle, 131 isAuthenticated: session.isAuthenticated, 132 ...session.data, 133 }; 134 } 135 136 // Create session cookie header 137 createSessionCookie(sessionId: string): string { 138 return serializeCookie(this.options.cookieName, sessionId, { 139 ...this.options.cookieOptions, 140 maxAge: Math.floor(this.options.sessionTTL / 1000), // Convert to seconds 141 }); 142 } 143 144 // Create logout cookie header (clears the session) 145 createLogoutCookie(): string { 146 return serializeCookie(this.options.cookieName, "", { 147 ...this.options.cookieOptions, 148 maxAge: 0, 149 }); 150 } 151 152 // Cleanup on destroy 153 destroy() { 154 if (this.cleanupTimer) { 155 clearInterval(this.cleanupTimer); 156 this.cleanupTimer = undefined; 157 } 158 } 159}