auth media
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
authid.txt
|
authid.txt
|
||||||
|
auth-view/node_modules/
|
||||||
|
auth-view/dist/
|
||||||
@@ -321,6 +321,26 @@ deploy-analytics-view:
|
|||||||
docker image prune -af
|
docker image prune -af
|
||||||
ENDSSH
|
ENDSSH
|
||||||
|
|
||||||
|
build-auth-view:
|
||||||
|
stage: build
|
||||||
|
image: node:22-alpine
|
||||||
|
cache:
|
||||||
|
key: "${CI_COMMIT_REF_SLUG}-auth-view"
|
||||||
|
paths:
|
||||||
|
- auth-view/node_modules
|
||||||
|
script:
|
||||||
|
- cd auth-view
|
||||||
|
- npm ci
|
||||||
|
- npm run build
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- auth-view/dist
|
||||||
|
expire_in: 1h
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH == "main"
|
||||||
|
changes:
|
||||||
|
- auth-view/**/*
|
||||||
|
|
||||||
# Deploy all services at once (manual trigger)
|
# Deploy all services at once (manual trigger)
|
||||||
deploy-all:
|
deploy-all:
|
||||||
<<: *deploy_setup
|
<<: *deploy_setup
|
||||||
|
|||||||
11
auth-view/docker/Dockerfile
Normal file
11
auth-view/docker/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM node:22-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html/auth
|
||||||
|
EXPOSE 80
|
||||||
8
auth-view/docker/nginx.conf
Normal file
8
auth-view/docker/nginx.conf
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
|
location /auth/ {
|
||||||
|
try_files $uri $uri/ /auth/index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
auth-view/eslint.config.js
Normal file
28
auth-view/eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import globals from "globals";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ["dist"] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"react-refresh/only-export-components": [
|
||||||
|
"warn",
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
13
auth-view/index.html
Normal file
13
auth-view/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/auth/favicon.ico" sizes="any" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Auth – Post Hub</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3688
auth-view/package-lock.json
generated
Normal file
3688
auth-view/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
auth-view/package.json
Normal file
33
auth-view/package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "auth-view",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
|
"react-router-dom": "^7.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.36.0",
|
||||||
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
|
"@types/react": "^19.1.13",
|
||||||
|
"@types/react-dom": "^19.1.9",
|
||||||
|
"@vitejs/plugin-react": "^5.0.3",
|
||||||
|
"autoprefixer": "^10.4.23",
|
||||||
|
"eslint": "^9.36.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
|
"globals": "^16.4.0",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^4.1.18",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vite": "^7.1.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
auth-view/postcss.config.js
Normal file
5
auth-view/postcss.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
15
auth-view/src/App.tsx
Normal file
15
auth-view/src/App.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
|
||||||
|
import LoginPage from "./pages/LoginPage";
|
||||||
|
import RegisterPage from "./pages/RegisterPage";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter basename="/auth">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route path="/register" element={<RegisterPage />} />
|
||||||
|
<Route path="*" element={<Navigate to="/login" replace />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
auth-view/src/index.css
Normal file
1
auth-view/src/index.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
10
auth-view/src/main.tsx
Normal file
10
auth-view/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import "./index.css";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
178
auth-view/src/pages/LoginPage.tsx
Normal file
178
auth-view/src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import { useEffect, useState, FormEvent } from "react";
|
||||||
|
import { useSearchParams, Link } from "react-router-dom";
|
||||||
|
|
||||||
|
function saveTokensAndRedirect(token: string, refreshToken: string, redirect: string) {
|
||||||
|
localStorage.setItem("token", token);
|
||||||
|
localStorage.setItem("refreshToken", refreshToken);
|
||||||
|
window.location.href = redirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const redirect = searchParams.get("redirect") ?? "/ragview";
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Handle OAuth2 callback — token & refreshToken in URL
|
||||||
|
useEffect(() => {
|
||||||
|
const token = searchParams.get("token");
|
||||||
|
const refreshToken = searchParams.get("refreshToken");
|
||||||
|
if (token && refreshToken) {
|
||||||
|
saveTokensAndRedirect(token, refreshToken, redirect);
|
||||||
|
}
|
||||||
|
}, [searchParams, redirect]);
|
||||||
|
|
||||||
|
async function handleSubmit(e: FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
setError("");
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success && data.payload?.token) {
|
||||||
|
saveTokensAndRedirect(data.payload.token, data.payload.refreshToken ?? "", redirect);
|
||||||
|
} else {
|
||||||
|
setError(data.message ?? "Login failed");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setError("Network error. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerHref = `/register?redirect=${encodeURIComponent(redirect)}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-950 flex items-center justify-center px-4">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
{/* Card */}
|
||||||
|
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-8 shadow-2xl">
|
||||||
|
<h1 className="text-2xl font-bold text-white mb-2 text-center">Welcome back</h1>
|
||||||
|
<p className="text-gray-400 text-sm text-center mb-8">Sign in to your account</p>
|
||||||
|
|
||||||
|
{/* Email/Password form */}
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-1.5">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
className="w-full bg-gray-800 border border-gray-700 text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-500 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-1.5">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
className="w-full bg-gray-800 border border-gray-700 text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-500 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p className="text-red-400 text-sm bg-red-950/50 border border-red-800 rounded-lg px-3 py-2">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold rounded-lg py-2.5 text-sm transition"
|
||||||
|
>
|
||||||
|
{loading ? "Signing in…" : "Sign in"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="flex items-center gap-3 my-6">
|
||||||
|
<div className="flex-1 h-px bg-gray-800" />
|
||||||
|
<span className="text-gray-500 text-xs font-medium">or</span>
|
||||||
|
<div className="flex-1 h-px bg-gray-800" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* OAuth2 buttons */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<a
|
||||||
|
href="/oauth2/authorization/google"
|
||||||
|
className="flex items-center gap-3 w-full bg-gray-800 hover:bg-gray-700 border border-gray-700 text-gray-200 rounded-lg px-4 py-2.5 text-sm font-medium transition"
|
||||||
|
>
|
||||||
|
<GoogleIcon />
|
||||||
|
Login with Google
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/oauth2/authorization/github"
|
||||||
|
className="flex items-center gap-3 w-full bg-gray-800 hover:bg-gray-700 border border-gray-700 text-gray-200 rounded-lg px-4 py-2.5 text-sm font-medium transition"
|
||||||
|
>
|
||||||
|
<GitHubIcon />
|
||||||
|
Login with GitHub
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/oauth2/authorization/facebook"
|
||||||
|
className="flex items-center gap-3 w-full bg-gray-800 hover:bg-gray-700 border border-gray-700 text-gray-200 rounded-lg px-4 py-2.5 text-sm font-medium transition"
|
||||||
|
>
|
||||||
|
<FacebookIcon />
|
||||||
|
Login with Facebook
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Register link */}
|
||||||
|
<p className="text-center text-sm text-gray-500 mt-6">
|
||||||
|
Don't have an account?{" "}
|
||||||
|
<Link
|
||||||
|
to={registerHref}
|
||||||
|
className="text-indigo-400 hover:text-indigo-300 font-medium transition"
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GoogleIcon() {
|
||||||
|
return (
|
||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17.64 9.205c0-.639-.057-1.252-.164-1.841H9v3.481h4.844a4.14 4.14 0 0 1-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615z" fill="#4285F4"/>
|
||||||
|
<path d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z" fill="#34A853"/>
|
||||||
|
<path d="M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z" fill="#FBBC05"/>
|
||||||
|
<path d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z" 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.599.111.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="#1877F2" 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.874v2.25h3.328l-.532 3.49h-2.796V24C19.612 23.094 24 18.1 24 12.073z"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
143
auth-view/src/pages/RegisterPage.tsx
Normal file
143
auth-view/src/pages/RegisterPage.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { useState, FormEvent } from "react";
|
||||||
|
import { useSearchParams, Link } from "react-router-dom";
|
||||||
|
|
||||||
|
function saveTokensAndRedirect(token: string, refreshToken: string, redirect: string) {
|
||||||
|
localStorage.setItem("token", token);
|
||||||
|
localStorage.setItem("refreshToken", refreshToken);
|
||||||
|
window.location.href = redirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const redirect = searchParams.get("redirect") ?? "/ragview";
|
||||||
|
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
async function handleSubmit(e: FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError("Passwords do not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/auth/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ username, email, password }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success && data.payload?.token) {
|
||||||
|
saveTokensAndRedirect(data.payload.token, data.payload.refreshToken ?? "", redirect);
|
||||||
|
} else {
|
||||||
|
setError(data.message ?? "Registration failed");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setError("Network error. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginHref = `/login?redirect=${encodeURIComponent(redirect)}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-950 flex items-center justify-center px-4">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
{/* Card */}
|
||||||
|
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-8 shadow-2xl">
|
||||||
|
<h1 className="text-2xl font-bold text-white mb-2 text-center">Create account</h1>
|
||||||
|
<p className="text-gray-400 text-sm text-center mb-8">Sign up to get started</p>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-1.5">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
placeholder="johndoe"
|
||||||
|
className="w-full bg-gray-800 border border-gray-700 text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-500 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-1.5">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
className="w-full bg-gray-800 border border-gray-700 text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-500 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-1.5">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
className="w-full bg-gray-800 border border-gray-700 text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-500 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-1.5">
|
||||||
|
Confirm Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
className="w-full bg-gray-800 border border-gray-700 text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-500 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p className="text-red-400 text-sm bg-red-950/50 border border-red-800 rounded-lg px-3 py-2">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold rounded-lg py-2.5 text-sm transition"
|
||||||
|
>
|
||||||
|
{loading ? "Creating account…" : "Create account"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Login link */}
|
||||||
|
<p className="text-center text-sm text-gray-500 mt-6">
|
||||||
|
Already have an account?{" "}
|
||||||
|
<Link
|
||||||
|
to={loginHref}
|
||||||
|
className="text-indigo-400 hover:text-indigo-300 font-medium transition"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
auth-view/src/vite-env.d.ts
vendored
Normal file
1
auth-view/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
22
auth-view/tsconfig.app.json
Normal file
22
auth-view/tsconfig.app.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
auth-view/tsconfig.json
Normal file
7
auth-view/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
20
auth-view/tsconfig.node.json
Normal file
20
auth-view/tsconfig.node.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
7
auth-view/vite.config.ts
Normal file
7
auth-view/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
base: "/auth/",
|
||||||
|
});
|
||||||
@@ -94,7 +94,7 @@ jwt:
|
|||||||
|
|
||||||
# ---- OAuth2 redirect after success ----
|
# ---- OAuth2 redirect after success ----
|
||||||
oauth2:
|
oauth2:
|
||||||
redirect-uri: ${OAUTH2_REDIRECT_URI:https://balexvic.com/login}
|
redirect-uri: ${OAUTH2_REDIRECT_URI:https://balexvic.com/auth/login}
|
||||||
|
|
||||||
# ---- Auth path config ----
|
# ---- Auth path config ----
|
||||||
auth:
|
auth:
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { TOKEN_UNDEFINED } from "./features/constants";
|
|||||||
|
|
||||||
import RootLayout from "./pages/RootLayout";
|
import RootLayout from "./pages/RootLayout";
|
||||||
import HomePage from "./pages/HomePage";
|
import HomePage from "./pages/HomePage";
|
||||||
import RegisterPage from "./pages/RegisterPage";
|
|
||||||
import LoginPage from "./pages/LoginPage";
|
|
||||||
import UploadPage from "./pages/UploadPage";
|
import UploadPage from "./pages/UploadPage";
|
||||||
import RagPage from "./pages/RagPage";
|
import RagPage from "./pages/RagPage";
|
||||||
|
|
||||||
@@ -18,8 +16,6 @@ const router = createBrowserRouter(
|
|||||||
element: <RootLayout />,
|
element: <RootLayout />,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <HomePage /> },
|
{ index: true, element: <HomePage /> },
|
||||||
{ path: "register", element: <RegisterPage /> },
|
|
||||||
{ path: "login", element: <LoginPage /> },
|
|
||||||
{ path: "upload", element: <UploadPage /> },
|
{ path: "upload", element: <UploadPage /> },
|
||||||
{ path: "rag", element: <RagPage /> },
|
{ path: "rag", element: <RagPage /> },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ export const METHOD_POST_QUERY = "POST";
|
|||||||
export const METHOD_GET_QUERY = "GET";
|
export const METHOD_GET_QUERY = "GET";
|
||||||
export const METHOD_DELETE_QUERY = "DELETE";
|
export const METHOD_DELETE_QUERY = "DELETE";
|
||||||
export const HEADER_CONTENT_TYPE = "Content-Type";
|
export const HEADER_CONTENT_TYPE = "Content-Type";
|
||||||
export const JWT_TOKEN = "jwt";
|
export const JWT_TOKEN = "token";
|
||||||
export const JWT_REFRESH_TOKEN = "jwt-refresh";
|
export const JWT_REFRESH_TOKEN = "refreshToken";
|
||||||
export const APPLICATION_JSON = "application/json";
|
export const APPLICATION_JSON = "application/json";
|
||||||
export const USER_NAME_UNDEFINED = "userNameUndefined";
|
export const USER_NAME_UNDEFINED = "userNameUndefined";
|
||||||
export const USER_EMAIL_UNDEFINED = "userEmailUndefined";
|
export const USER_EMAIL_UNDEFINED = "userEmailUndefined";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import fetchRefreshToken from "./fetchRefreshToken";
|
import fetchRefreshToken from "./fetchRefreshToken";
|
||||||
import { JWT_TOKEN, BASE_URL, PREFIX_AUTH } from "../constants";
|
import { JWT_TOKEN } from "../constants";
|
||||||
|
|
||||||
const SESSION_INVALIDATED = "SESSION_INVALIDATED";
|
const SESSION_INVALIDATED = "SESSION_INVALIDATED";
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ export const fetchWithAuth = async (
|
|||||||
|
|
||||||
if (responseText === SESSION_INVALIDATED) {
|
if (responseText === SESSION_INVALIDATED) {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
window.location.href = BASE_URL + PREFIX_AUTH + "/login";
|
window.location.href = "/auth/login?redirect=/ragview";
|
||||||
return firstRes;
|
return firstRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -143,13 +143,13 @@ function HomePage() {
|
|||||||
{!auth ? (
|
{!auth ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<button
|
<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"
|
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
|
Register
|
||||||
</button>
|
</button>
|
||||||
<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"
|
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
|
Login
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
Reference in New Issue
Block a user