User authentication complete

This commit is contained in:
Leons Aleksandrovs
2025-07-06 21:40:32 +03:00
parent d18f9f9706
commit 4e730bfe12
6 changed files with 104 additions and 22 deletions

View File

@@ -16,14 +16,14 @@ func Success(c *gin.Context, data gin.H) {
func Error(c *gin.Context, err string, code int) { func Error(c *gin.Context, err string, code int) {
// Return error to api // Return error to api
c.JSON(code, gin.H{ c.AbortWithStatusJSON(code, gin.H{
"success": false, "success": false,
"error": err, "error": err,
}) })
} }
func NeedsToLogin(c *gin.Context) { func NeedsToLogin(c *gin.Context) {
c.JSON(http.StatusUnauthorized, gin.H{ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"success": false, "success": false,
"error": "Authentication required", "error": "Authentication required",
"needsAuthentication": true, // only appears in this error "needsAuthentication": true, // only appears in this error

View File

@@ -1,5 +1,8 @@
import Header from "@/components/Header"; import Header from "@/components/Header";
import Container from "@/components/ui/container"; 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 { interface Props {
children: React.ReactNode; children: React.ReactNode;
@@ -7,6 +10,13 @@ interface Props {
} }
export default function Authorised({ children, className = "" }: Props) { export default function Authorised({ children, className = "" }: Props) {
// Check authentication
const info = useQuery({
queryKey: ["user_info"],
queryFn: () => requests.get<TokenUserInfo>("/info", {}),
staleTime: 60 * 1000, // 1 minutes
});
return ( return (
<> <>
<Header /> <Header />

View File

@@ -15,9 +15,74 @@ interface PostProps<T> extends RequestProps<T> {
data: Record<string, any>; data: Record<string, any>;
} }
interface GetProps<T> extends RequestProps<T> {
params?: Record<string, any>;
}
class Requests { class Requests {
constructor() {} constructor() {}
async verifyData<T>(res: Response): Promise<T> {
// Get response data
const { data, error } = await tryCatch<ApiResponse<T>>(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<T>(url: string, props: GetProps<T>): Promise<T | void> {
// 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<T>(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<T>(url: string, props: PostProps<T>): Promise<T | void> { async post<T>(url: string, props: PostProps<T>): Promise<T | void> {
props.before?.(); props.before?.();
@@ -34,25 +99,12 @@ class Requests {
body: JSON.stringify(props.data), body: JSON.stringify(props.data),
}); });
// Get response data // Verify data
const { data, error } = await tryCatch<ApiResponse<T>>(res.json()); const responseData = await this.verifyData<T>(res);
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);
}
// Otherwise return response data // Otherwise return response data
props.success?.(data.data); props.success?.(responseData);
return data.data; return responseData;
} catch (error) { } catch (error) {
const err = error as Error; const err = error as Error;
// Show notification, and call error callback // Show notification, and call error callback

View File

@@ -5,7 +5,6 @@ import Guest from "@/layouts/Guest";
import requests from "@/lib/requests"; import requests from "@/lib/requests";
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
import toast from "react-hot-toast";
import * as z from "zod/v4"; import * as z from "zod/v4";
export const Route = createFileRoute("/login")({ export const Route = createFileRoute("/login")({
@@ -36,9 +35,15 @@ function RouteComponent() {
// use state to true loading // use state to true loading
loading[1](true); loading[1](true);
}, },
success(data) { success() {
navigate({ to: "/" }); navigate({ to: "/" });
}, },
error() {
form.setFieldValue("password", "");
},
finally() {
loading[1](false);
},
}); });
}, },
}); });
@@ -77,7 +82,7 @@ function RouteComponent() {
/> />
)} )}
/> />
<Button onClick={form.handleSubmit} className="w-full"> <Button disabled={loading[0]} onClick={form.handleSubmit} className="w-full">
Login Login
</Button> </Button>
</CardContent> </CardContent>

View File

@@ -11,3 +11,10 @@ export interface ErrorResponse {
} }
export type ApiResponse<T> = SuccessResponse<T> | ErrorResponse; export type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;
// user info returned by /info route
export interface TokenUserInfo {
id: number;
name: string;
email: string;
}

8
frontend/src/types/global.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
// types/global.d.ts or at the top of a relevant file
export {};
declare global {
interface Window {
navigateToLogin?: Promise<void>;
}
}