user_registration #10

Closed
Roflin wants to merge 2 commits from user_registration into main
11 changed files with 331 additions and 92 deletions

View File

@ -144,7 +144,7 @@ impl<'r> FromRequest<'r> for User {
let id = token.claims.uid; let id = token.claims.uid;
let conn = req.guard::<DbConn>().await.unwrap(); let conn = req.guard::<DbConn>().await.unwrap();
return match get_user(conn, id).await { return match get_user(&conn, id).await {
Ok(o) => Outcome::Success(o), Ok(o) => Outcome::Success(o),
Err(_) => Outcome::Forward(()), Err(_) => Outcome::Forward(()),
}; };
@ -354,8 +354,32 @@ pub async fn gamenights_delete_json_unauthorized() -> ApiResponseVariant {
ApiResponseVariant::Status(Status::Unauthorized) ApiResponseVariant::Status(Status::Unauthorized)
} }
#[post("/register", format = "application/json", data = "<register_json>")] #[post(
pub async fn register_post_json(conn: DbConn, register_json: Json<Register>) -> ApiResponseVariant { "/register/<registration_token>",
format = "application/json",
data = "<register_json>"
)]
pub async fn register_post_json(
conn: DbConn,
config: &State<AppConfig>,
register_json: Json<Register>,
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 = register_json.into_inner();
let register_clone = register.clone(); let register_clone = register.clone();
match conn match conn
@ -366,11 +390,25 @@ pub async fn register_post_json(conn: DbConn, register_json: Json<Register>) ->
Err(error) => { Err(error) => {
return ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) 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 { match create_jwt_token(&user, config) {
Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)), Ok(token) => ApiResponseVariant::Value(json!(ApiResponse::login_response(user, token))),
Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), Err(error) => ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))),
} }
} }
@ -381,6 +419,24 @@ struct Claims {
role: Role, role: Role,
} }
fn create_jwt_token(
user: &User,
config: &State<AppConfig>,
) -> Result<String, jsonwebtoken::errors::Error> {
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 = "<login_json>")] #[post("/login", format = "application/json", data = "<login_json>")]
pub async fn login_post_json( pub async fn login_post_json(
conn: DbConn, conn: DbConn,
@ -397,18 +453,7 @@ pub async fn login_post_json(
} }
let user = login_result.user.unwrap(); let user = login_result.user.unwrap();
let my_claims = Claims { match create_jwt_token(&user, config) {
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()),
) {
Ok(token) => { Ok(token) => {
ApiResponseVariant::Value(json!(ApiResponse::login_response(user, token))) ApiResponseVariant::Value(json!(ApiResponse::login_response(user, token)))
} }
@ -560,7 +605,7 @@ pub async fn delete_registration_tokens(
if user.role != Role::Admin { if user.role != Role::Admin {
return ApiResponseVariant::Status(Status::Unauthorized); return ApiResponseVariant::Status(Status::Unauthorized);
} }
let uuid = Uuid::parse_str(&gamenight_id).unwrap(); let uuid = Uuid::parse_str(&gamenight_id).unwrap();
match schema::admin::delete_registration_token(&conn, uuid).await { match schema::admin::delete_registration_token(&conn, uuid).await {
Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)), Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)),

View File

@ -1,7 +1,7 @@
use crate::schema::{DatabaseError, DbConn}; use crate::schema::{DatabaseError, DbConn};
use chrono::DateTime; use chrono::DateTime;
use chrono::Utc; use chrono::Utc;
use diesel::{QueryDsl, RunQueryDsl, ExpressionMethods}; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
@ -52,3 +52,16 @@ pub async fn delete_registration_token(conn: &DbConn, id: Uuid) -> Result<usize,
}) })
.await?) .await?)
} }
pub async fn get_registration_token(
conn: &DbConn,
token: String,
) -> Result<RegistrationToken, DatabaseError> {
Ok(conn
.run(|c| {
registration_tokens::table
.filter(registration_tokens::token.eq(token))
.first(c)
})
.await?)
}

View File

