Fresha-style booking: shops sign up, add services/therapists/hours, and get a public booking page they can drop into their Google Business Profile so customers book from Maps. Deposits via Stripe stop no-shows. Stack: Hono + Cloudflare Workers + D1 + Stripe, server-rendered HTML. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
38 lines
1.8 KiB
JavaScript
38 lines
1.8 KiB
JavaScript
export async function hashPassword(password) {
|
|
const enc = new TextEncoder()
|
|
const salt = crypto.getRandomValues(new Uint8Array(16))
|
|
const key = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveBits'])
|
|
const bits = await crypto.subtle.deriveBits({ name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' }, key, 256)
|
|
const toHex = buf => Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
return toHex(salt.buffer) + ':' + toHex(bits)
|
|
}
|
|
|
|
export async function verifyPassword(password, hash) {
|
|
const [saltHex, hashHex] = hash.split(':')
|
|
const salt = new Uint8Array(saltHex.match(/.{2}/g).map(h => parseInt(h, 16)))
|
|
const enc = new TextEncoder()
|
|
const key = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveBits'])
|
|
const bits = await crypto.subtle.deriveBits({ name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' }, key, 256)
|
|
const computed = Array.from(new Uint8Array(bits)).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
return computed === hashHex
|
|
}
|
|
|
|
export const genId = () => crypto.randomUUID().replace(/-/g, '')
|
|
|
|
export async function createSession(db, userId) {
|
|
const id = genId()
|
|
const expires = Math.floor(Date.now() / 1000) + 86400 * 30
|
|
await db.prepare('INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)').bind(id, userId, expires).run()
|
|
return id
|
|
}
|
|
|
|
export async function getSession(db, sessionId) {
|
|
if (!sessionId) return null
|
|
return db.prepare(
|
|
'SELECT s.user_id, u.email, u.name FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.id = ? AND s.expires_at > ?'
|
|
).bind(sessionId, Math.floor(Date.now() / 1000)).first()
|
|
}
|
|
|
|
export async function deleteSession(db, sessionId) {
|
|
await db.prepare('DELETE FROM sessions WHERE id = ?').bind(sessionId).run()
|
|
}
|