0a3aaa5798
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>
76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
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);
|
|
});
|