- Bottling form: jar size and best-before from the product with source, differs-hints before saving, prefill status, water-content confirmation naming the measured value, idempotency key renewed on body change; product form gets shelf life (months) and whole-gram jar weight. - Telegram frontend: link card with step-up dialog, t.me-only deep link, status polling, unlink; system Telegram card with receiver/sender state and confirmed retry; StepUpDialog shared with the backup import. - Reauth calls never refresh-and-retry on 401 (no double lockout count); German texts for expired session and 429; service worker never caches Telegram, notification settings and system responses. - Telegram backend round 2: shared link removal on every disconnect path, lease released on shutdown, connection errors not counted as poison. - ARCHITECTURE.md §2, §6 (Annahmen 56–58), §10, §11.12, §11.13; TASKS.md T4. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
85 lines
2.9 KiB
TypeScript
85 lines
2.9 KiB
TypeScript
import { apiFetch, apiFetchBlob } from "@/lib/auth/api-client";
|
|
import { MfaEnrollment, ProfileSummary, UpdateProfileInput } from "./types";
|
|
|
|
export function updateProfile(input: UpdateProfileInput): Promise<ProfileSummary> {
|
|
return apiFetch<ProfileSummary>("/profile", {
|
|
method: "PATCH",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
export function changePassword(currentPassword: string, newPassword: string): Promise<void> {
|
|
return apiFetch<void>("/profile/password", {
|
|
method: "POST",
|
|
body: JSON.stringify({ currentPassword, newPassword }),
|
|
}, { refreshOn401: false });
|
|
}
|
|
|
|
export function deactivateAccount(currentPassword: string): Promise<void> {
|
|
return apiFetch<void>("/profile", {
|
|
method: "DELETE",
|
|
body: JSON.stringify({ currentPassword }),
|
|
}, { refreshOn401: false });
|
|
}
|
|
|
|
// Sent as a base64 data: URL in a plain JSON body rather than
|
|
// multipart/form-data: this deployment's Cloudflare proxy has a known
|
|
// history of mishandling multipart uploads regardless of size (see the
|
|
// registry-push workaround in .gitea/workflows/*-image.yml), while plain
|
|
// JSON POSTs to this same backend have been reliable all along.
|
|
function fileToDataUrl(file: File): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => resolve(String(reader.result));
|
|
reader.onerror = () => reject(reader.error);
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
export async function uploadAvatar(file: File): Promise<void> {
|
|
const data = await fileToDataUrl(file);
|
|
return apiFetch<void>("/profile/avatar", {
|
|
method: "POST",
|
|
body: JSON.stringify({ data }),
|
|
});
|
|
}
|
|
|
|
export function deleteAvatar(): Promise<void> {
|
|
return apiFetch<void>("/profile/avatar", { method: "DELETE" });
|
|
}
|
|
|
|
// Fetches the avatar as a blob (the endpoint requires the caller's own
|
|
// Bearer token, so a plain <img src="..."> can't hit it directly) and hands
|
|
// back an object URL for use as an <img> source. Returns null when the user
|
|
// has no avatar (404) or the request otherwise fails.
|
|
export async function fetchAvatarUrl(): Promise<string | null> {
|
|
try {
|
|
const { blob } = await apiFetchBlob("/profile/avatar", "avatar");
|
|
return URL.createObjectURL(blob);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function enrollMfa(): Promise<MfaEnrollment> {
|
|
return apiFetch<MfaEnrollment>("/profile/mfa/enroll", { method: "POST" });
|
|
}
|
|
|
|
export function confirmMfaEnrollment(
|
|
setupToken: string,
|
|
code: string,
|
|
currentPassword: string,
|
|
): Promise<void> {
|
|
return apiFetch<void>("/profile/mfa/enroll/confirm", {
|
|
method: "POST",
|
|
body: JSON.stringify({ setupToken, code, currentPassword }),
|
|
}, { refreshOn401: false });
|
|
}
|
|
|
|
export function disableMfa(currentPassword: string, mfaCode: string): Promise<void> {
|
|
return apiFetch<void>("/profile/mfa/disable", {
|
|
method: "POST",
|
|
body: JSON.stringify({ currentPassword, mfaCode }),
|
|
}, { refreshOn401: false });
|
|
}
|