refactor: extract typed authentication API
This commit is contained in:
@@ -137,48 +137,73 @@
|
|||||||
bridge.decoderRegistry = decoderRegistry;
|
bridge.decoderRegistry = decoderRegistry;
|
||||||
bridge.onDecoderRegistryReady = onDecoderRegistryReady;
|
bridge.onDecoderRegistryReady = onDecoderRegistryReady;
|
||||||
|
|
||||||
|
// src/api/auth.ts
|
||||||
|
function decodeAuthSession(value) {
|
||||||
|
if (typeof value !== "object" || value === null) {
|
||||||
|
throw new TypeError("The authentication response is malformed");
|
||||||
|
}
|
||||||
|
const session = value;
|
||||||
|
if (typeof session.authenticated !== "boolean") {
|
||||||
|
throw new TypeError("The authentication response has no authenticated flag");
|
||||||
|
}
|
||||||
|
if (session.role !== void 0 && session.role !== "rx" && session.role !== "control") {
|
||||||
|
throw new TypeError("The authentication response has an invalid role");
|
||||||
|
}
|
||||||
|
if (session.auth_disabled !== void 0 && typeof session.auth_disabled !== "boolean") {
|
||||||
|
throw new TypeError("The authentication response has an invalid auth_disabled flag");
|
||||||
|
}
|
||||||
|
const decoded = { authenticated: session.authenticated };
|
||||||
|
if (session.role !== void 0) decoded.role = session.role;
|
||||||
|
if (session.auth_disabled !== void 0) decoded.auth_disabled = session.auth_disabled;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
var authDisabledSession = {
|
||||||
|
authenticated: true,
|
||||||
|
role: "control",
|
||||||
|
auth_disabled: true
|
||||||
|
};
|
||||||
|
async function fetchAuthSession() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/auth/session");
|
||||||
|
if (response.status === 404) return authDisabledSession;
|
||||||
|
if (!response.ok) return { authenticated: false };
|
||||||
|
return decodeAuthSession(await response.json());
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Auth check failed:", error);
|
||||||
|
return { authenticated: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function login(passphrase) {
|
||||||
|
const response = await fetch("/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ passphrase })
|
||||||
|
});
|
||||||
|
if (response.status === 404) return authDisabledSession;
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = await response.text();
|
||||||
|
throw new Error(message || "Login failed");
|
||||||
|
}
|
||||||
|
return decodeAuthSession(await response.json());
|
||||||
|
}
|
||||||
|
async function logout() {
|
||||||
|
const response = await fetch("/auth/logout", { method: "POST" });
|
||||||
|
if (response.status !== 404 && !response.ok) throw new Error("Logout failed");
|
||||||
|
}
|
||||||
|
|
||||||
// src/app.js
|
// src/app.js
|
||||||
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
|
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
|
||||||
var authRole = null;
|
var authRole = null;
|
||||||
var authEnabled = true;
|
var authEnabled = true;
|
||||||
async function checkAuthStatus() {
|
async function checkAuthStatus() {
|
||||||
try {
|
return fetchAuthSession();
|
||||||
const resp = await fetch("/auth/session");
|
|
||||||
if (resp.status === 404) {
|
|
||||||
return { authenticated: true, role: "control", auth_disabled: true };
|
|
||||||
}
|
|
||||||
if (!resp.ok) return { authenticated: false };
|
|
||||||
const data = await resp.json();
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Auth check failed:", e);
|
|
||||||
return { authenticated: false };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
async function authLogin(passphrase) {
|
async function authLogin(passphrase) {
|
||||||
try {
|
return login(passphrase);
|
||||||
const resp = await fetch("/auth/login", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ passphrase })
|
|
||||||
});
|
|
||||||
if (resp.status === 404) {
|
|
||||||
return { authenticated: true, role: "control", auth_disabled: true };
|
|
||||||
}
|
|
||||||
if (!resp.ok) {
|
|
||||||
const text = await resp.text();
|
|
||||||
throw new Error(text || "Login failed");
|
|
||||||
}
|
|
||||||
const data = await resp.json();
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
async function authLogout() {
|
async function authLogout() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/auth/logout", { method: "POST" });
|
await logout();
|
||||||
if (resp.status !== 404 && !resp.ok) throw new Error("Logout failed");
|
|
||||||
authRole = null;
|
authRole = null;
|
||||||
disconnect();
|
disconnect();
|
||||||
setDecodeHistoryOverlayVisible(false);
|
setDecodeHistoryOverlayVisible(false);
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
export type AuthRole = "rx" | "control";
|
||||||
|
|
||||||
|
export interface AuthSession {
|
||||||
|
authenticated: boolean;
|
||||||
|
role?: AuthRole;
|
||||||
|
auth_disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeAuthSession(value: unknown): AuthSession {
|
||||||
|
if (typeof value !== "object" || value === null) {
|
||||||
|
throw new TypeError("The authentication response is malformed");
|
||||||
|
}
|
||||||
|
const session = value as Record<string, unknown>;
|
||||||
|
if (typeof session.authenticated !== "boolean") {
|
||||||
|
throw new TypeError("The authentication response has no authenticated flag");
|
||||||
|
}
|
||||||
|
if (session.role !== undefined && session.role !== "rx" && session.role !== "control") {
|
||||||
|
throw new TypeError("The authentication response has an invalid role");
|
||||||
|
}
|
||||||
|
if (session.auth_disabled !== undefined && typeof session.auth_disabled !== "boolean") {
|
||||||
|
throw new TypeError("The authentication response has an invalid auth_disabled flag");
|
||||||
|
}
|
||||||
|
const decoded: AuthSession = { authenticated: session.authenticated };
|
||||||
|
if (session.role !== undefined) decoded.role = session.role;
|
||||||
|
if (session.auth_disabled !== undefined) decoded.auth_disabled = session.auth_disabled;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authDisabledSession: AuthSession = {
|
||||||
|
authenticated: true,
|
||||||
|
role: "control",
|
||||||
|
auth_disabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function fetchAuthSession(): Promise<AuthSession> {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/auth/session");
|
||||||
|
if (response.status === 404) return authDisabledSession;
|
||||||
|
if (!response.ok) return { authenticated: false };
|
||||||
|
return decodeAuthSession(await response.json());
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("Auth check failed:", error);
|
||||||
|
return { authenticated: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function login(passphrase: string): Promise<AuthSession> {
|
||||||
|
const response = await fetch("/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ passphrase }),
|
||||||
|
});
|
||||||
|
if (response.status === 404) return authDisabledSession;
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = await response.text();
|
||||||
|
throw new Error(message || "Login failed");
|
||||||
|
}
|
||||||
|
return decodeAuthSession(await response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logout(): Promise<void> {
|
||||||
|
const response = await fetch("/auth/logout", { method: "POST" });
|
||||||
|
if (response.status !== 404 && !response.ok) throw new Error("Logout failed");
|
||||||
|
}
|
||||||
@@ -15,6 +15,11 @@ import {
|
|||||||
decoderRegistry,
|
decoderRegistry,
|
||||||
loadDecoderRegistry,
|
loadDecoderRegistry,
|
||||||
} from "./core/decoder-registry.js";
|
} from "./core/decoder-registry.js";
|
||||||
|
import {
|
||||||
|
fetchAuthSession,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
} from "./api/auth.js";
|
||||||
|
|
||||||
// --- Decoder registry (fetched from /decoders on load) ---
|
// --- Decoder registry (fetched from /decoders on load) ---
|
||||||
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
|
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
|
||||||
@@ -24,46 +29,16 @@ let authRole = null; // null (not authenticated), "rx" (read-only), or "control
|
|||||||
let authEnabled = true;
|
let authEnabled = true;
|
||||||
|
|
||||||
async function checkAuthStatus() {
|
async function checkAuthStatus() {
|
||||||
try {
|
return fetchAuthSession();
|
||||||
const resp = await fetch("/auth/session");
|
|
||||||
if (resp.status === 404) {
|
|
||||||
// Auth API not exposed -> treat as auth-disabled mode.
|
|
||||||
return { authenticated: true, role: "control", auth_disabled: true };
|
|
||||||
}
|
|
||||||
if (!resp.ok) return { authenticated: false };
|
|
||||||
const data = await resp.json();
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Auth check failed:", e);
|
|
||||||
return { authenticated: false };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function authLogin(passphrase) {
|
async function authLogin(passphrase) {
|
||||||
try {
|
return login(passphrase);
|
||||||
const resp = await fetch("/auth/login", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ passphrase }),
|
|
||||||
});
|
|
||||||
if (resp.status === 404) {
|
|
||||||
return { authenticated: true, role: "control", auth_disabled: true };
|
|
||||||
}
|
|
||||||
if (!resp.ok) {
|
|
||||||
const text = await resp.text();
|
|
||||||
throw new Error(text || "Login failed");
|
|
||||||
}
|
|
||||||
const data = await resp.json();
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function authLogout() {
|
async function authLogout() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/auth/logout", { method: "POST" });
|
await logout();
|
||||||
if (resp.status !== 404 && !resp.ok) throw new Error("Logout failed");
|
|
||||||
authRole = null;
|
authRole = null;
|
||||||
// Disconnect and show auth gate without page reload
|
// Disconnect and show auth gate without page reload
|
||||||
disconnect();
|
disconnect();
|
||||||
|
|||||||
Reference in New Issue
Block a user