feat(release): publish minimal self-contained Magent source

This commit is contained in:
Magent release tooling
2026-09-19 16:58:12 +12:00
commit 5fa5d45535
272 changed files with 79305 additions and 0 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;
}
+100
View File
@@ -0,0 +1,100 @@
const AUTH_STATE_COOKIE = "magent_logged_in";
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`;
};
const clearCookie = (name: string) => {
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;
};
export const setToken = (_token: string) => {
setCookie(AUTH_STATE_COOKIE, "1", 60 * 60 * 12);
};
export const clearToken = () => {
clearCookie(AUTH_STATE_COOKIE);
if (typeof window === "undefined") return;
const baseUrl = getApiBase();
void fetch(`${baseUrl}/auth/logout`, {
method: "POST",
credentials: "include",
keepalive: true,
}).catch(() => undefined);
};
export const logout = async () => {
const baseUrl = getApiBase();
clearCookie(AUTH_STATE_COOKIE);
await fetch(`${baseUrl}/auth/logout`, {
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" });
};
export const getEventStreamToken = async () => {
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 data = await response.json();
const token = typeof data?.stream_token === "string" ? data.stream_token : "";
if (!token) {
throw new Error("Stream token not returned");
}
return token;
};
export class UnauthorizedError extends Error {
constructor() {
super("Unauthorized");
this.name = "UnauthorizedError";
}
}
export class ForbiddenError extends Error {
constructor() {
super("Forbidden");
this.name = "ForbiddenError";
}
}
export const authFetchOrThrow = async (input: RequestInfo | URL, init?: RequestInit) => {
const response = await authFetch(input, init);
if (response.status === 401) {
clearToken();
throw new UnauthorizedError();
}
if (response.status === 403) {
throw new ForbiddenError();
}
return response;
};
export const readResponseText = async (response: Response) => {
try {
return (await response.text()).trim();
} catch {
return "";
}
};
+43
View File
@@ -0,0 +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>;
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";
}
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")
);
}
+51
View File
@@ -0,0 +1,51 @@
import { describe, expect, it } from "vitest";
import { loginErrorMessage } from "./login-errors";
const errorResponse = (status: number, payload: unknown) => new Response(JSON.stringify(payload), { status });
describe("login error messages", () => {
it("identifies a site security rejection without blaming the account", async () => {
expect(await loginErrorMessage(errorResponse(403, { detail: "Cross-origin state change rejected" }))).toBe(
"Sign-in was blocked by the site's security configuration. Please contact an administrator.",
);
});
it.each(["User is blocked", "User access has expired", "Unknown upstream error"])(
"keeps a generic account message for %s",
async (detail) => {
expect(await loginErrorMessage(errorResponse(403, { detail }))).toBe(
"This account cannot sign in. Please contact an administrator.",
);
},
);
it.each([
null,
[],
{ detail: ["Cross-origin state change rejected"] },
{ detail: "Cross-origin state change rejected: private upstream detail" },
{ detail: "<script>private upstream detail</script>" },
])("does not render or loosely match unexpected response bodies: %j", async (payload) => {
expect(await loginErrorMessage(errorResponse(403, payload))).toBe(
"This account cannot sign in. Please contact an administrator.",
);
});
it("handles a non-JSON proxy denial safely", async () => {
expect(await loginErrorMessage(new Response("<html>Forbidden</html>", { status: 403 }))).toBe(
"This account cannot sign in. Please contact an administrator.",
);
});
it.each([
[401, "Check your username and password, then try again."],
[400, "Check your username and password, then try again."],
[429, "Too many attempts. Please wait a moment and try again."],
[500, "Sign-in is temporarily unavailable. Please try again shortly."],
[502, "Sign-in is temporarily unavailable. Please try again shortly."],
])("preserves the existing message for HTTP %s", async (status, expected) => {
expect(
await loginErrorMessage(errorResponse(status as number, { detail: "Cross-origin state change rejected" })),
).toBe(expected);
});
});
+17
View File
@@ -0,0 +1,17 @@
export async function loginErrorMessage(response: Response): Promise<string> {
if (response.status === 429) return "Too many attempts. Please wait a moment and try again.";
if (response.status >= 500) return "Sign-in is temporarily unavailable. Please try again shortly.";
if (response.status === 403) {
const payload: unknown = await response.json().catch(() => null);
if (
payload !== null &&
typeof payload === "object" &&
"detail" in payload &&
payload.detail === "Cross-origin state change rejected"
) {
return "Sign-in was blocked by the site's security configuration. Please contact an administrator.";
}
return "This account cannot sign in. Please contact an administrator.";
}
return "Check your username and password, then try again.";
}
+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),
},
];
});
};
+15
View File
@@ -0,0 +1,15 @@
let locks = 0;
let previous = "";
export function lockBodyScroll() {
if (locks++ === 0) {
previous = document.body.style.overflow;
document.body.style.overflow = "hidden";
}
let released = false;
return () => {
if (released) return;
released = true;
if (--locks === 0) document.body.style.overflow = previous;
};
}
+46
View File
@@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import { getEffectiveRole, isAdminPage } from "./user-view-policy";
describe("user view preview policy", () => {
it("downgrades only the displayed administrator role during preview", () => {
expect(getEffectiveRole("admin", true)).toBe("user");
expect(getEffectiveRole("admin", false)).toBe("admin");
for (const role of ["user", null, undefined]) {
expect(getEffectiveRole(role, true)).toBe(role);
expect(getEffectiveRole(role, false)).toBe(role);
}
});
it("covers configuration, nested admin pages, user management and setup", () => {
for (const path of [
"/admin",
"/admin/",
"/admin/backups",
"/admin/recaps",
"/users",
"/users/42",
"/setup",
"/admin?section=site",
"/%61dmin/diagnostics",
]) {
expect(isAdminPage(path), path).toBe(true);
}
});
it("does not restrict normal member pages or similarly named paths", () => {
for (const path of [
"/",
"/profile",
"/profile/invites",
"/portal/issues",
"/requests/3580",
"/insights",
"/administrator",
"/users-guide",
]) {
expect(isAdminPage(path), path).toBe(false);
}
});
it("keeps public first-install setup separate from admin authentication", () => {
expect(isAdminPage("/setup", false)).toBe(false);
expect(isAdminPage("/admin/backups", false)).toBe(true);
});
});
+16
View File
@@ -0,0 +1,16 @@
// Preview never promotes a user or changes server-side account permissions.
export function getEffectiveRole(role: string | null | undefined, preview: boolean) {
return preview && role === "admin" ? "user" : role;
}
export function isAdminPage(pathname: string, includeSetup = true): boolean {
let path = pathname.split(/[?#]/, 1)[0];
try {
path = decodeURIComponent(path);
} catch {
// Let the router handle malformed URLs; never infer a more privileged role.
}
path = path.replace(/\/{2,}/g, "/");
const roots = includeSetup ? ["/admin", "/users", "/setup"] : ["/admin", "/users"];
return roots.some((root) => path === root || path.startsWith(`${root}/`));
}
+68
View File
@@ -0,0 +1,68 @@
"use client";
import { useEffect, useSyncExternalStore } from "react";
import { getEffectiveRole } from "./user-view-policy";
const USER_VIEW_STORAGE_KEY = "magent_user_view_preview";
const USER_VIEW_EVENT = "magent:user-view-change";
let fallbackPreview = false;
const readUserViewPreview = () => {
if (typeof window === "undefined") return false;
try {
return window.sessionStorage.getItem(USER_VIEW_STORAGE_KEY) === "1";
} catch {
return fallbackPreview;
}
};
const applyDocumentMode = (enabled: boolean) => {
if (typeof document === "undefined") return;
document.documentElement.dataset.userView = enabled ? "true" : "false";
};
export const setUserViewPreview = (enabled: boolean) => {
if (typeof window === "undefined") return;
fallbackPreview = enabled;
try {
if (enabled) {
window.sessionStorage.setItem(USER_VIEW_STORAGE_KEY, "1");
} else {
window.sessionStorage.removeItem(USER_VIEW_STORAGE_KEY);
}
} catch {
// Preview still works for this document when browser storage is unavailable.
}
applyDocumentMode(enabled);
window.dispatchEvent(new CustomEvent(USER_VIEW_EVENT, { detail: { enabled } }));
};
const subscribe = (notify: () => void) => {
window.addEventListener(USER_VIEW_EVENT, notify);
window.addEventListener("storage", notify);
return () => {
window.removeEventListener(USER_VIEW_EVENT, notify);
window.removeEventListener("storage", notify);
};
};
// Unknown during server rendering/initial hydration: admin pages must not mount
// and fetch privileged data before the saved per-tab preview mode is known.
const serverSnapshot = (): boolean | null => null;
export const useUserViewState = () => {
const value = useSyncExternalStore(subscribe, readUserViewPreview, serverSnapshot);
useEffect(() => {
if (value !== null) applyDocumentMode(value);
}, [value]);
return { enabled: value === true, ready: value !== null };
};
export const useUserViewPreview = () => useUserViewState().enabled;
export const useEffectiveRole = (role?: string | null) => {
const { enabled, ready } = useUserViewState();
return getEffectiveRole(role, !ready || enabled);
};