Initial Commit Fix

This commit is contained in:
DeathKaioken 2025-09-08 16:04:45 +02:00
parent b3acaef775
commit 5b69ae10e8
23 changed files with 208 additions and 110 deletions

View File

@ -3,7 +3,7 @@ import { log } from "../../../../utils/logger";
export async function fetchAdminUserFullData({ id, accessToken }) { export async function fetchAdminUserFullData({ id, accessToken }) {
log("fetchAdminUserFullData called", { id, accessToken }); log("fetchAdminUserFullData called", { id, accessToken });
const res = await fetch( 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", method: "GET",
headers: { headers: {
@ -26,7 +26,7 @@ export async function fetchAdminUserFullData({ id, accessToken }) {
export async function fetchAdminUserDocuments({ id, accessToken }) { export async function fetchAdminUserDocuments({ id, accessToken }) {
log("fetchAdminUserDocuments called", { id, accessToken }); log("fetchAdminUserDocuments called", { id, accessToken });
const res = await fetch( 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", method: "GET",
headers: { headers: {

View File

@ -52,7 +52,7 @@ export async function fetchUserFull(accessToken, id) {
log("[fetchUserFull] accessToken:", accessToken); log("[fetchUserFull] accessToken:", accessToken);
log("[fetchUserFull] Request headers:", headers); log("[fetchUserFull] Request headers:", headers);
const res = await authFetch( 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", method: "GET",
headers, headers,

View File

@ -1,6 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { showToast } from "../../../toast/toastUtils"; // adjust import if needed import { showToast } from "../../../toast/toastUtils"; // adjust import if needed
import useAuthStore from "../../../../store/authStore"; // <-- use zustand auth store
export default function AdminUserActionsSection({ export default function AdminUserActionsSection({
userId, // <-- Ensure userId is passed as prop userId, // <-- Ensure userId is passed as prop
@ -18,8 +19,12 @@ export default function AdminUserActionsSection({
showToast({ type: "error", tKey: "toast:passwordResetGenericError" }); showToast({ type: "error", tKey: "toast:passwordResetGenericError" });
return; 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) { if (!token) {
showToast({ type: "error", tKey: "toast:unauthorized" }); showToast({ type: "error", tKey: "toast:unauthorized" });
return; return;

View File

@ -2,7 +2,19 @@ import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export default function AdminUserPermissionsSection({ permissions }) { export default function AdminUserPermissionsSection({ permissions }) {
const { t } = useTranslation('user_management'); const { t, i18n } = useTranslation('user_management');
// resolve permission display: try namespace key "permissions.names.<permName>" 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 ( return (
<div className="mb-10"> <div className="mb-10">
<h2 className="text-lg font-semibold text-gray-800 mb-4 flex items-center gap-2"> <h2 className="text-lg font-semibold text-gray-800 mb-4 flex items-center gap-2">
@ -19,7 +31,7 @@ export default function AdminUserPermissionsSection({ permissions }) {
key={perm.id || perm.name || perm} 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" 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)}
</span> </span>
)) ))
) : ( ) : (

View File

@ -27,7 +27,7 @@ const PermissionModal = ({ open, userId, onClose, onSuccess }) => {
setLoading(true); setLoading(true);
setMessage(""); setMessage("");
const baseUrl = import.meta.env.VITE_API_BASE_URL; 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`; const allPermUrl = `${baseUrl}/api/permissions`;
console.log("PermissionModal: Requesting user permissions from", userPermUrl); console.log("PermissionModal: Requesting user permissions from", userPermUrl);

View File

@ -35,7 +35,13 @@ function AdminUserProfilePage() {
}, [user, profile]); }, [user, profile]);
const [permissionModalOpen, setPermissionModalOpen] = React.useState(false); 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 // Refresh permissions after modal update
const refreshPermissions = async () => { const refreshPermissions = async () => {

View File

@ -26,7 +26,7 @@ export async function fetchVerificationPendingUsers(accessToken) {
export async function fetchVerifyUserFull(accessToken, id) { export async function fetchVerifyUserFull(accessToken, id) {
log("fetchVerifyUserFull called", { accessToken, id }); log("fetchVerifyUserFull called", { accessToken, id });
const res = await authFetch( 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", method: "GET",
headers: { headers: {

View File

@ -4,7 +4,7 @@ import { log } from "../../../utils/logger";
export async function fetchUserStatusApi() { export async function fetchUserStatusApi() {
log("fetchUserStatusApi called"); log("fetchUserStatusApi called");
const res = await authFetch( 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" } { method: "GET", credentials: "include" }
); );
if (!res.ok) { if (!res.ok) {
@ -19,7 +19,7 @@ export async function fetchUserStatusApi() {
export async function fetchUserApi() { export async function fetchUserApi() {
log("fetchUserApi called"); log("fetchUserApi called");
const res = await authFetch( 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" } { method: "GET", credentials: "include" }
); );
if (!res.ok) { if (!res.ok) {
@ -34,7 +34,7 @@ export async function fetchUserApi() {
export async function refreshTokenApi() { export async function refreshTokenApi() {
log("refreshTokenApi called"); log("refreshTokenApi called");
const res = await fetch( 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" } { method: "POST", credentials: "include" }
); );
if (!res.ok) { if (!res.ok) {

View File

@ -37,8 +37,8 @@ export async function loginApi(email, password) {
const normalizedBase = rawBase.replace(/\/+$/, ''); const normalizedBase = rawBase.replace(/\/+$/, '');
log('loginApi normalized base:', normalizedBase); log('loginApi normalized base:', normalizedBase);
const endpoint = /\/api$/i.test(normalizedBase) const endpoint = /\/api$/i.test(normalizedBase)
? normalizedBase + '/auth/login' ? normalizedBase + '/login'
: normalizedBase + '/api/auth/login'; : normalizedBase + '/api/login';
log('loginApi endpoint:', endpoint); log('loginApi endpoint:', endpoint);
function maskEmail(e) { function maskEmail(e) {

View File

@ -49,7 +49,7 @@ function Header() {
const handleLogout = async () => { const handleLogout = async () => {
log("🚪 Header: User logout initiated"); log("🚪 Header: User logout initiated");
try { try {
log("🌐 Header: Calling Zustand logout (will call /api/auth/logout)"); log("🌐 Header: Calling Zustand logout (will call /api/logout)");
await logout(); await logout();
log("✅ Header: Zustand logout completed"); log("✅ Header: Zustand logout completed");
showToast({ type: "success", tKey: "toast:logoutSuccess" }); showToast({ type: "success", tKey: "toast:logoutSuccess" });

View File

@ -2,7 +2,7 @@ import { log } from "../../../utils/logger";
export async function requestPasswordResetApi(email) { export async function requestPasswordResetApi(email) {
log("requestPasswordResetApi called", { 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", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }) body: JSON.stringify({ email })
@ -28,12 +28,12 @@ export async function requestPasswordResetApi(email) {
export async function verifyPasswordResetTokenApi(token) { export async function verifyPasswordResetTokenApi(token) {
log("verifyPasswordResetTokenApi called", { 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) { export async function resetPasswordApi(token, newPassword) {
log("resetPasswordApi called", { token, newPassword: !!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", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token, newPassword }) body: JSON.stringify({ token, newPassword })

View File

@ -4,7 +4,7 @@ import { log } from "../../../utils/logger";
export async function fetchUserProfile(accessToken) { export async function fetchUserProfile(accessToken) {
log("fetchUserProfile called", { accessToken }); log("fetchUserProfile called", { accessToken });
const res = await authFetch( 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", method: "GET",
headers: { headers: {
@ -26,7 +26,7 @@ export async function fetchUserProfile(accessToken) {
export async function fetchUserPermissions(userId, accessToken) { export async function fetchUserPermissions(userId, accessToken) {
log("fetchUserPermissions called", { userId, accessToken }); log("fetchUserPermissions called", { userId, accessToken });
const res = await authFetch( 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", method: "GET",
headers: { headers: {

View File

@ -186,7 +186,7 @@ function PersonalCompleteProfileForm({
return ( return (
<form <form
className="w-full px-4 py-8 space-y-8 bg-white className="w-full px-4 py-8 space-y-8 bg-white
sm:max-w-2xl sm:mx-auto sm:mt-12 sm:border sm:border-gray-200 sm:rounded-2xl sm:shadow-2xl sm:px-16 sm:py-12" md:max-w-[65%] md:mx-auto md:mt-12 md:border md:border-gray-200 md:rounded-2xl md:shadow-2xl md:px-16 md:py-12"
onSubmit={handleValidatedSubmit} onSubmit={handleValidatedSubmit}
> >
<h2 className="text-xl sm:text-2xl font-extrabold text-blue-900 mb-2 text-center"> <h2 className="text-xl sm:text-2xl font-extrabold text-blue-900 mb-2 text-center">

View File

@ -6,7 +6,7 @@ export async function sendVerificationEmailApi() {
const lang = getCurrentLanguage(); const lang = getCurrentLanguage();
log("sendVerificationEmailApi called, lang:", lang); log("sendVerificationEmailApi called, lang:", lang);
const res = await authFetch( 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", method: "POST",
credentials: "include", credentials: "include",
@ -34,7 +34,7 @@ export async function verifyEmailCodeApi(code) {
const lang = getCurrentLanguage(); const lang = getCurrentLanguage();
log("verifyEmailCodeApi called, code:", code, "lang:", lang); log("verifyEmailCodeApi called, code:", code, "lang:", lang);
const res = await authFetch( 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", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },

View File

@ -1,5 +1,7 @@
import { authFetch } from "../../../utils/authFetch"; import { authFetch } from "../../../utils/authFetch";
import { log } from "../../../utils/logger"; import { log } from "../../../utils/logger";
import useAuthStore from "../../../store/authStore";
import { showToast } from "../../toast/toastUtils"; // show user feedback
export async function createReferralLinkApi({ expiresInDays, maxUses }) { export async function createReferralLinkApi({ expiresInDays, maxUses }) {
log("[referralApi] Creating referral link (authFetch)", { 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`; const url = `${import.meta.env.VITE_API_BASE_URL}/api/referral/create`;
log("[referralApi] POST", url, "payload:", payload); 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, { const res = await authFetch(url, {
method: "POST", method: "POST",
headers: { headers,
"Content-Type": "application/json",
},
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
@ -22,12 +35,35 @@ export async function createReferralLinkApi({ expiresInDays, maxUses }) {
if (!res.ok) { if (!res.ok) {
let errorMsg = "Failed to generate referral link"; let errorMsg = "Failed to generate referral link";
try { try {
const data = await res.json(); const data = await res.json().catch(() => null);
errorMsg = data.message || errorMsg; errorMsg = data?.message || errorMsg;
} catch {} // 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); 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(); const result = await res.json();
log("[referralApi] Referral link created:", result); log("[referralApi] Referral link created:", result);
return result; return result;
@ -51,22 +87,58 @@ export async function listReferralLinksApi() {
export async function deactivateReferralLinkApi(tokenId) { export async function deactivateReferralLinkApi(tokenId) {
log("[referralApi] Deactivating referral link (authFetch)...", { tokenId }); log("[referralApi] Deactivating referral link (authFetch)...", { tokenId });
const url = `${import.meta.env.VITE_API_BASE_URL}/api/referral/deactivate`; 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, { const res = await authFetch(url, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers,
body: JSON.stringify({ tokenId }), body: JSON.stringify({ tokenId }),
}); });
log("[referralApi] deactivate response status:", res.status); 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) { if (!res.ok) {
let errorMsg = "Failed to deactivate referral link"; let errorMsg = "Failed to deactivate referral link";
try { try {
const data = await res.json(); const data = await res.json().catch(() => null);
errorMsg = data.message || errorMsg; errorMsg = data?.message || errorMsg;
} catch {}
// 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); 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(); const result = await res.json();
log("[referralApi] Referral link deactivated:", result); log("[referralApi] Referral link deactivated:", result);
return result; return result;

View File

@ -3,6 +3,7 @@ import useAuthStore from "../../../store/authStore";
import { showToast } from "../../toast/toastUtils"; import { showToast } from "../../toast/toastUtils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { log } from "../../../utils/logger"; import { log } from "../../../utils/logger";
import { createReferralLinkApi } from "../api/referralApi";
function GenerateReferralForm({ onReferralGenerated }) { function GenerateReferralForm({ onReferralGenerated }) {
const [expiresInDays, setExpiresInDays] = useState(3); // can become number | 'unlimited' const [expiresInDays, setExpiresInDays] = useState(3); // can become number | 'unlimited'
@ -29,41 +30,21 @@ function GenerateReferralForm({ onReferralGenerated }) {
: { expiresInDays, maxUses }; : { expiresInDays, maxUses };
log("[GenerateReferralForm] Payload to /api/referral/create:", body); log("[GenerateReferralForm] Payload to /api/referral/create:", body);
const res = await fetch( const data = await createReferralLinkApi(body);
`${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();
log("[GenerateReferralForm] Referral link response:", data); log("[GenerateReferralForm] Referral link response:", data);
if (data && data.link) {
if (data.link) {
showToast({ type: "success", message: t('toast.generateSuccess') }); showToast({ type: "success", message: t('toast.generateSuccess') });
} else { } else {
showToast({ type: "error", message: t('toast.generateNoLink') }); showToast({ type: "error", message: t('toast.generateNoLink') });
} }
await onReferralGenerated(); await onReferralGenerated();
} catch (error) { } catch (error) {
log("[GenerateReferralForm] Error generating referral link:", 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); setLoading(false);
}; };

View File

@ -1,7 +1,8 @@
import React, { useState } from "react"; 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 { useTranslation } from "react-i18next";
import { log } from "../../../utils/logger"; import { log } from "../../../utils/logger";
import { deactivateReferralLinkApi } from "../api/referralApi"; // use API helper
function ReferralLinksTable({ links, onLinkUpdate }) { function ReferralLinksTable({ links, onLinkUpdate }) {
const [deactivatingLinks, setDeactivatingLinks] = useState(new Set()); const [deactivatingLinks, setDeactivatingLinks] = useState(new Set());
@ -13,43 +14,23 @@ function ReferralLinksTable({ links, onLinkUpdate }) {
setDeactivatingLinks((prev) => new Set(prev).add(tokenId)); setDeactivatingLinks((prev) => new Set(prev).add(tokenId));
log("[ReferralLinksTable] Deactivating link...", { tokenId }); log("[ReferralLinksTable] Deactivating link...", { tokenId });
try { try {
const res = await fetch( // Use API helper which reads token from zustand and handles errors/toasts centrally
`${import.meta.env.VITE_API_BASE_URL}/api/referral/deactivate`, const data = await deactivateReferralLinkApi(tokenId);
{ log("[ReferralLinksTable] Deactivate response:", data);
method: "POST", if (data && data.success) {
headers: { showToast({ type: "success", message: t('toast.deactivateSuccess') });
"Content-Type": "application/json", if (onLinkUpdate) onLinkUpdate();
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'),
});
}
} else { } else {
log("[ReferralLinksTable] Failed to deactivate link"); showToast({ type: "error", message: data?.message || t('toast.deactivateFail') });
showToast({
type: "error",
message: t('toast.deactivateFail'),
});
} }
} catch (error) { } catch (error) {
log("[ReferralLinksTable] Error deactivating link:", error); log("[ReferralLinksTable] Error deactivating link:", error);
console.error("Error deactivating referral 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 { } finally {
setDeactivatingLinks((prev) => { setDeactivatingLinks((prev) => {
const newSet = new Set(prev); const newSet = new Set(prev);

View File

@ -34,7 +34,9 @@
"deactivated": "Deaktiviert", "deactivated": "Deaktiviert",
"unknown": "Unbekannt", "unknown": "Unbekannt",
"copy": "Kopieren", "copy": "Kopieren",
"copySuccess": "Link in die Zwischenablage kopiert!" "copySuccess": "Link in die Zwischenablage kopiert!",
"never": "Nie",
"unlimited": "Unbegrenzt"
}, },
"form": { "form": {
"generateHeading": "Neuen Empfehlungslink erstellen", "generateHeading": "Neuen Empfehlungslink erstellen",
@ -42,6 +44,8 @@
"linkExpiration": "Link Ablauf", "linkExpiration": "Link Ablauf",
"maximumUses": "Maximale Verwendungen", "maximumUses": "Maximale Verwendungen",
"option": { "option": {
"expirationUnlimited": "Unbegrenzt",
"unlimited": "Unbegrenzt",
"1Day": "24 Stunden (1 Tag)", "1Day": "24 Stunden (1 Tag)",
"3Days": "72 Stunden (3 Tage)", "3Days": "72 Stunden (3 Tage)",
"1Week": "168 Stunden (1 Woche)", "1Week": "168 Stunden (1 Woche)",
@ -63,6 +67,8 @@
"generateFail": "Empfehlungslink konnte nicht erstellt werden. Bitte erneut versuchen.", "generateFail": "Empfehlungslink konnte nicht erstellt werden. Bitte erneut versuchen.",
"generateNoLink": "Erstellung fehlgeschlagen kein Link zurückgegeben.", "generateNoLink": "Erstellung fehlgeschlagen kein Link zurückgegeben.",
"generateError": "Beim Erstellen des Empfehlungslinks ist ein Fehler aufgetreten.", "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}})."
} }
} }

View File

@ -24,10 +24,11 @@
"enlarge": "Vergrößern", "enlarge": "Vergrößern",
"download": "Herunterladen", "download": "Herunterladen",
"sendPasswordReset": "Passwort-Reset senden", "sendPasswordReset": "Passwort-Reset senden",
"sendingPasswordReset": "Passwort-Reset wird gesendet...",
"editPermissions": "Berechtigungen bearbeiten", "editPermissions": "Berechtigungen bearbeiten",
"statistic": "Statistik", "statistic": "Statistiken",
"exportUserData": "Benutzerdaten exportieren", "exportUserData": "Benutzerdaten exportieren",
"logs": "Logs", "logs": "Protokolle",
"delete": "Löschen" "delete": "Löschen"
}, },
"filters": { "filters": {
@ -117,7 +118,10 @@
"download": "Download" "download": "Download"
}, },
"permissions": { "permissions": {
"none": "Keine Berechtigungen" "none": "Keine speziellen Berechtigungen zugewiesen.",
"names": {
"can_create_referrals": "Darf Empfehlungslinks erstellen"
}
}, },
"misc": { "misc": {
"loading": "Lädt...", "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?", "confirmText": "Sind Sie sicher, dass Sie diesen Benutzer und alle zugehörigen Daten dauerhaft löschen möchten?",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"delete": "Löschen" "delete": "Löschen"
},
"toast": {
"passwordResetSuccess": "Passwort-Reset-Link erfolgreich gesendet!",
"passwordResetGenericError": "Passwort-Reset konnte nicht angefordert werden. Bitte erneut versuchen.",
"unauthorized": "Nicht autorisiert."
} }
} }

View File

@ -34,7 +34,9 @@
"deactivated": "Deactivated", "deactivated": "Deactivated",
"unknown": "Unknown", "unknown": "Unknown",
"copy": "Copy", "copy": "Copy",
"copySuccess": "Link copied to clipboard!" "copySuccess": "Link copied to clipboard!",
"never": "Never",
"unlimited": "Unlimited"
}, },
"form": { "form": {
"generateHeading": "Generate New Referral Link", "generateHeading": "Generate New Referral Link",
@ -42,6 +44,8 @@
"linkExpiration": "Link Expiration", "linkExpiration": "Link Expiration",
"maximumUses": "Maximum Uses", "maximumUses": "Maximum Uses",
"option": { "option": {
"expirationUnlimited": "Unlimited",
"unlimited": "Unlimited",
"1Day": "24 Hours (1 Day)", "1Day": "24 Hours (1 Day)",
"3Days": "72 Hours (3 Days)", "3Days": "72 Hours (3 Days)",
"1Week": "168 Hours (1 Week)", "1Week": "168 Hours (1 Week)",
@ -63,6 +67,8 @@
"generateFail": "Failed to generate referral link. Please try again.", "generateFail": "Failed to generate referral link. Please try again.",
"generateNoLink": "Referral link generation failed - no link provided.", "generateNoLink": "Referral link generation failed - no link provided.",
"generateError": "An error occurred while generating the referral link.", "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}})."
} }
} }

View File

@ -23,10 +23,11 @@
"edit": "Edit", "edit": "Edit",
"enlarge": "Enlarge", "enlarge": "Enlarge",
"download": "Download", "download": "Download",
"sendPasswordReset": "Send Password Reset Link", "sendPasswordReset": "Send password reset",
"editPermissions": "Edit Permissions", "sendingPasswordReset": "Sending password reset...",
"statistic": "Statistic", "editPermissions": "Edit permissions",
"exportUserData": "Export User Data", "statistic": "Statistics",
"exportUserData": "Export user data",
"logs": "Logs", "logs": "Logs",
"delete": "Delete" "delete": "Delete"
}, },
@ -117,7 +118,10 @@
"download": "Download" "download": "Download"
}, },
"permissions": { "permissions": {
"none": "No permissions" "none": "No special permissions assigned.",
"names": {
"can_create_referrals": "Can create referral links"
}
}, },
"misc": { "misc": {
"loading": "Loading...", "loading": "Loading...",
@ -135,5 +139,10 @@
"confirmText": "Are you sure you want to permanently delete this user and all related data?", "confirmText": "Are you sure you want to permanently delete this user and all related data?",
"cancel": "Cancel", "cancel": "Cancel",
"delete": "Delete" "delete": "Delete"
},
"toast": {
"passwordResetSuccess": "Password reset link sent successfully!",
"passwordResetGenericError": "Failed to request password reset. Please try again.",
"unauthorized": "Not authorized."
} }
} }

View File

@ -91,7 +91,7 @@ const useAuthStore = create((set, get) => ({
logout: async () => { logout: async () => {
log("🚪 Zustand: Logging out — revoking refresh token on server"); log("🚪 Zustand: Logging out — revoking refresh token on server");
try { 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); log("🌐 Zustand: Calling logout endpoint:", logoutUrl);
const res = await fetch(logoutUrl, { const res = await fetch(logoutUrl, {
method: "POST", method: "POST",
@ -127,12 +127,23 @@ const useAuthStore = create((set, get) => ({
return get().refreshPromise; 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"); log("🔄 Zustand: refreshAuthToken - starting new refresh");
// create promise so concurrent callers can await it // create promise so concurrent callers can await it
const p = (async () => { const p = (async () => {
set({ isRefreshing: true }); set({ isRefreshing: true });
try { 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); log("🌐 Zustand: Calling refresh endpoint:", refreshUrl);
const res = await fetch(refreshUrl, { const res = await fetch(refreshUrl, {