14 standardize and clean api and fix bruno configuration (#25)

* ADD JWT authentication support with token generation and validation

* ADD JWT handling after successful login

* ADD user authentication and standardize user retrieval

* COMBINE token dtos

* ADD JWT authentication filter

* IMPROVE token handling

* STANDARDIZE API endpoints and improve JWT handling

* REMOVE extra logging

* REMOVE redundant job existence checks

* UPDATE Bruno Google token

* REFACTOR some classes

* ADD JWT cookie check

* ADD AuthProvider and CORS configuration; UPDATE API endpoints for consistency

* ADD JWT validation check;

* ADD profile picture to database

* ADD reload after login to update page

* PATCH login issue

* REMOVE unused classes

* ADJUST logging in JwtFilter

* REMOVE unused React component
This commit is contained in:
Dylan De Faoite
2025-08-10 22:41:37 +02:00
committed by GitHub
parent 20f7ec8db4
commit 662966f138
35 changed files with 916 additions and 252 deletions

View File

@@ -2,7 +2,10 @@ import { Menu, X } from 'lucide-react';
import MenuButton from "./buttons/MenuButton.tsx";
import clsx from "clsx";
import type {User} from "../utils/types.ts";
import { login } from "../utils/endpoints.ts";
import { Dropdown, DropdownItem } from "./Dropdown.tsx";
import { GoogleOAuthProvider, GoogleLogin } from '@react-oauth/google';
import {useNavigate} from "react-router-dom";
type props = {
sidebarToggled: boolean;
@@ -12,9 +15,12 @@ type props = {
}
const Topbar = ({sidebarToggled, setSidebarToggled, user, className}: props) => {
const apiUrl = import.meta.env.VITE_API_URL;
const loginUrl = `${apiUrl}/oauth2/authorization/google`;
const logoutUrl = `${apiUrl}/api/v1/auth/logout`;
const navigate = useNavigate();
const handleLogout = () => {
// delete token cookie
document.cookie = "token=; Secure; SameSite=None; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
return (
<div className={clsx(className, "flex justify-between")}>
@@ -26,22 +32,30 @@ const Topbar = ({sidebarToggled, setSidebarToggled, user, className}: props) =>
<div>
<img
className={"w-8 h-8 rounded-full inline-block"}
src={user.profilePicture}
src={user.profilePictureUrl}
referrerPolicy="no-referrer"
/>
<Dropdown label={user.name}>
<DropdownItem item="Logout"
onClick={() => globalThis.location.href = logoutUrl}
onClick={() => handleLogout()}
className={"text-red-500 font-medium"} />
</Dropdown>
</div>
) :
(
<MenuButton className={"w-20 text-gray-600"}
onClick={() => globalThis.location.href = loginUrl}>
Login
</MenuButton>
<GoogleOAuthProvider
clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}>
<GoogleLogin
onSuccess={(credentialResponse) => {
if (!credentialResponse.credential) {
console.error("No credential received from Google Login");
return;
}
login(credentialResponse.credential).then(() => {navigate(0)});
}}
/>
</GoogleOAuthProvider>
)}
</div>

View File

@@ -1,4 +1,5 @@
const Home = () => {
return (
<div className={"max-h-screen flex flex-col justify-center items-center px-6 py-12 text-gray-900"}>
{/* Logo */}

View File

@@ -1,5 +1,35 @@
import type {VideoMetadata, APIResponse, User, Clip, ProgressResult } from "./types.ts";
const API_URL = import.meta.env.VITE_API_URL;
/**
* Login function
* @param GoogleToken - The Google token received from the frontend.
* @return A promise that resolves to a JWT
*/
const login = async (GoogleToken: string): Promise<string> => {
const response = await fetch(API_URL + '/api/v1/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: GoogleToken }),
credentials: 'include'
});
if (!response.ok) {
throw new Error(`Login failed: ${response.status}`);
}
const result: APIResponse = await response.json();
if (result.status === "error") {
throw new Error(`Login failed: ${result.message}`);
}
return result.data.token;
}
/**
* Uploads a file to the backend.
* @param file - The file to upload.
@@ -8,7 +38,7 @@ const uploadFile = async (file: File): Promise<string> => {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/v1/upload', {
const response = await fetch(API_URL + '/api/v1/upload', {
method: 'POST',
body: formData,
});
@@ -36,7 +66,7 @@ const editFile = async (uuid: string, videoMetadata: VideoMetadata) => {
}
}
const response = await fetch(`/api/v1/edit/${uuid}`, {
const response = await fetch(API_URL + `/api/v1/edit/${uuid}`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
@@ -61,7 +91,7 @@ const editFile = async (uuid: string, videoMetadata: VideoMetadata) => {
* @param uuid - The UUID of the video file to process.
*/
const processFile = async (uuid: string) => {
const response = await fetch(`/api/v1/process/${uuid}`);
const response = await fetch(API_URL + `/api/v1/process/${uuid}`, {credentials: "include"});
if (!response.ok) {
throw new Error(`Failed to process file: ${response.status}`);
@@ -75,7 +105,7 @@ const processFile = async (uuid: string) => {
};
const convertFile = async (uuid: string) => {
const response = await fetch(`/api/v1/convert/${uuid}`);
const response = await fetch(API_URL + `/api/v1/convert/${uuid}`);
if (!response.ok) {
throw new Error(`Failed to convert file: ${response.status}`);
@@ -93,7 +123,7 @@ const convertFile = async (uuid: string) => {
* @param uuid - The UUID of the video file.
*/
const getProgress = async (uuid: string): Promise<ProgressResult> => {
const response = await fetch(`/api/v1/progress/${uuid}`);
const response = await fetch(API_URL + `/api/v1/progress/${uuid}`);
if (!response.ok) {
throw new Error(`Failed to fetch progress: ${response.status}`);
@@ -117,7 +147,7 @@ const getProgress = async (uuid: string): Promise<ProgressResult> => {
* @param uuid - The UUID of the video file.
*/
const getMetadata = async (uuid: string): Promise<VideoMetadata> => {
const response = await fetch(`/api/v1/metadata/original/${uuid}`);
const response = await fetch(API_URL + `/api/v1/metadata/original/${uuid}`, {credentials: "include"});
if (!response.ok) {
throw new Error(`Failed to fetch metadata: ${response.status}`);
@@ -136,7 +166,7 @@ const getMetadata = async (uuid: string): Promise<VideoMetadata> => {
* Fetches the current user information. Returns null if not authenticated.
*/
const getUser = async (): Promise<null | User > => {
const response = await fetch('/api/v1/auth/user', {credentials: "include",});
const response = await fetch(API_URL + '/api/v1/auth/user', {credentials: "include"});
if (!response.ok) {
return null;
@@ -148,6 +178,8 @@ const getUser = async (): Promise<null | User > => {
return null;
}
console.log(result.data);
return result.data;
}
@@ -155,7 +187,7 @@ const getUser = async (): Promise<null | User > => {
* Fetches all clips for the current user.
*/
const getClips = async (): Promise<Clip[]> => {
const response = await fetch('/api/v1/clips/', { credentials: 'include' });
const response = await fetch(API_URL + '/api/v1/clips', { credentials: 'include'});
if (!response.ok) {
const errorResult: APIResponse = await response.json();
@@ -175,7 +207,7 @@ const getClips = async (): Promise<Clip[]> => {
* @param id
*/
const getClipById = async (id: string): Promise<Clip | null> => {
const response = await fetch(`/api/v1/clips/${id}`, {credentials: "include",});
const response = await fetch(API_URL + `/api/v1/clips/${id}`, {credentials: "include",});
if (!response.ok) {
throw new Error(`Failed to fetch clip: ${response.status}`);
@@ -191,7 +223,7 @@ const getClipById = async (id: string): Promise<Clip | null> => {
};
const isThumbnailAvailable = async (id: number): Promise<boolean> => {
const response = await fetch(`/api/v1/download/thumbnail/${id}`);
const response = await fetch(API_URL + `/api/v1/download/thumbnail/${id}`, {credentials: "include"});
if (!response.ok) {
return false;
}
@@ -200,6 +232,7 @@ const isThumbnailAvailable = async (id: number): Promise<boolean> => {
}
export {
login,
uploadFile,
editFile,
processFile,

View File

@@ -18,7 +18,7 @@ type APIResponse = {
type User = {
name: string,
email: string,
profilePicture: string
profilePictureUrl: string
}
type Clip = {