NobGit
public anord read

Ledger

Why work hard when you can work easier?

Languages

Repository composition by tracked source files.

TypeScript
TypeScript 86% CSS 10% SQL 3% Shell 1% HTML 0%
Create file Wiki Documentation
Clone
https://nobgit.com/orgs/anord/ledger.git
ssh://[email protected]:2222/orgs/anord/ledger.git
auth.ts
import { Router } from "express";
import { z } from "zod";
import { createSessionToken, getUserForSession, hashPassword, verifyPassword } from "../../services/auth.js";
import { pool } from "../../db/pool.js";
import { env, isProduction } from "../../config/env.js";
import { logAudit } from "../../services/audit.js";

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
});

const registerSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  displayName: z.string().min(2)
});

export const authRouter = Router();

authRouter.post("/register", async (req, res) => {
  const userCount = await pool.query(`SELECT COUNT(*)::int AS count FROM users`);
  if (userCount.rows[0].count === 0) {
    return res.status(403).json({ error: "Complete initial Ledger setup first" });
  }

  const input = registerSchema.parse(req.body);
  const existing = await pool.query(`SELECT id FROM users WHERE email = $1`, [input.email]);

  if (existing.rowCount) {
    return res.status(409).json({ error: "User already exists" });
  }

  const role = await pool.query(`SELECT id FROM roles WHERE key = 'viewer'`);
  const user = await pool.query(
    `
      INSERT INTO users (email, password_hash, display_name, primary_role_id)
      VALUES ($1, $2, $3, $4)
      RETURNING id
    `,
    [input.email, await hashPassword(input.password), input.displayName, role.rows[0].id]
  );

  const sessionUser = await getUserForSession(user.rows[0].id);
  const token = createSessionToken(sessionUser!);

  res.cookie(env.SESSION_COOKIE_NAME, token, {
    httpOnly: true,
    sameSite: "lax",
    secure: isProduction
  });

  await logAudit(sessionUser!.id, "auth.register", "user", sessionUser!.id, {
    email: sessionUser!.email
  });

  return res.status(201).json({ user: sessionUser });
});

authRouter.post("/login", async (req, res) => {
  const input = loginSchema.parse(req.body);
  const user = await pool.query(
    `
      SELECT u.id, u.password_hash
      FROM users u
      WHERE u.email = $1 AND u.is_active = true
    `,
    [input.email]
  );

  if (!user.rowCount || !user.rows[0].password_hash) {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  const passwordValid = await verifyPassword(input.password, user.rows[0].password_hash);
  if (!passwordValid) {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  const sessionUser = await getUserForSession(user.rows[0].id);
  const token = createSessionToken(sessionUser!);

  res.cookie(env.SESSION_COOKIE_NAME, token, {
    httpOnly: true,
    sameSite: "lax",
    secure: isProduction
  });

  await logAudit(sessionUser!.id, "auth.login", "user", sessionUser!.id);
  return res.json({ user: sessionUser });
});

authRouter.post("/logout", async (_req, res) => {
  res.clearCookie(env.SESSION_COOKIE_NAME);
  return res.status(204).send();
});

authRouter.get("/session", async (req, res) => {
  return res.json({ user: req.user ?? null });
});