From 5b69ae10e866677549143d40f978919d1ce9e740 Mon Sep 17 00:00:00 2001 From: DeathKaioken Date: Mon, 8 Sep 2025 16:04:45 +0200 Subject: [PATCH] Initial Commit Fix --- .../userManagement/api/adminUserProfileApi.js | 4 +- .../userManagement/api/userManagementApi.js | 2 +- .../components/AdminUserActionsSection.jsx | 9 +- .../components/AdminUserPermissionSection.jsx | 0 .../AdminUserPermissionsSection.jsx | 16 +++- .../components/PermissionModal.jsx | 2 +- .../pages/AdminUserProfilePage.jsx | 8 +- .../admin/verifyUser/api/verifyUserApi.js | 2 +- src/features/dashboard/api/dashboardApi.js | 6 +- src/features/login/api/loginApi.js | 4 +- src/features/nav/Header.jsx | 2 +- .../password-reset/api/usePasswordResetApi.js | 6 +- src/features/profile/api/profileApi.js | 4 +- .../PersonalCompleteProfileForm.jsx | 2 +- .../verifyEmail/api/verifyEmailApi.js | 4 +- .../referralManagement/api/referralApi.js | 96 ++++++++++++++++--- .../components/GenerateReferralForm.jsx | 35 ++----- .../components/ReferralLinksTable.jsx | 47 +++------ src/i18n/ressources/de/referral.json | 10 +- src/i18n/ressources/de/user_management.json | 15 ++- src/i18n/ressources/en/referral.json | 10 +- src/i18n/ressources/en/user_management.json | 19 +++- src/store/authStore.jsx | 15 ++- 23 files changed, 208 insertions(+), 110 deletions(-) delete mode 100644 src/features/admin/userManagement/components/AdminUserPermissionSection.jsx diff --git a/src/features/admin/userManagement/api/adminUserProfileApi.js b/src/features/admin/userManagement/api/adminUserProfileApi.js index fbc26fa..51dd30b 100644 --- a/src/features/admin/userManagement/api/adminUserProfileApi.js +++ b/src/features/admin/userManagement/api/adminUserProfileApi.js @@ -3,7 +3,7 @@ import { log } from "../../../../utils/logger"; export async function fetchAdminUserFullData({ id, accessToken }) { log("fetchAdminUserFullData called", { id, accessToken }); const res = await fetch( - `${import.meta.env.VITE_API_BASE_URL}/api/auth/admin/users/${id}/full`, + `${import.meta.env.VITE_API_BASE_URL}/api/admin/users/${id}/full`, { method: "GET", headers: { @@ -26,7 +26,7 @@ export async function fetchAdminUserFullData({ id, accessToken }) { export async function fetchAdminUserDocuments({ id, accessToken }) { log("fetchAdminUserDocuments called", { id, accessToken }); const res = await fetch( - `${import.meta.env.VITE_API_BASE_URL}/api/auth/users/${id}/documents`, + `${import.meta.env.VITE_API_BASE_URL}/api/users/${id}/documents`, { method: "GET", headers: { diff --git a/src/features/admin/userManagement/api/userManagementApi.js b/src/features/admin/userManagement/api/userManagementApi.js index d03f564..0083298 100644 --- a/src/features/admin/userManagement/api/userManagementApi.js +++ b/src/features/admin/userManagement/api/userManagementApi.js @@ -52,7 +52,7 @@ export async function fetchUserFull(accessToken, id) { log("[fetchUserFull] accessToken:", accessToken); log("[fetchUserFull] Request headers:", headers); const res = await authFetch( - `${import.meta.env.VITE_API_BASE_URL}/api/auth/users/${id}/full`, + `${import.meta.env.VITE_API_BASE_URL}/api/users/${id}/full`, { method: "GET", headers, diff --git a/src/features/admin/userManagement/components/AdminUserActionsSection.jsx b/src/features/admin/userManagement/components/AdminUserActionsSection.jsx index 83f3c4b..28a923e 100644 --- a/src/features/admin/userManagement/components/AdminUserActionsSection.jsx +++ b/src/features/admin/userManagement/components/AdminUserActionsSection.jsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { showToast } from "../../../toast/toastUtils"; // adjust import if needed +import useAuthStore from "../../../../store/authStore"; // <-- use zustand auth store export default function AdminUserActionsSection({ userId, // <-- Ensure userId is passed as prop @@ -18,8 +19,12 @@ export default function AdminUserActionsSection({ showToast({ type: "error", tKey: "toast:passwordResetGenericError" }); return; } - const token = sessionStorage.getItem("accessToken"); // <-- use sessionStorage - console.log("JWT accessToken for request:", token); + + // Prefer in-memory zustand token, fallback to sessionStorage only if absent + const storeToken = useAuthStore.getState().accessToken; + const token = storeToken || sessionStorage.getItem("accessToken"); + console.log("JWT accessToken for request (store -> session fallback):", token); + if (!token) { showToast({ type: "error", tKey: "toast:unauthorized" }); return; diff --git a/src/features/admin/userManagement/components/AdminUserPermissionSection.jsx b/src/features/admin/userManagement/components/AdminUserPermissionSection.jsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/features/admin/userManagement/components/AdminUserPermissionsSection.jsx b/src/features/admin/userManagement/components/AdminUserPermissionsSection.jsx index 7cb573b..6d37a06 100644 --- a/src/features/admin/userManagement/components/AdminUserPermissionsSection.jsx +++ b/src/features/admin/userManagement/components/AdminUserPermissionsSection.jsx @@ -2,7 +2,19 @@ import React from "react"; import { useTranslation } from "react-i18next"; export default function AdminUserPermissionsSection({ permissions }) { - const { t } = useTranslation('user_management'); + const { t, i18n } = useTranslation('user_management'); + + // resolve permission display: try namespace key "permissions.names." else use perm.name/raw + const resolvePermissionLabel = (perm) => { + const name = perm?.name || perm; + const key = `permissions.names.${name}`; + if (i18n?.exists(`user_management:${key}`)) { + return t(key); + } + // fallback to description if present, else raw name + return perm?.description || name; + }; + return (

@@ -19,7 +31,7 @@ export default function AdminUserPermissionsSection({ permissions }) { key={perm.id || perm.name || perm} className="inline-block bg-green-100 text-green-700 px-3 py-1 rounded-full text-xs font-medium border border-green-200" > - {perm.name || perm} + {resolvePermissionLabel(perm)} )) ) : ( diff --git a/src/features/admin/userManagement/components/PermissionModal.jsx b/src/features/admin/userManagement/components/PermissionModal.jsx index de7e4b0..ed0730a 100644 --- a/src/features/admin/userManagement/components/PermissionModal.jsx +++ b/src/features/admin/userManagement/components/PermissionModal.jsx @@ -27,7 +27,7 @@ const PermissionModal = ({ open, userId, onClose, onSuccess }) => { setLoading(true); setMessage(""); const baseUrl = import.meta.env.VITE_API_BASE_URL; - const userPermUrl = `${baseUrl}/api/auth/users/${userId}/permissions`; + const userPermUrl = `${baseUrl}/api/users/${userId}/permissions`; const allPermUrl = `${baseUrl}/api/permissions`; console.log("PermissionModal: Requesting user permissions from", userPermUrl); diff --git a/src/features/admin/userManagement/pages/AdminUserProfilePage.jsx b/src/features/admin/userManagement/pages/AdminUserProfilePage.jsx index 00f7b54..89f295f 100644 --- a/src/features/admin/userManagement/pages/AdminUserProfilePage.jsx +++ b/src/features/admin/userManagement/pages/AdminUserProfilePage.jsx @@ -35,7 +35,13 @@ function AdminUserProfilePage() { }, [user, profile]); const [permissionModalOpen, setPermissionModalOpen] = React.useState(false); - const [permissionsState, setPermissionsState] = React.useState(permissions); + // initialize empty and sync below so async-loaded permissions update the UI + const [permissionsState, setPermissionsState] = React.useState(() => permissions || []); + + // Keep local permissionsState in sync with fetched permissions prop + React.useEffect(() => { + setPermissionsState(permissions || []); + }, [permissions]); // Refresh permissions after modal update const refreshPermissions = async () => { diff --git a/src/features/admin/verifyUser/api/verifyUserApi.js b/src/features/admin/verifyUser/api/verifyUserApi.js index 4e63b62..7dcd42a 100644 --- a/src/features/admin/verifyUser/api/verifyUserApi.js +++ b/src/features/admin/verifyUser/api/verifyUserApi.js @@ -26,7 +26,7 @@ export async function fetchVerificationPendingUsers(accessToken) { export async function fetchVerifyUserFull(accessToken, id) { log("fetchVerifyUserFull called", { accessToken, id }); const res = await authFetch( - `${import.meta.env.VITE_API_BASE_URL}/api/auth/users/${id}/full`, + `${import.meta.env.VITE_API_BASE_URL}/api/users/${id}/full`, { method: "GET", headers: { diff --git a/src/features/dashboard/api/dashboardApi.js b/src/features/dashboard/api/dashboardApi.js index 57c7a56..5665313 100644 --- a/src/features/dashboard/api/dashboardApi.js +++ b/src/features/dashboard/api/dashboardApi.js @@ -4,7 +4,7 @@ import { log } from "../../../utils/logger"; export async function fetchUserStatusApi() { log("fetchUserStatusApi called"); const res = await authFetch( - `${import.meta.env.VITE_API_BASE_URL}/api/auth/user/status`, + `${import.meta.env.VITE_API_BASE_URL}/api/user/status`, { method: "GET", credentials: "include" } ); if (!res.ok) { @@ -19,7 +19,7 @@ export async function fetchUserStatusApi() { export async function fetchUserApi() { log("fetchUserApi called"); const res = await authFetch( - `${import.meta.env.VITE_API_BASE_URL}/api/auth/me`, + `${import.meta.env.VITE_API_BASE_URL}/api/me`, { method: "GET", credentials: "include" } ); if (!res.ok) { @@ -34,7 +34,7 @@ export async function fetchUserApi() { export async function refreshTokenApi() { log("refreshTokenApi called"); const res = await fetch( - `${import.meta.env.VITE_API_BASE_URL}/api/auth/refresh`, + `${import.meta.env.VITE_API_BASE_URL}/api/refresh`, { method: "POST", credentials: "include" } ); if (!res.ok) { diff --git a/src/features/login/api/loginApi.js b/src/features/login/api/loginApi.js index 73408bb..13b229e 100644 --- a/src/features/login/api/loginApi.js +++ b/src/features/login/api/loginApi.js @@ -37,8 +37,8 @@ export async function loginApi(email, password) { const normalizedBase = rawBase.replace(/\/+$/, ''); log('loginApi normalized base:', normalizedBase); const endpoint = /\/api$/i.test(normalizedBase) - ? normalizedBase + '/auth/login' - : normalizedBase + '/api/auth/login'; + ? normalizedBase + '/login' + : normalizedBase + '/api/login'; log('loginApi endpoint:', endpoint); function maskEmail(e) { diff --git a/src/features/nav/Header.jsx b/src/features/nav/Header.jsx index 478ade5..0c44f76 100644 --- a/src/features/nav/Header.jsx +++ b/src/features/nav/Header.jsx @@ -49,7 +49,7 @@ function Header() { const handleLogout = async () => { log("🚪 Header: User logout initiated"); try { - log("🌐 Header: Calling Zustand logout (will call /api/auth/logout)"); + log("🌐 Header: Calling Zustand logout (will call /api/logout)"); await logout(); log("✅ Header: Zustand logout completed"); showToast({ type: "success", tKey: "toast:logoutSuccess" }); diff --git a/src/features/password-reset/api/usePasswordResetApi.js b/src/features/password-reset/api/usePasswordResetApi.js index 01f2b32..28aaeb1 100644 --- a/src/features/password-reset/api/usePasswordResetApi.js +++ b/src/features/password-reset/api/usePasswordResetApi.js @@ -2,7 +2,7 @@ import { log } from "../../../utils/logger"; export async function requestPasswordResetApi(email) { log("requestPasswordResetApi called", { email }); - const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/auth/request-password-reset`, { + const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/request-password-reset`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email }) @@ -28,12 +28,12 @@ export async function requestPasswordResetApi(email) { export async function verifyPasswordResetTokenApi(token) { log("verifyPasswordResetTokenApi called", { token }); - return fetch(`${import.meta.env.VITE_API_BASE_URL}/api/auth/verify-password-reset?token=${encodeURIComponent(token)}`); + return fetch(`${import.meta.env.VITE_API_BASE_URL}/api/verify-password-reset?token=${encodeURIComponent(token)}`); } export async function resetPasswordApi(token, newPassword) { log("resetPasswordApi called", { token, newPassword: !!newPassword }); - return fetch(`${import.meta.env.VITE_API_BASE_URL}/api/auth/reset-password`, { + return fetch(`${import.meta.env.VITE_API_BASE_URL}/api/reset-password`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token, newPassword }) diff --git a/src/features/profile/api/profileApi.js b/src/features/profile/api/profileApi.js index 284695a..cef6dcc 100644 --- a/src/features/profile/api/profileApi.js +++ b/src/features/profile/api/profileApi.js @@ -4,7 +4,7 @@ import { log } from "../../../utils/logger"; export async function fetchUserProfile(accessToken) { log("fetchUserProfile called", { accessToken }); const res = await authFetch( - `${import.meta.env.VITE_API_BASE_URL}/api/auth/me`, + `${import.meta.env.VITE_API_BASE_URL}/api/me`, { method: "GET", headers: { @@ -26,7 +26,7 @@ export async function fetchUserProfile(accessToken) { export async function fetchUserPermissions(userId, accessToken) { log("fetchUserPermissions called", { userId, accessToken }); const res = await authFetch( - `${import.meta.env.VITE_API_BASE_URL}/api/auth/users/${userId}/permissions`, + `${import.meta.env.VITE_API_BASE_URL}/api/users/${userId}/permissions`, { method: "GET", headers: { diff --git a/src/features/quickactions/completeProfile/components/PersonalCompleteProfileForm.jsx b/src/features/quickactions/completeProfile/components/PersonalCompleteProfileForm.jsx index f208b5e..c8b23c0 100644 --- a/src/features/quickactions/completeProfile/components/PersonalCompleteProfileForm.jsx +++ b/src/features/quickactions/completeProfile/components/PersonalCompleteProfileForm.jsx @@ -186,7 +186,7 @@ function PersonalCompleteProfileForm({ return (

diff --git a/src/features/quickactions/verifyEmail/api/verifyEmailApi.js b/src/features/quickactions/verifyEmail/api/verifyEmailApi.js index d6f8e4d..88971ef 100644 --- a/src/features/quickactions/verifyEmail/api/verifyEmailApi.js +++ b/src/features/quickactions/verifyEmail/api/verifyEmailApi.js @@ -6,7 +6,7 @@ export async function sendVerificationEmailApi() { const lang = getCurrentLanguage(); log("sendVerificationEmailApi called, lang:", lang); const res = await authFetch( - `${import.meta.env.VITE_API_BASE_URL || "https://profit-planet.partners"}/api/auth/send-verification-email`, + `${import.meta.env.VITE_API_BASE_URL || "https://profit-planet.partners"}/api/send-verification-email`, { method: "POST", credentials: "include", @@ -34,7 +34,7 @@ export async function verifyEmailCodeApi(code) { const lang = getCurrentLanguage(); log("verifyEmailCodeApi called, code:", code, "lang:", lang); const res = await authFetch( - `${import.meta.env.VITE_API_BASE_URL || "https://profit-planet.partners"}/api/auth/verify-email-code`, + `${import.meta.env.VITE_API_BASE_URL || "https://profit-planet.partners"}/api/verify-email-code`, { method: "POST", headers: { "Content-Type": "application/json" }, diff --git a/src/features/referralManagement/api/referralApi.js b/src/features/referralManagement/api/referralApi.js index c27b598..ec26f69 100644 --- a/src/features/referralManagement/api/referralApi.js +++ b/src/features/referralManagement/api/referralApi.js @@ -1,5 +1,7 @@ import { authFetch } from "../../../utils/authFetch"; import { log } from "../../../utils/logger"; +import useAuthStore from "../../../store/authStore"; +import { showToast } from "../../toast/toastUtils"; // show user feedback export async function createReferralLinkApi({ expiresInDays, maxUses }) { log("[referralApi] Creating referral link (authFetch)", { expiresInDays, maxUses }); @@ -7,11 +9,22 @@ export async function createReferralLinkApi({ expiresInDays, maxUses }) { const url = `${import.meta.env.VITE_API_BASE_URL}/api/referral/create`; log("[referralApi] POST", url, "payload:", payload); + // Always use in-memory token from zustand + const token = useAuthStore.getState().accessToken; + if (!token) { + log("[referralApi] No accessToken in auth store - aborting request"); + showToast({ type: "error", message: "Nicht autorisiert. Bitte melden Sie sich erneut an." }); + throw new Error("No access token"); + } + + const headers = { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}`, + }; + const res = await authFetch(url, { method: "POST", - headers: { - "Content-Type": "application/json", - }, + headers, body: JSON.stringify(payload), }); @@ -22,12 +35,35 @@ export async function createReferralLinkApi({ expiresInDays, maxUses }) { if (!res.ok) { let errorMsg = "Failed to generate referral link"; try { - const data = await res.json(); - errorMsg = data.message || errorMsg; - } catch {} + const data = await res.json().catch(() => null); + errorMsg = data?.message || errorMsg; + // If server says max links reached, show explicit toast with server message + if (res.status === 403 && data?.message) { + // Try to extract count and role from message like: + // "Maximum active referral links reached (15) for your role (admin)." + const msg = data.message; + const countMatch = msg.match(/\((\d+)\)/); + const roleMatch = msg.match(/for your role\s*\(([^)]+)\)/i); + const count = countMatch ? countMatch[1] : undefined; + const role = roleMatch ? roleMatch[1] : undefined; + // Prefer a translated toast with values; fallback to server message if translation not present + showToast({ + type: "error", + tKey: "referral:toast.maxLinksReached", + values: { count, role, defaultValue: msg }, + }); + } else { + showToast({ type: "error", message: "Empfehlungslink konnte nicht erstellt werden. Bitte erneut versuchen." }); + } + } catch (parseErr) { + showToast({ type: "error", message: "Empfehlungslink konnte nicht erstellt werden. Bitte erneut versuchen." }); + } log("[referralApi] Error creating referral link:", errorMsg); - throw new Error(errorMsg); + const err = new Error(errorMsg); + err.handled = true; // signal caller that toast was already shown + throw err; } + const result = await res.json(); log("[referralApi] Referral link created:", result); return result; @@ -51,22 +87,58 @@ export async function listReferralLinksApi() { export async function deactivateReferralLinkApi(tokenId) { log("[referralApi] Deactivating referral link (authFetch)...", { tokenId }); const url = `${import.meta.env.VITE_API_BASE_URL}/api/referral/deactivate`; + + // Always use in-memory token from zustand + const token = useAuthStore.getState().accessToken; + if (!token) { + log("[referralApi] No accessToken in auth store - aborting deactivate request"); + showToast({ type: "error", message: "Nicht autorisiert. Bitte melden Sie sich erneut an." }); + throw new Error("No access token"); + } + + const headers = { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}`, + }; + const res = await authFetch(url, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers, body: JSON.stringify({ tokenId }), }); log("[referralApi] deactivate response status:", res.status); + const rawText = await res.clone().text().catch(() => null); + log("[referralApi] deactivate raw response body:", rawText); + if (!res.ok) { let errorMsg = "Failed to deactivate referral link"; try { - const data = await res.json(); - errorMsg = data.message || errorMsg; - } catch {} + const data = await res.json().catch(() => null); + errorMsg = data?.message || errorMsg; + + // Show specific toast messages based on status + if (res.status === 401) { + showToast({ type: "error", message: "Nicht autorisiert. Bitte melden Sie sich erneut an." }); + } else if (res.status === 403) { + showToast({ type: "error", message: data?.message || "Sie haben keine Berechtigung." }); + } else if (res.status === 404) { + showToast({ type: "error", message: "Empfehlungslink nicht gefunden." }); + } else if (res.status === 429) { + showToast({ type: "error", message: "Zu viele Anfragen. Bitte später erneut versuchen." }); + } else { + showToast({ type: "error", message: data?.message || "Deaktivierung des Empfehlungslinks fehlgeschlagen" }); + } + } catch (parseErr) { + showToast({ type: "error", message: "Deaktivierung des Empfehlungslinks fehlgeschlagen" }); + } + log("[referralApi] Error deactivating referral link:", errorMsg); - throw new Error(errorMsg); + const err = new Error(errorMsg); + err.handled = true; // signal caller that toast was already shown + throw err; } + const result = await res.json(); log("[referralApi] Referral link deactivated:", result); return result; diff --git a/src/features/referralManagement/components/GenerateReferralForm.jsx b/src/features/referralManagement/components/GenerateReferralForm.jsx index cb23583..b409e59 100644 --- a/src/features/referralManagement/components/GenerateReferralForm.jsx +++ b/src/features/referralManagement/components/GenerateReferralForm.jsx @@ -3,6 +3,7 @@ import useAuthStore from "../../../store/authStore"; import { showToast } from "../../toast/toastUtils"; import { useTranslation } from "react-i18next"; import { log } from "../../../utils/logger"; +import { createReferralLinkApi } from "../api/referralApi"; function GenerateReferralForm({ onReferralGenerated }) { const [expiresInDays, setExpiresInDays] = useState(3); // can become number | 'unlimited' @@ -29,41 +30,21 @@ function GenerateReferralForm({ onReferralGenerated }) { : { expiresInDays, maxUses }; log("[GenerateReferralForm] Payload to /api/referral/create:", body); - const res = await fetch( - `${import.meta.env.VITE_API_BASE_URL}/api/referral/create`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify(body), - } - ); - log("[GenerateReferralForm] Response status:", res.status, "headers:", Object.fromEntries(res.headers.entries())); - const rawText = await res.clone().text(); - log("[GenerateReferralForm] Raw response body:", rawText); - - if (!res.ok) { - log("[GenerateReferralForm] Failed to generate referral link"); - showToast({ type: "error", message: t('toast.generateFail') }); - setLoading(false); - return; - } - - const data = await res.json(); + const data = await createReferralLinkApi(body); log("[GenerateReferralForm] Referral link response:", data); - - if (data.link) { + if (data && data.link) { showToast({ type: "success", message: t('toast.generateSuccess') }); } else { showToast({ type: "error", message: t('toast.generateNoLink') }); } - await onReferralGenerated(); } catch (error) { log("[GenerateReferralForm] Error generating referral link:", error); - showToast({ type: "error", message: t('toast.generateError') }); + // createReferralLinkApi will have shown server message toast for known cases, + // show a generic error only if no user-friendly message was already shown + if (!error?.handled) { + showToast({ type: "error", message: t('toast.generateError') }); + } } setLoading(false); }; diff --git a/src/features/referralManagement/components/ReferralLinksTable.jsx b/src/features/referralManagement/components/ReferralLinksTable.jsx index 041f682..b982dab 100644 --- a/src/features/referralManagement/components/ReferralLinksTable.jsx +++ b/src/features/referralManagement/components/ReferralLinksTable.jsx @@ -1,7 +1,8 @@ import React, { useState } from "react"; -import { showToast } from "../../../features/toast/toastUtils.js"; +import { showToast } from "../../toast/toastUtils"; // correct relative path import { useTranslation } from "react-i18next"; import { log } from "../../../utils/logger"; +import { deactivateReferralLinkApi } from "../api/referralApi"; // use API helper function ReferralLinksTable({ links, onLinkUpdate }) { const [deactivatingLinks, setDeactivatingLinks] = useState(new Set()); @@ -13,43 +14,23 @@ function ReferralLinksTable({ links, onLinkUpdate }) { setDeactivatingLinks((prev) => new Set(prev).add(tokenId)); log("[ReferralLinksTable] Deactivating link...", { tokenId }); try { - const res = await fetch( - `${import.meta.env.VITE_API_BASE_URL}/api/referral/deactivate`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${sessionStorage.getItem("accessToken")}`, - }, - body: JSON.stringify({ tokenId }), - } - ); - if (res.ok) { - const data = await res.json(); - log("[ReferralLinksTable] Deactivate response:", data); - if (data.success) { - showToast({ - type: "success", - message: t('toast.deactivateSuccess'), - }); - if (onLinkUpdate) onLinkUpdate(); - } else { - showToast({ - type: "error", - message: data.message || t('toast.deactivateFail'), - }); - } + // Use API helper which reads token from zustand and handles errors/toasts centrally + const data = await deactivateReferralLinkApi(tokenId); + log("[ReferralLinksTable] Deactivate response:", data); + if (data && data.success) { + showToast({ type: "success", message: t('toast.deactivateSuccess') }); + if (onLinkUpdate) onLinkUpdate(); } else { - log("[ReferralLinksTable] Failed to deactivate link"); - showToast({ - type: "error", - message: t('toast.deactivateFail'), - }); + showToast({ type: "error", message: data?.message || t('toast.deactivateFail') }); } } catch (error) { log("[ReferralLinksTable] Error deactivating link:", error); console.error("Error deactivating referral link:", error); - showToast({ type: "error", message: t('toast.deactivateFail') }); + // deactivateReferralLinkApi already shows specific toasts for many statuses; + // show fallback if needed + if (!error?.handled) { + showToast({ type: "error", message: t('toast.deactivateFail') }); + } } finally { setDeactivatingLinks((prev) => { const newSet = new Set(prev); diff --git a/src/i18n/ressources/de/referral.json b/src/i18n/ressources/de/referral.json index 72d1831..1988cad 100644 --- a/src/i18n/ressources/de/referral.json +++ b/src/i18n/ressources/de/referral.json @@ -34,7 +34,9 @@ "deactivated": "Deaktiviert", "unknown": "Unbekannt", "copy": "Kopieren", - "copySuccess": "Link in die Zwischenablage kopiert!" + "copySuccess": "Link in die Zwischenablage kopiert!", + "never": "Nie", + "unlimited": "Unbegrenzt" }, "form": { "generateHeading": "Neuen Empfehlungslink erstellen", @@ -42,6 +44,8 @@ "linkExpiration": "Link Ablauf", "maximumUses": "Maximale Verwendungen", "option": { + "expirationUnlimited": "Unbegrenzt", + "unlimited": "Unbegrenzt", "1Day": "24 Stunden (1 Tag)", "3Days": "72 Stunden (3 Tage)", "1Week": "168 Stunden (1 Woche)", @@ -63,6 +67,8 @@ "generateFail": "Empfehlungslink konnte nicht erstellt werden. Bitte erneut versuchen.", "generateNoLink": "Erstellung fehlgeschlagen – kein Link zurückgegeben.", "generateError": "Beim Erstellen des Empfehlungslinks ist ein Fehler aufgetreten.", - "noPermission": "Du hast keine Berechtigung für die Empfehlungsverwaltung." + "noPermission": "Du hast keine Berechtigung für die Empfehlungsverwaltung.", + "unauthorized": "Nicht autorisiert. Bitte melden Sie sich erneut an.", + "maxLinksReached": "Maximale Anzahl aktiver Empfehlungslinks erreicht ({{count}}) für Ihre Rolle ({{role}})." } } diff --git a/src/i18n/ressources/de/user_management.json b/src/i18n/ressources/de/user_management.json index b313e0d..3928260 100644 --- a/src/i18n/ressources/de/user_management.json +++ b/src/i18n/ressources/de/user_management.json @@ -24,10 +24,11 @@ "enlarge": "Vergrößern", "download": "Herunterladen", "sendPasswordReset": "Passwort-Reset senden", + "sendingPasswordReset": "Passwort-Reset wird gesendet...", "editPermissions": "Berechtigungen bearbeiten", - "statistic": "Statistik", + "statistic": "Statistiken", "exportUserData": "Benutzerdaten exportieren", - "logs": "Logs", + "logs": "Protokolle", "delete": "Löschen" }, "filters": { @@ -117,7 +118,10 @@ "download": "Download" }, "permissions": { - "none": "Keine Berechtigungen" + "none": "Keine speziellen Berechtigungen zugewiesen.", + "names": { + "can_create_referrals": "Darf Empfehlungslinks erstellen" + } }, "misc": { "loading": "Lädt...", @@ -135,5 +139,10 @@ "confirmText": "Sind Sie sicher, dass Sie diesen Benutzer und alle zugehörigen Daten dauerhaft löschen möchten?", "cancel": "Abbrechen", "delete": "Löschen" + }, + "toast": { + "passwordResetSuccess": "Passwort-Reset-Link erfolgreich gesendet!", + "passwordResetGenericError": "Passwort-Reset konnte nicht angefordert werden. Bitte erneut versuchen.", + "unauthorized": "Nicht autorisiert." } } diff --git a/src/i18n/ressources/en/referral.json b/src/i18n/ressources/en/referral.json index 1a6de93..c2f1d66 100644 --- a/src/i18n/ressources/en/referral.json +++ b/src/i18n/ressources/en/referral.json @@ -34,7 +34,9 @@ "deactivated": "Deactivated", "unknown": "Unknown", "copy": "Copy", - "copySuccess": "Link copied to clipboard!" + "copySuccess": "Link copied to clipboard!", + "never": "Never", + "unlimited": "Unlimited" }, "form": { "generateHeading": "Generate New Referral Link", @@ -42,6 +44,8 @@ "linkExpiration": "Link Expiration", "maximumUses": "Maximum Uses", "option": { + "expirationUnlimited": "Unlimited", + "unlimited": "Unlimited", "1Day": "24 Hours (1 Day)", "3Days": "72 Hours (3 Days)", "1Week": "168 Hours (1 Week)", @@ -63,6 +67,8 @@ "generateFail": "Failed to generate referral link. Please try again.", "generateNoLink": "Referral link generation failed - no link provided.", "generateError": "An error occurred while generating the referral link.", - "noPermission": "You do not have permission to access Referral Management." + "noPermission": "You do not have permission to access Referral Management.", + "unauthorized": "Not authorized. Please sign in again.", + "maxLinksReached": "Maximum active referral links reached ({{count}}) for your role ({{role}})." } } diff --git a/src/i18n/ressources/en/user_management.json b/src/i18n/ressources/en/user_management.json index ca9a96d..8494b40 100644 --- a/src/i18n/ressources/en/user_management.json +++ b/src/i18n/ressources/en/user_management.json @@ -23,10 +23,11 @@ "edit": "Edit", "enlarge": "Enlarge", "download": "Download", - "sendPasswordReset": "Send Password Reset Link", - "editPermissions": "Edit Permissions", - "statistic": "Statistic", - "exportUserData": "Export User Data", + "sendPasswordReset": "Send password reset", + "sendingPasswordReset": "Sending password reset...", + "editPermissions": "Edit permissions", + "statistic": "Statistics", + "exportUserData": "Export user data", "logs": "Logs", "delete": "Delete" }, @@ -117,7 +118,10 @@ "download": "Download" }, "permissions": { - "none": "No permissions" + "none": "No special permissions assigned.", + "names": { + "can_create_referrals": "Can create referral links" + } }, "misc": { "loading": "Loading...", @@ -135,5 +139,10 @@ "confirmText": "Are you sure you want to permanently delete this user and all related data?", "cancel": "Cancel", "delete": "Delete" + }, + "toast": { + "passwordResetSuccess": "Password reset link sent successfully!", + "passwordResetGenericError": "Failed to request password reset. Please try again.", + "unauthorized": "Not authorized." } } diff --git a/src/store/authStore.jsx b/src/store/authStore.jsx index 51ea8e0..05f71ac 100644 --- a/src/store/authStore.jsx +++ b/src/store/authStore.jsx @@ -91,7 +91,7 @@ const useAuthStore = create((set, get) => ({ logout: async () => { log("🚪 Zustand: Logging out — revoking refresh token on server"); try { - const logoutUrl = `${import.meta.env.VITE_API_BASE_URL}/api/auth/logout`; + const logoutUrl = `${import.meta.env.VITE_API_BASE_URL}/api/logout`; log("🌐 Zustand: Calling logout endpoint:", logoutUrl); const res = await fetch(logoutUrl, { method: "POST", @@ -127,12 +127,23 @@ const useAuthStore = create((set, get) => ({ return get().refreshPromise; } + // SHORT-CIRCUIT: if we already have a valid accessToken that's not about to expire, skip refresh + const currentToken = get().accessToken; + if (currentToken) { + const expiry = getTokenExpiry(currentToken); + if (expiry && expiry.getTime() - Date.now() > 60 * 1000) { // more than 60s left + log("⏸️ Zustand: accessToken present and valid, skipping refresh"); + return Promise.resolve(true); + } + } + log("🔄 Zustand: refreshAuthToken - starting new refresh"); // create promise so concurrent callers can await it const p = (async () => { set({ isRefreshing: true }); try { - const refreshUrl = `${import.meta.env.VITE_API_BASE_URL}/api/auth/refresh`; + // NOTE: backend expects /api/refresh (align with other clients) + const refreshUrl = `${import.meta.env.VITE_API_BASE_URL}/api/refresh`; log("🌐 Zustand: Calling refresh endpoint:", refreshUrl); const res = await fetch(refreshUrl, {