forked from
nekomimi.pet/wisp.place-monorepo
Monorepo for Wisp.place. A static site hosting service built on top of the AT Protocol.
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}