ADD Google OAuth login and logout functionality on frontend

This commit is contained in:
2025-08-11 15:16:47 +02:00
parent 662966f138
commit 0c11abc7b9
7 changed files with 113 additions and 44 deletions

View File

@@ -15,6 +15,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"flowbite-react": "^0.11.8", "flowbite-react": "^0.11.8",
"js-cookie": "^3.0.5",
"lucide-react": "^0.511.0", "lucide-react": "^0.511.0",
"path": "^0.12.7", "path": "^0.12.7",
"react": "^19.1.0", "react": "^19.1.0",
@@ -3358,6 +3359,15 @@
"jiti": "lib/jiti-cli.mjs" "jiti": "lib/jiti-cli.mjs"
} }
}, },
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@@ -18,6 +18,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"flowbite-react": "^0.11.8", "flowbite-react": "^0.11.8",
"js-cookie": "^3.0.5",
"lucide-react": "^0.511.0", "lucide-react": "^0.511.0",
"path": "^0.12.7", "path": "^0.12.7",
"react": "^19.1.0", "react": "^19.1.0",

View File

@@ -7,6 +7,7 @@ import Home from './pages/Home';
import {useEffect} from "react"; import {useEffect} from "react";
import MyClips from './pages/MyClips'; import MyClips from './pages/MyClips';
import VideoPlayer from "./pages/VideoPlayer.tsx"; import VideoPlayer from "./pages/VideoPlayer.tsx";
import { GoogleOAuthProvider } from '@react-oauth/google';
function App() { function App() {
@@ -15,17 +16,19 @@ function App() {
}, []); }, []);
return ( return (
<Router> <GoogleOAuthProvider clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}>
<Routes> <Router>
<Route element={<MainLayout />}> <Routes>
<Route path="/" element={<Home />} /> <Route element={<MainLayout />}>
<Route path="/create" element={<ClipUpload />} /> <Route path="/" element={<Home />} />
<Route path="/create/:id" element={<ClipEdit />} /> <Route path="/create" element={<ClipUpload />} />
<Route path="/my-clips" element={<MyClips />} /> <Route path="/create/:id" element={<ClipEdit />} />
<Route path="/video/:id" element={<VideoPlayer />} /> <Route path="/my-clips" element={<MyClips />} />
</Route> <Route path="/video/:id" element={<VideoPlayer />} />
</Routes> </Route>
</Router> </Routes>
</Router>
</ GoogleOAuthProvider>
); );
} }

View File

@@ -1,25 +1,53 @@
import { Menu, X } from 'lucide-react'; import { Menu, X } from 'lucide-react';
import { login, logout } from "../utils/endpoints.ts";
import { Dropdown, DropdownItem } from "./Dropdown.tsx";
import { GoogleLogin } from '@react-oauth/google';
import type { User } from "../utils/types.ts";
import type { CredentialResponse } from '@react-oauth/google';
import MenuButton from "./buttons/MenuButton.tsx"; import MenuButton from "./buttons/MenuButton.tsx";
import clsx from "clsx"; 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 = { type props = {
sidebarToggled: boolean; sidebarToggled: boolean;
setSidebarToggled: Function; setSidebarToggled: (toggled: boolean) => void;
user: User | null; user: User | null;
fetchUser: () => void;
className?: string; className?: string;
} }
const Topbar = ({sidebarToggled, setSidebarToggled, user, className}: props) => { const Topbar = ({
const navigate = useNavigate(); sidebarToggled,
setSidebarToggled,
user,
fetchUser,
className}: props) => {
const handleLogin = (response: CredentialResponse) => {
if (!response.credential) {
console.error("No credential received from Google login.");
return;
}
login(response.credential)
.then(() => {
fetchUser();
})
.catch((error) => {
console.error("Login failed:", error);
});
}
const handleLogout = () => { const handleLogout = () => {
// delete token cookie logout()
document.cookie = "token=; Secure; SameSite=None; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; .then(() => {
fetchUser();
})
.catch((error) => {
console.error("Logout failed:", error);
});
} }
return ( return (
@@ -44,20 +72,11 @@ const Topbar = ({sidebarToggled, setSidebarToggled, user, className}: props) =>
</div> </div>
) : ) :
( (
<GoogleOAuthProvider <GoogleLogin
clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}> shape={"pill"}
<GoogleLogin useOneTap={false}
onSuccess={(credentialResponse) => { onSuccess={(credentialResponse) => handleLogin(credentialResponse)} />
if (!credentialResponse.credential) {
console.error("No credential received from Google Login");
return;
}
login(credentialResponse.credential).then(() => {navigate(0)});
}}
/>
</GoogleOAuthProvider>
)} )}
</div> </div>
) )
} }

View File

@@ -10,16 +10,16 @@ const MainLayout = () => {
const [sidebarToggled, setSidebarToggled] = useState(false); const [sidebarToggled, setSidebarToggled] = useState(false);
const [user, setUser] = useState<null | User>(null); const [user, setUser] = useState<null | User>(null);
useEffect(() => { const fetchUser = async () => {
const fetchUser = async () => { try {
try { const userData = await getUser();
const userData = await getUser(); setUser(userData);
setUser(userData); } catch (error) {
} catch (error) { console.error('Failed to fetch user:', error);
console.error('Failed to fetch user:', error); }
} };
};
useEffect(() => {
fetchUser(); fetchUser();
}, []); }, []);
@@ -32,7 +32,8 @@ const MainLayout = () => {
className={"transition-all duration-300"} className={"transition-all duration-300"}
sidebarToggled={sidebarToggled} sidebarToggled={sidebarToggled}
setSidebarToggled={setSidebarToggled} setSidebarToggled={setSidebarToggled}
user={user}/> user={user}
fetchUser={fetchUser}/>
<div className="overflow-auto"> <div className="overflow-auto">
<Outlet /> <Outlet />
</div> </div>

View File

@@ -30,6 +30,23 @@ const login = async (GoogleToken: string): Promise<string> => {
return result.data.token; return result.data.token;
} }
const logout = async () => {
const response = await fetch(API_URL + '/api/v1/auth/logout', {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
throw new Error(`Logout failed: ${response.status}`);
}
const result: APIResponse = await response.json();
if (result.status === "error") {
throw new Error(`Logout failed: ${result.message}`);
}
}
/** /**
* Uploads a file to the backend. * Uploads a file to the backend.
* @param file - The file to upload. * @param file - The file to upload.
@@ -233,6 +250,7 @@ const isThumbnailAvailable = async (id: number): Promise<boolean> => {
export { export {
login, login,
logout,
uploadFile, uploadFile,
editFile, editFile,
processFile, processFile,

View File

@@ -51,4 +51,21 @@ public class UserController {
new APIResponse<>("success", "Logged in successfully", new TokenDTO(jwt)) new APIResponse<>("success", "Logged in successfully", new TokenDTO(jwt))
); );
} }
@PostMapping("/logout")
public ResponseEntity<APIResponse<Void>> logout(HttpServletResponse response) {
ResponseCookie cookie = ResponseCookie.from("token", "")
.httpOnly(true)
.maxAge(0)
.sameSite("None")
.secure(true)
.path("/")
.build();
response.addHeader("Set-Cookie", cookie.toString());
return ResponseEntity.ok(
new APIResponse<>("success", "Logged out successfully", null)
);
}
} }