@ -86,7 +86,7 @@ pub struct Register {
pub password_repeat: String, pub password_repeat: String,
} }
pub async fn insert_user(conn: DbConn, new_user: Register) -> Result<usize, DatabaseError> { pub async fn insert_user(conn: &DbConn, new_user: Register) -> Result<User, DatabaseError> {
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default(); let argon2 = Argon2::default();
@ -114,7 +114,8 @@ pub async fn insert_user(conn: DbConn, new_user: Register) -> Result<usize, Data
user_id: id, user_id: id,
password: password_hash, password: password_hash,
}) })
.execute(c) .execute(c)?;
users::table.filter(users::id.eq(id)).first::<User>(c)
}) })
}) })
.await?) .await?)
@ -155,7 +156,7 @@ pub async fn login(conn: DbConn, login: Login) -> Result<LoginResult, DatabaseEr
.await .await
} }
pub async fn get_user(conn: DbConn, id: Uuid) -> Result<User, DatabaseError> { pub async fn get_user(conn: &DbConn, id: Uuid) -> Result<User, DatabaseError> {
Ok(conn Ok(conn
.run(move |c| users::table.filter(users::id.eq(id)).first(c)) .run(move |c| users::table.filter(users::id.eq(id)).first(c))
.await?) .await?)

View File

@ -1,10 +1,11 @@
import './App.css'; import './App.css';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import MenuBar from './components/MenuBar'; import MenuBar from './components/MenuBar';
import Login from './components/Login'; import Login from './components/Login';
import Gamenights from './components/Gamenights'; import Gamenights from './components/Gamenights';
import Gamenight from './components/Gamenight'; import Gamenight from './components/Gamenight';
import AdminPanel from './components/AdminPanel'; import AdminPanel from './components/AdminPanel';
import Register from './components/Register';
import { get_gamenights, get_games, unpack_api_result, login } from './api/Api'; import { get_gamenights, get_games, unpack_api_result, login } from './api/Api';
@ -21,20 +22,21 @@ function App() {
const handleLogin = (input) => { const handleLogin = (input) => {
unpack_api_result(login(input), setFlashData) unpack_api_result(login(input), setFlashData)
.then(result => { .then(result => {
setUser(result.user); if(result !== undefined) {
localStorage.setItem(localStorageUserKey, JSON.stringify(result.user)); setUser(result.user);
}) localStorage.setItem(localStorageUserKey, JSON.stringify(result.user));
.then(() => setAppState('LoggedIn')) setAppState('LoggedIn')
}
});
}; };
useEffect(() => { useEffect(() => {
if(activeGamenightId !== null) { if(activeGamenightId !== null) {
setAppState('GamenightDetails'); setAppState('GamenightDetails');
} else { } else {
setAppState('LoggedIn') setAppState(user === null ? 'LoggedOut' : 'LoggedIn')
} }
}, [activeGamenightId, user])
}, [activeGamenightId])
const onLogout = () => { const onLogout = () => {
setUser(null); setUser(null);
@ -50,31 +52,47 @@ function App() {
setAppState('UserPage') setAppState('UserPage')
} }
const onRegister = () => {
setAppState('RegisterPage')
}
const onReset = () => { const onReset = () => {
setAppState('LoggedIn') setAppState(user === null ? 'LoggedOut' : 'LoggedIn')
}
const onRegistered = (user) => {
setUser(user);
} }
const setFlash = (data) => { const setFlash = (data) => {
setFlashData(data); setFlashData(data);
}; };
const refetchGamenights = () => { const refetchGamenights = useCallback(() => {
setUser({...user}); unpack_api_result(get_gamenights(user.jwt), setFlashData)
}; .then(result => {
if (result !== undefined) {
setGamenights(result.gamenights);
}
});
}, [user]);
useEffect(() => { useEffect(() => {
if (appState === 'LoggedIn') { if (appState === 'LoggedIn') {
unpack_api_result(get_gamenights(user.jwt), setFlashData) refetchGamenights()
.then(result => setGamenights(result.gamenights));
} }
}, [appState]) }, [appState, refetchGamenights])
useEffect(() => { useEffect(() => {
if (appState === 'LoggedIn') { if (appState === 'LoggedIn') {
unpack_api_result(get_games(user.jwt), setFlashData) unpack_api_result(get_games(user.jwt), setFlashData)
.then(result => setGames(result.games)); .then(result => {
if (result !== undefined) {
setGames(result.games)
}
});
} }
}, [appState]) }, [appState, user])
useEffect(() => { useEffect(() => {
setUser(JSON.parse(localStorage.getItem(localStorageUserKey))); setUser(JSON.parse(localStorage.getItem(localStorageUserKey)));
@ -82,12 +100,22 @@ function App() {
let mainview; let mainview;
if(appState === 'LoggedOut') { if(appState === 'LoggedOut') {
return ( mainview = (
<div className="App"> <div className="App">
<Login onChange={handleLogin}/> <Login onChange={handleLogin}/>
</div> </div>
); );
} else if(appState === 'GamenightDetails') { } else if(appState === 'RegisterPage') {
mainview = (
<Register
onRegistered={onRegistered}
setFlash={setFlash}/>
);
} else if(appState === 'UserPage') {
mainview = (
<span>UserPage</span>
)
}else if(appState === 'GamenightDetails') {
mainview = ( mainview = (
<Gamenight <Gamenight
gamenightId={activeGamenightId} gamenightId={activeGamenightId}
@ -117,6 +145,7 @@ function App() {
<MenuBar <MenuBar
user={user} user={user}
onUser={onUser} onUser={onUser}
onRegister={onRegister}
onAdmin={onAdmin} onAdmin={onAdmin}
onLogout={onLogout} onLogout={onLogout}
onReset={onReset}/> onReset={onReset}/>

View File

@ -2,21 +2,18 @@
import fetchResource from './FetchResource' import fetchResource from './FetchResource'
export function unpack_api_result(promise, onError) { export function unpack_api_result(promise, onError) {
promise.then(result => { return promise.then(result => {
if(result.result !== 'Ok') { if(result.result !== 'Ok') {
onError({ throw new Error(result.message);
type: 'Error',
message: result.message
});
} }
return result;
}) })
.catch(error => { .catch(error => {
onError({ onError({
type: 'Error', type: 'Error',
message: `${error.status} ${error.message}` message: `${error.status === null ?? error.status} ${error.message}`
}); });
}); });
return promise;
} }
export function get_gamenights(token) { export function get_gamenights(token) {
@ -116,4 +113,14 @@ export function delete_registration_token(token, registration_token_id) {
'Authorization': `Bearer ${token}`, '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)
});
} }

View File

@ -47,11 +47,13 @@ function AddGameNight(props) {
unpack_api_result(post_gamenight(input, props.user.jwt), props.setFlash) unpack_api_result(post_gamenight(input, props.user.jwt), props.setFlash)
.then(result => { .then(result => {
setExpanded(false); if(result !== undefined) {
setGameName(""); setExpanded(false);
setDate(null); setGameName("");
setDate(null);
}
}) })
.then(() => props.refetchGamenights()) .then(() => props.refetchGamenights());
} }
}; };

View File

@ -1,4 +1,4 @@
import {useState, useEffect} from 'react'; import {useState, useEffect, useCallback} from 'react';
import Checkbox from '@mui/material/Checkbox'; import Checkbox from '@mui/material/Checkbox';
import Table from '@mui/material/Table'; import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody'; import TableBody from '@mui/material/TableBody';
@ -35,17 +35,25 @@ function AdminPanel(props) {
setPage(0); setPage(0);
}; };
const refetchTokens = () => { const refetchTokens = useCallback(() => {
if(props.user !== null) { if(props.user !== null) {
unpack_api_result(get_registration_tokens(props.user.jwt), props.setFlash) 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);
}
});
}
}, [props.setFlash, props.user]);
const deleteToken = (id) => { const deleteToken = (id) => {
if(props.user !== null) { if(props.user !== null) {
unpack_api_result(delete_registration_token(props.user.jwt, id), props.setFlash) unpack_api_result(delete_registration_token(props.user.jwt, id), props.setFlash)
.then(() => refetchTokens()) .then(result => {
if(result !== undefined) {
refetchTokens();
}
});
} }
} }
@ -57,13 +65,17 @@ function AdminPanel(props) {
if(props.user !== null) { if(props.user !== null) {
unpack_api_result(add_registration_token(props.user.jwt, input), props.setFlash) unpack_api_result(add_registration_token(props.user.jwt, input), props.setFlash)
.then(() => refetchTokens()) .then(result => {
if(result !== undefined) {
refetchTokens();
}
});
} }
} }
useEffect(() => { useEffect(() => {
refetchTokens() refetchTokens()
}, []) }, [refetchTokens])
let columns = [ let columns = [
{ {
@ -92,7 +104,7 @@ function AdminPanel(props) {
<div className="Add-GameNight"> <div className="Add-GameNight">
<form autoComplete="off" onSubmit={e => { e.preventDefault(); }}> <form autoComplete="off" onSubmit={e => { e.preventDefault(); }}>
<DateTimePicker <DateTimePicker
label="Gamenight date and time" label="Token expires at"
variant="standard" variant="standard"
value={expires} value={expires}
onChange={setExpires} onChange={setExpires}
@ -153,8 +165,8 @@ function AdminPanel(props) {
e.stopPropagation(); e.stopPropagation();
deleteToken(row.id) deleteToken(row.id)
}}> }}>
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
</TableCell> </TableCell>
</TableRow> </TableRow>
); );

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import List from '@mui/material/List'; import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem'; import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText'; import ListItemText from '@mui/material/ListItemText';
@ -13,17 +13,21 @@ import {unpack_api_result, get_gamenight, patch_gamenight} from '../api/Api';
function Gamenight(props) { function Gamenight(props) {
const [dense, setDense] = useState(true); const dense = true;
const [gamenight, setGamenight] = useState(null); const [gamenight, setGamenight] = useState(null);
const fetchGamenight = () => { const fetchGamenight = useCallback(() => {
if (props.user !== null) { if (props.user !== null) {
unpack_api_result(get_gamenight(props.gamenightId, props.user.jwt), props.setFlash) 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);
}
});
} }
} }, [props.gamenightId, props.user, props.setFlash]);
useEffect(fetchGamenight, []); useEffect(fetchGamenight, [fetchGamenight]);
let games = gamenight?.game_list.map(g => let games = gamenight?.game_list.map(g =>
( (
@ -51,7 +55,11 @@ function Gamenight(props) {
}; };
unpack_api_result(patch_gamenight(gamenight.id, input, props.user.jwt), props.setFlash) unpack_api_result(patch_gamenight(gamenight.id, input, props.user.jwt), props.setFlash)
.then(() => fetchGamenight()); .then(result => {
if(result !== undefined) {
fetchGamenight();
}
});
}; };
const Leave = () => { const Leave = () => {
@ -60,7 +68,11 @@ function Gamenight(props) {
}; };
unpack_api_result(patch_gamenight(gamenight.id, input, props.user.jwt), props.setFlash) 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; let join_or_leave_button;

View File

@ -12,13 +12,18 @@ import AddGameNight from './AddGameNight';
import {delete_gamenight, unpack_api_result} from '../api/Api'; import {delete_gamenight, unpack_api_result} from '../api/Api';
function Gamenights(props) { function Gamenights(props) {
const [dense, setDense] = React.useState(false); const dense = true;
const DeleteGamenight = (game_id) => { const DeleteGamenight = (game_id) => {
if (props.user !== null) { if (props.user !== null) {
const input = { game_id: game_id }; const input = { game_id: game_id };
unpack_api_result(delete_gamenight(input, props.user.jwt), props.setFlash) unpack_api_result(delete_gamenight(input, props.user.jwt), props.setFlash)
.then(() => props.refetchGamenights()); .then(result => {
if(result !== undefined) {
console.log("hello?");
props.refetchGamenights();
}
});
} }
} }

View File

@ -8,19 +8,13 @@ import MenuIcon from '@mui/icons-material/Menu';
function MenuBar(props) { function MenuBar(props) {
let adminPanelButton = null;
if (props.user?.role === 'Admin') {
adminPanelButton = (
<Button
color="inherit"
onClick={props.onAdmin}>
AdminPanel
</Button>
);
}
let userButton = null; let userButton = null;
if (props.user != null) { let logoutButton = null;
let adminPanelButton = null;
let registerButton = null;
if (props.user !== null) {
userButton = ( userButton = (
<Button <Button
color="inherit" color="inherit"
@ -28,6 +22,30 @@ function MenuBar(props) {
{props.user.username} {props.user.username}
</Button> </Button>
); );
logoutButton = (
<Button
color="inherit"
onClick={props.onLogout}>
Logout
</Button>
);
if (props.user.role === 'Admin') {
adminPanelButton = (
<Button
color="inherit"
onClick={props.onAdmin}>
AdminPanel
</Button>
);
}
} else {
registerButton = (
<Button
color="inherit"
onClick={props.onRegister}>
Register
</Button>
)
} }
return ( return (
@ -50,12 +68,9 @@ function MenuBar(props) {
Gamenight! Gamenight!
</Typography> </Typography>
{userButton !== null && userButton} {userButton !== null && userButton}
{registerButton !== null && registerButton}
{adminPanelButton !== null && adminPanelButton} {adminPanelButton !== null && adminPanelButton}
<Button {logoutButton !== null && logoutButton}
color="inherit"
onClick={props.onLogout}>
Logout
</Button>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
); );

View File

@ -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 (
<FormControl>
<Input
id="registration_token"
aria-describedby="registration_token-helper-text"
value={registrationToken}
onChange={(e) => {setRegistrationToken(e.target.value)}} />
<FormHelperText
id="registration_token-helper-text">
Registration token given by a gamenight admin
</FormHelperText>
<Input
id="username"
aria-describedby="email-helper-text"
value={username}
onChange={(e) => {setUsername(e.target.value)}} />
<FormHelperText
id="username-helper-text">
Username to display everywhere
</FormHelperText>
<Input
id="email"
aria-describedby="email-helper-text"
value={email}
onChange={(e) => {setEmail(e.target.value)}} />
<FormHelperText
id="email-helper-text">
E-mail used for notifications and password resets
</FormHelperText>
<Input
id="password"
type="password"
aria-describedby="password-helper-text"
value={password}
onChange={(e) => {setPassword(e.target.value)}} />
<FormHelperText
id="password-helper-text">
Password atleast 10 characters long
</FormHelperText>
<Input
id="password_repeat"
type="password"
aria-describedby="password_repeat-helper-text"
value={passwordRepeat}
onChange={(e) => {setPasswordRepeat(e.target.value)}} />
<FormHelperText
id="password_repeat-helper-text">
Confirm your password
</FormHelperText>
<Button
variant="outlined"
color="success"
onClick={onRegister}>
Register
</Button>
</FormControl>
);
}
export default Register;