User authentication complete
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
8
frontend/src/types/global.d.ts
vendored
Normal 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>;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user