diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/PageFactory.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/PageFactory.java index b396135fd..36a5e9b59 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/PageFactory.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/PageFactory.java @@ -248,6 +248,11 @@ public class PageFactory { } public Page registerPage() throws IOException { + if (config.get().isTrue(PluginSettings.FRONTEND_BETA)) { + String reactHtml = getResource("index.html"); + return () -> reactHtml; + } + return new LoginPage(getResource("register.html"), serverInfo.get(), locale.get(), theme.get(), versionChecker.get()); } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java index a04dec52b..693e4d445 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java @@ -287,6 +287,7 @@ public enum HtmlLang implements Lang { REGISTER_COMPLETE_INSTRUCTIONS_2("html.register.completion2", "Code expires in 15 minutes"), REGISTER_COMPLETE_INSTRUCTIONS_3("html.register.completion3", "Use the following command in game to finish registration:"), REGISTER_COMPLETE_INSTRUCTIONS_4("html.register.completion4", "Or using console:"), + REGISTER_SUCCESS("html.register.success", "Registered a new user successfully! You can now login."), REGISTER_FAILED("html.register.error.failed", "Registration failed: "), REGISTER_CHECK_FAILED("html.register.error.checkFailed", "Checking registration status failed: "), diff --git a/Plan/react/dashboard/src/App.js b/Plan/react/dashboard/src/App.js index 2e25d97b0..fbe40cad1 100644 --- a/Plan/react/dashboard/src/App.js +++ b/Plan/react/dashboard/src/App.js @@ -28,7 +28,6 @@ const ServerPvpPve = React.lazy(() => import("./views/server/ServerPvpPve")); const PlayerbaseOverview = React.lazy(() => import("./views/server/PlayerbaseOverview")); const ServerPlayers = React.lazy(() => import("./views/server/ServerPlayers")); const ServerGeolocations = React.lazy(() => import("./views/server/ServerGeolocations")); -const LoginPage = React.lazy(() => import("./views/layout/LoginPage")); const ServerPerformance = React.lazy(() => import("./views/server/ServerPerformance")); const ServerPluginData = React.lazy(() => import("./views/server/ServerPluginData")); const ServerWidePluginData = React.lazy(() => import("./views/server/ServerWidePluginData")); @@ -50,6 +49,8 @@ const QueryPage = React.lazy(() => import("./views/layout/QueryPage")); const NewQueryView = React.lazy(() => import("./views/query/NewQueryView")); const QueryResultView = React.lazy(() => import("./views/query/QueryResultView")); +const LoginPage = React.lazy(() => import("./views/layout/LoginPage")); +const RegisterPage = React.lazy(() => import("./views/layout/RegisterPage")); const ErrorsPage = React.lazy(() => import("./views/layout/ErrorsPage")); const SwaggerView = React.lazy(() => import("./views/SwaggerView")); @@ -90,6 +91,7 @@ function App() { <Route path="" element={<MainPageRedirect/>}/> <Route path="/" element={<MainPageRedirect/>}/> <Route path="/login" element={<Lazy><LoginPage/></Lazy>}/> + <Route path="/register" element={<Lazy><RegisterPage/></Lazy>}/> <Route path="/player/:identifier" element={<Lazy><PlayerPage/></Lazy>}> <Route path="" element={<Lazy><OverviewRedirect/></Lazy>}/> <Route path="overview" element={<Lazy><PlayerOverview/></Lazy>}/> diff --git a/Plan/react/dashboard/src/components/modal/FinalizeRegistrationModal.js b/Plan/react/dashboard/src/components/modal/FinalizeRegistrationModal.js new file mode 100644 index 000000000..1d1ab22ec --- /dev/null +++ b/Plan/react/dashboard/src/components/modal/FinalizeRegistrationModal.js @@ -0,0 +1,35 @@ +import React from 'react'; +import {useTranslation} from "react-i18next"; +import {useMetadata} from "../../hooks/metadataHook"; +import {Modal} from "react-bootstrap-v5"; +import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; +import {faHandPointRight} from "@fortawesome/free-regular-svg-icons"; + +const FinalizeRegistrationModal = ({show, toggle, registerCode}) => { + const {t} = useTranslation(); + const {mainCommand} = useMetadata(); + + return ( + <Modal id={"finalizeModal"} + aria-labelledby={"finalizeModalLable"} + show={show} + onHide={toggle} + > + <Modal.Header className="bg-white"> + <Modal.Title id={"finalizeModalLabel"}> + <Fa icon={faHandPointRight}/> {t('html.register.completion')} + </Modal.Title> + <button aria-label="Close" className="btn-close" onClick={toggle}/> + </Modal.Header> + <Modal.Body className={"bg-white"}> + <p>{t('html.register.completion1')} {t('html.register.completion2')}</p> + <p>{t('html.register.completion3')}</p> + <p><code>/{mainCommand} register --code {registerCode}</code></p> + <p>{t('html.register.completion4')}</p> + <p><code>{mainCommand} register superuser --code {registerCode}</code></p> + </Modal.Body> + </Modal> + ) +}; + +export default FinalizeRegistrationModal \ No newline at end of file diff --git a/Plan/react/dashboard/src/service/authenticationService.js b/Plan/react/dashboard/src/service/authenticationService.js index 29dc9daf0..cc275309b 100644 --- a/Plan/react/dashboard/src/service/authenticationService.js +++ b/Plan/react/dashboard/src/service/authenticationService.js @@ -8,4 +8,14 @@ export const fetchWhoAmI = async () => { export const fetchLogin = async (username, password) => { const url = '/auth/login'; return doSomePostRequest(url, [standard200option], `user=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`); +} + +export const postRegister = async (username, password) => { + const url = '/auth/register'; + return doSomePostRequest(url, [standard200option], `user=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`); +} + +export const fetchRegisterCheck = async (code) => { + const url = `/auth/register?code=${encodeURIComponent(code)}`; + return doGetRequest(url); } \ No newline at end of file diff --git a/Plan/react/dashboard/src/views/layout/LoginPage.js b/Plan/react/dashboard/src/views/layout/LoginPage.js index ff10e9899..ac1256d6e 100644 --- a/Plan/react/dashboard/src/views/layout/LoginPage.js +++ b/Plan/react/dashboard/src/views/layout/LoginPage.js @@ -125,6 +125,7 @@ const LoginPage = () => { const [forgotPasswordModalOpen, setForgotPasswordModalOpen] = useState(false); + const [successMessage, setSuccessMessage] = useState('') const [failMessage, setFailMessage] = useState(''); const [redirectTo, setRedirectTo] = useState(undefined); @@ -138,6 +139,9 @@ const LoginPage = () => { const cameFrom = urlParams.get('from'); if (cameFrom) setRedirectTo(cameFrom); + const registerSuccess = urlParams.get('registerSuccess'); + if (registerSuccess) setSuccessMessage(t('html.register.success')) + return () => { document.body.classList.remove("bg-plan", "plan-bg-gradient"); } @@ -189,8 +193,9 @@ const LoginPage = () => { <Logo/> <LoginCard> {failMessage && <Alert className='alert-danger'>{failMessage}</Alert>} + {successMessage && <Alert className='alert-success'>{successMessage}</Alert>} <LoginForm login={login}/> - <hr className="bg-secondary"/> + <hr className="col-secondary"/> <ForgotPasswordButton onClick={togglePasswordModal}/> <CreateAccountLink/> <ColorChooserButton/> diff --git a/Plan/react/dashboard/src/views/layout/RegisterPage.js b/Plan/react/dashboard/src/views/layout/RegisterPage.js new file mode 100644 index 000000000..7b214f415 --- /dev/null +++ b/Plan/react/dashboard/src/views/layout/RegisterPage.js @@ -0,0 +1,200 @@ +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 {useAuth} from "../../hooks/authenticationHook"; +import FinalizeRegistrationModal from "../../components/modal/FinalizeRegistrationModal"; +import {fetchRegisterCheck, postRegister} from "../../service/authenticationService"; +import {baseAddress} from "../../service/backendConfiguration"; + +const Logo = () => { + return ( + <Col md={12} className='mt-5 text-center'> + <img alt="logo" className="w-15" src={logo}/> + </Col> + ) +}; + +const RegisterCard = ({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 RegisterForm = ({register}) => { + const {t} = useTranslation(); + + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + + const onRegister = useCallback(event => { + event.preventDefault(); + register(username, password).then(() => setPassword('')); + }, [username, password, setPassword, register]); + + 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 className={"form-text"}>{t('html.register.usernameTip')}</div> + </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 className={"form-text"}>{t('html.register.passwordTip')}</div> + </div> + <button className="btn bg-plan btn-user w-100" id="register-button" onClick={onRegister}> + {t('html.register.register')} + </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 LoginLink = () => { + const {t} = useTranslation(); + + return ( + <div className='text-center'> + <Link to='/login' className='col-plan small'>{t('html.register.login')}</Link> + </div> + ) +} + +const RegisterPage = () => { + const {t} = useTranslation(); + const navigate = useNavigate(); + const {authLoaded, authRequired, loggedIn} = useAuth(); + + const [finalizeRegistrationModalOpen, setFinalizeRegistrationModalOpen] = useState(false); + + const [registerCode, setRegisterCode] = useState(undefined); + const [failMessage, setFailMessage] = useState(''); + + const toggleRegistrationModal = useCallback(() => setFinalizeRegistrationModalOpen(!finalizeRegistrationModalOpen), + [setFinalizeRegistrationModalOpen, finalizeRegistrationModalOpen]) + + useEffect(() => { + document.body.classList.add("bg-plan", "plan-bg-gradient"); + + return () => { + document.body.classList.remove("bg-plan", "plan-bg-gradient"); + } + }, []); + + const checkRegistration = async (code) => { + if (!code) { + setFinalizeRegistrationModalOpen(false); + return setFailMessage("Register code was not received."); + } + if (!finalizeRegistrationModalOpen) { + setFinalizeRegistrationModalOpen(true); + } + + const {data, error} = await fetchRegisterCheck(code); + if (error) { + setFailMessage(t('html.register.error.checkFailed') + error) + } else if (data && data.success) { + navigate(baseAddress + '/login?registerSuccess=true'); + } else { + setTimeout(() => checkRegistration(code), 5000); + } + } + + const register = 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 postRegister(username, password); + + if (error) { + setFailMessage(t('html.register.error.failed') + (error.data && error.data.error ? error.data.error : error.message)); + } else if (data && data.code) { + setRegisterCode(data.code); + setFinalizeRegistrationModalOpen(true); + setTimeout(() => checkRegistration(data.code), 10000); + } else { + setFailMessage(t('html.register.error.failed') + data ? data.error : t('generic.noData')); + } + } + + if (!authLoaded) { + return <></> + } + + if (!authRequired || loggedIn) { + navigate('../'); + } + + return ( + <> + <main className="container"> + <Logo/> + <RegisterCard> + <div className="text-center"> + <h1 className="h4 text-gray-900 mb-4">{t('html.register.createNewUser')}</h1> + </div> + {failMessage && <Alert className='alert-danger'>{failMessage}</Alert>} + <RegisterForm register={register}/> + <hr className="col-secondary"/> + <LoginLink/> + <ColorChooserButton/> + </RegisterCard> + </main> + <aside> + <ColorSelectorModal/> + <FinalizeRegistrationModal + show={finalizeRegistrationModalOpen} + toggle={toggleRegistrationModal} + registerCode={registerCode} + /> + </aside> + </> + ) +}; + +export default RegisterPage \ No newline at end of file