From 4e730bfe128fe6da311dad08bff370bbe190f09a Mon Sep 17 00:00:00 2001 From: Leons Aleksandrovs <58330666+Skrazzo@users.noreply.github.com> Date: Sun, 6 Jul 2025 21:40:32 +0300 Subject: [PATCH] User authentication complete --- backend/utils/responses/responses.go | 4 +- frontend/src/layouts/Authorised.tsx | 10 ++++ frontend/src/lib/requests.ts | 86 ++++++++++++++++++++++------ frontend/src/routes/login.tsx | 11 +++- frontend/src/types/api.ts | 7 +++ frontend/src/types/global.d.ts | 8 +++ 6 files changed, 104 insertions(+), 22 deletions(-) create mode 100644 frontend/src/types/global.d.ts diff --git a/backend/utils/responses/responses.go b/backend/utils/responses/responses.go index 40a7c0f..f77783a 100644 --- a/backend/utils/responses/responses.go +++ b/backend/utils/responses/responses.go @@ -16,14 +16,14 @@ func Success(c *gin.Context, data gin.H) { func Error(c *gin.Context, err string, code int) { // Return error to api - c.JSON(code, gin.H{ + c.AbortWithStatusJSON(code, gin.H{ "success": false, "error": err, }) } func NeedsToLogin(c *gin.Context) { - c.JSON(http.StatusUnauthorized, gin.H{ + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "success": false, "error": "Authentication required", "needsAuthentication": true, // only appears in this error diff --git a/frontend/src/layouts/Authorised.tsx b/frontend/src/layouts/Authorised.tsx index 8e5d8b1..cb0afa0 100644 --- a/frontend/src/layouts/Authorised.tsx +++ b/frontend/src/layouts/Authorised.tsx @@ -1,5 +1,8 @@ import Header from "@/components/Header"; import Container from "@/components/ui/container"; +import requests from "@/lib/requests"; +import { useQuery } from "@tanstack/react-query"; +import type { TokenUserInfo } from "@/types/api"; interface Props { children: React.ReactNode; @@ -7,6 +10,13 @@ interface Props { } export default function Authorised({ children, className = "" }: Props) { + // Check authentication + const info = useQuery({ + queryKey: ["user_info"], + queryFn: () => requests.get("/info", {}), + staleTime: 60 * 1000, // 1 minutes + }); + return ( <>
diff --git a/frontend/src/lib/requests.ts b/frontend/src/lib/requests.ts index 6c6ce87..104f9d6 100644 --- a/frontend/src/lib/requests.ts +++ b/frontend/src/lib/requests.ts @@ -15,9 +15,74 @@ interface PostProps extends RequestProps { data: Record; } +interface GetProps extends RequestProps { + params?: Record; +} + class Requests { constructor() {} + async verifyData(res: Response): Promise { + // Get response data + const { data, error } = await tryCatch>(res.json()); + if (error) { + throw new Error(`Parsing error: ${res.statusText} - ${res.status}`); + } + + // Check if authentication is required + if ("needsAuthentication" in data && data.needsAuthentication) { + window.location.replace("/login"); + throw new Error("Authentication is required"); + } + + // Check if data is ok + if ("success" in data && !data.success) { + throw new Error(data.error); + } + + // Another check for unexpected error + if (!res.ok) { + throw new Error("Unexpected API ERROR with code: " + res.status); + } + + // Return response data + return data.data; + } + + async get(url: string, props: GetProps): Promise { + // Call before + props.before?.(); + + // Get url parameters + const urlParams = props.params ? new URLSearchParams(props.params).toString() : ""; + // Normalize url + const finalUrl = normalizeLink(`${API_BASE}/${url}${urlParams}`); + + try { + // Do request + const res = await fetch(finalUrl, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + // Verify data + const responseData = await this.verifyData(res); + + // Otherwise return response data + props.success?.(responseData); + return responseData; + } catch (error) { + const err = error as Error; + // Show notification, and call error callback + toast.error(err.message); + props.error?.(err); + } finally { + props.finally?.(); + } + } + async post(url: string, props: PostProps): Promise { props.before?.(); @@ -34,25 +99,12 @@ class Requests { body: JSON.stringify(props.data), }); - // Get response data - const { data, error } = await tryCatch>(res.json()); - if (error) { - throw new Error(`Parsing error: ${res.statusText} - ${res.status}`); - } - - // Check if data is ok - if ("success" in data && !data.success) { - throw new Error(data.error); - } - - // Another check for unexpected error - if (!res.ok) { - throw new Error("Unexpected API ERROR with code: " + res.status); - } + // Verify data + const responseData = await this.verifyData(res); // Otherwise return response data - props.success?.(data.data); - return data.data; + props.success?.(responseData); + return responseData; } catch (error) { const err = error as Error; // Show notification, and call error callback diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx index 5b0fdf9..6c71e53 100644 --- a/frontend/src/routes/login.tsx +++ b/frontend/src/routes/login.tsx @@ -5,7 +5,6 @@ import Guest from "@/layouts/Guest"; import requests from "@/lib/requests"; import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { useState } from "react"; -import toast from "react-hot-toast"; import * as z from "zod/v4"; export const Route = createFileRoute("/login")({ @@ -36,9 +35,15 @@ function RouteComponent() { // use state to true loading loading[1](true); }, - success(data) { + success() { navigate({ to: "/" }); }, + error() { + form.setFieldValue("password", ""); + }, + finally() { + loading[1](false); + }, }); }, }); @@ -77,7 +82,7 @@ function RouteComponent() { /> )} /> - diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index c3869b8..a38fafc 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -11,3 +11,10 @@ export interface ErrorResponse { } export type ApiResponse = SuccessResponse | ErrorResponse; + +// user info returned by /info route +export interface TokenUserInfo { + id: number; + name: string; + email: string; +} diff --git a/frontend/src/types/global.d.ts b/frontend/src/types/global.d.ts new file mode 100644 index 0000000..742ede8 --- /dev/null +++ b/frontend/src/types/global.d.ts @@ -0,0 +1,8 @@ +// types/global.d.ts or at the top of a relevant file +export {}; + +declare global { + interface Window { + navigateToLogin?: Promise; + } +}