fixed expiring login creds
This commit is contained in:
@@ -21,6 +21,7 @@ import type {
|
||||
|
||||
import type {
|
||||
ApiAccountUsersListParams,
|
||||
ChangePassword,
|
||||
CustomUser,
|
||||
PaginatedCustomUserList,
|
||||
PatchedCustomUser,
|
||||
@@ -56,6 +57,92 @@ type NonReadonly<T> = [T] extends [UnionToIntersection<T>]
|
||||
}
|
||||
: DistributeReadOnlyOverUnions<T>;
|
||||
|
||||
/**
|
||||
* @summary Change password for the authenticated user
|
||||
*/
|
||||
export const apiAccountPasswordChangeCreate = (
|
||||
changePassword: ChangePassword,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return privateMutator<void>({
|
||||
url: `/api/account/password-change/`,
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
data: changePassword,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getApiAccountPasswordChangeCreateMutationOptions = <
|
||||
TError = void,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof apiAccountPasswordChangeCreate>>,
|
||||
TError,
|
||||
{ data: ChangePassword },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof apiAccountPasswordChangeCreate>>,
|
||||
TError,
|
||||
{ data: ChangePassword },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["apiAccountPasswordChangeCreate"];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof apiAccountPasswordChangeCreate>>,
|
||||
{ data: ChangePassword }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return apiAccountPasswordChangeCreate(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type ApiAccountPasswordChangeCreateMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof apiAccountPasswordChangeCreate>>
|
||||
>;
|
||||
export type ApiAccountPasswordChangeCreateMutationBody = ChangePassword;
|
||||
export type ApiAccountPasswordChangeCreateMutationError = void;
|
||||
|
||||
/**
|
||||
* @summary Change password for the authenticated user
|
||||
*/
|
||||
export const useApiAccountPasswordChangeCreate = <
|
||||
TError = void,
|
||||
TContext = unknown,
|
||||
>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof apiAccountPasswordChangeCreate>>,
|
||||
TError,
|
||||
{ data: ChangePassword },
|
||||
TContext
|
||||
>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof apiAccountPasswordChangeCreate>>,
|
||||
TError,
|
||||
{ data: ChangePassword },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(
|
||||
getApiAccountPasswordChangeCreateMutationOptions(options),
|
||||
queryClient,
|
||||
);
|
||||
};
|
||||
/**
|
||||
* Returns details of the currently authenticated user based on JWT token or session.
|
||||
* @summary Get current authenticated user
|
||||
|
||||
@@ -20,4 +20,5 @@ export type ApiAccountUsersListParams = {
|
||||
postal_code?: string;
|
||||
role?: string;
|
||||
street?: string;
|
||||
username?: string;
|
||||
};
|
||||
|
||||
10
frontend/src/api/generated/private/models/changePassword.ts
Normal file
10
frontend/src/api/generated/private/models/changePassword.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Generated by orval v8.8.0 🍺
|
||||
* Do not edit manually.
|
||||
* OpenAPI spec version: 0.0.0
|
||||
*/
|
||||
|
||||
export interface ChangePassword {
|
||||
current_password: string;
|
||||
new_password: string;
|
||||
}
|
||||
@@ -44,4 +44,6 @@ export interface CustomUser {
|
||||
is_active?: boolean;
|
||||
/** @nullable */
|
||||
avatar?: string | null;
|
||||
/** @nullable */
|
||||
banner?: string | null;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ export * from "./gopayGetStatus200";
|
||||
export * from "./gopayRefundPayment200";
|
||||
export * from "./hub";
|
||||
export * from "./hubPermission";
|
||||
export * from "./changePassword";
|
||||
export * from "./chat";
|
||||
export * from "./chatMember";
|
||||
export * from "./chatTypeEnum";
|
||||
|
||||
@@ -44,4 +44,6 @@ export interface PatchedCustomUser {
|
||||
is_active?: boolean;
|
||||
/** @nullable */
|
||||
avatar?: string | null;
|
||||
/** @nullable */
|
||||
banner?: string | null;
|
||||
}
|
||||
|
||||
10
frontend/src/api/generated/public/models/changePassword.ts
Normal file
10
frontend/src/api/generated/public/models/changePassword.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Generated by orval v8.8.0 🍺
|
||||
* Do not edit manually.
|
||||
* OpenAPI spec version: 0.0.0
|
||||
*/
|
||||
|
||||
export interface ChangePassword {
|
||||
current_password: string;
|
||||
new_password: string;
|
||||
}
|
||||
@@ -44,4 +44,6 @@ export interface CustomUser {
|
||||
is_active?: boolean;
|
||||
/** @nullable */
|
||||
avatar?: string | null;
|
||||
/** @nullable */
|
||||
banner?: string | null;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ export * from "./downloaderStats";
|
||||
export * from "./errorResponse";
|
||||
export * from "./hub";
|
||||
export * from "./hubPermission";
|
||||
export * from "./changePassword";
|
||||
export * from "./chat";
|
||||
export * from "./chatMember";
|
||||
export * from "./chatTypeEnum";
|
||||
|
||||
@@ -44,4 +44,6 @@ export interface PatchedCustomUser {
|
||||
is_active?: boolean;
|
||||
/** @nullable */
|
||||
avatar?: string | null;
|
||||
/** @nullable */
|
||||
banner?: string | null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios, { type AxiosRequestConfig } from "axios";
|
||||
import { AUTH_FLAG } from "@/context/AuthContext";
|
||||
import { AUTH_FLAG } from "@/hooks/useAuth";
|
||||
|
||||
export const privateApi = axios.create({
|
||||
withCredentials: true,
|
||||
@@ -38,7 +38,7 @@ privateApi.interceptors.response.use(
|
||||
}
|
||||
}
|
||||
|
||||
if (error.response?.status === 401 && !original._retry) {
|
||||
if ((error.response?.status === 401 || error.response?.status === 403) && !original._retry) {
|
||||
if (original.url?.includes("/api/account/logout/")) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@@ -79,8 +79,11 @@ privateApi.interceptors.response.use(
|
||||
await privateApi.post("/api/account/token/refresh/");
|
||||
processQueue();
|
||||
return privateApi(original);
|
||||
} catch (refreshError) {
|
||||
} catch (refreshError: any) {
|
||||
processQueue(refreshError);
|
||||
// Refresh failed for any reason (400 missing cookie, 401 expired) — clear auth and redirect
|
||||
localStorage.removeItem(AUTH_FLAG);
|
||||
window.location.href = "/social/login";
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios, { type AxiosRequestConfig } from "axios";
|
||||
import { AUTH_FLAG } from "@/context/AuthContext";
|
||||
import { AUTH_FLAG } from "@/hooks/useAuth";
|
||||
|
||||
export const publicApi = axios.create({
|
||||
withCredentials: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
|
||||
export default function Ad() {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
FaHandsHelping,
|
||||
} from "react-icons/fa";
|
||||
import { FaClapperboard, FaCubes } from "react-icons/fa6";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import Avatar from "@/components/ui/Avatar";
|
||||
import styles from "./navbar.module.css";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Message as MessageModel } from "@/api/generated/private/models/mes
|
||||
import type { Chat } from "@/api/generated/private/models/chat";
|
||||
import Avatar from "@/components/ui/Avatar";
|
||||
import IconButton from "@/components/ui/IconButton";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { canDeleteMessage } from "@/hooks/usePermissions";
|
||||
import { formatRelative } from "@/utils/relativeTime";
|
||||
import { apiSocialMessagesDestroy } from "@/api/generated/private/chat/chat";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FiTrash2, FiMoreHorizontal } from "react-icons/fi";
|
||||
import type { Post } from "@/api/generated/private/models/post";
|
||||
import Avatar from "@/components/ui/Avatar";
|
||||
import IconButton from "@/components/ui/IconButton";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { canDeletePost, canEditPost } from "@/hooks/usePermissions";
|
||||
import { formatRelative } from "@/utils/relativeTime";
|
||||
import { apiSocialPostsDestroy } from "@/api/generated/private/posts/posts";
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { createContext, useContext, useState, useEffect, useRef } from "react";
|
||||
import { createContext, useState, useEffect, useRef } from "react";
|
||||
|
||||
import { apiAccountLogoutCreate } from "@/api/generated/public/account";
|
||||
import { apiAccountLoginCreate, apiAccountLogoutCreate } from "@/api/generated/public/account";
|
||||
import { apiAccountUserMeRetrieve } from "@/api/generated/private/account/account";
|
||||
import { privateApi } from "@/api/privateClient";
|
||||
import { AUTH_FLAG } from "@/hooks/useAuth";
|
||||
|
||||
import type { CustomTokenObtainPair } from "@/api/generated/public/models/customTokenObtainPair";
|
||||
import type { CustomUser } from "@/api/generated/private/models/customUser";
|
||||
|
||||
export const AUTH_FLAG = "vontor_was_logged_in";
|
||||
|
||||
interface AuthContextType {
|
||||
export interface AuthContextType {
|
||||
user: CustomUser | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
@@ -19,7 +17,7 @@ interface AuthContextType {
|
||||
refreshUser: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<CustomUser | null>(null);
|
||||
@@ -56,23 +54,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}, []);
|
||||
|
||||
async function login(payload: CustomTokenObtainPair) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await privateApi.post("/api/account/login/", payload);
|
||||
localStorage.setItem(AUTH_FLAG, "true");
|
||||
await refreshUser();
|
||||
} catch (err: any) {
|
||||
setIsLoading(false);
|
||||
const data = err.response?.data;
|
||||
const errorMessage =
|
||||
data?.detail ||
|
||||
(typeof data === "object" && !Array.isArray(data)
|
||||
? Object.values(data).flat().filter(Boolean).join(" ")
|
||||
: null) ||
|
||||
err.message ||
|
||||
"Login failed";
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
await apiAccountLoginCreate(payload);
|
||||
localStorage.setItem(AUTH_FLAG, "true");
|
||||
await refreshUser();
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
@@ -94,9 +78,3 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error("useAuth must be used inside AuthProvider");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
10
frontend/src/hooks/useAuth.ts
Normal file
10
frontend/src/hooks/useAuth.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
import { AuthContext } from "@/context/AuthContext";
|
||||
|
||||
export const AUTH_FLAG = "vontor_was_logged_in";
|
||||
|
||||
export function useAuth() {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error("useAuth must be used inside AuthProvider");
|
||||
return ctx;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
FiBookmark,
|
||||
FiLogOut,
|
||||
} from "react-icons/fi";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import Avatar from "@/components/ui/Avatar";
|
||||
|
||||
interface NavItem {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { FiArrowLeft, FiUser, FiLock, FiCamera, FiImage } from "react-icons/fi";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { privateApi } from "@/api/privateClient";
|
||||
import { mediaUrl } from "@/utils/mediaUrl";
|
||||
import Avatar from "@/components/ui/Avatar";
|
||||
@@ -187,11 +187,11 @@ export default function AccountSettingsPage() {
|
||||
{bannerSrc && (
|
||||
<img src={bannerSrc} alt="" className="h-full w-full object-cover" />
|
||||
)}
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/0 transition-colors group-hover:bg-black/40">
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/20 transition-colors group-hover:bg-black/40">
|
||||
{bannerUploading ? (
|
||||
<Spinner size={22} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<div className="flex flex-col items-center gap-1 opacity-60 transition-opacity group-hover:opacity-100">
|
||||
<FiImage size={20} className="text-white" />
|
||||
<span className="text-xs text-white/90">Změnit banner</span>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FiLogOut, FiUser } from "react-icons/fi";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import Avatar from "@/components/ui/Avatar";
|
||||
import Card from "@/components/ui/Card";
|
||||
import EmptyState from "@/components/ui/EmptyState";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { FiArrowLeft, FiLogOut, FiSettings, FiUser, FiCalendar, FiMapPin } from "react-icons/fi";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useApiSocialPostsList } from "@/api/generated/private/posts/posts";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { privateApi } from "@/api/privateClient";
|
||||
import Avatar from "@/components/ui/Avatar";
|
||||
import { mediaUrl } from "@/utils/mediaUrl";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import {
|
||||
FiUser,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FiAtSign, FiLock } from "react-icons/fi";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Spinner from "@/components/ui/Spinner";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Navigate, Outlet, useLocation } from "react-router-dom";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import Spinner from "@/components/ui/Spinner";
|
||||
|
||||
export default function PrivateRoute() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import Spinner from "@/components/ui/Spinner";
|
||||
|
||||
export default function PublicOnlyRoute() {
|
||||
|
||||
Reference in New Issue
Block a user