Signed-off-by: Max <ai-agent@topdoglabs.com>

This commit is contained in:
Max 2026-02-24 18:14:57 -06:00
parent 2f0f11e111
commit 718683748a
2 changed files with 49 additions and 2 deletions

View File

@ -98,6 +98,10 @@ Task and sprint board built with Next.js + Zustand and Supabase-backed API persi
- API routes now enforce auth on task read/write/delete (`/api/tasks` returns `401` when unauthenticated).
- Added `PATCH /api/auth/account` to update profile and password for the current user.
- Session cookie: `gantt_session` (HTTP-only, same-site lax, secure in production).
- Optional machine-to-machine auth for API routes:
- Set `GANTT_MACHINE_TOKEN` in server env.
- Send either `Authorization: Bearer <token>` or `x-gantt-machine-token: <token>`.
- Optional identity fields: `GANTT_MACHINE_USER_ID`, `GANTT_MACHINE_USER_NAME`, `GANTT_MACHINE_USER_EMAIL`.
- Added `Remember me` in auth forms:
- Checked: persistent 30-day cookie/session.
- Unchecked: browser-session cookie (clears on browser close), with server-side short-session expiry.

View File

@ -1,11 +1,12 @@
import { randomBytes, createHash } from "crypto";
import { cookies } from "next/headers";
import { randomBytes, createHash, timingSafeEqual } from "crypto";
import { cookies, headers } from "next/headers";
import { createClient } from "@supabase/supabase-js";
import { getServiceSupabase } from "@/lib/supabase/client";
const SESSION_COOKIE_NAME = "gantt_session";
const SESSION_HOURS_SHORT = 12;
const SESSION_DAYS_REMEMBER = 30;
const MACHINE_AUTH_HEADER = "x-gantt-machine-token";
export interface AuthUser {
id: string;
@ -66,6 +67,19 @@ function hashSessionToken(token: string): string {
return createHash("sha256").update(token).digest("hex");
}
function safeTokenEquals(left: string, right: string): boolean {
const leftBuffer = Buffer.from(left, "utf8");
const rightBuffer = Buffer.from(right, "utf8");
if (leftBuffer.length !== rightBuffer.length) return false;
return timingSafeEqual(leftBuffer, rightBuffer);
}
function extractBearerToken(authorization: string | null): string | null {
if (!authorization) return null;
const match = authorization.match(/^Bearer\s+(.+)$/i);
return match?.[1]?.trim() || null;
}
async function deleteExpiredSessions() {
const supabase = getServiceSupabase();
const { error } = await supabase
@ -446,7 +460,36 @@ export async function getSessionTokenFromCookies(): Promise<string | null> {
return cookieStore.get(SESSION_COOKIE_NAME)?.value ?? null;
}
async function getMachineAuthenticatedUser(): Promise<AuthUser | null> {
const configuredToken = process.env.GANTT_MACHINE_TOKEN;
if (!configuredToken) return null;
const requestHeaders = await headers();
const headerToken = requestHeaders.get(MACHINE_AUTH_HEADER);
const bearerToken = extractBearerToken(requestHeaders.get("authorization"));
const incomingToken = bearerToken || headerToken;
if (!incomingToken) return null;
if (!safeTokenEquals(incomingToken, configuredToken)) {
return null;
}
const machineUserId = process.env.GANTT_MACHINE_USER_ID || "machine-service-user";
const machineUserName = process.env.GANTT_MACHINE_USER_NAME || "Mission Control Service";
const machineUserEmail = process.env.GANTT_MACHINE_USER_EMAIL || "mission-control@internal.local";
return {
id: machineUserId,
name: machineUserName,
email: machineUserEmail,
createdAt: "1970-01-01T00:00:00.000Z",
};
}
export async function getAuthenticatedUser(): Promise<AuthUser | null> {
const machineUser = await getMachineAuthenticatedUser();
if (machineUser) return machineUser;
const token = await getSessionTokenFromCookies();
if (!token) return null;
return getUserBySessionToken(token);