ubiquitous-invention/apps/web/lib/auth.ts
Randall Stillwell 663bc77afe feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:

App
- New routes: ai, forms, planner, settings (templates/types), teams,
  doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
  builder/renderer/responses, types manager, objects creation dialog,
  card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
  health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in

Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
  (uses CT 102 shared services), removes host port mappings, adds
  Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
  metadata title flipped to ECHODO

Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
  credentials.*, *.key, *.crt, *.pem, ssh keys

Made-with: Cursor
2026-04-26 14:34:34 -05:00

116 lines
3.3 KiB
TypeScript

import NextAuth from "next-auth";
import type { DefaultSession, NextAuthConfig } from "next-auth";
import Authentik from "next-auth/providers/authentik";
import Credentials from "next-auth/providers/credentials";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
/**
* Optional: `pnpm add @auth/drizzle-adapter` then wire DrizzleAdapter + session strategy "database".
* Using JWT + Credentials/OAuth; `db` is loaded dynamically inside `authorize` (Node route handler only).
* User lookup uses the postgres.js client from Drizzle (`db.$client`) so we avoid a direct `drizzle-orm` import in this app.
*/
declare module "next-auth" {
interface Session {
user: {
id: string;
} & DefaultSession["user"];
}
}
const providers: NextAuthConfig["providers"] = [
Credentials({
name: "Email",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
const email = credentials?.email as string | undefined;
const password = credentials?.password as string | undefined;
if (!email?.trim() || !password) return null;
const devPassword = process.env.AUTH_DEV_PASSWORD;
if (!devPassword) {
console.warn("[auth] AUTH_DEV_PASSWORD is not set; credentials sign-in disabled.");
return null;
}
if (password !== devPassword) return null;
const { db } = await import("@tasks/database/client");
const sql = (db as { $client: (t: TemplateStringsArray, ...v: unknown[]) => Promise<unknown[]> })
.$client;
const rows = (await sql`
SELECT id, email, name, avatar_url AS "avatarUrl"
FROM users
WHERE lower(email) = lower(${email.trim()})
LIMIT 1
`) as { id: string; email: string; name: string | null; avatarUrl: string | null }[];
const user = rows[0];
if (!user) return null;
return {
id: user.id,
email: user.email,
name: user.name ?? undefined,
image: user.avatarUrl ?? undefined,
};
},
}),
];
if (process.env.AUTH_GITHUB_ID && process.env.AUTH_GITHUB_SECRET) {
providers.push(
GitHub({
clientId: process.env.AUTH_GITHUB_ID,
clientSecret: process.env.AUTH_GITHUB_SECRET,
}),
);
}
if (process.env.AUTH_GOOGLE_ID && process.env.AUTH_GOOGLE_SECRET) {
providers.push(
Google({
clientId: process.env.AUTH_GOOGLE_ID,
clientSecret: process.env.AUTH_GOOGLE_SECRET,
}),
);
}
if (
process.env.AUTH_AUTHENTIK_ID &&
process.env.AUTH_AUTHENTIK_SECRET &&
process.env.AUTH_AUTHENTIK_ISSUER
) {
providers.push(
Authentik({
clientId: process.env.AUTH_AUTHENTIK_ID,
clientSecret: process.env.AUTH_AUTHENTIK_SECRET,
issuer: process.env.AUTH_AUTHENTIK_ISSUER,
}),
);
}
export const { handlers, auth, signIn, signOut } = NextAuth({
session: { strategy: "jwt" },
pages: {
signIn: "/sign-in",
},
providers,
callbacks: {
async jwt({ token, user }) {
if (user?.id) {
token.id = user.id;
}
return token;
},
async session({ session, token }) {
if (session.user && token.id) {
session.user.id = token.id as string;
}
return session;
},
},
trustHost: true,
});