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 }) {
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: {

View File

@ -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,

View File

@ -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;

View File

@ -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.<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 (
<div className="mb-10">
<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}
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>
))
) : (

View File

@ -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);

View File

@ -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 () => {

View File

@ -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: {

View File

@ -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) {

View File

@ -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) {

View File

@ -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" });

View File

@ -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 })

View File

@ -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: {

View File

@ -186,7 +186,7 @@ function PersonalCompleteProfileForm({
return (
<form
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}
>
<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();
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" },

View File

@ -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 {}
log("[referralApi] Error creating referral link:", errorMsg);
throw new Error(errorMsg);
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);
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 {}
log("[referralApi] Error deactivating referral link:", errorMsg);
throw new Error(errorMsg);
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);
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;

View File

@ -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,42 +30,22 @@ 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);
// 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);
};

View File

@ -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();
// 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.success) {
showToast({
type: "success",
message: t('toast.deactivateSuccess'),
});
if (data && data.success) {
showToast({ type: "success", message: t('toast.deactivateSuccess') });
if (onLinkUpdate) onLinkUpdate();
} else {
showToast({
type: "error",
message: data.message || t('toast.deactivateFail'),
});
}
} 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);
// 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);

View File

@ -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}})."
}
}

View File

@ -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."
}
}

View File

@ -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}})."
}
}

View File

@ -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."
}
}

View File

@ -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, {