Add session-based authentication with login/logout

- Users and sessions tables (Argon2 password hashing, SHA-256 session tokens)
- Server hooks validate session cookie on every request
- (app) routes redirect to /login if not authenticated
- Login page with email/password, styled matching budget app
- Logout via POST form action (invalidates session)
- User display name and sign out button in header
- create-user CLI script: npm run create-user <email> <password> [name]
- 30-day sessions with auto-refresh after 15 days

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 10:21:06 +07:00
parent cc4d8480cf
commit 04ca0a8299
14 changed files with 1232 additions and 3 deletions
+54
View File
@@ -0,0 +1,54 @@
/**
* Create a user for buildfor_life_repair.
*
* Usage:
* npx tsx scripts/create-user.ts <email> <password> [displayName]
*
* Example:
* npx tsx scripts/create-user.ts admin@b4l.co.th mypassword "Admin"
*/
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import pg from 'pg';
import { users } from '../src/lib/server/db/schema.js';
import { hash } from '@node-rs/argon2';
import { encodeBase32LowerCaseNoPadding } from '@oslojs/encoding';
const args = process.argv.slice(2);
if (args.length < 2) {
console.error('Usage: npx tsx scripts/create-user.ts <email> <password> [displayName]');
process.exit(1);
}
const [email, password, displayName] = args;
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzle(pool);
async function main() {
const id = encodeBase32LowerCaseNoPadding(crypto.getRandomValues(new Uint8Array(15)));
const passwordHash = await hash(password!, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1
});
await db.insert(users).values({
id,
email: email!.toLowerCase().trim(),
displayName: displayName || null,
passwordHash
});
console.log(`User created: ${email} (id: ${id})`);
await pool.end();
}
main().catch((err) => {
console.error('Failed to create user:', err.message);
process.exit(1);
});