feat: Profilseite, Etikettendruck mit QR-Code, mehr Automatisierungen und Entra-ID-SSO
- Profilseite mit MFA-Einrichtung, Passwort/E-Mail/Name aendern, Konto deaktivieren - Bearbeiten-Buttons fuer Bienenstaende/Voelker/Koeniginnen/Aufgaben-Datensaetze - Restliche englische Enum-Werte durch deutsche Labels ersetzt (SelectValue-Fix) - Sidebar-Eintraege fuer Ernten/Schleuderungen ergaenzt - Automatisierungen von 3 auf 6 Regeln erweitert (Schwarmstimmung, kritischer Gesundheitsstatus, Lagerbestand) - Responsive Grid-Layout fuer Volk-/Koeniginnen-/Honigchargen-/Bienenstand-Detailseiten - Etikettenvorlagen mit Platzhaltern inkl. QR-Code (verweist auf Herkunftsseite) und PDF-Druck - Entra-ID-SSO (Authorization Code + PKCE, kein Auto-Provisioning, Secrets nur ueber Environment-Variablen)
This commit is contained in:
1 parent
0a52b29c57
commit
8432313fa9
83 files changed
+2684
-289
No files matched your search
@@ -72,6 +72,39 @@ export function refreshRequest(): Promise<AccessTokenResponse | null> {
|
||||
return inFlightRefresh;
|
||||
}
|
||||
|
||||
export async function ssoStatusRequest(): Promise<boolean> {
|
||||
const apiUrl = await getApiUrl();
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/auth/sso/status`);
|
||||
if (!response.ok) {
|
||||
return false;
|
||||
}
|
||||
const data = (await response.json()) as { enabled: boolean };
|
||||
return data.enabled;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function ssoLoginUrl(): Promise<string> {
|
||||
const apiUrl = await getApiUrl();
|
||||
return `${apiUrl}/auth/sso/login`;
|
||||
}
|
||||
|
||||
// The one-time completion code lives only in an httpOnly cookie the callback
|
||||
// set (never in the URL or a request body) - the browser sends it automatically.
|
||||
export async function ssoCompleteRequest(): Promise<AccessTokenResponse> {
|
||||
const apiUrl = await getApiUrl();
|
||||
const response = await fetch(`${apiUrl}/auth/sso/complete`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('SSO-Anmeldung fehlgeschlagen. Bitte erneut versuchen.');
|
||||
}
|
||||
return response.json() as Promise<AccessTokenResponse>;
|
||||
}
|
||||
|
||||
export async function logoutRequest(): Promise<void> {
|
||||
const apiUrl = await getApiUrl();
|
||||
await fetch(`${apiUrl}/auth/logout`, { method: 'POST', credentials: 'include' });
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
loginRequest,
|
||||
logoutRequest,
|
||||
refreshRequest,
|
||||
ssoCompleteRequest,
|
||||
verifyMfaRequest,
|
||||
} from './api';
|
||||
import { getAccessToken, setAccessToken as storeAccessToken } from './token-store';
|
||||
@@ -29,6 +30,7 @@ interface AuthContextValue {
|
||||
isLoading: boolean;
|
||||
login: (email: string, password: string) => Promise<LoginOutcome>;
|
||||
verifyMfa: (mfaToken: string, code: string) => Promise<void>;
|
||||
completeSso: () => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
hasPermission: (permission: string) => boolean;
|
||||
refreshUser: () => Promise<void>;
|
||||
@@ -76,6 +78,11 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
setUser(await applyAccessToken(result.accessToken));
|
||||
}, []);
|
||||
|
||||
const completeSso = useCallback(async () => {
|
||||
const result = await ssoCompleteRequest();
|
||||
setUser(await applyAccessToken(result.accessToken));
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
await logoutRequest();
|
||||
storeAccessToken(null);
|
||||
@@ -100,7 +107,16 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{ user, isLoading, login, verifyMfa, logout, hasPermission, refreshUser }}
|
||||
value={{
|
||||
user,
|
||||
isLoading,
|
||||
login,
|
||||
verifyMfa,
|
||||
completeSso,
|
||||
logout,
|
||||
hasPermission,
|
||||
refreshUser,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
export type AutomationTriggerType =
|
||||
| "QUEENLESS_NO_EGGS"
|
||||
| "VARROA_THRESHOLD"
|
||||
| "FEED_STATUS_CRITICAL";
|
||||
| "FEED_STATUS_CRITICAL"
|
||||
| "SWARM_MOOD_HIGH"
|
||||
| "COLONY_HEALTH_CRITICAL"
|
||||
| "STOCK_ITEM_LOW";
|
||||
|
||||
export interface AutomationRule {
|
||||
id: string;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { apiFetch, apiFetchBlob } from "@/lib/auth/api-client";
|
||||
import { downloadBlob } from "@/lib/export/api";
|
||||
import { CreateLabelTemplateInput, LabelTemplate, UpdateLabelTemplateInput } from "./types";
|
||||
|
||||
export function fetchLabelTemplates(): Promise<LabelTemplate[]> {
|
||||
return apiFetch<LabelTemplate[]>("/label-templates");
|
||||
}
|
||||
|
||||
export function createLabelTemplate(input: CreateLabelTemplateInput): Promise<LabelTemplate> {
|
||||
return apiFetch<LabelTemplate>("/label-templates", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function updateLabelTemplate(
|
||||
id: string,
|
||||
input: UpdateLabelTemplateInput,
|
||||
): Promise<LabelTemplate> {
|
||||
return apiFetch<LabelTemplate>(`/label-templates/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function deactivateLabelTemplate(id: string): Promise<LabelTemplate> {
|
||||
return apiFetch<LabelTemplate>(`/label-templates/${id}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
export async function printLabels(templateId: string, bottlingBatchId: string): Promise<void> {
|
||||
const { blob, filename } = await apiFetchBlob(
|
||||
`/label-templates/${templateId}/print/${bottlingBatchId}`,
|
||||
"etiketten.pdf",
|
||||
);
|
||||
downloadBlob(blob, filename);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
export interface LabelTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
content: string;
|
||||
widthMm: number;
|
||||
heightMm: number;
|
||||
isDefault: boolean;
|
||||
active: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateLabelTemplateInput {
|
||||
name: string;
|
||||
content: string;
|
||||
widthMm?: number;
|
||||
heightMm?: number;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateLabelTemplateInput {
|
||||
name?: string;
|
||||
content?: string;
|
||||
widthMm?: number;
|
||||
heightMm?: number;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export const LABEL_PLACEHOLDERS: { key: string; description: string }[] = [
|
||||
{ key: "batchNumber", description: "Chargennummer der Honigcharge" },
|
||||
{ key: "honeyType", description: "Honigsorte" },
|
||||
{ key: "lotNumber", description: "Los-/Chargennummer der Abfüllung" },
|
||||
{ key: "bottlingDate", description: "Abfülldatum" },
|
||||
{ key: "bestBeforeDate", description: "Mindesthaltbarkeitsdatum (MHD)" },
|
||||
{ key: "jarSize", description: "Glasgröße" },
|
||||
{ key: "productName", description: "Produktname" },
|
||||
{
|
||||
key: "qrCode",
|
||||
description: "QR-Code zur öffentlichen Herkunftsseite der Charge (Honigsorte, Daten, Rückverfolgbarkeit)",
|
||||
},
|
||||
];
|
||||
@@ -15,6 +15,13 @@ export function changePassword(currentPassword: string, newPassword: string): Pr
|
||||
});
|
||||
}
|
||||
|
||||
export function deactivateAccount(currentPassword: string): Promise<void> {
|
||||
return apiFetch<void>("/profile", {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({ currentPassword }),
|
||||
});
|
||||
}
|
||||
|
||||
export function uploadAvatar(file: File): Promise<void> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
Reference in new issue
Block a user