From 6efccd631d2f526393ba52a80bc79a39cd03aad9 Mon Sep 17 00:00:00 2001 From: Dennis Brentjes Date: Sun, 5 Jun 2022 16:15:01 +0200 Subject: [PATCH 1/2] Adds register page. --- backend/src/api.rs | 83 +++++++++++++++----- backend/src/schema/admin.rs | 15 +++- backend/src/schema/users.rs | 7 +- frontend/src/App.js | 57 ++++++++++---- frontend/src/api/Api.js | 25 +++--- frontend/src/components/AddGameNight.jsx | 10 ++- frontend/src/components/AdminPanel.jsx | 26 +++++-- frontend/src/components/Gamenight.jsx | 18 ++++- frontend/src/components/Gamenights.jsx | 7 +- frontend/src/components/MenuBar.jsx | 47 ++++++++---- frontend/src/components/Register.jsx | 98 ++++++++++++++++++++++++ 11 files changed, 316 insertions(+), 77 deletions(-) create mode 100644 frontend/src/components/Register.jsx diff --git a/backend/src/api.rs b/backend/src/api.rs index 58fa305..7f563eb 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -144,7 +144,7 @@ impl<'r> FromRequest<'r> for User { let id = token.claims.uid; let conn = req.guard::().await.unwrap(); - return match get_user(conn, id).await { + return match get_user(&conn, id).await { Ok(o) => Outcome::Success(o), Err(_) => Outcome::Forward(()), }; @@ -354,8 +354,32 @@ pub async fn gamenights_delete_json_unauthorized() -> ApiResponseVariant { ApiResponseVariant::Status(Status::Unauthorized) } -#[post("/register", format = "application/json", data = "")] -pub async fn register_post_json(conn: DbConn, register_json: Json) -> ApiResponseVariant { +#[post( + "/register/", + format = "application/json", + data = "" +)] +pub async fn register_post_json( + conn: DbConn, + config: &State, + register_json: Json, + registration_token: String, +) -> ApiResponseVariant { + let token = match schema::admin::get_registration_token(&conn, registration_token).await { + Ok(res) => res, + Err(error) => { + return ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) + } + }; + + if let Some(expiry) = token.expires { + if expiry < Utc::now() { + return ApiResponseVariant::Value(json!(ApiResponse::error( + "Registration token has expired".to_string() + ))); + } + } + let register = register_json.into_inner(); let register_clone = register.clone(); match conn @@ -366,11 +390,25 @@ pub async fn register_post_json(conn: DbConn, register_json: Json) -> Err(error) => { return ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) } + }; + + let user = match insert_user(&conn, register).await { + Ok(user) => user, + Err(err) => return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), + }; + + if token.single_use { + match schema::admin::delete_registration_token(&conn, token.id).await { + Ok(_) => (), + Err(err) => { + return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))) + } + } } - match insert_user(conn, register).await { - Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)), - Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), + match create_jwt_token(&user, config) { + Ok(token) => ApiResponseVariant::Value(json!(ApiResponse::login_response(user, token))), + Err(error) => ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))), } } @@ -381,6 +419,24 @@ struct Claims { role: Role, } +fn create_jwt_token( + user: &User, + config: &State, +) -> Result { + let my_claims = Claims { + exp: Utc::now().timestamp() + chrono::Duration::days(7).num_seconds(), + uid: user.id, + role: user.role, + }; + + let secret = &config.inner().jwt_secret; + encode( + &Header::default(), + &my_claims, + &EncodingKey::from_secret(secret.as_bytes()), + ) +} + #[post("/login", format = "application/json", data = "")] pub async fn login_post_json( conn: DbConn, @@ -397,18 +453,7 @@ pub async fn login_post_json( } let user = login_result.user.unwrap(); - let my_claims = Claims { - exp: Utc::now().timestamp() + chrono::Duration::days(7).num_seconds(), - uid: user.id, - role: user.role, - }; - - let secret = &config.inner().jwt_secret; - match encode( - &Header::default(), - &my_claims, - &EncodingKey::from_secret(secret.as_bytes()), - ) { + match create_jwt_token(&user, config) { Ok(token) => { ApiResponseVariant::Value(json!(ApiResponse::login_response(user, token))) } @@ -560,7 +605,7 @@ pub async fn delete_registration_tokens( if user.role != Role::Admin { return ApiResponseVariant::Status(Status::Unauthorized); } - + let uuid = Uuid::parse_str(&gamenight_id).unwrap(); match schema::admin::delete_registration_token(&conn, uuid).await { Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)), diff --git a/backend/src/schema/admin.rs b/backend/src/schema/admin.rs index 218a783..49b3278 100644 --- a/backend/src/schema/admin.rs +++ b/backend/src/schema/admin.rs @@ -1,7 +1,7 @@ use crate::schema::{DatabaseError, DbConn}; use chrono::DateTime; use chrono::Utc; -use diesel::{QueryDsl, RunQueryDsl, ExpressionMethods}; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -52,3 +52,16 @@ pub async fn delete_registration_token(conn: &DbConn, id: Uuid) -> Result Result { + Ok(conn + .run(|c| { + registration_tokens::table + .filter(registration_tokens::token.eq(token)) + .first(c) + }) + .await?) +} diff --git a/backend/src/schema/users.rs b/backend/src/schema/users.rs index a29ca50..be30d79 100644 --- a/backend/src/schema/users.rs +++ b/backend/src/schema/users.rs @@ -86,7 +86,7 @@ pub struct Register { pub password_repeat: String, } -pub async fn insert_user(conn: DbConn, new_user: Register) -> Result { +pub async fn insert_user(conn: &DbConn, new_user: Register) -> Result { let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); @@ -114,7 +114,8 @@ pub async fn insert_user(conn: DbConn, new_user: Register) -> Result(c) }) }) .await?) @@ -155,7 +156,7 @@ pub async fn login(conn: DbConn, login: Login) -> Result Result { +pub async fn get_user(conn: &DbConn, id: Uuid) -> Result { Ok(conn .run(move |c| users::table.filter(users::id.eq(id)).first(c)) .await?) diff --git a/frontend/src/App.js b/frontend/src/App.js index 00f2da9..fca69ae 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -5,6 +5,7 @@ import Login from './components/Login'; import Gamenights from './components/Gamenights'; import Gamenight from './components/Gamenight'; import AdminPanel from './components/AdminPanel'; +import Register from './components/Register'; import { get_gamenights, get_games, unpack_api_result, login } from './api/Api'; @@ -21,20 +22,21 @@ function App() { const handleLogin = (input) => { unpack_api_result(login(input), setFlashData) .then(result => { - setUser(result.user); - localStorage.setItem(localStorageUserKey, JSON.stringify(result.user)); - }) - .then(() => setAppState('LoggedIn')) + if(result !== undefined) { + setUser(result.user); + localStorage.setItem(localStorageUserKey, JSON.stringify(result.user)); + setAppState('LoggedIn') + } + }); }; useEffect(() => { if(activeGamenightId !== null) { setAppState('GamenightDetails'); } else { - setAppState('LoggedIn') + setAppState(user === null ? 'LoggedOut' : 'LoggedIn') } - - }, [activeGamenightId]) + }, [activeGamenightId, user]) const onLogout = () => { setUser(null); @@ -50,8 +52,16 @@ function App() { setAppState('UserPage') } + const onRegister = () => { + setAppState('RegisterPage') + } + const onReset = () => { - setAppState('LoggedIn') + setAppState(user === null ? 'LoggedOut' : 'LoggedIn') + } + + const onRegistered = (user) => { + setUser(user); } const setFlash = (data) => { @@ -59,20 +69,28 @@ function App() { }; const refetchGamenights = () => { - setUser({...user}); + unpack_api_result(get_gamenights(user.jwt), setFlashData) + .then(result => { + if (result !== undefined) { + setGamenights(result.gamenights); + } + }); }; useEffect(() => { if (appState === 'LoggedIn') { - unpack_api_result(get_gamenights(user.jwt), setFlashData) - .then(result => setGamenights(result.gamenights)); + refetchGamenights() } }, [appState]) useEffect(() => { if (appState === 'LoggedIn') { unpack_api_result(get_games(user.jwt), setFlashData) - .then(result => setGames(result.games)); + .then(result => { + if (result !== undefined) { + setGames(result.games) + } + }); } }, [appState]) @@ -82,12 +100,22 @@ function App() { let mainview; if(appState === 'LoggedOut') { - return ( + mainview = (
); - } else if(appState === 'GamenightDetails') { + } else if(appState === 'RegisterPage') { + mainview = ( + + ); + } else if(appState === 'UserPage') { + mainview = ( + UserPage + ) + }else if(appState === 'GamenightDetails') { mainview = ( diff --git a/frontend/src/api/Api.js b/frontend/src/api/Api.js index 35262d3..b9ddec0 100644 --- a/frontend/src/api/Api.js +++ b/frontend/src/api/Api.js @@ -2,21 +2,18 @@ import fetchResource from './FetchResource' export function unpack_api_result(promise, onError) { - promise.then(result => { + return promise.then(result => { if(result.result !== 'Ok') { - onError({ - type: 'Error', - message: result.message - }); + throw new Error(result.message); } + return result; }) .catch(error => { - onError({ + onError({ type: 'Error', - message: `${error.status} ${error.message}` + message: `${error.status === null ?? error.status} ${error.message}` }); - }); - return promise; + }); } export function get_gamenights(token) { @@ -116,4 +113,14 @@ export function delete_registration_token(token, registration_token_id) { 'Authorization': `Bearer ${token}`, } }); +} + +export function register(registration_token, input) { + return fetchResource(`api/register/${registration_token}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(input) + }); } \ No newline at end of file diff --git a/frontend/src/components/AddGameNight.jsx b/frontend/src/components/AddGameNight.jsx index 83b34a5..3a6cf52 100644 --- a/frontend/src/components/AddGameNight.jsx +++ b/frontend/src/components/AddGameNight.jsx @@ -47,11 +47,13 @@ function AddGameNight(props) { unpack_api_result(post_gamenight(input, props.user.jwt), props.setFlash) .then(result => { - setExpanded(false); - setGameName(""); - setDate(null); + if(result !== undefined) { + setExpanded(false); + setGameName(""); + setDate(null); + } }) - .then(() => props.refetchGamenights()) + .then(() => props.refetchGamenights()); } }; diff --git a/frontend/src/components/AdminPanel.jsx b/frontend/src/components/AdminPanel.jsx index 2a1b50b..69b9a8e 100644 --- a/frontend/src/components/AdminPanel.jsx +++ b/frontend/src/components/AdminPanel.jsx @@ -38,14 +38,22 @@ function AdminPanel(props) { const refetchTokens = () => { if(props.user !== null) { unpack_api_result(get_registration_tokens(props.user.jwt), props.setFlash) - .then(result => setRegistrationTokens(result.registration_tokens)); - } + .then(result => { + if(result !== undefined) { + setRegistrationTokens(result.registration_tokens); + } + }); + } } const deleteToken = (id) => { if(props.user !== null) { unpack_api_result(delete_registration_token(props.user.jwt, id), props.setFlash) - .then(() => refetchTokens()) + .then(result => { + if(result !== undefined) { + refetchTokens(); + } + }); } } @@ -57,7 +65,11 @@ function AdminPanel(props) { if(props.user !== null) { unpack_api_result(add_registration_token(props.user.jwt, input), props.setFlash) - .then(() => refetchTokens()) + .then(result => { + if(result !== undefined) { + refetchTokens(); + } + }); } } @@ -92,7 +104,7 @@ function AdminPanel(props) {
{ e.preventDefault(); }}> - - + + ); diff --git a/frontend/src/components/Gamenight.jsx b/frontend/src/components/Gamenight.jsx index 624196e..9b037a3 100644 --- a/frontend/src/components/Gamenight.jsx +++ b/frontend/src/components/Gamenight.jsx @@ -19,7 +19,11 @@ function Gamenight(props) { const fetchGamenight = () => { if (props.user !== null) { unpack_api_result(get_gamenight(props.gamenightId, props.user.jwt), props.setFlash) - .then(result => setGamenight(result.gamenight)); + .then(result => { + if(result !== undefined) { + setGamenight(result.gamenight); + } + }); } } @@ -51,7 +55,11 @@ function Gamenight(props) { }; unpack_api_result(patch_gamenight(gamenight.id, input, props.user.jwt), props.setFlash) - .then(() => fetchGamenight()); + .then(result => { + if(result !== undefined) { + fetchGamenight(); + } + }); }; const Leave = () => { @@ -60,7 +68,11 @@ function Gamenight(props) { }; unpack_api_result(patch_gamenight(gamenight.id, input, props.user.jwt), props.setFlash) - .then(() => fetchGamenight()); + .then(result => { + if(result !== undefined) { + fetchGamenight(); + } + }); }; let join_or_leave_button; diff --git a/frontend/src/components/Gamenights.jsx b/frontend/src/components/Gamenights.jsx index a86903c..a8c22c9 100644 --- a/frontend/src/components/Gamenights.jsx +++ b/frontend/src/components/Gamenights.jsx @@ -18,7 +18,12 @@ function Gamenights(props) { if (props.user !== null) { const input = { game_id: game_id }; unpack_api_result(delete_gamenight(input, props.user.jwt), props.setFlash) - .then(() => props.refetchGamenights()); + .then(result => { + if(result !== undefined) { + console.log("hello?"); + props.refetchGamenights(); + } + }); } } diff --git a/frontend/src/components/MenuBar.jsx b/frontend/src/components/MenuBar.jsx index 93258b5..04f5d87 100644 --- a/frontend/src/components/MenuBar.jsx +++ b/frontend/src/components/MenuBar.jsx @@ -8,19 +8,13 @@ import MenuIcon from '@mui/icons-material/Menu'; function MenuBar(props) { - let adminPanelButton = null; - if (props.user?.role === 'Admin') { - adminPanelButton = ( - - ); - } + let userButton = null; - if (props.user != null) { + let logoutButton = null; + let adminPanelButton = null; + let registerButton = null; + if (props.user !== null) { userButton = ( + ); + if (props.user.role === 'Admin') { + adminPanelButton = ( + + ); + } + } else { + registerButton = ( + + ) } return ( @@ -50,12 +68,9 @@ function MenuBar(props) { Gamenight! {userButton !== null && userButton} + {registerButton !== null && registerButton} {adminPanelButton !== null && adminPanelButton} - + {logoutButton !== null && logoutButton} ); diff --git a/frontend/src/components/Register.jsx b/frontend/src/components/Register.jsx new file mode 100644 index 0000000..50df9c9 --- /dev/null +++ b/frontend/src/components/Register.jsx @@ -0,0 +1,98 @@ +import {useState} from 'react'; +import FormControl from '@mui/material/FormControl'; +import Input from '@mui/material/Input'; +import FormHelperText from '@mui/material/FormHelperText'; +import Button from '@mui/material/Button'; + +import {register, unpack_api_result} from '../api/Api'; + +function Register(props) { + const [registrationToken, setRegistrationToken] = useState(""); + const [username, setUsername] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [passwordRepeat, setPasswordRepeat] = useState(""); + + const onRegister = () => { + let input = { + username, + email, + password, + password_repeat: passwordRepeat + } + + unpack_api_result(register(registrationToken, input), props.setFlash) + .then(result => { + if(result !== undefined) { + props.onRegistered(result.user); + } + }); + } + + return ( + + {setRegistrationToken(e.target.value)}} /> + + Registration token given by a gamenight admin + + + {setUsername(e.target.value)}} /> + + Username to display everywhere + + + {setEmail(e.target.value)}} /> + + E-mail used for notifications and password resets + + + {setPassword(e.target.value)}} /> + + Password atleast 10 characters long + + + {setPasswordRepeat(e.target.value)}} /> + + Confirm your password + + + + + + + ); +} + +export default Register; \ No newline at end of file -- 2.46.0 From 79b7312896aa5153ac67e26946aa3bacaee9045e Mon Sep 17 00:00:00 2001 From: Dennis Brentjes Date: Sun, 5 Jun 2022 16:48:26 +0200 Subject: [PATCH 2/2] Fixes some React warnings. --- frontend/src/App.js | 10 +++++----- frontend/src/components/AdminPanel.jsx | 8 ++++---- frontend/src/components/Gamenight.jsx | 10 +++++----- frontend/src/components/Gamenights.jsx | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/frontend/src/App.js b/frontend/src/App.js index fca69ae..6384dff 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,5 +1,5 @@ import './App.css'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import MenuBar from './components/MenuBar'; import Login from './components/Login'; import Gamenights from './components/Gamenights'; @@ -68,20 +68,20 @@ function App() { setFlashData(data); }; - const refetchGamenights = () => { + const refetchGamenights = useCallback(() => { unpack_api_result(get_gamenights(user.jwt), setFlashData) .then(result => { if (result !== undefined) { setGamenights(result.gamenights); } }); - }; + }, [user]); useEffect(() => { if (appState === 'LoggedIn') { refetchGamenights() } - }, [appState]) + }, [appState, refetchGamenights]) useEffect(() => { if (appState === 'LoggedIn') { @@ -92,7 +92,7 @@ function App() { } }); } - }, [appState]) + }, [appState, user]) useEffect(() => { setUser(JSON.parse(localStorage.getItem(localStorageUserKey))); diff --git a/frontend/src/components/AdminPanel.jsx b/frontend/src/components/AdminPanel.jsx index 69b9a8e..750a37d 100644 --- a/frontend/src/components/AdminPanel.jsx +++ b/frontend/src/components/AdminPanel.jsx @@ -1,4 +1,4 @@ -import {useState, useEffect} from 'react'; +import {useState, useEffect, useCallback} from 'react'; import Checkbox from '@mui/material/Checkbox'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; @@ -35,7 +35,7 @@ function AdminPanel(props) { setPage(0); }; - const refetchTokens = () => { + const refetchTokens = useCallback(() => { if(props.user !== null) { unpack_api_result(get_registration_tokens(props.user.jwt), props.setFlash) .then(result => { @@ -44,7 +44,7 @@ function AdminPanel(props) { } }); } - } + }, [props.setFlash, props.user]); const deleteToken = (id) => { if(props.user !== null) { @@ -75,7 +75,7 @@ function AdminPanel(props) { useEffect(() => { refetchTokens() - }, []) + }, [refetchTokens]) let columns = [ { diff --git a/frontend/src/components/Gamenight.jsx b/frontend/src/components/Gamenight.jsx index 9b037a3..7b54e65 100644 --- a/frontend/src/components/Gamenight.jsx +++ b/frontend/src/components/Gamenight.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; @@ -13,10 +13,10 @@ import {unpack_api_result, get_gamenight, patch_gamenight} from '../api/Api'; function Gamenight(props) { - const [dense, setDense] = useState(true); + const dense = true; const [gamenight, setGamenight] = useState(null); - const fetchGamenight = () => { + const fetchGamenight = useCallback(() => { if (props.user !== null) { unpack_api_result(get_gamenight(props.gamenightId, props.user.jwt), props.setFlash) .then(result => { @@ -25,9 +25,9 @@ function Gamenight(props) { } }); } - } + }, [props.gamenightId, props.user, props.setFlash]); - useEffect(fetchGamenight, []); + useEffect(fetchGamenight, [fetchGamenight]); let games = gamenight?.game_list.map(g => ( diff --git a/frontend/src/components/Gamenights.jsx b/frontend/src/components/Gamenights.jsx index a8c22c9..206b107 100644 --- a/frontend/src/components/Gamenights.jsx +++ b/frontend/src/components/Gamenights.jsx @@ -12,7 +12,7 @@ import AddGameNight from './AddGameNight'; import {delete_gamenight, unpack_api_result} from '../api/Api'; function Gamenights(props) { - const [dense, setDense] = React.useState(false); + const dense = true; const DeleteGamenight = (game_id) => { if (props.user !== null) { -- 2.46.0