MVP-1: helpdesk core with dashboard, Telegram channel, customer portal

Next.js 16 + Drizzle/SQLite (matching ~/project-claude conventions), SSE-based
realtime updates, admin dashboard with status columns, ticket thread view,
Telegram long-polling integration (inbound + outbound), token-based customer
portal, Docker Compose packaging.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcXH24ky6zjk2UyK5oZUPH
This commit is contained in:
ogrechkoandClaude Sonnet 5 committed 2026-07-26 12:40:56 +00:00
1 parent 888b7b82ba
commit cf0b6cf58e
59 files changed
+12556

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
/**
* Creates the initial admin user from ADMIN_BOOTSTRAP_EMAIL /
* ADMIN_BOOTSTRAP_PASSWORD if no user with that email exists yet.
* Safe to run on every container start — idempotent.
*/
import { db } from "../src/lib/db/client";
import { users } from "../src/lib/db/schema";
import { hashPassword } from "../src/lib/auth/password";
async function main() {
const email = process.env.ADMIN_BOOTSTRAP_EMAIL;
const password = process.env.ADMIN_BOOTSTRAP_PASSWORD;
const name = process.env.ADMIN_BOOTSTRAP_NAME ?? "Admin";
if (!email || !password) {
console.log("ADMIN_BOOTSTRAP_EMAIL/PASSWORD not set — skipping admin bootstrap.");
return;
}
const normalizedEmail = email.toLowerCase().trim();
const existing = await db.query.users.findFirst({
where: (u, { eq }) => eq(u.email, normalizedEmail),
});
if (existing) {
console.log(`Admin user ${normalizedEmail} already exists — skipping.`);
return;
}
const passwordHash = await hashPassword(password);
await db.insert(users).values({
email: normalizedEmail,
passwordHash,
name,
role: "admin",
});
console.log(`Bootstrapped admin user ${normalizedEmail}.`);
}
main()
.then(() => process.exit(0))
.catch((err) => {
console.error("Admin bootstrap failed:", err);
process.exit(1);
});
+7
View File
@@ -0,0 +1,7 @@
/**
* Generates a fresh AES-256-GCM key for CREDENTIALS_ENCRYPTION_KEY.
* Usage: npm run generate-key
*/
import crypto from "node:crypto";
console.log(crypto.randomBytes(32).toString("base64"));