Add frontend #1
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<string> => {
|
||||
const response = await fetch(makeApiUrl('/login'), {
|
||||
@@ -16,3 +17,11 @@ export const login = async (name: string, password: string): Promise<string> =>
|
||||
const data = await response.json();
|
||||
return data.token;
|
||||
};
|
||||
|
||||
export const getMe = async (): Promise<User> => {
|
||||
const response = await fetchWithAuth('/me', { method: 'GET' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch current user');
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
@@ -34,13 +34,7 @@ const Navbar = ({ userName, profilePictureUrl }: NavbarProps) => {
|
||||
alt={userName + ' profile image'}
|
||||
/>
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{userName
|
||||
? userName
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
: 'A'}
|
||||
{userName ? userName.charAt(0).toUpperCase() : 'A'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
+7
-29
@@ -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(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
!localStorage.getItem('token') ? <Navigate to="/login" /> : <MainPage />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/sites/new"
|
||||
element={
|
||||
!localStorage.getItem('token') ? <Navigate to="/login" /> : <NewSite />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/sites/:id"
|
||||
element={
|
||||
!localStorage.getItem('token') ? (
|
||||
<Navigate to="/login" />
|
||||
) : (
|
||||
<SiteOverview />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/users"
|
||||
element={
|
||||
!localStorage.getItem('token') ? <Navigate to="/login" /> : <Users />
|
||||
}
|
||||
/>
|
||||
<Route path="/" element={<MainPage />} />
|
||||
<Route path="/sites/new" element={<NewSite />} />
|
||||
<Route path="/sites/:id" element={<SiteOverview />} />
|
||||
<Route path="/users" element={<Users />} />
|
||||
<Route path="/logout" element={<Logout />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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<PageProps>) => {
|
||||
const Page = ({ children, title, className, requireAuth = true }: PropsWithChildren<PageProps>) => {
|
||||
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 (
|
||||
<div>
|
||||
<Navbar
|
||||
profilePictureUrl="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.pinimg.com%2Foriginals%2Fca%2Fa9%2F89%2Fcaa98995f578f038373cb6874b68fdfb.jpg&f=1&nofb=1&ipt=0e7adaafec6ff834508c5ea329c5686552e35fe6bc06c299f38ebee88b22cf0a"
|
||||
userName="jan"
|
||||
/>
|
||||
<Navbar profilePictureUrl={''} userName={userName} />
|
||||
<main className={`max-w-(--breakpoint-xl) mx-auto px-4 ${className || ''}`}>
|
||||
{children}
|
||||
</main>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user