Monorepo for Wisp.place. A static site hosting service built on top of the AT Protocol.
at main 6.0 kB view raw
1// Admin authentication system 2import { db } from './db' 3import { randomBytes, createHash } from 'crypto' 4 5interface AdminUser { 6 id: number 7 username: string 8 password_hash: string 9 created_at: Date 10} 11 12interface AdminSession { 13 sessionId: string 14 username: string 15 expiresAt: Date 16} 17 18// In-memory session storage 19const sessions = new Map<string, AdminSession>() 20const SESSION_DURATION = 24 * 60 * 60 * 1000 // 24 hours 21 22// Hash password using SHA-256 with salt 23function hashPassword(password: string, salt: string): string { 24 return createHash('sha256').update(password + salt).digest('hex') 25} 26 27// Generate random salt 28function generateSalt(): string { 29 return randomBytes(32).toString('hex') 30} 31 32// Generate session ID 33function generateSessionId(): string { 34 return randomBytes(32).toString('hex') 35} 36 37// Generate a secure random password 38function generatePassword(length: number = 20): string { 39 const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*' 40 const bytes = randomBytes(length) 41 let password = '' 42 for (let i = 0; i < length; i++) { 43 password += chars[bytes[i] % chars.length] 44 } 45 return password 46} 47 48export const adminAuth = { 49 // Initialize admin table 50 async init() { 51 await db` 52 CREATE TABLE IF NOT EXISTS admin_users ( 53 id SERIAL PRIMARY KEY, 54 username TEXT UNIQUE NOT NULL, 55 password_hash TEXT NOT NULL, 56 salt TEXT NOT NULL, 57 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 58 ) 59 ` 60 }, 61 62 // Check if any admin exists 63 async hasAdmin(): Promise<boolean> { 64 const result = await db`SELECT COUNT(*) as count FROM admin_users` 65 return result[0].count > 0 66 }, 67 68 // Create admin user 69 async createAdmin(username: string, password: string): Promise<boolean> { 70 try { 71 const salt = generateSalt() 72 const passwordHash = hashPassword(password, salt) 73 74 await db`INSERT INTO admin_users (username, password_hash, salt) VALUES (${username}, ${passwordHash}, ${salt})` 75 76 console.log(`✓ Admin user '${username}' created successfully`) 77 return true 78 } catch (error) { 79 console.error('Failed to create admin user:', error) 80 return false 81 } 82 }, 83 84 // Verify admin credentials 85 async verify(username: string, password: string): Promise<boolean> { 86 try { 87 const result = await db`SELECT password_hash, salt FROM admin_users WHERE username = ${username}` 88 89 if (result.length === 0) { 90 return false 91 } 92 93 const { password_hash, salt } = result[0] 94 const hash = hashPassword(password, salt as string) 95 return hash === password_hash 96 } catch (error) { 97 console.error('Failed to verify admin:', error) 98 return false 99 } 100 }, 101 102 // Create session 103 createSession(username: string): string { 104 const sessionId = generateSessionId() 105 const expiresAt = new Date(Date.now() + SESSION_DURATION) 106 107 sessions.set(sessionId, { 108 sessionId, 109 username, 110 expiresAt 111 }) 112 113 // Clean up expired sessions 114 this.cleanupSessions() 115 116 return sessionId 117 }, 118 119 // Verify session 120 verifySession(sessionId: string): AdminSession | null { 121 const session = sessions.get(sessionId) 122 123 if (!session) { 124 return null 125 } 126 127 if (session.expiresAt.getTime() < Date.now()) { 128 sessions.delete(sessionId) 129 return null 130 } 131 132 return session 133 }, 134 135 // Delete session 136 deleteSession(sessionId: string) { 137 sessions.delete(sessionId) 138 }, 139 140 // Cleanup expired sessions 141 cleanupSessions() { 142 const now = Date.now() 143 for (const [sessionId, session] of sessions.entries()) { 144 if (session.expiresAt.getTime() < now) { 145 sessions.delete(sessionId) 146 } 147 } 148 } 149} 150 151// Prompt for admin creation on startup 152export async function promptAdminSetup() { 153 await adminAuth.init() 154 155 const hasAdmin = await adminAuth.hasAdmin() 156 if (hasAdmin) { 157 return 158 } 159 160 // Skip prompt if SKIP_ADMIN_SETUP is set 161 if (process.env.SKIP_ADMIN_SETUP === 'true') { 162 console.log('\n╔════════════════════════════════════════════════════════════════╗') 163 console.log('║ ADMIN SETUP REQUIRED ║') 164 console.log('╚════════════════════════════════════════════════════════════════╝\n') 165 console.log('No admin user found.') 166 console.log('Create one with: bun run create-admin.ts\n') 167 return 168 } 169 170 console.log('\n===========================================') 171 console.log(' ADMIN SETUP REQUIRED') 172 console.log('===========================================\n') 173 console.log('No admin user found. Creating one automatically...\n') 174 175 // Auto-generate admin credentials with random password 176 const username = 'admin' 177 const password = generatePassword(20) 178 179 await adminAuth.createAdmin(username, password) 180 181 console.log('╔════════════════════════════════════════════════════════════════╗') 182 console.log('║ ADMIN USER CREATED SUCCESSFULLY ║') 183 console.log('╚════════════════════════════════════════════════════════════════╝\n') 184 console.log(`Username: ${username}`) 185 console.log(`Password: ${password}`) 186 console.log('\n⚠️ IMPORTANT: Save this password securely!') 187 console.log('This password will not be shown again.\n') 188 console.log('Change it with: bun run change-admin-password.ts admin NEW_PASSWORD\n') 189} 190 191// Elysia middleware to protect admin routes 192export function requireAdmin({ cookie, set }: any) { 193 const sessionId = cookie.admin_session?.value 194 195 if (!sessionId) { 196 set.status = 401 197 return { error: 'Unauthorized' } 198 } 199 200 const session = adminAuth.verifySession(sessionId) 201 if (!session) { 202 set.status = 401 203 return { error: 'Unauthorized' } 204 } 205 206 // Session is valid, continue 207 return 208}