mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-12-27 09:00:28 +08:00
Implement Login page using react
This commit is contained in:
parent
767eb844c9
commit
b5005afbc5
@ -222,6 +222,11 @@ public class PageFactory {
|
||||
}
|
||||
|
||||
public Page loginPage() throws IOException {
|
||||
if (config.get().isTrue(PluginSettings.FRONTEND_BETA)) {
|
||||
String reactHtml = getResource("index.html");
|
||||
return () -> reactHtml;
|
||||
}
|
||||
|
||||
return new LoginPage(getResource("login.html"), serverInfo.get(), locale.get(), theme.get(), versionChecker.get());
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,7 @@ import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Optional;
|
||||
|
||||
@ -42,12 +43,19 @@ import java.util.Optional;
|
||||
@Path("/v1/metadata")
|
||||
public class MetadataJSONResolver implements NoAuthResolver {
|
||||
|
||||
private final String mainCommand;
|
||||
private final PlanConfig config;
|
||||
private final Theme theme;
|
||||
private final ServerInfo serverInfo;
|
||||
|
||||
@Inject
|
||||
public MetadataJSONResolver(PlanConfig config, Theme theme, ServerInfo serverInfo) {
|
||||
public MetadataJSONResolver(
|
||||
@Named("mainCommandName") String mainCommand,
|
||||
PlanConfig config,
|
||||
Theme theme,
|
||||
ServerInfo serverInfo
|
||||
) {
|
||||
this.mainCommand = mainCommand;
|
||||
this.config = config;
|
||||
// Dagger inject constructor
|
||||
this.theme = theme;
|
||||
@ -74,6 +82,7 @@ public class MetadataJSONResolver implements NoAuthResolver {
|
||||
.put("isProxy", serverInfo.getServer().isProxy())
|
||||
.put("serverName", serverInfo.getServer().getIdentifiableName())
|
||||
.put("networkName", serverInfo.getServer().isProxy() ? config.get(ProxySettings.NETWORK_NAME) : null)
|
||||
.put("mainCommand", mainCommand)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ function drawSine(canvasId) {
|
||||
|
||||
function draw() {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (canvas == null) return;
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
context.clearRect(0, 0, 1000, 150);
|
||||
@ -60,6 +61,7 @@ function drawSine(canvasId) {
|
||||
|
||||
function fix_dpi() {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (canvas == null) return;
|
||||
let dpi = window.devicePixelRatio;
|
||||
canvas.getContext('2d');
|
||||
const style_width = getComputedStyle(canvas).getPropertyValue("width").slice(0, -2);
|
||||
|
@ -28,6 +28,7 @@ import ServerPlayers from "./views/server/ServerPlayers";
|
||||
import PlayersPage from "./views/layout/PlayersPage";
|
||||
import AllPlayers from "./views/players/AllPlayers";
|
||||
import ServerGeolocations from "./views/server/ServerGeolocations";
|
||||
import LoginPage from "./views/layout/LoginPage";
|
||||
|
||||
const SwaggerView = React.lazy(() => import("./views/SwaggerView"));
|
||||
|
||||
@ -57,6 +58,8 @@ function App() {
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="" element={<MainPageRedirect/>}/>
|
||||
<Route path="/" element={<MainPageRedirect/>}/>
|
||||
<Route path="/login" element={<LoginPage/>}/>
|
||||
<Route path="/player/:identifier" element={<PlayerPage/>}>
|
||||
<Route path="" element={<OverviewRedirect/>}/>
|
||||
<Route path="overview" element={<PlayerOverview/>}/>
|
||||
@ -86,8 +89,13 @@ function App() {
|
||||
<Route path="geolocations" element={<ServerGeolocations/>}/>
|
||||
<Route path="performance" element={<></>}/>
|
||||
<Route path="plugins-overview" element={<></>}/>
|
||||
<Route path="*" element={<ErrorView error={{
|
||||
message: 'Unknown tab address, please correct the address',
|
||||
title: 'No such tab',
|
||||
icon: faMapSigns
|
||||
}}/>}/>
|
||||
</Route>
|
||||
<Route path="docs" element={<React.Suspense fallback={<></>}>
|
||||
<Route path="/docs" element={<React.Suspense fallback={<></>}>
|
||||
<SwaggerView/>
|
||||
</React.Suspense>}/>
|
||||
</Routes>
|
||||
|
@ -24,7 +24,7 @@ const ColorSelectorModal = () => {
|
||||
aria-labelledby="colorChooserModalLabel"
|
||||
show={theme.colorChooserOpen}
|
||||
onHide={theme.toggleColorChooser}>
|
||||
<Modal.Header>
|
||||
<Modal.Header className="bg-white">
|
||||
<Modal.Title id="colorChooserModalLabel">
|
||||
<Fa icon={faPalette}/> {t('html.label.themeSelect')}
|
||||
</Modal.Title>
|
||||
|
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import {Modal} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faHandPointRight} from "@fortawesome/free-regular-svg-icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useMetadata} from "../../hooks/metadataHook";
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
const ForgotPasswordModal = ({show, toggle}) => {
|
||||
const {t} = useTranslation();
|
||||
const {mainCommand} = useMetadata();
|
||||
|
||||
return (
|
||||
<Modal id="forgotPasswordModal"
|
||||
aria-labelledby="forgotModalLabel"
|
||||
show={show}
|
||||
onHide={toggle}
|
||||
>
|
||||
<Modal.Header className="bg-white">
|
||||
<Modal.Title id="forgotModalLabel">
|
||||
<Fa icon={faHandPointRight}/> {t('html.login.forgotPassword1')}
|
||||
</Modal.Title>
|
||||
<button aria-label="Close" className="btn-close" onClick={toggle}/>
|
||||
</Modal.Header>
|
||||
<Modal.Body className="bg-white">
|
||||
<p>{t('html.login.forgotPassword2')}</p>
|
||||
<p><code>/{mainCommand || 'plan'} unregister</code></p>
|
||||
<p>{t('html.login.forgotPassword3')}</p>
|
||||
<p><code>/{mainCommand || 'plan'} unregister [username]</code></p>
|
||||
<p>{t('html.login.forgotPassword4')} <Link to="/register"
|
||||
className="col-plan">{t('html.login.register')}</Link></p>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
)
|
||||
};
|
||||
|
||||
export default ForgotPasswordModal
|
@ -49,19 +49,18 @@ const MainPageRedirect = () => {
|
||||
const {authLoaded, authRequired, loggedIn, user} = useAuth();
|
||||
const {isProxy, serverName} = useMetadata();
|
||||
|
||||
console.log(authLoaded, authRequired, loggedIn, user)
|
||||
|
||||
if (!authLoaded || !serverName) {
|
||||
return <RedirectPlaceholder/>
|
||||
}
|
||||
|
||||
if (authRequired && !loggedIn) {
|
||||
return (<Navigate to={"login"} replace={true}/>)
|
||||
return (<Navigate to="login" replace={true}/>)
|
||||
} else if (authRequired && loggedIn) {
|
||||
if (isProxy && user.permissions.includes('page.network')) {
|
||||
return (<Navigate to={"network/overview"} replace={true}/>)
|
||||
} else if (user.permissions.includes('page.server')) {
|
||||
return (<Navigate to={"server/overview"} replace={true}/>)
|
||||
return (<Navigate to={"server/" + encodeURIComponent(serverName) + "/overview"} replace={true}/>)
|
||||
} else if (user.permissions.includes('page.player.other')) {
|
||||
return (<Navigate to={"players"} replace={true}/>)
|
||||
} else if (user.permissions.includes('page.player.self')) {
|
||||
|
@ -23,15 +23,6 @@ export const AuthenticationContextProvider = ({children}) => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const login = useCallback(async (username, password) => {
|
||||
// TODO implement later when login page is done with React
|
||||
await updateLoginDetails();
|
||||
}, [updateLoginDetails]);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
// TODO implement later when login page is done with React
|
||||
}, []);
|
||||
|
||||
const hasPermission = useCallback(permission => {
|
||||
return !authRequired || (loggedIn && user && user.permissions.filter(perm => perm === permission).length);
|
||||
}, [authRequired, loggedIn, user]);
|
||||
@ -49,11 +40,10 @@ export const AuthenticationContextProvider = ({children}) => {
|
||||
authRequired,
|
||||
loggedIn,
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
loginError,
|
||||
hasPermission,
|
||||
hasPermissionOtherThan
|
||||
hasPermissionOtherThan,
|
||||
updateLoginDetails
|
||||
}
|
||||
return (<AuthenticationContext.Provider value={sharedState}>
|
||||
{children}
|
||||
|
@ -1,6 +1,11 @@
|
||||
import {doGetRequest} from "./backendConfiguration";
|
||||
import {doGetRequest, doSomePostRequest, standard200option} from "./backendConfiguration";
|
||||
|
||||
export const fetchWhoAmI = async () => {
|
||||
const url = '/v1/whoami';
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchLogin = async (username, password) => {
|
||||
const url = '/auth/login';
|
||||
return doSomePostRequest(url, [standard200option], `user=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`);
|
||||
}
|
@ -11,9 +11,17 @@ const isCurrentAddress = (address) => {
|
||||
export const baseAddress = "PLAN_BASE_ADDRESS" === toBeReplaced || !isCurrentAddress(toBeReplaced) ? "" : toBeReplaced;
|
||||
|
||||
export const doSomeGetRequest = async (url, statusOptions) => {
|
||||
return doSomeRequest(url, statusOptions, async () => axios.get(url));
|
||||
}
|
||||
|
||||
export const doSomePostRequest = async (url, statusOptions, body) => {
|
||||
return doSomeRequest(url, statusOptions, async () => axios.post(url, body));
|
||||
}
|
||||
|
||||
export const doSomeRequest = async (url, statusOptions, axiosFunction) => {
|
||||
let response = undefined;
|
||||
try {
|
||||
response = await axios.get(baseAddress + url);
|
||||
response = await axiosFunction.call();
|
||||
|
||||
for (const statusOption of statusOptions) {
|
||||
if (response.status === statusOption.status) {
|
||||
|
75
Plan/react/dashboard/src/util/loginSineRenderer.js
Normal file
75
Plan/react/dashboard/src/util/loginSineRenderer.js
Normal file
@ -0,0 +1,75 @@
|
||||
// https://gist.github.com/gkhays/e264009c0832c73d5345847e673a64ab
|
||||
export default function drawSine(canvasId) {
|
||||
let step;
|
||||
|
||||
function drawPoint(ctx, x, y) {
|
||||
const radius = 2;
|
||||
ctx.beginPath();
|
||||
|
||||
// Hold x constant at 4 so the point only moves up and down.
|
||||
ctx.arc(x - 5, y, radius, 0, 2 * Math.PI, false);
|
||||
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fill();
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function plotSine(ctx, xOffset) {
|
||||
const width = ctx.canvas.width;
|
||||
const height = ctx.canvas.height;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = "#fff";
|
||||
|
||||
// Drawing point
|
||||
|
||||
let x = -2;
|
||||
let y = 0;
|
||||
const amplitude = 50;
|
||||
const frequency = 50;
|
||||
|
||||
ctx.moveTo(x, 50);
|
||||
while (x <= width) {
|
||||
y = height / 2 + amplitude * Math.sin((x + xOffset) / frequency) * Math.cos((x + xOffset) / (frequency * 0.54515978463));
|
||||
ctx.lineTo(x, y);
|
||||
x += 5;
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.save();
|
||||
drawPoint(ctx, x, y);
|
||||
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (canvas == null) return;
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
context.clearRect(0, 0, 1000, 150);
|
||||
context.save();
|
||||
|
||||
plotSine(context, step);
|
||||
context.restore();
|
||||
|
||||
step += 0.5;
|
||||
window.requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
function fix_dpi() {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (canvas == null) return;
|
||||
let dpi = window.devicePixelRatio;
|
||||
canvas.getContext('2d');
|
||||
const style_width = getComputedStyle(canvas).getPropertyValue("width").slice(0, -2);
|
||||
// Scale the canvas
|
||||
canvas.setAttribute('width', `${style_width * dpi}`);
|
||||
}
|
||||
|
||||
fix_dpi();
|
||||
step = -1;
|
||||
window.requestAnimationFrame(draw);
|
||||
}
|
208
Plan/react/dashboard/src/views/layout/LoginPage.js
Normal file
208
Plan/react/dashboard/src/views/layout/LoginPage.js
Normal file
@ -0,0 +1,208 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
|
||||
import logo from '../../Flaticon_circle.png'
|
||||
import {Alert, Card, Col, Row} from "react-bootstrap-v5";
|
||||
import {Link, useNavigate} from "react-router-dom";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faPalette} from "@fortawesome/free-solid-svg-icons";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import ColorSelectorModal from "../../components/modal/ColorSelectorModal";
|
||||
import drawSine from "../../util/loginSineRenderer";
|
||||
import {fetchLogin} from "../../service/authenticationService";
|
||||
import ForgotPasswordModal from "../../components/modal/ForgotPasswordModal";
|
||||
import {useAuth} from "../../hooks/authenticationHook";
|
||||
|
||||
const Logo = () => {
|
||||
return (
|
||||
<Col md={12} className='mt-5 text-center'>
|
||||
<img alt="logo" className="w-15" src={logo}/>
|
||||
</Col>
|
||||
)
|
||||
};
|
||||
|
||||
const LoginCard = ({children}) => {
|
||||
return (
|
||||
<Row className="justify-content-center container-fluid">
|
||||
<Col xl={6} lg={7} md={9}>
|
||||
<Card className='o-hidden border-0 shadow-lg my-5'>
|
||||
<Card.Body className='p-0'>
|
||||
<Row>
|
||||
<Col lg={12}>
|
||||
<div className='p-5'>
|
||||
{children}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
const LoginForm = ({login}) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
const onLogin = async event => {
|
||||
event.preventDefault();
|
||||
await login(username, password);
|
||||
setPassword('');
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="user">
|
||||
<div className="mb-3">
|
||||
<input autoComplete="username" className="form-control form-control-user"
|
||||
id="inputUser"
|
||||
placeholder={t('html.login.username')} type="text"
|
||||
value={username} onChange={event => setUsername(event.target.value)}/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<input autoComplete="current-password" className="form-control form-control-user"
|
||||
id="inputPassword" placeholder={t('html.login.password')} type="password"
|
||||
value={password} onChange={event => setPassword(event.target.value)}/>
|
||||
</div>
|
||||
<button className="btn bg-plan btn-user w-100" id="login-button" onClick={onLogin}>
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
const ColorChooserButton = () => {
|
||||
const {t} = useTranslation();
|
||||
const {toggleColorChooser} = useTheme();
|
||||
|
||||
return (
|
||||
<div className='text-center'>
|
||||
<button className="btn col-plan" onClick={toggleColorChooser}
|
||||
title={t('html.label.themeSelect')}>
|
||||
<Fa icon={faPalette}/>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const ForgotPasswordButton = ({onClick}) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='text-center'>
|
||||
<button className='col-plan small' onClick={onClick}>{t('html.login.forgotPassword')}</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CreateAccountLink = () => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='text-center'>
|
||||
<Link to='/register' className='col-plan small'>{t('html.login.register')}</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Decoration = () => {
|
||||
useEffect(() => {
|
||||
drawSine('decoration');
|
||||
})
|
||||
|
||||
return (
|
||||
<Row className='justify-content-center'>
|
||||
<canvas className="col-xl-3 col-lg-3 col-md-5" id="decoration" style={{height: "100px"}}/>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
const LoginPage = () => {
|
||||
const {t} = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const {authLoaded, authRequired, loggedIn, updateLoginDetails} = useAuth();
|
||||
|
||||
const [forgotPasswordModalOpen, setForgotPasswordModalOpen] = useState(false);
|
||||
|
||||
const [failMessage, setFailMessage] = useState('');
|
||||
const [redirectTo, setRedirectTo] = useState(undefined);
|
||||
|
||||
const togglePasswordModal = useCallback(() => setForgotPasswordModalOpen(!forgotPasswordModalOpen),
|
||||
[setForgotPasswordModalOpen, forgotPasswordModalOpen])
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.add("bg-plan", "plan-bg-gradient");
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const cameFrom = urlParams.get('from');
|
||||
if (cameFrom) setRedirectTo(cameFrom);
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove("bg-plan", "plan-bg-gradient");
|
||||
}
|
||||
}, [setRedirectTo])
|
||||
|
||||
const login = async (username, password) => {
|
||||
if (!username || username.length < 1) {
|
||||
return setFailMessage(t('html.register.error.noUsername'));
|
||||
}
|
||||
if (username.length > 50) {
|
||||
return setFailMessage(t('html.register.error.usernameLength') + username.length);
|
||||
}
|
||||
if (!password || password.length < 1) {
|
||||
return setFailMessage(t('html.register.error.noPassword'));
|
||||
}
|
||||
|
||||
const {data, error} = await fetchLogin(username, password);
|
||||
|
||||
if (error) {
|
||||
if (error.message === 'Request failed with status code 403') {
|
||||
// Too many logins, reload browser to show forbidden page
|
||||
window.location.reload();
|
||||
} else {
|
||||
setFailMessage(t('html.login.failed') + (error.data && error.data.error ? error.data.error : error.message));
|
||||
}
|
||||
} else if (data && data.success) {
|
||||
if (redirectTo && !redirectTo.startsWith('http') && !redirectTo.startsWith('file') && !redirectTo.startsWith('javascript')) {
|
||||
navigate(redirectTo.substring(redirectTo.indexOf('/')))
|
||||
} else {
|
||||
await updateLoginDetails();
|
||||
navigate('../');
|
||||
}
|
||||
} else {
|
||||
setFailMessage(t('html.login.failed') + data ? data.error : t('generic.noData'));
|
||||
}
|
||||
}
|
||||
|
||||
if (!authLoaded) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
if (!authRequired || loggedIn) {
|
||||
navigate('../');
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="container">
|
||||
<Logo/>
|
||||
<LoginCard>
|
||||
{failMessage && <Alert className='alert-danger'>{failMessage}</Alert>}
|
||||
<LoginForm login={login}/>
|
||||
<hr className="bg-secondary"/>
|
||||
<ForgotPasswordButton onClick={togglePasswordModal}/>
|
||||
<CreateAccountLink/>
|
||||
<ColorChooserButton/>
|
||||
</LoginCard>
|
||||
<Decoration/>
|
||||
</main>
|
||||
<aside>
|
||||
<ColorSelectorModal/>
|
||||
<ForgotPasswordModal show={forgotPasswordModalOpen} toggle={togglePasswordModal}/>
|
||||
</aside>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
export default LoginPage
|
@ -14,8 +14,6 @@ const ServerPvpPve = () => {
|
||||
const {data, loadingError} = useDataRequest(fetchPvpPve, [identifier]);
|
||||
const {data: killsData, loadingError: killsLoadingError} = useDataRequest(fetchKills, [identifier]);
|
||||
|
||||
console.log(killsData)
|
||||
|
||||
if (!data || !killsData) return <></>;
|
||||
if (loadingError) return <ErrorView error={loadingError}/>
|
||||
if (killsLoadingError) return <ErrorView error={killsLoadingError}/>
|
||||
|
Loading…
Reference in New Issue
Block a user