chore: standardize security and quality foundations
Magent CI/CD / verify (push) Failing after 9m34s
Magent CI/CD / deploy-beta (push) Skipped

This commit is contained in:
2026-09-17 20:03:47 +12:00
parent 5639dbcb83
commit f852e7c941
127 changed files with 17928 additions and 10741 deletions
+23
View File
@@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest";
import { apiUrl, requestJson } from "./api-client";
describe("api client", () => {
it("normalizes relative API paths", () => {
expect(apiUrl("health")).toBe("/api/health");
expect(apiUrl("/health")).toBe("/api/health");
});
it("returns typed JSON from successful responses", async () => {
const transport = async () => new Response(JSON.stringify({ status: "ok" }), { status: 200 });
const result = await requestJson<{ status: string }>("/health", undefined, transport);
expect(result).toEqual({ status: "ok" });
});
it("uses the API error detail when a request fails", async () => {
const transport = async () => new Response(JSON.stringify({ detail: "Not available" }), { status: 409 });
await expect(requestJson("/requests/1", undefined, transport)).rejects.toEqual(
expect.objectContaining({ status: 409, message: "Not available" }),
);
});
});
+52
View File
@@ -0,0 +1,52 @@
import { authFetchOrThrow, getApiBase } from "./auth";
export type ApiTransport = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
export class ApiClientError extends Error {
readonly status: number;
constructor(status: number, message: string) {
super(message);
this.name = "ApiClientError";
this.status = status;
}
}
const errorMessage = (payload: unknown, fallback: string) => {
if (!payload || typeof payload !== "object") return fallback;
const record = payload as Record<string, unknown>;
for (const key of ["detail", "error", "message"]) {
const value = record[key];
if (typeof value === "string" && value.trim()) return value.trim();
}
return fallback;
};
export const apiUrl = (path: string) => {
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
return `${getApiBase()}${normalizedPath}`;
};
export async function requestJson<T>(
path: string,
init?: RequestInit,
transport: ApiTransport = authFetchOrThrow,
): Promise<T> {
const response = await transport(apiUrl(path), init);
if (response.status === 204) return undefined as T;
const text = await response.text();
let payload: unknown = null;
if (text) {
try {
payload = JSON.parse(text);
} catch {
payload = text;
}
}
if (!response.ok) {
throw new ApiClientError(response.status, errorMessage(payload, text || `Request failed: ${response.status}`));
}
return payload as T;
}
+55 -55
View File
@@ -1,100 +1,100 @@
const AUTH_STATE_COOKIE = 'magent_logged_in'
const AUTH_STATE_COOKIE = "magent_logged_in";
export const getApiBase = () => process.env.NEXT_PUBLIC_API_BASE ?? '/api'
export const getApiBase = () => process.env.NEXT_PUBLIC_API_BASE ?? "/api";
const setCookie = (name: string, value: string, maxAgeSeconds: number) => {
if (typeof document === 'undefined') return
document.cookie = `${name}=${value}; Max-Age=${maxAgeSeconds}; Path=/; SameSite=Lax`
}
if (typeof document === "undefined") return;
document.cookie = `${name}=${value}; Max-Age=${maxAgeSeconds}; Path=/; SameSite=Lax`;
};
const clearCookie = (name: string) => {
if (typeof document === 'undefined') return
document.cookie = `${name}=; Max-Age=0; Path=/; SameSite=Lax`
}
if (typeof document === "undefined") return;
document.cookie = `${name}=; Max-Age=0; Path=/; SameSite=Lax`;
};
export const getToken = () => {
if (typeof document === 'undefined') return null
const cookies = document.cookie.split(';').map((entry) => entry.trim())
const marker = cookies.find((entry) => entry.startsWith(`${AUTH_STATE_COOKIE}=`))
if (!marker) return null
const [, value] = marker.split('=', 2)
return value || null
}
if (typeof document === "undefined") return null;
const cookies = document.cookie.split(";").map((entry) => entry.trim());
const marker = cookies.find((entry) => entry.startsWith(`${AUTH_STATE_COOKIE}=`));
if (!marker) return null;
const [, value] = marker.split("=", 2);
return value || null;
};
export const setToken = (_token: string) => {
setCookie(AUTH_STATE_COOKIE, '1', 60 * 60 * 12)
}
setCookie(AUTH_STATE_COOKIE, "1", 60 * 60 * 12);
};
export const clearToken = () => {
clearCookie(AUTH_STATE_COOKIE)
if (typeof window === 'undefined') return
const baseUrl = getApiBase()
clearCookie(AUTH_STATE_COOKIE);
if (typeof window === "undefined") return;
const baseUrl = getApiBase();
void fetch(`${baseUrl}/auth/logout`, {
method: 'POST',
credentials: 'include',
method: "POST",
credentials: "include",
keepalive: true,
}).catch(() => undefined)
}
}).catch(() => undefined);
};
export const logout = async () => {
const baseUrl = getApiBase()
clearCookie(AUTH_STATE_COOKIE)
const baseUrl = getApiBase();
clearCookie(AUTH_STATE_COOKIE);
await fetch(`${baseUrl}/auth/logout`, {
method: 'POST',
credentials: 'include',
})
}
method: "POST",
credentials: "include",
});
};
export const authFetch = (input: RequestInfo | URL, init?: RequestInit) => {
const headers = new Headers(init?.headers || {})
return fetch(input, { ...init, headers, credentials: 'include' })
}
const headers = new Headers(init?.headers || {});
return fetch(input, { ...init, headers, credentials: "include" });
};
export const getEventStreamToken = async () => {
const baseUrl = getApiBase()
const response = await authFetch(`${baseUrl}/auth/stream-token`)
const baseUrl = getApiBase();
const response = await authFetch(`${baseUrl}/auth/stream-token`);
if (!response.ok) {
const text = await response.text()
throw new Error(text || `Stream token request failed: ${response.status}`)
const text = await response.text();
throw new Error(text || `Stream token request failed: ${response.status}`);
}
const data = await response.json()
const token = typeof data?.stream_token === 'string' ? data.stream_token : ''
const data = await response.json();
const token = typeof data?.stream_token === "string" ? data.stream_token : "";
if (!token) {
throw new Error('Stream token not returned')
throw new Error("Stream token not returned");
}
return token
}
return token;
};
export class UnauthorizedError extends Error {
constructor() {
super('Unauthorized')
this.name = 'UnauthorizedError'
super("Unauthorized");
this.name = "UnauthorizedError";
}
}
export class ForbiddenError extends Error {
constructor() {
super('Forbidden')
this.name = 'ForbiddenError'
super("Forbidden");
this.name = "ForbiddenError";
}
}
export const authFetchOrThrow = async (input: RequestInfo | URL, init?: RequestInit) => {
const response = await authFetch(input, init)
const response = await authFetch(input, init);
if (response.status === 401) {
clearToken()
throw new UnauthorizedError()
clearToken();
throw new UnauthorizedError();
}
if (response.status === 403) {
throw new ForbiddenError()
throw new ForbiddenError();
}
return response
}
return response;
};
export const readResponseText = async (response: Response) => {
try {
return (await response.text()).trim()
return (await response.text()).trim();
} catch {
return ''
return "";
}
}
};
+39 -20
View File
@@ -1,24 +1,43 @@
export const FEATURES = [
{ key: 'stats', label: 'My Stats', description: 'View personal viewing history, reports and request report emails.' },
{ key: 'requests', label: 'My Requests', description: 'View existing requests, their progress and request actions.' },
{ key: 'new_requests', label: 'New Requests', description: 'Search for movies and TV shows and submit new requests.' },
{ key: 'issues', label: 'Issues', description: 'Report problems, follow up on issues and use available repair tools.' },
{ key: 'invites', label: 'Invites', description: 'Create and manage invitations within the existing invite limits.' },
{ key: 'ignore_profile_limits', label: 'Ignore profile limits', description: 'Allow explicit manual downloads outside quality, size, language or custom-format limits. Automatic searches keep the assigned profile.' },
] as const
export type Feature = typeof FEATURES[number]['key']
export type FeatureAccess = Record<Feature, boolean>
{ key: "stats", label: "My Stats", description: "View personal viewing history, reports and request report emails." },
{ key: "requests", label: "My Requests", description: "View existing requests, their progress and request actions." },
{
key: "new_requests",
label: "New Requests",
description: "Search for movies and TV shows and submit new requests.",
},
{
key: "issues",
label: "Issues",
description: "Report problems, follow up on issues and use available repair tools.",
},
{ key: "invites", label: "Invites", description: "Create and manage invitations within the existing invite limits." },
{
key: "ignore_profile_limits",
label: "Ignore profile limits",
description:
"Allow explicit manual downloads outside quality, size, language or custom-format limits. Automatic searches keep the assigned profile.",
},
] as const;
export type Feature = (typeof FEATURES)[number]["key"];
export type FeatureAccess = Record<Feature, boolean>;
export function featureForPath(path: string): Feature | undefined {
if (path === '/insights' || path.startsWith('/insights/')) return 'stats'
if (path === '/' || path.startsWith('/requests/')) return 'requests'
if (path === '/new-requests') return 'new_requests'
if (path.startsWith('/issues/confirm/') || path.startsWith('/portal/issues')) return 'issues'
if (path.startsWith('/profile/invites')) return 'invites'
if (path === '/portal/requests') return 'requests'
if (path === "/insights" || path.startsWith("/insights/")) return "stats";
if (path === "/" || path.startsWith("/requests/")) return "requests";
if (path === "/new-requests") return "new_requests";
if (path.startsWith("/issues/confirm/") || path.startsWith("/portal/issues")) return "issues";
if (path.startsWith("/profile/invites")) return "invites";
if (path === "/portal/requests") return "requests";
}
export function canAccess(user: { role?: string; features?: Partial<FeatureAccess>; invite_management_enabled?: boolean } | null, feature?: Feature) {
if (!feature) return true
if (!user) return false
if (user.role === 'admin') return true
return user.features?.[feature] ?? (feature === 'invites' ? Boolean(user.invite_management_enabled) : feature !== 'ignore_profile_limits')
export function canAccess(
user: { role?: string; features?: Partial<FeatureAccess>; invite_management_enabled?: boolean } | null,
feature?: Feature,
) {
if (!feature) return true;
if (!user) return false;
if (user.role === "admin") return true;
return (
user.features?.[feature] ??
(feature === "invites" ? Boolean(user.invite_management_enabled) : feature !== "ignore_profile_limits")
);
}
+17
View File
@@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";
import { normalizeRecentResults, normalizeSearchResults } from "./request-results";
describe("request result normalization", () => {
it("replaces placeholder request titles", () => {
expect(normalizeRecentResults([{ id: 42, title: "Request 42", year: 2024 }])).toEqual([
expect.objectContaining({ id: 42, title: "Request #42", year: 2024 }),
]);
});
it("drops malformed search results", () => {
expect(normalizeSearchResults([null, { title: "" }, { title: "Drive", requestId: 3991 }])).toEqual([
expect.objectContaining({ title: "Drive", requestId: 3991 }),
]);
});
});
+74
View File
@@ -0,0 +1,74 @@
export interface RecentRequest {
id: number;
title: string;
year?: number;
type?: string;
statusLabel?: string;
artwork?: { poster_url?: string; backdrop_url?: string };
createdAt?: string | null;
}
export interface RequestSearchResult {
title: string;
year?: number;
type?: string;
requestId?: number;
statusLabel?: string;
requestedBy?: string | null;
accessible?: boolean;
}
const recordValue = (value: unknown): Record<string, unknown> | null =>
value !== null && typeof value === "object" ? (value as Record<string, unknown>) : null;
const optionalString = (value: unknown) => (typeof value === "string" ? value : undefined);
const optionalNumber = (value: unknown) => (typeof value === "number" && Number.isFinite(value) ? value : undefined);
export const normalizeRecentResults = (items: unknown): RecentRequest[] => {
if (!Array.isArray(items)) return [];
return items.flatMap((value) => {
const item = recordValue(value);
const id = optionalNumber(item?.id);
if (!item || id === undefined) return [];
const rawTitle = optionalString(item.title);
const placeholder = rawTitle?.trim().toLowerCase() === `request ${id}`;
const rawArtwork = recordValue(item.artwork);
const artwork = rawArtwork
? {
poster_url: optionalString(rawArtwork.poster_url),
backdrop_url: optionalString(rawArtwork.backdrop_url),
}
: undefined;
return [
{
id,
title: !rawTitle || placeholder ? `Request #${id}` : rawTitle,
year: optionalNumber(item.year),
type: optionalString(item.type),
statusLabel: optionalString(item.statusLabel),
artwork,
createdAt: item.createdAt === null ? null : optionalString(item.createdAt),
},
];
});
};
export const normalizeSearchResults = (items: unknown): RequestSearchResult[] => {
if (!Array.isArray(items)) return [];
return items.flatMap((value) => {
const item = recordValue(value);
const title = optionalString(item?.title);
if (!item || !title) return [];
return [
{
title,
year: optionalNumber(item.year),
type: optionalString(item.type),
requestId: optionalNumber(item.requestId),
statusLabel: optionalString(item.statusLabel),
requestedBy: item.requestedBy === null ? null : optionalString(item.requestedBy),
accessible: Boolean(item.accessible),
},
];
});
};
+9 -9
View File
@@ -1,15 +1,15 @@
let locks = 0
let previous = ''
let locks = 0;
let previous = "";
export function lockBodyScroll() {
if (locks++ === 0) {
previous = document.body.style.overflow
document.body.style.overflow = 'hidden'
previous = document.body.style.overflow;
document.body.style.overflow = "hidden";
}
let released = false
let released = false;
return () => {
if (released) return
released = true
if (--locks === 0) document.body.style.overflow = previous
}
if (released) return;
released = true;
if (--locks === 0) document.body.style.overflow = previous;
};
}
+30 -30
View File
@@ -1,48 +1,48 @@
'use client'
"use client";
import { useEffect, useState } from 'react'
import { useEffect, useState } from "react";
const USER_VIEW_STORAGE_KEY = 'magent_user_view_preview'
const USER_VIEW_EVENT = 'magent:user-view-change'
const USER_VIEW_STORAGE_KEY = "magent_user_view_preview";
const USER_VIEW_EVENT = "magent:user-view-change";
const readUserViewPreview = () => {
if (typeof window === 'undefined') return false
return window.sessionStorage.getItem(USER_VIEW_STORAGE_KEY) === '1'
}
if (typeof window === "undefined") return false;
return window.sessionStorage.getItem(USER_VIEW_STORAGE_KEY) === "1";
};
const applyDocumentMode = (enabled: boolean) => {
if (typeof document === 'undefined') return
document.documentElement.dataset.userView = enabled ? 'true' : 'false'
}
if (typeof document === "undefined") return;
document.documentElement.dataset.userView = enabled ? "true" : "false";
};
export const setUserViewPreview = (enabled: boolean) => {
if (typeof window === 'undefined') return
if (typeof window === "undefined") return;
if (enabled) {
window.sessionStorage.setItem(USER_VIEW_STORAGE_KEY, '1')
window.sessionStorage.setItem(USER_VIEW_STORAGE_KEY, "1");
} else {
window.sessionStorage.removeItem(USER_VIEW_STORAGE_KEY)
window.sessionStorage.removeItem(USER_VIEW_STORAGE_KEY);
}
applyDocumentMode(enabled)
window.dispatchEvent(new CustomEvent(USER_VIEW_EVENT, { detail: { enabled } }))
}
applyDocumentMode(enabled);
window.dispatchEvent(new CustomEvent(USER_VIEW_EVENT, { detail: { enabled } }));
};
export const useUserViewPreview = () => {
const [enabled, setEnabled] = useState(false)
const [enabled, setEnabled] = useState(false);
useEffect(() => {
const sync = () => {
const nextValue = readUserViewPreview()
applyDocumentMode(nextValue)
setEnabled(nextValue)
}
sync()
window.addEventListener(USER_VIEW_EVENT, sync)
window.addEventListener('storage', sync)
const nextValue = readUserViewPreview();
applyDocumentMode(nextValue);
setEnabled(nextValue);
};
sync();
window.addEventListener(USER_VIEW_EVENT, sync);
window.addEventListener("storage", sync);
return () => {
window.removeEventListener(USER_VIEW_EVENT, sync)
window.removeEventListener('storage', sync)
}
}, [])
window.removeEventListener(USER_VIEW_EVENT, sync);
window.removeEventListener("storage", sync);
};
}, []);
return enabled
}
return enabled;
};