Signed-off-by: Max <ai-agent@topdoglabs.com>
This commit is contained in:
parent
2f0f11e111
commit
718683748a
@ -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).
|
- 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.
|
- 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).
|
- 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:
|
- Added `Remember me` in auth forms:
|
||||||
- Checked: persistent 30-day cookie/session.
|
- Checked: persistent 30-day cookie/session.
|
||||||
- Unchecked: browser-session cookie (clears on browser close), with server-side short-session expiry.
|
- Unchecked: browser-session cookie (clears on browser close), with server-side short-session expiry.
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { randomBytes, createHash } from "crypto";
|
import { randomBytes, createHash, timingSafeEqual } from "crypto";
|
||||||
import { cookies } from "next/headers";
|
import { cookies, headers } from "next/headers";
|
||||||
import { createClient } from "@supabase/supabase-js";
|
import { createClient } from "@supabase/supabase-js";
|
||||||
import { getServiceSupabase } from "@/lib/supabase/client";
|
import { getServiceSupabase } from "@/lib/supabase/client";
|
||||||
|
|
||||||
const SESSION_COOKIE_NAME = "gantt_session";
|
const SESSION_COOKIE_NAME = "gantt_session";
|
||||||
const SESSION_HOURS_SHORT = 12;
|
const SESSION_HOURS_SHORT = 12;
|
||||||
const SESSION_DAYS_REMEMBER = 30;
|
const SESSION_DAYS_REMEMBER = 30;
|
||||||
|
const MACHINE_AUTH_HEADER = "x-gantt-machine-token";
|
||||||
|
|
||||||
export interface AuthUser {
|
export interface AuthUser {
|
||||||
id: string;
|
id: string;
|
||||||
@ -66,6 +67,19 @@ function hashSessionToken(token: string): string {
|
|||||||
return createHash("sha256").update(token).digest("hex");
|
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() {
|
async function deleteExpiredSessions() {
|
||||||
const supabase = getServiceSupabase();
|
const supabase = getServiceSupabase();
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
@ -446,7 +460,36 @@ export async function getSessionTokenFromCookies(): Promise<string | null> {
|
|||||||
return cookieStore.get(SESSION_COOKIE_NAME)?.value ?? 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> {
|
export async function getAuthenticatedUser(): Promise<AuthUser | null> {
|
||||||
|
const machineUser = await getMachineAuthenticatedUser();
|
||||||
|
if (machineUser) return machineUser;
|
||||||
|
|
||||||
const token = await getSessionTokenFromCookies();
|
const token = await getSessionTokenFromCookies();
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
return getUserBySessionToken(token);
|
return getUserBySessionToken(token);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user