Phase 0 scaffold: SvelteKit 5 + Drizzle + auth + storage interface
Stack matches sibling buildfor_life_* apps: SvelteKit 5 with adapter-node, Svelte 5 runes, TypeScript, Tailwind v4 with @theme inline tokens, PostgreSQL via Drizzle ORM, Argon2id sessions via @node-rs/argon2 and @oslojs/crypto, EasyMDE ready for wiki/decision markdown, Sharp for thumbnails. Included in this commit: - Config: package.json, svelte.config.js, vite.config.ts, tsconfig.json, drizzle.config.ts, .gitignore, .env.example, .gitattributes, .npmrc - Tenancy schema: companies, users, company_users, sessions (10 enums pre-declared for the full domain so downstream migrations don't re-diff them; decision_scope widened to include asset + work_package per product decision) - Auth: password hashing + SHA-256-hashed session cookies, session lifetime 30d with sliding renewal at T-15d, login + logout + session refresh in hooks - Storage: StorageAdapter interface + LocalDiskStorage with HMAC-signed URLs served by /api/files, S3 drop-in with zero schema change - UI shell: dark-mode bootstrap in app.html identical to siblings, sidebar (w-64, h-14 header, amber attention band pattern from repair), topbar with breadcrumbs, theme toggle with cross-tab sync via storage event, blue-600 primary, responsive drawer - Routes: (app) authed group with auto-redirect to /login, (auth) login group, dashboard placeholder, error page, signed-file API - Scripts: create-user script for bootstrapping first admin user - Drizzle: initial migration generated (0000_init.sql) - Shared agents and skills committed under .claude/; per-user permissions gitignored Typecheck: 0 errors / 0 warnings across 555 files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import 'dotenv/config';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { pool } from '../src/lib/server/db/client';
|
||||
import { db } from '../src/lib/server/db/client';
|
||||
import { users, companies, companyUsers } from '../src/lib/server/db/schema/tenancy';
|
||||
import { hashPassword } from '../src/lib/server/auth/password';
|
||||
import { normalizeEmail } from '../src/lib/utils/email';
|
||||
|
||||
function readArg(flag: string, fallback?: string): string | undefined {
|
||||
const i = process.argv.indexOf(flag);
|
||||
return i >= 0 ? process.argv[i + 1] : fallback;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const email = readArg('--email');
|
||||
const password = readArg('--password');
|
||||
const name = readArg('--name');
|
||||
const companyName = readArg('--company');
|
||||
const role = (readArg('--role', 'admin') ?? 'admin') as 'admin' | 'manager' | 'user' | 'viewer';
|
||||
|
||||
if (!email || !password || !name) {
|
||||
console.error('Usage: npm run create-user -- --email <e> --password <p> --name <n> [--company <c>] [--role admin|manager|user|viewer]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const normalized = normalizeEmail(email);
|
||||
const hash = await hashPassword(password);
|
||||
|
||||
const [existing] = await db.select().from(users).where(eq(users.emailNormalized, normalized)).limit(1);
|
||||
let userId: string;
|
||||
if (existing) {
|
||||
console.log(`User ${normalized} already exists; updating password.`);
|
||||
await db.update(users).set({ passwordHash: hash, displayName: name }).where(eq(users.id, existing.id));
|
||||
userId = existing.id;
|
||||
} else {
|
||||
const [created] = await db
|
||||
.insert(users)
|
||||
.values({ email, emailNormalized: normalized, displayName: name, passwordHash: hash })
|
||||
.returning({ id: users.id });
|
||||
userId = created.id;
|
||||
console.log(`Created user ${normalized} (id ${userId}).`);
|
||||
}
|
||||
|
||||
if (companyName) {
|
||||
let [company] = await db.select().from(companies).where(eq(companies.name, companyName)).limit(1);
|
||||
if (!company) {
|
||||
const slug = companyName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
const [created] = await db
|
||||
.insert(companies)
|
||||
.values({ name: companyName, slug })
|
||||
.returning();
|
||||
company = created;
|
||||
console.log(`Created company ${companyName} (id ${company.id}).`);
|
||||
}
|
||||
const [link] = await db
|
||||
.select()
|
||||
.from(companyUsers)
|
||||
.where(eq(companyUsers.userId, userId))
|
||||
.limit(1);
|
||||
if (!link || link.companyId !== company.id) {
|
||||
await db
|
||||
.insert(companyUsers)
|
||||
.values({ companyId: company.id, userId, role })
|
||||
.onConflictDoNothing();
|
||||
console.log(`Linked user to company ${company.name} as ${role}.`);
|
||||
}
|
||||
}
|
||||
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user