auth media

This commit is contained in:
2026-03-14 23:16:06 +01:00
parent e587480abb
commit ab999c558d
26 changed files with 4219 additions and 381 deletions

View File

@@ -6,8 +6,6 @@ import { TOKEN_UNDEFINED } from "./features/constants";
import RootLayout from "./pages/RootLayout";
import HomePage from "./pages/HomePage";
import RegisterPage from "./pages/RegisterPage";
import LoginPage from "./pages/LoginPage";
import UploadPage from "./pages/UploadPage";
import RagPage from "./pages/RagPage";
@@ -18,8 +16,6 @@ const router = createBrowserRouter(
element: <RootLayout />,
children: [
{ index: true, element: <HomePage /> },
{ path: "register", element: <RegisterPage /> },
{ path: "login", element: <LoginPage /> },
{ path: "upload", element: <UploadPage /> },
{ path: "rag", element: <RagPage /> },
],

View File

@@ -5,8 +5,8 @@ export const METHOD_POST_QUERY = "POST";
export const METHOD_GET_QUERY = "GET";
export const METHOD_DELETE_QUERY = "DELETE";
export const HEADER_CONTENT_TYPE = "Content-Type";
export const JWT_TOKEN = "jwt";
export const JWT_REFRESH_TOKEN = "jwt-refresh";
export const JWT_TOKEN = "token";
export const JWT_REFRESH_TOKEN = "refreshToken";
export const APPLICATION_JSON = "application/json";
export const USER_NAME_UNDEFINED = "userNameUndefined";
export const USER_EMAIL_UNDEFINED = "userEmailUndefined";

View File

@@ -1,5 +1,5 @@
import fetchRefreshToken from "./fetchRefreshToken";
import { JWT_TOKEN, BASE_URL, PREFIX_AUTH } from "../constants";
import { JWT_TOKEN } from "../constants";
const SESSION_INVALIDATED = "SESSION_INVALIDATED";
@@ -24,7 +24,7 @@ export const fetchWithAuth = async (
if (responseText === SESSION_INVALIDATED) {
localStorage.clear();
window.location.href = BASE_URL + PREFIX_AUTH + "/login";
window.location.href = "/auth/login?redirect=/ragview";
return firstRes;
}

View File

@@ -143,13 +143,13 @@ function HomePage() {
{!auth ? (
<div className="space-y-3">
<button
onClick={() => navigate("/register")}
onClick={() => { window.location.href = "/auth/register?redirect=/ragview"; }}
className="w-full py-2.5 rounded-xl bg-indigo-500 hover:bg-indigo-400 active:bg-indigo-600 text-sm font-medium text-white shadow-md shadow-indigo-500/25 transition-colors"
>
Register
</button>
<button
onClick={() => navigate("/login")}
onClick={() => { window.location.href = "/auth/login?redirect=/ragview"; }}
className="w-full py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 active:bg-slate-600 text-sm font-medium text-white transition-colors"
>
Login

View File

@@ -1,207 +0,0 @@
import { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate, Link, useSearchParams } from "react-router-dom";
import { clearError, setOAuthTokens } from "../features/slices/details-slice";
import fetchLoginUser from "../features/fetch-async/fetchLoginUser";
function GoogleIcon() {
return (
<svg width="18" height="18" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47.532 24.552c0-1.636-.148-3.21-.422-4.728H24.48v8.95h12.954c-.558 3.012-2.254 5.564-4.8 7.278v6.048h7.774c4.548-4.19 7.124-10.364 7.124-17.548z" fill="#4285F4"/>
<path d="M24.48 48c6.504 0 11.958-2.156 15.944-5.9l-7.774-6.048c-2.156 1.444-4.912 2.298-8.17 2.298-6.282 0-11.606-4.244-13.51-9.946H2.918v6.244C6.886 42.692 15.1 48 24.48 48z" fill="#34A853"/>
<path d="M10.97 28.404A14.443 14.443 0 0 1 9.938 24c0-1.528.262-3.014.73-4.404V13.352H2.918A23.963 23.963 0 0 0 .48 24c0 3.868.926 7.526 2.438 10.648l8.052-6.244z" fill="#FBBC05"/>
<path d="M24.48 9.548c3.54 0 6.718 1.216 9.216 3.604l6.908-6.908C36.43 2.392 30.978 0 24.48 0 15.1 0 6.886 5.308 2.918 13.352l8.052 6.244c1.904-5.702 7.228-9.946 13.51-9.946-.002 0-.002-.002 0-.002z" fill="#EA4335"/>
</svg>
);
}
function GitHubIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.6.113.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"/>
</svg>
);
}
function FacebookIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M24 12.073C24 5.405 18.627 0 12 0S0 5.405 0 12.073C0 18.1 4.388 23.094 10.125 24v-8.437H7.078v-3.49h3.047V9.41c0-3.025 1.792-4.697 4.533-4.697 1.312 0 2.686.236 2.686.236v2.97h-1.513c-1.491 0-1.956.93-1.956 1.886v2.268h3.328l-.532 3.49h-2.796V24C19.612 23.094 24 18.1 24 12.073z"/>
</svg>
);
}
function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const dispatch = useDispatch();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { loading, error, token } = useSelector((state) => state.userDetails);
// Handle OAuth2 callback: ?token=...&refreshToken=...
useEffect(() => {
const urlToken = searchParams.get("token");
const urlRefreshToken = searchParams.get("refreshToken");
if (urlToken) {
dispatch(setOAuthTokens({ token: urlToken, refreshToken: urlRefreshToken }));
navigate("/", { replace: true });
}
}, [searchParams, dispatch, navigate]);
// Redirect if already logged in
useEffect(() => {
if (token) {
navigate("/");
}
}, [token, navigate]);
// Clear error on unmount
useEffect(() => {
return () => {
dispatch(clearError());
};
}, [dispatch]);
const handleSubmit = async (e) => {
e.preventDefault();
const result = await dispatch(fetchLoginUser({ email, password }));
if (fetchLoginUser.fulfilled.match(result)) {
navigate("/");
}
};
return (
<div className="min-h-[60vh] flex items-center justify-center">
<div className="w-full max-w-md bg-slate-900/80 backdrop-blur-xl rounded-2xl shadow-2xl border border-slate-700/60 p-8">
<h1 className="text-2xl font-semibold text-slate-50 text-center mb-2">
Login
</h1>
<p className="text-sm text-slate-400 text-center mb-6">
Enter your email and password, then press the button to continue.
</p>
{error && (
<div className="mb-4 p-3 rounded-xl bg-red-500/20 border border-red-500/50 text-red-300 text-sm text-center">
{error.message || `Error: ${error.status}`}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-5">
<div className="space-y-2">
<label className="block text-sm font-medium text-slate-200">
Email
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Your email"
className="w-full rounded-xl bg-slate-800/80 border border-slate-600 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/60 focus:outline-none px-4 py-2.5 text-slate-100 placeholder:text-slate-500 text-sm"
required
disabled={loading}
/>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-slate-200">
Password
</label>
<input
type="password"
value={password}
minLength={8}
onChange={(e) => setPassword(e.target.value)}
placeholder="Your password"
className="w-full rounded-xl bg-slate-800/80 border border-slate-600 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/60 focus:outline-none px-4 py-2.5 text-slate-100 placeholder:text-slate-500 text-sm"
required
disabled={loading}
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full mt-2 inline-flex items-center justify-center rounded-xl bg-indigo-500 hover:bg-indigo-400 active:bg-indigo-600 disabled:bg-indigo-500/50 disabled:cursor-not-allowed transition-colors px-4 py-2.5 text-sm font-semibold text-white shadow-lg shadow-indigo-500/30"
>
{loading ? (
<>
<svg
className="animate-spin -ml-1 mr-2 h-4 w-4 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Logging in...
</>
) : (
"Login"
)}
</button>
</form>
{/* OAuth2 section */}
<div className="mt-6 flex items-center gap-3">
<div className="flex-1 h-px bg-slate-700" />
<span className="text-xs text-slate-500 font-medium">or</span>
<div className="flex-1 h-px bg-slate-700" />
</div>
<div className="mt-4 space-y-3">
<a
href="/oauth2/authorization/google"
className="w-full inline-flex items-center justify-center gap-3 rounded-xl bg-slate-800 hover:bg-slate-700 active:bg-slate-900 border border-slate-600 hover:border-slate-500 transition-colors px-4 py-2.5 text-sm font-medium text-slate-200"
>
<GoogleIcon />
Login with Google
</a>
<a
href="/oauth2/authorization/github"
className="w-full inline-flex items-center justify-center gap-3 rounded-xl bg-slate-800 hover:bg-slate-700 active:bg-slate-900 border border-slate-600 hover:border-slate-500 transition-colors px-4 py-2.5 text-sm font-medium text-slate-200"
>
<GitHubIcon />
Login with GitHub
</a>
<a
href="/oauth2/authorization/facebook"
className="w-full inline-flex items-center justify-center gap-3 rounded-xl bg-slate-800 hover:bg-slate-700 active:bg-slate-900 border border-slate-600 hover:border-slate-500 transition-colors px-4 py-2.5 text-sm font-medium text-slate-200"
>
<FacebookIcon />
Login with Facebook
</a>
</div>
<p className="mt-6 text-center text-sm text-slate-400">
Don't have an account?{" "}
<Link
to="/register"
className="text-indigo-400 hover:text-indigo-300 transition-colors"
>
Register
</Link>
</p>
</div>
</div>
);
}
export default LoginPage;

View File

@@ -1,163 +0,0 @@
import { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import fetchRegisterUser from "../features/fetch-async/fetchRegisterUser";
import {
USER_NAME_UNDEFINED,
USER_EMAIL_UNDEFINED,
} from "../features/constants";
function RegisterPage() {
const [login, setLogin] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirm, setConfirm] = useState("");
const [validationError, setValidationError] = useState("");
const dispatch = useDispatch();
const navigate = useNavigate();
const { userName, userEmail, loading, error } = useSelector(
(state) => state.userDetails
);
useEffect(() => {
if (loading) return;
if (
userName !== USER_NAME_UNDEFINED &&
userEmail !== USER_EMAIL_UNDEFINED
) {
navigate("/");
}
}, [loading, userName, userEmail, navigate]);
const handleClear = () => {
setLogin("");
setEmail("");
setPassword("");
setConfirm("");
setValidationError("");
};
const handleSubmit = (e) => {
e.preventDefault();
setValidationError("");
if (login.length < 3) {
setValidationError("Login must be at least 3 characters.");
return;
}
if (password.length < 8) {
setValidationError("Password must be at least 8 characters.");
return;
}
if (password !== confirm) {
setValidationError("Password and Confirm password do not match.");
return;
}
const payload = {
username: login,
email: email,
password: password,
confirmPassword: confirm,
};
dispatch(fetchRegisterUser(payload));
};
return (
<div className="min-h-[60vh] flex items-center justify-center">
<div className="w-full max-w-md bg-slate-900/80 backdrop-blur-xl rounded-2xl shadow-2xl border border-slate-700/60 p-8">
<h1 className="text-2xl font-semibold text-slate-50 text-center mb-2">
Register
</h1>
<p className="text-sm text-slate-400 text-center mb-6">
Create a new account by choosing a login, email and a strong password.
</p>
{(validationError || error) && (
<div className="mb-4 text-sm text-red-400 text-center">
{validationError || error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-5">
<div className="space-y-2">
<label className="block text-sm font-medium text-slate-200">
Login
</label>
<input
type="text"
value={login}
onChange={(e) => setLogin(e.target.value)}
placeholder="Your login"
className="w-full rounded-xl bg-slate-800/80 border border-slate-600 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/60 focus:outline-none px-4 py-2.5 text-slate-100 placeholder:text-slate-500 text-sm"
/>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-slate-200">
Email
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Your email"
className="w-full rounded-xl bg-slate-800/80 border border-slate-600 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/60 focus:outline-none px-4 py-2.5 text-slate-100 placeholder:text-slate-500 text-sm"
/>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-slate-200">
Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Your password"
className="w-full rounded-xl bg-slate-800/80 border border-slate-600 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/60 focus:outline-none px-4 py-2.5 text-slate-100 placeholder:text-slate-500 text-sm"
/>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-slate-200">
Confirm password
</label>
<input
type="password"
value={confirm}
onChange={(e) => setConfirm(e.target.value)}
placeholder="Repeat your password"
className="w-full rounded-xl bg-slate-800/80 border border-slate-600 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/60 focus:outline-none px-4 py-2.5 text-slate-100 placeholder:text-slate-500 text-sm"
/>
</div>
<div className="flex gap-3 mt-2">
<button
type="submit"
disabled={loading}
className="flex-1 inline-flex items-center justify-center rounded-xl bg-indigo-500 hover:bg-indigo-400 disabled:opacity-60 disabled:cursor-not-allowed active:bg-indigo-600 transition-colors px-4 py-2.5 text-sm font-semibold text-white shadow-lg shadow-indigo-500/30"
>
{loading ? "Registering..." : "Register"}
</button>
{(validationError || error) && (
<button
type="button"
onClick={handleClear}
className="inline-flex items-center justify-center rounded-xl bg-slate-700 hover:bg-slate-600 active:bg-slate-800 transition-colors px-4 py-2.5 text-sm font-semibold text-slate-300"
>
Clear
</button>
)}
</div>
</form>
</div>
</div>
);
}
export default RegisterPage;