ADD Google OAuth login and logout functionality on frontend
This commit is contained in:
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user