feat(panel): implement email otp and passkey login interface
This commit is contained in:
13 files changed
+520
-116
No files matched your search
+30
-1
@@ -111,6 +111,24 @@ export const api = {
|
||||
bind: (code: string) =>
|
||||
request<BindResult>("POST", "/auth/bind", { code }),
|
||||
|
||||
authEmailStart: (email: string) =>
|
||||
request<{ sent: boolean; expires_at: string }>("POST", "/auth/email/start", { email }),
|
||||
|
||||
authEmailVerify: (email: string, code: string) =>
|
||||
request<{ user_id: string; role: string }>("POST", "/auth/email/verify", { email, code }),
|
||||
|
||||
authPasskeyLoginBegin: (email: string) =>
|
||||
request<any>("POST", "/auth/passkey/login/begin", { email }),
|
||||
|
||||
authPasskeyLoginFinish: (email: string, assertion: any) =>
|
||||
request<any>("POST", "/auth/passkey/login/finish", { email, assertion }),
|
||||
|
||||
authPasskeyDiscoverableBegin: () =>
|
||||
request<any>("POST", "/auth/passkey/login/discoverable/begin", {}),
|
||||
|
||||
authPasskeyDiscoverableFinish: (login_id: string, assertion: any) =>
|
||||
request<any>("POST", "/auth/passkey/login/discoverable/finish", { login_id, assertion }),
|
||||
|
||||
// changePassword is callable during the first-login lockdown (the route is
|
||||
// AllowDuringPasswordChange): the server re-verifies current_password, rejects an
|
||||
// unchanged or weak (8–72 byte) new password, writes the new hash, and revokes
|
||||
@@ -436,8 +454,19 @@ export function buildLogsStreamURL(apiBase: string, id: string): string {
|
||||
|
||||
/** humanizeError turns the stable error code into a user-facing line. */
|
||||
export function humanizeError(e: unknown): string {
|
||||
const err = e as Partial<ApiError>;
|
||||
const t = i18next.getFixedT(null, "errors");
|
||||
|
||||
if (e && typeof e === "object" && "name" in e) {
|
||||
const name = (e as any).name;
|
||||
if (name === "NotAllowedError") {
|
||||
return t("passkey_not_allowed");
|
||||
}
|
||||
if (name === "AbortError") {
|
||||
return t("passkey_aborted");
|
||||
}
|
||||
}
|
||||
|
||||
const err = e as Partial<ApiError>;
|
||||
switch (err.code) {
|
||||
// Local-password auth (spec §B1).
|
||||
case "local_auth_disabled":
|
||||
|
||||
@@ -5,3 +5,31 @@ import { twMerge } from "tailwind-merge";
|
||||
export function cn(...inputs: ClassValue[]): string {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function base64urlToBytes(str: string): ArrayBuffer {
|
||||
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const pad = base64.length % 4;
|
||||
if (pad) {
|
||||
base64 += "=".repeat(4 - pad);
|
||||
}
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
export function bytesToBase64url(bytes: ArrayBuffer): string {
|
||||
let binary = "";
|
||||
const uint8 = new Uint8Array(bytes);
|
||||
const len = uint8.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(uint8[i]);
|
||||
}
|
||||
const base64 = btoa(binary);
|
||||
return base64
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
}
|
||||
Reference in new issue
Block a user