From 36a5911fe4d6617e9324f48a45ea9e5936e96b70 Mon Sep 17 00:00:00 2001 From: KartoffelChipss Date: Sat, 2 May 2026 15:13:28 +0200 Subject: [PATCH] Improve frontend auth handling --- backend/app/handlers/user.go | 20 +++++++++++++ backend/app/middleware/auth.go | 6 ++-- backend/app/routes/routes.go | 1 + frontend/src/api/auth.api.ts | 11 +++++++- frontend/src/components/navbar.tsx | 8 +----- frontend/src/hooks/api/useCurrentUser.ts | 11 ++++++++ frontend/src/main.tsx | 36 +++++------------------- frontend/src/pages/Login/Login.tsx | 5 ++-- frontend/src/pages/Logout/Logout.tsx | 19 +++++++++++++ frontend/src/pages/Page.tsx | 31 ++++++++++++++++---- frontend/src/utils/credentials.ts | 25 ++++++++++++++++ 11 files changed, 126 insertions(+), 47 deletions(-) create mode 100644 frontend/src/hooks/api/useCurrentUser.ts create mode 100644 frontend/src/pages/Logout/Logout.tsx create mode 100644 frontend/src/utils/credentials.ts diff --git a/backend/app/handlers/user.go b/backend/app/handlers/user.go index fd9d0e5..ebf9ca6 100644 --- a/backend/app/handlers/user.go +++ b/backend/app/handlers/user.go @@ -267,3 +267,23 @@ func (h *UserHandler) DeleteUser(c fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) } + +// GetMe returns the currently authenticated user's details +func (h *UserHandler) GetMe(c fiber.Ctx) error { + uid, ok := c.Locals("user_id").(string) + if !ok || uid == "" { + return c.Status(fiber.StatusUnauthorized).JSON(&models.APIError{Message: "Unauthorized"}) + } + + user, err := h.Repo.GetUserById(uid) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "User not found"}) + } + log.Println("Error getting user: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while getting user"}) + } + + user.HashedPassword = "" + return c.JSON(user) +} diff --git a/backend/app/middleware/auth.go b/backend/app/middleware/auth.go index 80a6128..7cdde61 100644 --- a/backend/app/middleware/auth.go +++ b/backend/app/middleware/auth.go @@ -1,11 +1,11 @@ package middleware import ( - "strings" + "strings" - "quay/internal/security" + "quay/internal/security" - "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3" ) func RequireAuth() fiber.Handler { diff --git a/backend/app/routes/routes.go b/backend/app/routes/routes.go index 51a91ab..ae7bd68 100644 --- a/backend/app/routes/routes.go +++ b/backend/app/routes/routes.go @@ -75,6 +75,7 @@ func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig, d protected.Get("/users", userHandler.GetAllUsers) protected.Get("/users/:id", userHandler.GetUserById) protected.Get("/users/by-name/:name", userHandler.GetUserByName) + protected.Get("/me", userHandler.GetMe) // Allow creating the very first admin user without auth (bootstrap). // If an admin already exists, require auth to create users. diff --git a/frontend/src/api/auth.api.ts b/frontend/src/api/auth.api.ts index c4d9fc7..8b5c8bb 100644 --- a/frontend/src/api/auth.api.ts +++ b/frontend/src/api/auth.api.ts @@ -1,4 +1,5 @@ -import { makeApiUrl } from '.'; +import { makeApiUrl, fetchWithAuth } from '.'; +import type { User } from './types/user'; export const login = async (name: string, password: string): Promise => { const response = await fetch(makeApiUrl('/login'), { @@ -16,3 +17,11 @@ export const login = async (name: string, password: string): Promise => const data = await response.json(); return data.token; }; + +export const getMe = async (): Promise => { + const response = await fetchWithAuth('/me', { method: 'GET' }); + if (!response.ok) { + throw new Error('Failed to fetch current user'); + } + return response.json(); +}; diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx index 53a8487..3c1bd2c 100644 --- a/frontend/src/components/navbar.tsx +++ b/frontend/src/components/navbar.tsx @@ -34,13 +34,7 @@ const Navbar = ({ userName, profilePictureUrl }: NavbarProps) => { alt={userName + ' profile image'} /> - {userName - ? userName - .split(' ') - .map((n) => n[0]) - .join('') - .toUpperCase() - : 'A'} + {userName ? userName.charAt(0).toUpperCase() : 'A'} diff --git a/frontend/src/hooks/api/useCurrentUser.ts b/frontend/src/hooks/api/useCurrentUser.ts new file mode 100644 index 0000000..f7a3fc6 --- /dev/null +++ b/frontend/src/hooks/api/useCurrentUser.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { getMe } from '../../api/auth.api'; + +export function useCurrentUser() { + return useQuery({ + queryKey: ['me'], + queryFn: getMe, + retry: false, + refetchOnWindowFocus: false, + }); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index c0aa9d3..9cc6ccd 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,4 +1,4 @@ -import { BrowserRouter, Route, Routes, Navigate } from 'react-router'; +import { BrowserRouter, Route, Routes } from 'react-router'; import { createRoot } from 'react-dom/client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import MainPage from './pages/Main/Main'; @@ -9,6 +9,7 @@ import NewSite from './pages/NewSite/NewSite'; import SiteOverview from './pages/SiteOverview/SiteOverview'; import Users from './pages/Users/Users'; import Login from './pages/Login/Login'; +import Logout from './pages/Logout/Logout'; const queryClient = new QueryClient(); @@ -18,34 +19,11 @@ createRoot(document.getElementById('root')!).render( } /> - : - } - /> - : - } - /> - - ) : ( - - ) - } - /> - : - } - /> + } /> + } /> + } /> + } /> + } /> diff --git a/frontend/src/pages/Login/Login.tsx b/frontend/src/pages/Login/Login.tsx index ebfd477..02acbc5 100644 --- a/frontend/src/pages/Login/Login.tsx +++ b/frontend/src/pages/Login/Login.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router'; import { login } from '../../api/auth.api'; import { Input } from '../../components/ui/input'; import { Button } from '../../components/ui/button'; +import { setToken } from '../../utils/credentials'; const Login = () => { const [name, setName] = useState(''); @@ -15,9 +16,9 @@ const Login = () => { setError(null); try { const token = await login(name, password); - localStorage.setItem('token', token); + setToken(token); navigate('/'); - } catch (err) { + } catch { setError('Invalid credentials'); } }; diff --git a/frontend/src/pages/Logout/Logout.tsx b/frontend/src/pages/Logout/Logout.tsx new file mode 100644 index 0000000..083b4d1 --- /dev/null +++ b/frontend/src/pages/Logout/Logout.tsx @@ -0,0 +1,19 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router'; +import { useQueryClient } from '@tanstack/react-query'; +import { removeToken } from '../../utils/credentials'; + +const Logout = () => { + const navigate = useNavigate(); + const qc = useQueryClient(); + + useEffect(() => { + removeToken(); + qc.removeQueries({ queryKey: ['me'] }); + navigate('/login', { replace: true }); + }, [navigate, qc]); + + return null; +}; + +export default Logout; diff --git a/frontend/src/pages/Page.tsx b/frontend/src/pages/Page.tsx index a0acf93..88c17bf 100644 --- a/frontend/src/pages/Page.tsx +++ b/frontend/src/pages/Page.tsx @@ -1,22 +1,43 @@ import { useEffect, type PropsWithChildren } from 'react'; +import { useNavigate } from 'react-router'; import Navbar from '../components/navbar'; +import { useCurrentUser } from '../hooks/api/useCurrentUser'; +import type { User } from '../api/types/user'; +import { getToken } from '../utils/credentials'; interface PageProps { title: string; className?: string; + requireAuth?: boolean; } -const Page = ({ children, title, className }: PropsWithChildren) => { +const Page = ({ children, title, className, requireAuth = true }: PropsWithChildren) => { + const navigate = useNavigate(); + const { data: user, isLoading, isError } = useCurrentUser(); + useEffect(() => { if (title) document.title = title; }, [title]); + useEffect(() => { + if (!requireAuth) return; + const token = getToken(); + + if (!token) { + navigate('/login', { replace: true }); + return; + } + + if (!isLoading && isError) { + navigate('/login', { replace: true }); + } + }, [requireAuth, isLoading, isError, navigate]); + + const userName = (user as User | undefined)?.name; + return (
- +
{children}
diff --git a/frontend/src/utils/credentials.ts b/frontend/src/utils/credentials.ts new file mode 100644 index 0000000..44a5555 --- /dev/null +++ b/frontend/src/utils/credentials.ts @@ -0,0 +1,25 @@ +const TOKEN_STORAGE_KEY = 'token'; + +export const getToken = (): string | null => { + try { + return localStorage.getItem(TOKEN_STORAGE_KEY); + } catch { + return null; + } +}; + +export const setToken = (token: string) => { + try { + localStorage.setItem(TOKEN_STORAGE_KEY, token); + } catch { + // ingore + } +}; + +export const removeToken = () => { + try { + localStorage.removeItem(TOKEN_STORAGE_KEY); + } catch { + // ingore + } +};