Add full-screen API-key gate
In user-key mode a browser with no stored Ollama key now gets a polished gate instead of the app shell: one masked input with a show/hide toggle, a single Continue button, inline validation errors, and a "?" help sheet with the steps to a free key. - Validates the draft key through /api/health (real 1-token probe) before saving; invalid-key and network failures read differently. - Key still lives only in this browser's localStorage; nothing server-side. - Gate wraps /, /create and deck study; server-key and local-daemon modes skip it; removing the key in Settings brings it back. - Removes the now-redundant dashboard/create API-key banner. - Monochrome corner glows (dark near-black / light near-white), 24px card, 48px field and button, gentle fade/slide-in, reduced-motion aware. - Help modal: centred dialog on desktop, bottom sheet on mobile, closes via X, Escape or backdrop. - README and browser suites updated (34 gate checks, 15 key-flow checks, 12 shipped-default checks; ui/folders/docs/pwa/security/contrast green).
This commit is contained in:
parent
532b5a18fe
commit
dfa7b7502a
13 changed files with 505 additions and 94 deletions
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import type { Activity, Difficulty } from "./schemas";
|
||||
import { authHeaders } from "./settings";
|
||||
import { API_KEY_HEADER, authHeaders } from "./settings";
|
||||
|
||||
export type GenerateInput = {
|
||||
images?: string[];
|
||||
|
|
@ -207,3 +207,59 @@ export async function fetchHealth(): Promise<Health> {
|
|||
throw new ApiError("Could not reach the Freezmo server.", "network");
|
||||
}
|
||||
}
|
||||
|
||||
export type KeyCheck =
|
||||
| { result: "valid" }
|
||||
| { result: "invalid"; message: string }
|
||||
| { result: "offline"; message: string };
|
||||
|
||||
/**
|
||||
* Checks a *draft* key before it is saved anywhere, by asking our own
|
||||
* `/api/health` route (which performs a real one-token chat probe against
|
||||
* Ollama — model listings alone prove nothing). The key travels in the
|
||||
* request header for this single call and is never stored server-side.
|
||||
*/
|
||||
export async function verifyOllamaKey(apiKey: string): Promise<KeyCheck> {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch("/api/health", {
|
||||
cache: "no-store",
|
||||
headers: { [API_KEY_HEADER]: apiKey },
|
||||
});
|
||||
} catch {
|
||||
return {
|
||||
result: "offline",
|
||||
message: "Could not reach the Freezmo server. Is it still running?",
|
||||
};
|
||||
}
|
||||
|
||||
const body = (await response.json().catch(() => null)) as Health | null;
|
||||
if (!response.ok || !body) {
|
||||
return {
|
||||
result: "offline",
|
||||
message: "The Freezmo server answered unexpectedly. Try again in a moment.",
|
||||
};
|
||||
}
|
||||
|
||||
if (body.keyAccepted === false) {
|
||||
return {
|
||||
result: "invalid",
|
||||
message:
|
||||
body.error ??
|
||||
"Ollama rejected this key. Check for typos, or create a new one at ollama.com/settings/keys.",
|
||||
};
|
||||
}
|
||||
if (body.keyAccepted === true) return { result: "valid" };
|
||||
|
||||
if (!body.reachable) {
|
||||
return {
|
||||
result: "offline",
|
||||
message:
|
||||
body.error ?? "Couldn't reach Ollama just now. Check your connection and try again.",
|
||||
};
|
||||
}
|
||||
|
||||
// Reachable, but the probe was inconclusive (usually a model-availability
|
||||
// matter rather than a bad key). Let the user in — Settings shows details.
|
||||
return { result: "valid" };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue