Compare commits
2 Commits
main
...
user_regis
Author | SHA1 | Date | |
---|---|---|---|
79b7312896 | |||
6efccd631d |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1 @@
|
|||||||
/target/
|
|
||||||
**/*.rs.bk
|
|
||||||
Cargo.lock
|
|
||||||
.vscode
|
.vscode
|
||||||
|
3
backend-actix/.gitignore
vendored
3
backend-actix/.gitignore
vendored
@ -1,3 +0,0 @@
|
|||||||
target
|
|
||||||
src/models/
|
|
||||||
docs/
|
|
@ -1,20 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "backend-actix"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
gamenight-database = { path = "../gamenight-database"}
|
|
||||||
actix-web = "4"
|
|
||||||
actix-cors = "0.7"
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
uuid = { version = "1.3.0", features = ["serde", "v4"] }
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
|
||||||
jsonwebtoken = "9.3"
|
|
||||||
validator = { version = "0.20", features = ["derive"] }
|
|
||||||
rand_core = { version = "0.9" }
|
|
||||||
env_logger = "0.11"
|
|
||||||
tracing-actix-web = "0.7"
|
|
@ -1,29 +0,0 @@
|
|||||||
use std::{fs::{exists, read_dir, remove_dir_all, File}, io::Write, process::Command};
|
|
||||||
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
|
|
||||||
if exists("src/models").unwrap() {
|
|
||||||
remove_dir_all("src/models").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ =
|
|
||||||
Command::new("openapi-generator")
|
|
||||||
.args(["generate", "-i", "gamenight-api.yaml", "-g", "rust", "--global-property", "models"])
|
|
||||||
.output()
|
|
||||||
.expect("Failed to generate models sources for the gamenight API");
|
|
||||||
|
|
||||||
let mut file = File::create("src/models/mod.rs").unwrap();
|
|
||||||
let paths = read_dir("./src/models").unwrap();
|
|
||||||
for path in paths {
|
|
||||||
let path = path.unwrap();
|
|
||||||
let path = path.path();
|
|
||||||
let stem = path.file_stem().unwrap();
|
|
||||||
if stem == "mod" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
let line = format!("pub mod {};\n", stem.to_str().unwrap());
|
|
||||||
let _ = file.write(line.as_bytes()).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,249 +0,0 @@
|
|||||||
openapi: 3.0.0
|
|
||||||
info:
|
|
||||||
title: Gamenight
|
|
||||||
version: '1.0'
|
|
||||||
contact:
|
|
||||||
name: Dennis Brentjes
|
|
||||||
email: dennis@brentj.es
|
|
||||||
url: 'https://brentj.es'
|
|
||||||
description: Api specifaction for a Gamenight server
|
|
||||||
license:
|
|
||||||
name: MIT
|
|
||||||
servers:
|
|
||||||
- url: 'http://localhost:8080'
|
|
||||||
description: Gamenight
|
|
||||||
paths:
|
|
||||||
/token:
|
|
||||||
get:
|
|
||||||
summary: ''
|
|
||||||
operationId: get-token
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
$ref: '#/components/responses/TokenResponse'
|
|
||||||
'401':
|
|
||||||
$ref: '#/components/responses/FailureResponse'
|
|
||||||
requestBody:
|
|
||||||
$ref: '#/components/requestBodies/LoginRequest'
|
|
||||||
description: Submit your credentials to get a JWT-token to use with the rest of the api.
|
|
||||||
parameters: []
|
|
||||||
/user:
|
|
||||||
post:
|
|
||||||
summary: ''
|
|
||||||
operationId: post-register
|
|
||||||
requestBody:
|
|
||||||
$ref: '#/components/requestBodies/RegisterRequest'
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: ''
|
|
||||||
'422':
|
|
||||||
$ref: '#/components/responses/FailureResponse'
|
|
||||||
description: 'Create a new user given a registration token and user information, username and email must be unique, and password and password_repeat must match.'
|
|
||||||
parameters: []
|
|
||||||
get:
|
|
||||||
description: 'Get a user from primary id'
|
|
||||||
parameters: []
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
$ref: '#/components/responses/UserResponse'
|
|
||||||
/gamenights:
|
|
||||||
get:
|
|
||||||
summary: Your GET endpoint
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
$ref: '#/components/responses/GamenightsResponse'
|
|
||||||
'400':
|
|
||||||
$ref: '#/components/responses/FailureResponse'
|
|
||||||
'401':
|
|
||||||
$ref: '#/components/responses/FailureResponse'
|
|
||||||
operationId: get-gamenights
|
|
||||||
security:
|
|
||||||
- JWT-Auth: []
|
|
||||||
description: Retrieve the list of gamenights on this gamenight server. Requires authorization.
|
|
||||||
/gamenight:
|
|
||||||
post:
|
|
||||||
summary: ''
|
|
||||||
operationId: post-gamenight
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: OK
|
|
||||||
'401':
|
|
||||||
$ref: '#/components/responses/FailureResponse'
|
|
||||||
'422':
|
|
||||||
$ref: '#/components/responses/FailureResponse'
|
|
||||||
security:
|
|
||||||
- JWT-Auth: []
|
|
||||||
requestBody:
|
|
||||||
$ref: '#/components/requestBodies/AddGamenight'
|
|
||||||
description: 'Add a gamenight by providing a name and a date, only available when providing an JWT token.'
|
|
||||||
get:
|
|
||||||
summary: ''
|
|
||||||
operationId: get-gamenight
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
$ref: '#/components/responses/GamenightResponse'
|
|
||||||
'401':
|
|
||||||
$ref: '#/components/responses/FailureResponse'
|
|
||||||
'422':
|
|
||||||
$ref: '#/components/responses/FailureResponse'
|
|
||||||
requestBody:
|
|
||||||
$ref: '#/components/requestBodies/GetGamenight'
|
|
||||||
security:
|
|
||||||
- JWT-Auth: []
|
|
||||||
components:
|
|
||||||
schemas:
|
|
||||||
Gamenight:
|
|
||||||
title: Gamenight
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
datetime:
|
|
||||||
type: string
|
|
||||||
owner_id:
|
|
||||||
type: string
|
|
||||||
participants:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- id
|
|
||||||
- name
|
|
||||||
- datetime
|
|
||||||
- owner_id
|
|
||||||
Failure:
|
|
||||||
title: Failure
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
description: 'Failure Reason'
|
|
||||||
Token:
|
|
||||||
title: Token
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
jwt_token:
|
|
||||||
type: string
|
|
||||||
Login:
|
|
||||||
title: Login
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
password:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- username
|
|
||||||
- password
|
|
||||||
Registration:
|
|
||||||
title: Registration
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
email:
|
|
||||||
type: string
|
|
||||||
password:
|
|
||||||
type: string
|
|
||||||
password_repeat:
|
|
||||||
type: string
|
|
||||||
registration_token:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- username
|
|
||||||
- email
|
|
||||||
- password
|
|
||||||
- password_repeat
|
|
||||||
- registration_token
|
|
||||||
AddGamenightRequestBody:
|
|
||||||
title: AddGamenightRequestBody
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
datetime:
|
|
||||||
type: string
|
|
||||||
User:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: string
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
email:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- username
|
|
||||||
requestBodies:
|
|
||||||
LoginRequest:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Login'
|
|
||||||
RegisterRequest:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Registration'
|
|
||||||
AddGamenight:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/AddGamenightRequestBody'
|
|
||||||
GetGamenight:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
TokenResponse:
|
|
||||||
description: Example response
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Token'
|
|
||||||
FailureResponse:
|
|
||||||
description: Example response
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Failure'
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- message
|
|
||||||
GamenightsResponse:
|
|
||||||
description: Example response
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/Gamenight'
|
|
||||||
GamenightResponse:
|
|
||||||
description: A gamenight being hosted
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Gamenight'
|
|
||||||
UserResponse:
|
|
||||||
description: A user in the gamenight system
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
securitySchemes:
|
|
||||||
JWT-Auth:
|
|
||||||
type: http
|
|
||||||
scheme: bearer
|
|
||||||
bearerFormat: JWT
|
|
||||||
description: ''
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
|||||||
#[allow(unused_imports)]
|
|
||||||
pub mod models;
|
|
||||||
pub mod request;
|
|
||||||
|
|
||||||
use actix_cors::Cors;
|
|
||||||
use actix_web::middleware::Logger;
|
|
||||||
use actix_web::HttpServer;
|
|
||||||
use actix_web::App;
|
|
||||||
use actix_web::http;
|
|
||||||
use actix_web::web;
|
|
||||||
use request::{*, login, register, gamenights};
|
|
||||||
use tracing_actix_web::TracingLogger;
|
|
||||||
use gamenight_database::*;
|
|
||||||
|
|
||||||
#[actix_web::main]
|
|
||||||
async fn main() -> std::io::Result<()> {
|
|
||||||
let url = "postgres://root:root@127.0.0.1/gamenight";
|
|
||||||
|
|
||||||
let pool = get_connection_pool(url);
|
|
||||||
|
|
||||||
let mut conn = pool.get_conn();
|
|
||||||
run_migration(&mut conn);
|
|
||||||
|
|
||||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
|
||||||
|
|
||||||
HttpServer::new(move || {
|
|
||||||
let cors = Cors::default()
|
|
||||||
.allowed_origin("0.0.0.0")
|
|
||||||
.allowed_origin_fn(|_origin, _req_head| { true })
|
|
||||||
.allowed_methods(vec!["GET", "POST"])
|
|
||||||
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
|
|
||||||
.allowed_header(http::header::CONTENT_TYPE)
|
|
||||||
.max_age(3600);
|
|
||||||
|
|
||||||
App::new()
|
|
||||||
.wrap(cors)
|
|
||||||
.wrap(Logger::default())
|
|
||||||
.wrap(TracingLogger::default())
|
|
||||||
.app_data(web::Data::new(pool.clone()))
|
|
||||||
.service(login)
|
|
||||||
.service(register)
|
|
||||||
.service(gamenights)
|
|
||||||
.service(gamenight_post)
|
|
||||||
.service(gamenight_get)
|
|
||||||
.service(get_user)
|
|
||||||
.service(get_user_unauthenticated)
|
|
||||||
})
|
|
||||||
.bind(("::1", 8080))?
|
|
||||||
.run()
|
|
||||||
.await
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
use std::future::{Ready, ready};
|
|
||||||
|
|
||||||
use actix_web::{FromRequest, http, HttpRequest, dev::Payload, web::Data};
|
|
||||||
use chrono::Utc;
|
|
||||||
use jsonwebtoken::{encode, Header, EncodingKey, decode, DecodingKey, Validation};
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use gamenight_database::{user::{get_user, Role, User}, DbPool};
|
|
||||||
|
|
||||||
use super::error::ApiError;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Claims {
|
|
||||||
exp: i64,
|
|
||||||
uid: Uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AuthUser {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub username: String,
|
|
||||||
pub email: String,
|
|
||||||
pub role: Role,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<User> for AuthUser {
|
|
||||||
fn from(value: User) -> Self {
|
|
||||||
Self{
|
|
||||||
id: value.id,
|
|
||||||
username: value.username,
|
|
||||||
email: value.email,
|
|
||||||
role: value.role,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_claims(req: &HttpRequest) -> Result<Claims, ApiError> {
|
|
||||||
let token = req.headers()
|
|
||||||
.get(http::header::AUTHORIZATION)
|
|
||||||
.map(|h| h.to_str().unwrap().split_at(7).1.to_string());
|
|
||||||
|
|
||||||
let token = token.ok_or(ApiError{
|
|
||||||
status: 400,
|
|
||||||
message: "JWT-token was not specified in the Authorization header as Bearer: token".to_string()
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let secret = "secret";
|
|
||||||
Ok(decode::<Claims>(token.as_str(), &DecodingKey::from_secret(secret.as_bytes()), &Validation::default())?.claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_token(user: &User) -> Result<String, ApiError> {
|
|
||||||
let claims = Claims {
|
|
||||||
exp: Utc::now().timestamp() + chrono::Duration::days(7).num_seconds(),
|
|
||||||
uid: user.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
let secret = "secret";
|
|
||||||
Ok(encode(
|
|
||||||
&Header::default(),
|
|
||||||
&claims,
|
|
||||||
&EncodingKey::from_secret(secret.as_bytes()))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromRequest for AuthUser {
|
|
||||||
type Error = ApiError;
|
|
||||||
type Future = Ready<Result<Self, Self::Error>>;
|
|
||||||
|
|
||||||
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
|
|
||||||
ready(
|
|
||||||
(|| -> Result<AuthUser, ApiError>{
|
|
||||||
let pool = req.app_data::<Data<DbPool>>().expect("No database configured");
|
|
||||||
let mut conn = pool.get().expect("couldn't get db connection from pool");
|
|
||||||
let uid = get_claims(req)?.uid;
|
|
||||||
let user = get_user(&mut conn, uid)?;
|
|
||||||
|
|
||||||
Ok(user.into())
|
|
||||||
})()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
use actix_web::{ResponseError, error::BlockingError, HttpResponse, http::{header::ContentType, StatusCode}};
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use validator::ValidationErrors;
|
|
||||||
|
|
||||||
use gamenight_database::error::DatabaseError;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct ApiError {
|
|
||||||
#[serde(skip_serializing)]
|
|
||||||
pub status: u16,
|
|
||||||
pub message: String
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for ApiError {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
write!(f, "{}", self.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResponseError for ApiError {
|
|
||||||
fn error_response(&self) -> HttpResponse {
|
|
||||||
HttpResponse::build(StatusCode::from_u16(self.status).unwrap())
|
|
||||||
.content_type(ContentType::json())
|
|
||||||
.body(serde_json::to_string(&self).unwrap())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DatabaseError> for ApiError {
|
|
||||||
fn from(value: DatabaseError) -> Self {
|
|
||||||
ApiError {
|
|
||||||
//Todo, split this in unrecoverable and schema error
|
|
||||||
status: 500,
|
|
||||||
message: value.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<BlockingError> for ApiError {
|
|
||||||
fn from(value: BlockingError) -> Self {
|
|
||||||
ApiError {
|
|
||||||
status: 500,
|
|
||||||
message: value.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<serde_json::Error> for ApiError {
|
|
||||||
fn from(value: serde_json::Error) -> Self {
|
|
||||||
ApiError {
|
|
||||||
status: 500,
|
|
||||||
message: value.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<jsonwebtoken::errors::Error> for ApiError {
|
|
||||||
fn from(value: jsonwebtoken::errors::Error) -> Self {
|
|
||||||
ApiError {
|
|
||||||
status: 500,
|
|
||||||
message: value.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ValidationErrors> for ApiError {
|
|
||||||
fn from(value: ValidationErrors) -> Self {
|
|
||||||
ApiError {
|
|
||||||
status: 422,
|
|
||||||
message: value.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<chrono::ParseError> for ApiError {
|
|
||||||
fn from(value: chrono::ParseError) -> Self {
|
|
||||||
ApiError {
|
|
||||||
status: 422,
|
|
||||||
message: value.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<uuid::Error> for ApiError {
|
|
||||||
fn from(value: uuid::Error) -> Self {
|
|
||||||
ApiError {
|
|
||||||
status: 422,
|
|
||||||
message: value.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
use actix_web::{get, web, Responder, http::header::ContentType, HttpResponse, post};
|
|
||||||
use chrono::{DateTime, ParseError};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use gamenight_database::{gamenight::Gamenight, DbPool, GetConnection};
|
|
||||||
|
|
||||||
use crate::{models::{gamenight, add_gamenight_request_body::AddGamenightRequestBody, get_gamenight_request::GetGamenightRequest}, request::authorization::AuthUser};
|
|
||||||
use crate::request::error::ApiError;
|
|
||||||
|
|
||||||
|
|
||||||
impl AddGamenightRequestBody {
|
|
||||||
pub fn into_with_user(&self, user: AuthUser) -> Result<Gamenight, ParseError> {
|
|
||||||
Ok(Gamenight {
|
|
||||||
datetime: DateTime::parse_from_rfc3339(&self.clone().datetime.unwrap())?.with_timezone(&chrono::Utc),
|
|
||||||
id: Uuid::new_v4(),
|
|
||||||
name: self.clone().name.unwrap().clone(),
|
|
||||||
owner_id: user.id
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<GetGamenightRequest> for Uuid {
|
|
||||||
fn from(value: GetGamenightRequest) -> Self {
|
|
||||||
Uuid::parse_str(value.id.unwrap().as_str()).unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/gamenights")]
|
|
||||||
pub async fn gamenights(pool: web::Data<DbPool>, _user: AuthUser) -> Result<impl Responder, ApiError> {
|
|
||||||
let mut conn = pool.get_conn();
|
|
||||||
let gamenights: Vec<gamenight_database::gamenight::Gamenight> = gamenight_database::gamenights(&mut conn)?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok()
|
|
||||||
.content_type(ContentType::json())
|
|
||||||
.body(serde_json::to_string(&gamenights)?)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/gamenight")]
|
|
||||||
pub async fn gamenight_post(pool: web::Data<DbPool>, user: AuthUser, gamenight_data: web::Json<AddGamenightRequestBody>) -> Result<impl Responder, ApiError> {
|
|
||||||
let mut conn = pool.get_conn();
|
|
||||||
|
|
||||||
gamenight_database::gamenight::add_gamenight(&mut conn, gamenight_data.into_with_user(user)?)?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/gamenight")]
|
|
||||||
pub async fn gamenight_get(pool: web::Data<DbPool>, _user: AuthUser, gamenight_data: web::Json<GetGamenightRequest>) -> Result<impl Responder, ApiError> {
|
|
||||||
let mut conn = pool.get_conn();
|
|
||||||
|
|
||||||
let gamenight = gamenight_database::gamenight::get_gamenight(&mut conn, gamenight_data.into_inner().into())?;
|
|
||||||
let participants = gamenight_database::gamenight_participants::get_participants(&mut conn, &gamenight.id)?;
|
|
||||||
|
|
||||||
let model = gamenight::Gamenight{
|
|
||||||
id: gamenight.id.to_string(),
|
|
||||||
datetime: gamenight.datetime.to_rfc3339(),
|
|
||||||
name: gamenight.name,
|
|
||||||
owner_id: gamenight.owner_id.to_string(),
|
|
||||||
participants: Some(participants.iter().map(|x| {x.to_string()}).collect())
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok()
|
|
||||||
.content_type(ContentType::json())
|
|
||||||
.body(serde_json::to_string(&model)?))
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
|
|
||||||
mod user_handlers;
|
|
||||||
mod gamenight_handlers;
|
|
||||||
mod error;
|
|
||||||
mod authorization;
|
|
||||||
|
|
||||||
pub use user_handlers::login;
|
|
||||||
pub use user_handlers::register;
|
|
||||||
pub use gamenight_handlers::gamenights;
|
|
||||||
pub use gamenight_handlers::gamenight_post;
|
|
||||||
pub use gamenight_handlers::gamenight_get;
|
|
||||||
pub use user_handlers::get_user;
|
|
||||||
pub use user_handlers::get_user_unauthenticated;
|
|
@ -1,161 +0,0 @@
|
|||||||
|
|
||||||
use actix_web::http::header::ContentType;
|
|
||||||
use actix_web::{get, post, web, HttpResponse, Responder};
|
|
||||||
use gamenight_database::user::{count_users_with_email, count_users_with_username};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
use validator::{Validate, ValidateArgs, ValidationError};
|
|
||||||
use crate::models::login::Login;
|
|
||||||
use crate::models::registration::Registration;
|
|
||||||
use crate::models::token::Token;
|
|
||||||
use crate::models::user::User;
|
|
||||||
use crate::request::error::ApiError;
|
|
||||||
use crate::request::authorization::get_token;
|
|
||||||
use serde_json;
|
|
||||||
use gamenight_database::{DbPool, GetConnection};
|
|
||||||
|
|
||||||
use super::authorization::AuthUser;
|
|
||||||
|
|
||||||
impl From<Login> for gamenight_database::user::LoginUser {
|
|
||||||
fn from(val: Login) -> Self {
|
|
||||||
gamenight_database::user::LoginUser {
|
|
||||||
username: val.username,
|
|
||||||
password: val.password
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Registration> for gamenight_database::user::Register {
|
|
||||||
fn from(val: Registration) -> Self {
|
|
||||||
gamenight_database::user::Register {
|
|
||||||
email: val.email,
|
|
||||||
username: val.username,
|
|
||||||
password: val.password
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RegisterContext<'v_a> {
|
|
||||||
pub pool: &'v_a DbPool
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unique_username(username: &String, context: &RegisterContext) -> Result<(), ValidationError> {
|
|
||||||
let mut conn = context.pool.get_conn();
|
|
||||||
|
|
||||||
match count_users_with_username(&mut conn, username)
|
|
||||||
{
|
|
||||||
Ok(0) => Ok(()),
|
|
||||||
Ok(_) => Err(ValidationError::new("User already exists")),
|
|
||||||
Err(_) => Err(ValidationError::new("Database error while validating user")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unique_email(email: &String, context: &RegisterContext) -> Result<(), ValidationError> {
|
|
||||||
let mut conn = context.pool.get_conn();
|
|
||||||
|
|
||||||
match count_users_with_email(&mut conn, email)
|
|
||||||
{
|
|
||||||
Ok(0) => Ok(()),
|
|
||||||
Ok(_) => Err(ValidationError::new("email already exists")),
|
|
||||||
Err(_) => Err(ValidationError::new("Database error while validating email"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Validate)]
|
|
||||||
#[validate(context = RegisterContext::<'v_a>)]
|
|
||||||
pub struct ValidatableRegistration {
|
|
||||||
#[validate(
|
|
||||||
length(min = 1),
|
|
||||||
custom(function = "unique_username", use_context)
|
|
||||||
)]
|
|
||||||
pub username: String,
|
|
||||||
#[validate(
|
|
||||||
email,
|
|
||||||
custom(function = "unique_email", use_context)
|
|
||||||
)]
|
|
||||||
pub email: String,
|
|
||||||
#[validate(length(min = 10), must_match(other = "password_repeat", ))]
|
|
||||||
pub password: String,
|
|
||||||
pub password_repeat: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Registration> for ValidatableRegistration {
|
|
||||||
fn from(value: Registration) -> Self {
|
|
||||||
Self {
|
|
||||||
username: value.username,
|
|
||||||
email: value.email,
|
|
||||||
password: value.password,
|
|
||||||
password_repeat: value.password_repeat
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/token")]
|
|
||||||
pub async fn login(pool: web::Data<DbPool>, login_data: web::Json<Login>) -> Result<impl Responder, ApiError> {
|
|
||||||
let data = login_data.into_inner();
|
|
||||||
|
|
||||||
if let Ok(Some(user)) = web::block(move || {
|
|
||||||
let mut conn = pool.get_conn();
|
|
||||||
gamenight_database::login(&mut conn, data.into())
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
let token = get_token(&user)?;
|
|
||||||
let response = Token{ jwt_token: Some(token) };
|
|
||||||
Ok(HttpResponse::Ok()
|
|
||||||
.content_type(ContentType::json())
|
|
||||||
.body(serde_json::to_string(&response)?)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Err(ApiError{status: 401, message: "User doesn't exist or password doesn't match".to_string()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/user")]
|
|
||||||
pub async fn register(pool: web::Data<DbPool>, register_data: web::Json<Registration>) -> Result<impl Responder, ApiError> {
|
|
||||||
web::block(move || -> Result<(), ApiError> {
|
|
||||||
let validatable_registration: ValidatableRegistration = register_data.clone().into();
|
|
||||||
validatable_registration.validate_with_args(&RegisterContext{pool: &pool})?;
|
|
||||||
let register_request = register_data.into_inner().into();
|
|
||||||
let mut conn = pool.get_conn();
|
|
||||||
gamenight_database::register(&mut conn, register_request)?;
|
|
||||||
Ok(())
|
|
||||||
}).await??;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct UserInfo {
|
|
||||||
pub uuid: String
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<gamenight_database::user::User> for User {
|
|
||||||
fn from(value: gamenight_database::user::User) -> Self {
|
|
||||||
Self {
|
|
||||||
id: Some(value.id.to_string()),
|
|
||||||
username: value.username,
|
|
||||||
email: None,
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/user/{user_id}")]
|
|
||||||
pub async fn get_user(pool: web::Data<DbPool>, _user: AuthUser, path: web::Path<UserInfo>) -> Result<impl Responder, ApiError> {
|
|
||||||
let mut conn = pool.get_conn();
|
|
||||||
|
|
||||||
let user = gamenight_database::user::get_user(&mut conn, Uuid::parse_str(&path.uuid)?)?;
|
|
||||||
Ok(HttpResponse::Ok()
|
|
||||||
.content_type(ContentType::json())
|
|
||||||
.body(serde_json::to_string(&user)?))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/user/{user_id}")]
|
|
||||||
pub async fn get_user_unauthenticated(_path: web::Path<UserInfo>) -> Result<impl Responder, ApiError> {
|
|
||||||
Ok(HttpResponse::Forbidden())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
4
backend/.gitignore
vendored
Normal file
4
backend/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/target
|
||||||
|
.vscode
|
||||||
|
App.toml
|
||||||
|
*.sqlite
|
8
backend/App.toml.example
Normal file
8
backend/App.toml.example
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#Copy this file over to Rocket.toml after changing all relevant values.
|
||||||
|
|
||||||
|
[default]
|
||||||
|
jwt_secret = "some really good secret"
|
||||||
|
|
||||||
|
[global.databases]
|
||||||
|
gamenight_database = { url = "gamenight.sqlite" }
|
||||||
|
|
3172
backend-actix/Cargo.lock → backend/Cargo.lock
generated
3172
backend-actix/Cargo.lock → backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
26
backend/Cargo.toml
Normal file
26
backend/Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "gamenight"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Dennis Brentjes <d.brentjes@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rocket = { version = "0.5.0-rc.2", features = ["default", "json"] }
|
||||||
|
rocket_sync_db_pools = { version = "0.1.0-rc.2", features = ["diesel_postgres_pool"] }
|
||||||
|
rocket_dyn_templates = { version = "0.1.0-rc.2", features = ["handlebars"] }
|
||||||
|
diesel = {version = "1.4.8", features = ["uuidv07", "r2d2", "postgres", "chrono"]}
|
||||||
|
diesel_migrations = "1.4.0"
|
||||||
|
diesel-derive-enum = { version = "1.1", features = ["postgres"] }
|
||||||
|
chrono = {version = "0.4.19", features = ["serde"] }
|
||||||
|
serde = "1.0.136"
|
||||||
|
password-hash = "0.4"
|
||||||
|
argon2 = "0.4"
|
||||||
|
rand_core = { version = "0.6", features = ["std"] }
|
||||||
|
jsonwebtoken = "8.1"
|
||||||
|
validator = { version = "0.15", features = ["derive"] }
|
||||||
|
uuid = { version = "0.8.2", features = ["v4", "serde"] }
|
||||||
|
futures = "0.3.21"
|
||||||
|
rand = "0.8.5"
|
||||||
|
base64 = "0.13.0"
|
@ -2,5 +2,3 @@
|
|||||||
|
|
||||||
drop table pwd;
|
drop table pwd;
|
||||||
drop table users;
|
drop table users;
|
||||||
|
|
||||||
drop type Role;
|
|
@ -1,10 +1,8 @@
|
|||||||
CREATE TYPE Role AS ENUM ('user', 'admin');
|
|
||||||
|
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
id UUID NOT NULL PRIMARY KEY,
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
username VARCHAR UNIQUE NOT NULL,
|
username VARCHAR UNIQUE NOT NULL,
|
||||||
email VARCHAR UNIQUE NOT NULL,
|
email VARCHAR UNIQUE NOT NULL,
|
||||||
role Role NOT NULL
|
role VARCHAR NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE pwd (
|
CREATE TABLE pwd (
|
@ -1,3 +1,3 @@
|
|||||||
-- This file should undo anything in `up.sql`
|
-- This file should undo anything in `up.sql`
|
||||||
|
|
||||||
drop table gamenight_participant;
|
drop table gamenight_participants;
|
@ -1,6 +1,6 @@
|
|||||||
-- Your SQL goes here
|
-- Your SQL goes here
|
||||||
|
|
||||||
create table gamenight_participant (
|
create table gamenight_participants (
|
||||||
gamenight_id UUID NOT NULL,
|
gamenight_id UUID NOT NULL,
|
||||||
user_id UUID NOT NULL,
|
user_id UUID NOT NULL,
|
||||||
CONSTRAINT FK_gamenight_id FOREIGN KEY (gamenight_id) REFERENCES gamenight(id) ON DELETE CASCADE,
|
CONSTRAINT FK_gamenight_id FOREIGN KEY (gamenight_id) REFERENCES gamenight(id) ON DELETE CASCADE,
|
3
backend/requests/gamenights.sh
Executable file
3
backend/requests/gamenights.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
echo $JWT
|
||||||
|
|
||||||
|
curl -X GET -H "Authorization: Bearer ${JWT}" localhost:8000/api/gamenights
|
1
backend/requests/login.sh
Executable file
1
backend/requests/login.sh
Executable file
@ -0,0 +1 @@
|
|||||||
|
curl -X POST -H "Content-Type: application/json" -d '{"username": "a", "password": "c"}' localhost:8000/api/login
|
1
backend/requests/register.sh
Executable file
1
backend/requests/register.sh
Executable file
@ -0,0 +1 @@
|
|||||||
|
curl -X POST -H "Content-Type: application/json" -d '{"username": "roflin", "email": "user@example.com", "password": "oreokoekje123", "password_repeat": "oreokoekje123"}' localhost:8000/api/register
|
618
backend/src/api.rs
Normal file
618
backend/src/api.rs
Normal file
@ -0,0 +1,618 @@
|
|||||||
|
use crate::schema;
|
||||||
|
use crate::schema::admin::RegistrationToken;
|
||||||
|
use crate::schema::gamenight::*;
|
||||||
|
use crate::schema::users::*;
|
||||||
|
use crate::schema::DatabaseError;
|
||||||
|
use crate::schema::DbConn;
|
||||||
|
use crate::AppConfig;
|
||||||
|
use chrono::DateTime;
|
||||||
|
use chrono::Utc;
|
||||||
|
use futures::future::join_all;
|
||||||
|
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||||
|
use rocket::http::Status;
|
||||||
|
use rocket::request::{FromRequest, Outcome, Request};
|
||||||
|
use rocket::serde::json::{json, Json, Value};
|
||||||
|
use rocket::State;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use validator::ValidateArgs;
|
||||||
|
|
||||||
|
#[derive(Debug, Responder)]
|
||||||
|
pub enum ApiResponseVariant {
|
||||||
|
Status(Status),
|
||||||
|
Value(Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub enum ApiData {
|
||||||
|
#[serde(rename = "user")]
|
||||||
|
User(UserWithToken),
|
||||||
|
#[serde(rename = "gamenights")]
|
||||||
|
Gamenights(Vec<GamenightOutput>),
|
||||||
|
#[serde(rename = "gamenight")]
|
||||||
|
Gamenight(GamenightOutput),
|
||||||
|
#[serde(rename = "games")]
|
||||||
|
Games(Vec<Game>),
|
||||||
|
#[serde(rename = "registration_tokens")]
|
||||||
|
RegistrationTokens(Vec<RegistrationToken>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
struct ApiResponse {
|
||||||
|
result: Cow<'static, str>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
message: Option<Cow<'static, str>>,
|
||||||
|
#[serde(flatten, skip_serializing_if = "Option::is_none")]
|
||||||
|
data: Option<ApiData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiResponse {
|
||||||
|
const SUCCES_RESULT: Cow<'static, str> = Cow::Borrowed("Ok");
|
||||||
|
const FAILURE_RESULT: Cow<'static, str> = Cow::Borrowed("Failure");
|
||||||
|
|
||||||
|
const SUCCES: Self = Self {
|
||||||
|
result: Self::SUCCES_RESULT,
|
||||||
|
message: None,
|
||||||
|
data: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn error(message: String) -> Self {
|
||||||
|
Self {
|
||||||
|
result: Self::FAILURE_RESULT,
|
||||||
|
message: Some(Cow::Owned(message)),
|
||||||
|
data: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login_response(user: User, jwt: String) -> Self {
|
||||||
|
Self {
|
||||||
|
result: Self::SUCCES_RESULT,
|
||||||
|
message: None,
|
||||||
|
data: Some(ApiData::User(UserWithToken {
|
||||||
|
user: user,
|
||||||
|
jwt: jwt,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gamenights_response(gamenights: Vec<GamenightOutput>) -> Self {
|
||||||
|
Self {
|
||||||
|
result: Self::SUCCES_RESULT,
|
||||||
|
message: None,
|
||||||
|
data: Some(ApiData::Gamenights(gamenights)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gamenight_response(gamenight: GamenightOutput) -> Self {
|
||||||
|
Self {
|
||||||
|
result: Self::SUCCES_RESULT,
|
||||||
|
message: None,
|
||||||
|
data: Some(ApiData::Gamenight(gamenight)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn games_response(games: Vec<Game>) -> Self {
|
||||||
|
Self {
|
||||||
|
result: Self::SUCCES_RESULT,
|
||||||
|
message: None,
|
||||||
|
data: Some(ApiData::Games(games)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registration_tokens_response(tokens: Vec<RegistrationToken>) -> Self {
|
||||||
|
Self {
|
||||||
|
result: Self::SUCCES_RESULT,
|
||||||
|
message: None,
|
||||||
|
data: Some(ApiData::RegistrationTokens(tokens)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ApiError {
|
||||||
|
RequestError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
const AUTH_HEADER: &str = "Authorization";
|
||||||
|
const BEARER: &str = "Bearer ";
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for User {
|
||||||
|
type Error = ApiError;
|
||||||
|
|
||||||
|
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
|
let header = match req.headers().get_one(AUTH_HEADER) {
|
||||||
|
Some(header) => header,
|
||||||
|
None => return Outcome::Forward(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !header.starts_with(BEARER) {
|
||||||
|
return Outcome::Forward(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let app_config = req.guard::<&State<AppConfig>>().await.unwrap().inner();
|
||||||
|
let jwt = header.trim_start_matches(BEARER).to_owned();
|
||||||
|
let token = match decode::<Claims>(
|
||||||
|
&jwt,
|
||||||
|
&DecodingKey::from_secret(app_config.jwt_secret.as_bytes()),
|
||||||
|
&Validation::default(),
|
||||||
|
) {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(_) => return Outcome::Forward(()),
|
||||||
|
};
|
||||||
|
let id = token.claims.uid;
|
||||||
|
|
||||||
|
let conn = req.guard::<DbConn>().await.unwrap();
|
||||||
|
return match get_user(&conn, id).await {
|
||||||
|
Ok(o) => Outcome::Success(o),
|
||||||
|
Err(_) => Outcome::Forward(()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct GamenightOutput {
|
||||||
|
#[serde(flatten)]
|
||||||
|
gamenight: Gamenight,
|
||||||
|
game_list: Vec<Game>,
|
||||||
|
participants: Vec<User>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct GamenightUpdate {
|
||||||
|
action: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[patch(
|
||||||
|
"/gamenights/<gamenight_id>",
|
||||||
|
format = "application/json",
|
||||||
|
data = "<patch_json>"
|
||||||
|
)]
|
||||||
|
pub async fn patch_gamenight(
|
||||||
|
conn: DbConn,
|
||||||
|
user: User,
|
||||||
|
gamenight_id: String,
|
||||||
|
patch_json: Json<GamenightUpdate>,
|
||||||
|
) -> ApiResponseVariant {
|
||||||
|
let uuid = Uuid::parse_str(&gamenight_id).unwrap();
|
||||||
|
let patch = patch_json.into_inner();
|
||||||
|
match patch.action.as_str() {
|
||||||
|
"RemoveParticipant" => {
|
||||||
|
let entry = GamenightParticipantsEntry {
|
||||||
|
gamenight_id: uuid,
|
||||||
|
user_id: user.id,
|
||||||
|
};
|
||||||
|
match remove_participant(&conn, entry).await {
|
||||||
|
Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)),
|
||||||
|
Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"AddParticipant" => {
|
||||||
|
let entry = GamenightParticipantsEntry {
|
||||||
|
gamenight_id: uuid,
|
||||||
|
user_id: user.id,
|
||||||
|
};
|
||||||
|
match add_participant(&conn, entry).await {
|
||||||
|
Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)),
|
||||||
|
Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/gamenights/<gamenight_id>")]
|
||||||
|
pub async fn gamenight(conn: DbConn, _user: User, gamenight_id: String) -> ApiResponseVariant {
|
||||||
|
let uuid = Uuid::parse_str(&gamenight_id).unwrap();
|
||||||
|
let gamenight = match get_gamenight(&conn, uuid).await {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(err) => return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
|
||||||
|
};
|
||||||
|
let games = match get_games_of_gamenight(&conn, uuid).await {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(err) => return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
|
||||||
|
};
|
||||||
|
let participants = match load_participants(&conn, uuid).await {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(err) => return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
|
||||||
|
};
|
||||||
|
let gamenight_output = GamenightOutput {
|
||||||
|
gamenight: gamenight,
|
||||||
|
game_list: games,
|
||||||
|
participants: participants,
|
||||||
|
};
|
||||||
|
return ApiResponseVariant::Value(json!(ApiResponse::gamenight_response(gamenight_output)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/gamenights")]
|
||||||
|
pub async fn gamenights(conn: DbConn, _user: User) -> ApiResponseVariant {
|
||||||
|
let gamenights = match get_all_gamenights(&conn).await {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(err) => return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
|
||||||
|
};
|
||||||
|
|
||||||
|
let conn_ref = &conn;
|
||||||
|
|
||||||
|
let game_results: Result<Vec<GamenightOutput>, DatabaseError> =
|
||||||
|
join_all(gamenights.iter().map(|gn| async move {
|
||||||
|
let games = get_games_of_gamenight(conn_ref, gn.id).await?;
|
||||||
|
let participants = load_participants(conn_ref, gn.id).await?;
|
||||||
|
Ok(GamenightOutput {
|
||||||
|
gamenight: gn.clone(),
|
||||||
|
game_list: games,
|
||||||
|
participants: participants,
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match game_results {
|
||||||
|
Ok(result) => ApiResponseVariant::Value(json!(ApiResponse::gamenights_response(result))),
|
||||||
|
Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/gamenights", rank = 2)]
|
||||||
|
pub async fn gamenights_unauthorized() -> ApiResponseVariant {
|
||||||
|
ApiResponseVariant::Status(Status::Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct GamenightInput {
|
||||||
|
pub name: String,
|
||||||
|
pub datetime: DateTime<Utc>,
|
||||||
|
pub owner_id: Option<Uuid>,
|
||||||
|
pub game_list: Vec<Game>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Gamenight> for GamenightInput {
|
||||||
|
fn into(self) -> Gamenight {
|
||||||
|
Gamenight {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
name: self.name,
|
||||||
|
datetime: self.datetime,
|
||||||
|
owner_id: self.owner_id.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/gamenights", format = "application/json", data = "<gamenight_json>")]
|
||||||
|
pub async fn gamenights_post_json(
|
||||||
|
conn: DbConn,
|
||||||
|
user: User,
|
||||||
|
gamenight_json: Json<GamenightInput>,
|
||||||
|
) -> ApiResponseVariant {
|
||||||
|
let mut gamenight = gamenight_json.into_inner();
|
||||||
|
gamenight.owner_id = Some(user.id);
|
||||||
|
|
||||||
|
let mut mutable_game_list = gamenight.game_list.clone();
|
||||||
|
|
||||||
|
match add_unknown_games(&conn, &mut mutable_game_list).await {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(err) => return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
|
||||||
|
};
|
||||||
|
|
||||||
|
let gamenight_id = match insert_gamenight(&conn, gamenight.clone().into(), mutable_game_list)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(err) => return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
|
||||||
|
};
|
||||||
|
|
||||||
|
let participant = GamenightParticipantsEntry {
|
||||||
|
gamenight_id: gamenight_id,
|
||||||
|
user_id: user.id,
|
||||||
|
};
|
||||||
|
match add_participant(&conn, participant).await {
|
||||||
|
Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)),
|
||||||
|
Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/gamenights", rank = 2)]
|
||||||
|
pub async fn gamenights_post_json_unauthorized() -> ApiResponseVariant {
|
||||||
|
ApiResponseVariant::Status(Status::Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete(
|
||||||
|
"/gamenights",
|
||||||
|
format = "application/json",
|
||||||
|
data = "<delete_gamenight_json>"
|
||||||
|
)]
|
||||||
|
pub async fn gamenights_delete_json(
|
||||||
|
conn: DbConn,
|
||||||
|
user: User,
|
||||||
|
delete_gamenight_json: Json<DeleteGamenight>,
|
||||||
|
) -> ApiResponseVariant {
|
||||||
|
if user.role == Role::Admin {
|
||||||
|
if let Err(error) = delete_gamenight(&conn, delete_gamenight_json.game_id).await {
|
||||||
|
return ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string())));
|
||||||
|
}
|
||||||
|
return ApiResponseVariant::Value(json!(ApiResponse::SUCCES));
|
||||||
|
}
|
||||||
|
|
||||||
|
match get_gamenight(&conn, delete_gamenight_json.game_id).await {
|
||||||
|
Ok(gamenight) => {
|
||||||
|
if user.id == gamenight.owner_id {
|
||||||
|
if let Err(error) = delete_gamenight(&conn, delete_gamenight_json.game_id).await {
|
||||||
|
return ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string())));
|
||||||
|
}
|
||||||
|
return ApiResponseVariant::Value(json!(ApiResponse::SUCCES));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
return ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ApiResponseVariant::Status(Status::Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/gamenights", rank = 2)]
|
||||||
|
pub async fn gamenights_delete_json_unauthorized() -> ApiResponseVariant {
|
||||||
|
ApiResponseVariant::Status(Status::Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post(
|
||||||
|
"/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_clone = register.clone();
|
||||||
|
match conn
|
||||||
|
.run(move |c| register_clone.validate_args((c, c)))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => (),
|
||||||
|
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 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()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct Claims {
|
||||||
|
exp: i64,
|
||||||
|
uid: Uuid,
|
||||||
|
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>")]
|
||||||
|
pub async fn login_post_json(
|
||||||
|
conn: DbConn,
|
||||||
|
config: &State<AppConfig>,
|
||||||
|
login_json: Json<Login>,
|
||||||
|
) -> ApiResponseVariant {
|
||||||
|
match login(conn, login_json.into_inner()).await {
|
||||||
|
Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
|
||||||
|
Ok(login_result) => {
|
||||||
|
if !login_result.result {
|
||||||
|
return ApiResponseVariant::Value(json!(ApiResponse::error(String::from(
|
||||||
|
"username and password didn't match"
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = login_result.user.unwrap();
|
||||||
|
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())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/games")]
|
||||||
|
pub async fn games(conn: DbConn, _user: User) -> ApiResponseVariant {
|
||||||
|
match get_all_known_games(&conn).await {
|
||||||
|
Ok(games) => ApiResponseVariant::Value(json!(ApiResponse::games_response(games))),
|
||||||
|
Err(error) => ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/games", rank = 2)]
|
||||||
|
pub async fn games_unauthorized() -> ApiResponseVariant {
|
||||||
|
ApiResponseVariant::Status(Status::Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get(
|
||||||
|
"/participants",
|
||||||
|
format = "application/json",
|
||||||
|
data = "<gamenight_id_json>"
|
||||||
|
)]
|
||||||
|
pub async fn get_participants(
|
||||||
|
conn: DbConn,
|
||||||
|
_user: User,
|
||||||
|
gamenight_id_json: Json<GamenightId>,
|
||||||
|
) -> ApiResponseVariant {
|
||||||
|
match load_participants(&conn, gamenight_id_json.into_inner().gamenight_id).await {
|
||||||
|
Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)),
|
||||||
|
Err(error) => ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/participants", rank = 2)]
|
||||||
|
pub async fn get_participants_unauthorized() -> ApiResponseVariant {
|
||||||
|
ApiResponseVariant::Status(Status::Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/participants", format = "application/json", data = "<entry_json>")]
|
||||||
|
pub async fn post_participants(
|
||||||
|
conn: DbConn,
|
||||||
|
_user: User,
|
||||||
|
entry_json: Json<GamenightParticipantsEntry>,
|
||||||
|
) -> ApiResponseVariant {
|
||||||
|
match add_participant(&conn, entry_json.into_inner()).await {
|
||||||
|
Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)),
|
||||||
|
Err(error) => ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/participants", rank = 2)]
|
||||||
|
pub async fn post_participants_unauthorized() -> ApiResponseVariant {
|
||||||
|
ApiResponseVariant::Status(Status::Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/participants", format = "application/json", data = "<entry_json>")]
|
||||||
|
pub async fn delete_participants(
|
||||||
|
conn: DbConn,
|
||||||
|
_user: User,
|
||||||
|
entry_json: Json<GamenightParticipantsEntry>,
|
||||||
|
) -> ApiResponseVariant {
|
||||||
|
match remove_participant(&conn, entry_json.into_inner()).await {
|
||||||
|
Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)),
|
||||||
|
Err(error) => ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/participants", rank = 2)]
|
||||||
|
pub async fn delete_participants_unauthorized() -> ApiResponseVariant {
|
||||||
|
ApiResponseVariant::Status(Status::Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct RegistrationTokenData {
|
||||||
|
single_use: bool,
|
||||||
|
expires: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<RegistrationToken> for RegistrationTokenData {
|
||||||
|
fn into(self) -> RegistrationToken {
|
||||||
|
use rand::Rng;
|
||||||
|
let random_bytes = rand::thread_rng().gen::<[u8; 24]>();
|
||||||
|
RegistrationToken {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
token: base64::encode_config(random_bytes, base64::URL_SAFE),
|
||||||
|
single_use: self.single_use,
|
||||||
|
expires: self.expires,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post(
|
||||||
|
"/admin/registration_tokens",
|
||||||
|
format = "application/json",
|
||||||
|
data = "<token_json>"
|
||||||
|
)]
|
||||||
|
pub async fn add_registration_token(
|
||||||
|
conn: DbConn,
|
||||||
|
user: User,
|
||||||
|
token_json: Json<RegistrationTokenData>,
|
||||||
|
) -> ApiResponseVariant {
|
||||||
|
if user.role != Role::Admin {
|
||||||
|
return ApiResponseVariant::Status(Status::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
match schema::admin::add_registration_token(&conn, token_json.into_inner().into()).await {
|
||||||
|
Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)),
|
||||||
|
Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/admin/registration_tokens", rank = 2)]
|
||||||
|
pub async fn add_registration_token_unauthorized() -> ApiResponseVariant {
|
||||||
|
ApiResponseVariant::Status(Status::Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/admin/registration_tokens")]
|
||||||
|
pub async fn get_registration_tokens(conn: DbConn, user: User) -> ApiResponseVariant {
|
||||||
|
if user.role != Role::Admin {
|
||||||
|
return ApiResponseVariant::Status(Status::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
match schema::admin::get_all_registration_tokens(&conn).await {
|
||||||
|
Ok(results) => {
|
||||||
|
ApiResponseVariant::Value(json!(ApiResponse::registration_tokens_response(results)))
|
||||||
|
}
|
||||||
|
Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/admin/registration_tokens", rank = 2)]
|
||||||
|
pub async fn get_registration_tokens_unauthorized() -> ApiResponseVariant {
|
||||||
|
ApiResponseVariant::Status(Status::Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/admin/registration_tokens/<gamenight_id>")]
|
||||||
|
pub async fn delete_registration_tokens(
|
||||||
|
conn: DbConn,
|
||||||
|
user: User,
|
||||||
|
gamenight_id: String,
|
||||||
|
) -> ApiResponseVariant {
|
||||||
|
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)),
|
||||||
|
Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[delete("/admin/registration_tokens", rank = 2)]
|
||||||
|
pub async fn delete_registration_tokens_unauthorized() -> ApiResponseVariant {
|
||||||
|
ApiResponseVariant::Status(Status::Unauthorized)
|
||||||
|
}
|
81
backend/src/main.rs
Normal file
81
backend/src/main.rs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
#[macro_use]
|
||||||
|
extern crate rocket;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate diesel_migrations;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate diesel;
|
||||||
|
|
||||||
|
use rocket::{
|
||||||
|
fairing::AdHoc,
|
||||||
|
figment::{
|
||||||
|
providers::{Env, Format, Serialized, Toml},
|
||||||
|
Figment, Profile,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use rocket_dyn_templates::Template;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
pub mod schema;
|
||||||
|
mod site;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct AppConfig {
|
||||||
|
jwt_secret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppConfig {
|
||||||
|
fn default() -> AppConfig {
|
||||||
|
AppConfig {
|
||||||
|
jwt_secret: String::from("secret"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[launch]
|
||||||
|
async fn rocket() -> _ {
|
||||||
|
let figment = Figment::from(rocket::Config::default())
|
||||||
|
.merge(Serialized::defaults(AppConfig::default()))
|
||||||
|
.merge(Toml::file("App.toml").nested())
|
||||||
|
.merge(Env::prefixed("APP_").global())
|
||||||
|
.select(Profile::from_env_or("APP_PROFILE", "default"));
|
||||||
|
|
||||||
|
let rocket = rocket::custom(figment)
|
||||||
|
.attach(schema::DbConn::fairing())
|
||||||
|
.attach(Template::fairing())
|
||||||
|
.attach(site::CORS)
|
||||||
|
.attach(AdHoc::on_ignite("Run Migrations", schema::run_migrations))
|
||||||
|
.attach(AdHoc::config::<AppConfig>())
|
||||||
|
.mount("/", routes![site::index, site::files])
|
||||||
|
.mount(
|
||||||
|
"/api",
|
||||||
|
routes![
|
||||||
|
api::gamenight,
|
||||||
|
api::patch_gamenight,
|
||||||
|
api::gamenights,
|
||||||
|
api::gamenights_unauthorized,
|
||||||
|
api::gamenights_post_json,
|
||||||
|
api::gamenights_post_json_unauthorized,
|
||||||
|
api::register_post_json,
|
||||||
|
api::login_post_json,
|
||||||
|
api::gamenights_delete_json,
|
||||||
|
api::gamenights_delete_json_unauthorized,
|
||||||
|
api::games,
|
||||||
|
api::games_unauthorized,
|
||||||
|
api::get_participants,
|
||||||
|
api::get_participants_unauthorized,
|
||||||
|
api::post_participants,
|
||||||
|
api::post_participants_unauthorized,
|
||||||
|
api::delete_participants,
|
||||||
|
api::delete_participants_unauthorized,
|
||||||
|
api::add_registration_token,
|
||||||
|
api::add_registration_token_unauthorized,
|
||||||
|
api::get_registration_tokens,
|
||||||
|
api::get_registration_tokens_unauthorized,
|
||||||
|
api::delete_registration_tokens,
|
||||||
|
api::delete_registration_tokens_unauthorized,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
rocket
|
||||||
|
}
|
67
backend/src/schema/admin.rs
Normal file
67
backend/src/schema/admin.rs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
use crate::schema::{DatabaseError, DbConn};
|
||||||
|
use chrono::DateTime;
|
||||||
|
use chrono::Utc;
|
||||||
|
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
table! {
|
||||||
|
registration_tokens (id) {
|
||||||
|
id -> diesel::sql_types::Uuid,
|
||||||
|
token -> Char,
|
||||||
|
single_use -> Bool,
|
||||||
|
expires -> Nullable<Timestamptz>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
|
||||||
|
#[table_name = "registration_tokens"]
|
||||||
|
pub struct RegistrationToken {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub token: String,
|
||||||
|
pub single_use: bool,
|
||||||
|
pub expires: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_all_registration_tokens(
|
||||||
|
conn: &DbConn,
|
||||||
|
) -> Result<Vec<RegistrationToken>, DatabaseError> {
|
||||||
|
Ok(conn
|
||||||
|
.run(|c| registration_tokens::table.load::<RegistrationToken>(c))
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_registration_token(
|
||||||
|
conn: &DbConn,
|
||||||
|
token: RegistrationToken,
|
||||||
|
) -> Result<usize, DatabaseError> {
|
||||||
|
Ok(conn
|
||||||
|
.run(|c| {
|
||||||
|
diesel::insert_into(registration_tokens::table)
|
||||||
|
.values(token)
|
||||||
|
.execute(c)
|
||||||
|
})
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_registration_token(conn: &DbConn, id: Uuid) -> Result<usize, DatabaseError> {
|
||||||
|
Ok(conn
|
||||||
|
.run(move |c| {
|
||||||
|
diesel::delete(registration_tokens::table.filter(registration_tokens::id.eq(id)))
|
||||||
|
.execute(c)
|
||||||
|
})
|
||||||
|
.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?)
|
||||||
|
}
|
215
backend/src/schema/gamenight.rs
Normal file
215
backend/src/schema/gamenight.rs
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
use crate::schema::users::{users, User};
|
||||||
|
use crate::schema::{DatabaseError, DbConn};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use diesel::{Connection, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
table! {
|
||||||
|
gamenight (id) {
|
||||||
|
id -> diesel::sql_types::Uuid,
|
||||||
|
name -> VarChar,
|
||||||
|
datetime -> Timestamptz,
|
||||||
|
owner_id -> Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
known_games (id) {
|
||||||
|
id -> diesel::sql_types::Uuid,
|
||||||
|
name -> VarChar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
gamenight_gamelist(gamenight_id, game_id) {
|
||||||
|
gamenight_id -> diesel::sql_types::Uuid,
|
||||||
|
game_id -> diesel::sql_types::Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
gamenight_participants(gamenight_id, user_id) {
|
||||||
|
gamenight_id -> diesel::sql_types::Uuid,
|
||||||
|
user_id -> diesel::sql_types::Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Queryable, Clone, Insertable)]
|
||||||
|
#[table_name = "known_games"]
|
||||||
|
pub struct Game {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Queryable, Insertable, Clone)]
|
||||||
|
#[table_name = "gamenight"]
|
||||||
|
pub struct Gamenight {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub datetime: DateTime<Utc>,
|
||||||
|
pub owner_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Queryable, Insertable)]
|
||||||
|
#[table_name = "gamenight_gamelist"]
|
||||||
|
pub struct GamenightGameListEntry {
|
||||||
|
pub gamenight_id: Uuid,
|
||||||
|
pub game_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Queryable, Insertable, Identifiable)]
|
||||||
|
#[table_name = "gamenight_participants"]
|
||||||
|
#[primary_key(gamenight_id, user_id)]
|
||||||
|
pub struct GamenightParticipantsEntry {
|
||||||
|
pub gamenight_id: Uuid,
|
||||||
|
pub user_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Queryable)]
|
||||||
|
pub struct DeleteGamenight {
|
||||||
|
pub game_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Queryable)]
|
||||||
|
pub struct GamenightId {
|
||||||
|
pub gamenight_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_all_gamenights(conn: &DbConn) -> Result<Vec<Gamenight>, DatabaseError> {
|
||||||
|
Ok(conn.run(|c| gamenight::table.load::<Gamenight>(c)).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert_gamenight(
|
||||||
|
conn: &DbConn,
|
||||||
|
new_gamenight: Gamenight,
|
||||||
|
game_list: Vec<Game>,
|
||||||
|
) -> Result<Uuid, DatabaseError> {
|
||||||
|
Ok(conn
|
||||||
|
.run(move |c| {
|
||||||
|
c.transaction::<_, DatabaseError, _>(|| {
|
||||||
|
let id: Uuid = diesel::insert_into(gamenight::table)
|
||||||
|
.values(&new_gamenight)
|
||||||
|
.returning(gamenight::id)
|
||||||
|
.get_result(c)?;
|
||||||
|
|
||||||
|
let entries: Vec<GamenightGameListEntry> = game_list
|
||||||
|
.iter()
|
||||||
|
.map(|g| GamenightGameListEntry {
|
||||||
|
gamenight_id: new_gamenight.id.clone(),
|
||||||
|
game_id: g.id.clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
diesel::insert_into(gamenight_gamelist::table)
|
||||||
|
.values(entries)
|
||||||
|
.execute(c)?;
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_gamenight(conn: &DbConn, gamenight_id: Uuid) -> Result<Gamenight, DatabaseError> {
|
||||||
|
Ok(conn
|
||||||
|
.run(move |c| gamenight::table.find(gamenight_id).first(c))
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_gamenight(conn: &DbConn, game_id: Uuid) -> Result<usize, DatabaseError> {
|
||||||
|
Ok(conn
|
||||||
|
.run(move |c| diesel::delete(gamenight::table.filter(gamenight::id.eq(game_id))).execute(c))
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_all_known_games(conn: &DbConn) -> Result<Vec<Game>, DatabaseError> {
|
||||||
|
Ok(conn.run(|c| known_games::table.load::<Game>(c)).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_games_of_gamenight(
|
||||||
|
conn: &DbConn,
|
||||||
|
gamenight_id: Uuid,
|
||||||
|
) -> Result<Vec<Game>, DatabaseError> {
|
||||||
|
Ok(conn
|
||||||
|
.run::<_, Result<Vec<Game>, _>>(move |c| {
|
||||||
|
let linked_game_ids: Vec<GamenightGameListEntry> = gamenight_gamelist::table
|
||||||
|
.filter(gamenight_gamelist::gamenight_id.eq(gamenight_id))
|
||||||
|
.load::<GamenightGameListEntry>(c)?;
|
||||||
|
|
||||||
|
linked_game_ids
|
||||||
|
.iter()
|
||||||
|
.map(|l| {
|
||||||
|
known_games::table
|
||||||
|
.filter(known_games::id.eq(l.game_id))
|
||||||
|
.first::<Game>(c)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_game(conn: &DbConn, game: Game) -> Result<usize, DatabaseError> {
|
||||||
|
Ok(conn
|
||||||
|
.run(|c| {
|
||||||
|
diesel::insert_into(known_games::table)
|
||||||
|
.values(game)
|
||||||
|
.execute(c)
|
||||||
|
})
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_unknown_games(conn: &DbConn, games: &mut Vec<Game>) -> Result<(), DatabaseError> {
|
||||||
|
let all_games = get_all_known_games(conn).await?;
|
||||||
|
for game in games.iter_mut() {
|
||||||
|
if !all_games.iter().any(|g| g.name == game.name) {
|
||||||
|
game.id = Uuid::new_v4();
|
||||||
|
add_game(conn, game.clone()).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_participants(
|
||||||
|
conn: &DbConn,
|
||||||
|
gamenight_id: Uuid,
|
||||||
|
) -> Result<Vec<User>, DatabaseError> {
|
||||||
|
Ok(conn
|
||||||
|
.run::<_, Result<Vec<User>, _>>(move |c| {
|
||||||
|
let linked_participants = gamenight_participants::table
|
||||||
|
.filter(gamenight_participants::gamenight_id.eq(gamenight_id))
|
||||||
|
.load::<GamenightParticipantsEntry>(c)?;
|
||||||
|
linked_participants
|
||||||
|
.iter()
|
||||||
|
.map(|l| {
|
||||||
|
users::table
|
||||||
|
.filter(users::id.eq(l.user_id))
|
||||||
|
.first::<User>(c)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_participant(
|
||||||
|
conn: &DbConn,
|
||||||
|
participant: GamenightParticipantsEntry,
|
||||||
|
) -> Result<usize, DatabaseError> {
|
||||||
|
Ok(conn
|
||||||
|
.run(move |c| {
|
||||||
|
diesel::insert_into(gamenight_participants::table)
|
||||||
|
.values(&participant)
|
||||||
|
.on_conflict_do_nothing()
|
||||||
|
.execute(c)
|
||||||
|
})
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_participant(
|
||||||
|
conn: &DbConn,
|
||||||
|
participant: GamenightParticipantsEntry,
|
||||||
|
) -> Result<usize, DatabaseError> {
|
||||||
|
Ok(conn
|
||||||
|
.run(move |c| diesel::delete(&participant).execute(c))
|
||||||
|
.await?)
|
||||||
|
}
|
58
backend/src/schema/mod.rs
Normal file
58
backend/src/schema/mod.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
pub mod admin;
|
||||||
|
pub mod gamenight;
|
||||||
|
pub mod users;
|
||||||
|
|
||||||
|
use rocket::{Build, Rocket};
|
||||||
|
use rocket_sync_db_pools::database;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
#[database("gamenight_database")]
|
||||||
|
pub struct DbConn(diesel::PgConnection);
|
||||||
|
|
||||||
|
impl Deref for DbConn {
|
||||||
|
type Target = rocket_sync_db_pools::Connection<DbConn, diesel::PgConnection>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum DatabaseError {
|
||||||
|
Hash(password_hash::Error),
|
||||||
|
Query(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_migrations(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||||
|
// This macro from `diesel_migrations` defines an `embedded_migrations`
|
||||||
|
// module containing a function named `run`. This allows the example to be
|
||||||
|
// run and tested without any outside setup of the database.
|
||||||
|
embed_migrations!();
|
||||||
|
|
||||||
|
let conn = DbConn::get_one(&rocket).await.expect("database connection");
|
||||||
|
conn.run(|c| embedded_migrations::run(c))
|
||||||
|
.await
|
||||||
|
.expect("can run migrations");
|
||||||
|
|
||||||
|
rocket
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<diesel::result::Error> for DatabaseError {
|
||||||
|
fn from(error: diesel::result::Error) -> Self {
|
||||||
|
Self::Query(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<password_hash::Error> for DatabaseError {
|
||||||
|
fn from(error: password_hash::Error) -> Self {
|
||||||
|
Self::Hash(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for DatabaseError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||||
|
match self {
|
||||||
|
DatabaseError::Hash(err) => write!(f, "{}", err),
|
||||||
|
DatabaseError::Query(err) => write!(f, "{}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
192
backend/src/schema/users.rs
Normal file
192
backend/src/schema/users.rs
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
use crate::schema::{DatabaseError, DbConn};
|
||||||
|
use argon2::password_hash::SaltString;
|
||||||
|
use argon2::PasswordHash;
|
||||||
|
use argon2::PasswordVerifier;
|
||||||
|
use argon2::{
|
||||||
|
password_hash::{rand_core::OsRng, PasswordHasher},
|
||||||
|
Argon2,
|
||||||
|
};
|
||||||
|
use diesel::{Connection, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
use diesel_derive_enum::DbEnum;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use validator::{Validate, ValidationError};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, DbEnum, Clone, Copy, PartialEq)]
|
||||||
|
pub enum Role {
|
||||||
|
Admin,
|
||||||
|
User,
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
users(id) {
|
||||||
|
id -> diesel::sql_types::Uuid,
|
||||||
|
username -> VarChar,
|
||||||
|
email -> VarChar,
|
||||||
|
role -> crate::schema::users::RoleMapping,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
pwd(user_id) {
|
||||||
|
user_id -> diesel::sql_types::Uuid,
|
||||||
|
password -> VarChar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
|
||||||
|
#[table_name = "pwd"]
|
||||||
|
struct Pwd {
|
||||||
|
user_id: Uuid,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
|
||||||
|
#[table_name = "users"]
|
||||||
|
pub struct User {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub username: String,
|
||||||
|
pub email: String,
|
||||||
|
pub role: Role,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct UserWithToken {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub user: User,
|
||||||
|
pub jwt: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Login {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct LoginResult {
|
||||||
|
pub result: bool,
|
||||||
|
pub user: Option<User>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
|
||||||
|
pub struct Register {
|
||||||
|
#[validate(
|
||||||
|
length(min = 1),
|
||||||
|
custom(function = "unique_username", arg = "&'v_a diesel::PgConnection")
|
||||||
|
)]
|
||||||
|
pub username: String,
|
||||||
|
#[validate(
|
||||||
|
email,
|
||||||
|
custom(function = "unique_email", arg = "&'v_a diesel::PgConnection")
|
||||||
|
)]
|
||||||
|
pub email: String,
|
||||||
|
#[validate(length(min = 10), must_match = "password_repeat")]
|
||||||
|
pub password: String,
|
||||||
|
pub password_repeat: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert_user(conn: &DbConn, new_user: Register) -> Result<User, DatabaseError> {
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
|
||||||
|
let password_hash = argon2
|
||||||
|
.hash_password(new_user.password.as_bytes(), &salt)?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok(conn
|
||||||
|
.run(move |c| {
|
||||||
|
c.transaction(|| {
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
|
||||||
|
diesel::insert_into(users::table)
|
||||||
|
.values(User {
|
||||||
|
id: id.clone(),
|
||||||
|
username: new_user.username,
|
||||||
|
email: new_user.email,
|
||||||
|
role: Role::User,
|
||||||
|
})
|
||||||
|
.execute(c)?;
|
||||||
|
|
||||||
|
diesel::insert_into(pwd::table)
|
||||||
|
.values(Pwd {
|
||||||
|
user_id: id,
|
||||||
|
password: password_hash,
|
||||||
|
})
|
||||||
|
.execute(c)?;
|
||||||
|
users::table.filter(users::id.eq(id)).first::<User>(c)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login(conn: DbConn, login: Login) -> Result<LoginResult, DatabaseError> {
|
||||||
|
conn.run(move |c| -> Result<LoginResult, DatabaseError> {
|
||||||
|
let id: Uuid = users::table
|
||||||
|
.filter(users::username.eq(&login.username))
|
||||||
|
.or_filter(users::email.eq(&login.username))
|
||||||
|
.select(users::id)
|
||||||
|
.first(c)?;
|
||||||
|
|
||||||
|
let pwd: String = pwd::table
|
||||||
|
.filter(pwd::user_id.eq(id))
|
||||||
|
.select(pwd::password)
|
||||||
|
.first(c)?;
|
||||||
|
|
||||||
|
let parsed_hash = PasswordHash::new(&pwd)?;
|
||||||
|
|
||||||
|
if Argon2::default()
|
||||||
|
.verify_password(&login.password.as_bytes(), &parsed_hash)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
let user: User = users::table.find(id).first(c)?;
|
||||||
|
|
||||||
|
Ok(LoginResult {
|
||||||
|
result: true,
|
||||||
|
user: Some(user),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(LoginResult {
|
||||||
|
result: false,
|
||||||
|
user: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user(conn: &DbConn, id: Uuid) -> Result<User, DatabaseError> {
|
||||||
|
Ok(conn
|
||||||
|
.run(move |c| users::table.filter(users::id.eq(id)).first(c))
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unique_username(
|
||||||
|
username: &String,
|
||||||
|
conn: &diesel::PgConnection,
|
||||||
|
) -> Result<(), ValidationError> {
|
||||||
|
match users::table
|
||||||
|
.count()
|
||||||
|
.filter(users::username.eq(username))
|
||||||
|
.get_result(conn)
|
||||||
|
{
|
||||||
|
Ok(0) => Ok(()),
|
||||||
|
Ok(_) => Err(ValidationError::new("User already exists")),
|
||||||
|
Err(_) => Err(ValidationError::new("Database error while validating user")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unique_email(email: &String, conn: &diesel::PgConnection) -> Result<(), ValidationError> {
|
||||||
|
match users::table
|
||||||
|
.count()
|
||||||
|
.filter(users::email.eq(email))
|
||||||
|
.get_result(conn)
|
||||||
|
{
|
||||||
|
Ok(0) => Ok(()),
|
||||||
|
Ok(_) => Err(ValidationError::new("email already exists")),
|
||||||
|
Err(_) => Err(ValidationError::new(
|
||||||
|
"Database error while validating email",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
42
backend/src/site.rs
Normal file
42
backend/src/site.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use rocket::fs::NamedFile;
|
||||||
|
use rocket::{
|
||||||
|
fairing::{Fairing, Info, Kind},
|
||||||
|
http::Header,
|
||||||
|
Request, Response,
|
||||||
|
};
|
||||||
|
use std::io;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
pub struct CORS;
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl Fairing for CORS {
|
||||||
|
fn info(&self) -> Info {
|
||||||
|
Info {
|
||||||
|
name: "Attaching CORS headers to responses",
|
||||||
|
kind: Kind::Response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
|
||||||
|
response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
|
||||||
|
response.set_header(Header::new(
|
||||||
|
"Access-Control-Allow-Methods",
|
||||||
|
"POST, GET, PATCH, OPTIONS",
|
||||||
|
));
|
||||||
|
response.set_header(Header::new("Access-Control-Allow-Headers", "*"));
|
||||||
|
response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/<file..>", rank = 10)]
|
||||||
|
pub async fn files(file: PathBuf) -> Option<NamedFile> {
|
||||||
|
NamedFile::open(Path::new("../frontend/build/").join(file))
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
pub async fn index() -> io::Result<NamedFile> {
|
||||||
|
NamedFile::open("../frontend/build/index.html").await
|
||||||
|
}
|
5
backend/templates/flash.html.hbs
Normal file
5
backend/templates/flash.html.hbs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{{#if has_data}}
|
||||||
|
<div>
|
||||||
|
<p>{{kind}}: {{message}}</p>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
16
backend/templates/gamenight_add.html.hbs
Normal file
16
backend/templates/gamenight_add.html.hbs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
{{> flash flash }}
|
||||||
|
|
||||||
|
<form action="{{post_url}}" method="post">
|
||||||
|
<label for="game">Game:</label><br>
|
||||||
|
<input type="text" id="game" name="game"><br>
|
||||||
|
<label for="datetime">Wanneer:</label><br>
|
||||||
|
<input type="text" id="datetime" name="datetime">
|
||||||
|
<input type="submit" value="Submit">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
14
backend/templates/gamenights.html.hbs
Normal file
14
backend/templates/gamenights.html.hbs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{> flash flash }}
|
||||||
|
|
||||||
|
{{#each gamenights}}
|
||||||
|
<div>
|
||||||
|
<span>game: {{this.game}}</span>
|
||||||
|
<span>when: {{this.datetime}}</span>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</body>
|
||||||
|
</html>
|
19
backend/templates/register.html.hbs
Normal file
19
backend/templates/register.html.hbs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{> flash flash }}
|
||||||
|
|
||||||
|
<form action="/api/register" method="post">
|
||||||
|
<label for="username">Username:</label><br>
|
||||||
|
<input type="text" id="username" name="username" required><br>
|
||||||
|
<label for="email">Email:</label><br>
|
||||||
|
<input type="text" id="email" name="email" required><br>
|
||||||
|
<label for="password">Password:</label><br>
|
||||||
|
<input type="password" id="password" name="password" required><br>
|
||||||
|
<label for="password_repeat">Repeat password:</label><br>
|
||||||
|
<input type="password" id="password_repeat" name="password_repeat" required><br>
|
||||||
|
<input type="submit">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,7 +1,11 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
db:
|
db:
|
||||||
container_name: pg_container
|
container_name: pg_container
|
||||||
image: postgres
|
image: postgres
|
||||||
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: root
|
POSTGRES_USER: root
|
||||||
POSTGRES_PASSWORD: root
|
POSTGRES_PASSWORD: root
|
||||||
@ -11,6 +15,7 @@ services:
|
|||||||
pgadmin:
|
pgadmin:
|
||||||
container_name: pgadmin4_container
|
container_name: pgadmin4_container
|
||||||
image: dpage/pgadmin4
|
image: dpage/pgadmin4
|
||||||
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
PGADMIN_DEFAULT_EMAIL: admin@admin.com
|
PGADMIN_DEFAULT_EMAIL: admin@admin.com
|
||||||
PGADMIN_DEFAULT_PASSWORD: root
|
PGADMIN_DEFAULT_PASSWORD: root
|
||||||
|
23
frontend/.gitignore
vendored
Normal file
23
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
70
frontend/README.md
Normal file
70
frontend/README.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# Getting Started with Create React App
|
||||||
|
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.\
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||||
|
|
||||||
|
The page will reload when you make changes.\
|
||||||
|
You may also see any lint errors in the console.
|
||||||
|
|
||||||
|
### `npm test`
|
||||||
|
|
||||||
|
Launches the test runner in the interactive watch mode.\
|
||||||
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.\
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.\
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `npm run eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||||
|
|
||||||
|
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||||
|
|
||||||
|
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||||
|
|
||||||
|
### Code Splitting
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||||
|
|
||||||
|
### Analyzing the Bundle Size
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||||
|
|
||||||
|
### Making a Progressive Web App
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||||
|
|
||||||
|
### `npm run build` fails to minify
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
28642
frontend/package-lock.json
generated
Normal file
28642
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
60
frontend/package.json
Normal file
60
frontend/package.json
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.9.0",
|
||||||
|
"@emotion/styled": "^11.8.1",
|
||||||
|
"@material-ui/icons": "^4.11.3",
|
||||||
|
"@mui/icons-material": "^5.8.0",
|
||||||
|
"@mui/material": "^5.8.0",
|
||||||
|
"@mui/x-date-pickers": "^5.0.0-alpha.4",
|
||||||
|
"@testing-library/jest-dom": "^5.16.4",
|
||||||
|
"@testing-library/react": "^13.0.1",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"date-fns": "^2.28.0",
|
||||||
|
"moment": "^2.29.3",
|
||||||
|
"react": "^18.1.0",
|
||||||
|
"react-datetime": "^3.1.1",
|
||||||
|
"react-dom": "^18.1.0",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject",
|
||||||
|
"watch": "npm-watch"
|
||||||
|
},
|
||||||
|
"watch": {
|
||||||
|
"build": {
|
||||||
|
"patterns": [
|
||||||
|
"src/"
|
||||||
|
],
|
||||||
|
"extensions": "js,jsx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"dotenv-cli": "^5.1.0",
|
||||||
|
"npm-watch": "^0.11.0"
|
||||||
|
}
|
||||||
|
}
|
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
43
frontend/public/index.html
Normal file
43
frontend/public/index.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>It's gamenight</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
frontend/public/logo192.png
Normal file
BIN
frontend/public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/public/logo512.png
Normal file
BIN
frontend/public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
25
frontend/public/manifest.json
Normal file
25
frontend/public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
3
frontend/public/robots.txt
Normal file
3
frontend/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
51
frontend/src/App.css
Normal file
51
frontend/src/App.css
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
.App {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-logo {
|
||||||
|
height: 40vmin;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.App-logo {
|
||||||
|
animation: App-logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-header {
|
||||||
|
background-color: #282c34;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-link {
|
||||||
|
color: #61dafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset label {
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=submit] {
|
||||||
|
margin:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes App-logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
159
frontend/src/App.js
Normal file
159
frontend/src/App.js
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import './App.css';
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import MenuBar from './components/MenuBar';
|
||||||
|
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';
|
||||||
|
|
||||||
|
const localStorageUserKey = 'user';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [gamenights, setGamenights] = useState([]);
|
||||||
|
const [flashData, setFlashData] = useState({});
|
||||||
|
const [games, setGames] = useState([]);
|
||||||
|
const [activeGamenightId, setActiveGamenightId] = useState(null);
|
||||||
|
const [appState, setAppState] = useState('LoggedOut')
|
||||||
|
|
||||||
|
const handleLogin = (input) => {
|
||||||
|
unpack_api_result(login(input), setFlashData)
|
||||||
|
.then(result => {
|
||||||
|
if(result !== undefined) {
|
||||||
|
setUser(result.user);
|
||||||
|
localStorage.setItem(localStorageUserKey, JSON.stringify(result.user));
|
||||||
|
setAppState('LoggedIn')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(activeGamenightId !== null) {
|
||||||
|
setAppState('GamenightDetails');
|
||||||
|
} else {
|
||||||
|
setAppState(user === null ? 'LoggedOut' : 'LoggedIn')
|
||||||
|
}
|
||||||
|
}, [activeGamenightId, user])
|
||||||
|
|
||||||
|
const onLogout = () => {
|
||||||
|
setUser(null);
|
||||||
|
localStorage.removeItem(localStorageUserKey);
|
||||||
|
setAppState('LoggedOut')
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAdmin = () => {
|
||||||
|
setAppState('AdminPanel')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUser = () => {
|
||||||
|
setAppState('UserPage')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRegister = () => {
|
||||||
|
setAppState('RegisterPage')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onReset = () => {
|
||||||
|
setAppState(user === null ? 'LoggedOut' : 'LoggedIn')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRegistered = (user) => {
|
||||||
|
setUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFlash = (data) => {
|
||||||
|
setFlashData(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
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, refetchGamenights])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (appState === 'LoggedIn') {
|
||||||
|
unpack_api_result(get_games(user.jwt), setFlashData)
|
||||||
|
.then(result => {
|
||||||
|
if (result !== undefined) {
|
||||||
|
setGames(result.games)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [appState, user])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setUser(JSON.parse(localStorage.getItem(localStorageUserKey)));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
let mainview;
|
||||||
|
if(appState === 'LoggedOut') {
|
||||||
|
mainview = (
|
||||||
|
<div className="App">
|
||||||
|
<Login onChange={handleLogin}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if(appState === 'RegisterPage') {
|
||||||
|
mainview = (
|
||||||
|
<Register
|
||||||
|
onRegistered={onRegistered}
|
||||||
|
setFlash={setFlash}/>
|
||||||
|
);
|
||||||
|
} else if(appState === 'UserPage') {
|
||||||
|
mainview = (
|
||||||
|
<span>UserPage</span>
|
||||||
|
)
|
||||||
|
}else if(appState === 'GamenightDetails') {
|
||||||
|
mainview = (
|
||||||
|
<Gamenight
|
||||||
|
gamenightId={activeGamenightId}
|
||||||
|
onDismis={() => setActiveGamenightId(null)}
|
||||||
|
setFlash={setFlash}
|
||||||
|
user={user}
|
||||||
|
/>)
|
||||||
|
} else if(appState === 'LoggedIn') {
|
||||||
|
mainview = (
|
||||||
|
<Gamenights
|
||||||
|
user={user}
|
||||||
|
games={games}
|
||||||
|
setFlash={setFlash}
|
||||||
|
refetchGamenights={refetchGamenights}
|
||||||
|
gamenights={gamenights}
|
||||||
|
onSelectGamenight={(g) => setActiveGamenightId(g.id)}/>
|
||||||
|
);
|
||||||
|
} else if(appState === 'AdminPanel') {
|
||||||
|
mainview = (
|
||||||
|
<AdminPanel
|
||||||
|
user={user}
|
||||||
|
setFlash={setFlash}/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let page = (
|
||||||
|
<>
|
||||||
|
<MenuBar
|
||||||
|
user={user}
|
||||||
|
onUser={onUser}
|
||||||
|
onRegister={onRegister}
|
||||||
|
onAdmin={onAdmin}
|
||||||
|
onLogout={onLogout}
|
||||||
|
onReset={onReset}/>
|
||||||
|
{mainview}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
8
frontend/src/App.test.js
Normal file
8
frontend/src/App.test.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
test('renders learn react link', () => {
|
||||||
|
render(<App />);
|
||||||
|
const linkElement = screen.getByText(/learn react/i);
|
||||||
|
expect(linkElement).toBeInTheDocument();
|
||||||
|
});
|
126
frontend/src/api/Api.js
Normal file
126
frontend/src/api/Api.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
|
||||||
|
import fetchResource from './FetchResource'
|
||||||
|
|
||||||
|
export function unpack_api_result(promise, onError) {
|
||||||
|
return promise.then(result => {
|
||||||
|
if(result.result !== 'Ok') {
|
||||||
|
throw new Error(result.message);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
onError({
|
||||||
|
type: 'Error',
|
||||||
|
message: `${error.status === null ?? error.status} ${error.message}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get_gamenights(token) {
|
||||||
|
return fetchResource('api/gamenights', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get_gamenight(gamenight_id, token) {
|
||||||
|
return fetchResource(`api/gamenights/${gamenight_id}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function post_gamenight(input, token) {
|
||||||
|
return fetchResource('api/gamenights', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(input)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function patch_gamenight(gamenight_id, input, token) {
|
||||||
|
return fetchResource(`api/gamenights/${gamenight_id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(input)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function delete_gamenight(input, token) {
|
||||||
|
return fetchResource('api/gamenights', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(input)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get_games(token) {
|
||||||
|
return fetchResource('api/games', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function login(body) {
|
||||||
|
return fetchResource('api/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get_registration_tokens(token) {
|
||||||
|
return fetchResource('api/admin/registration_tokens', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function add_registration_token(token, registration_token) {
|
||||||
|
return fetchResource('api/admin/registration_tokens', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(registration_token)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function delete_registration_token(token, registration_token_id) {
|
||||||
|
return fetchResource(`api/admin/registration_tokens/${registration_token_id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'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)
|
||||||
|
});
|
||||||
|
}
|
73
frontend/src/api/FetchResource.js
Normal file
73
frontend/src/api/FetchResource.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
const API_URL = '';
|
||||||
|
|
||||||
|
function ApiError(message, data, status) {
|
||||||
|
let response = null;
|
||||||
|
let isObject = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = JSON.parse(data);
|
||||||
|
isObject = true;
|
||||||
|
} catch (e) {
|
||||||
|
response = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.response = response;
|
||||||
|
this.message = message;
|
||||||
|
this.status = status;
|
||||||
|
this.toString = function () {
|
||||||
|
return `${ this.message }\nResponse:\n${ isObject ? JSON.stringify(this.response, null, 2) : this.response }`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchResource = (path, userOptions = {}) => {
|
||||||
|
const defaultOptions = {};
|
||||||
|
const defaultHeaders = {};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
...defaultOptions,
|
||||||
|
...userOptions,
|
||||||
|
headers: {
|
||||||
|
...defaultHeaders,
|
||||||
|
...userOptions.headers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = `${ API_URL }/${ path }`;
|
||||||
|
const isFile = options.body instanceof File;
|
||||||
|
|
||||||
|
if (options.body && typeof options.body === 'object' && !isFile) {
|
||||||
|
options.body = JSON.stringify(options.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = null;
|
||||||
|
|
||||||
|
return fetch(url, options)
|
||||||
|
.then(responseObject => {
|
||||||
|
response = responseObject;
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status < 200 || response.status >= 300) {
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(parsedResponse => {
|
||||||
|
if (response.status < 200 || response.status >= 300) {
|
||||||
|
throw parsedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedResponse;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (response) {
|
||||||
|
throw new ApiError(`Request failed with status ${ response.status }.`, error, response.status);
|
||||||
|
} else {
|
||||||
|
throw new ApiError(error.toString(), null, 'REQUEST_FAILED');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default fetchResource;
|
116
frontend/src/components/AddGameNight.jsx
Normal file
116
frontend/src/components/AddGameNight.jsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import { DateTimePicker } from '@mui/x-date-pickers';
|
||||||
|
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
||||||
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||||
|
|
||||||
|
import GameAdder from './GameAdder';
|
||||||
|
import { post_gamenight, unpack_api_result} from '../api/Api';
|
||||||
|
|
||||||
|
function AddGameNight(props) {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [gameName, setGameName] = useState("");
|
||||||
|
const [date, setDate] = useState(Date.now());
|
||||||
|
const [gameList, setGameList] = useState([]);
|
||||||
|
|
||||||
|
const handleNameChange = (event) => {
|
||||||
|
setGameName(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDateChange = (date) => {
|
||||||
|
setDate(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onGamesListChange = (gameList) => {
|
||||||
|
setGameList(gameList);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(!expanded) {
|
||||||
|
setGameName("");
|
||||||
|
setDate(null);
|
||||||
|
}
|
||||||
|
}, [expanded]);
|
||||||
|
|
||||||
|
const handleAddGamenight = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (props.user !== null) {
|
||||||
|
let input = {
|
||||||
|
name: gameName,
|
||||||
|
datetime: date,
|
||||||
|
game_list: gameList,
|
||||||
|
}
|
||||||
|
|
||||||
|
unpack_api_result(post_gamenight(input, props.user.jwt), props.setFlash)
|
||||||
|
.then(result => {
|
||||||
|
if(result !== undefined) {
|
||||||
|
setExpanded(false);
|
||||||
|
setGameName("");
|
||||||
|
setDate(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => props.refetchGamenights());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if(expanded) {
|
||||||
|
return (
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||||
|
<div className="Add-GameNight">
|
||||||
|
<form autoComplete="off" onSubmit={e => { e.preventDefault(); }}>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Gamenight</legend>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
id="game-name"
|
||||||
|
label="Name"
|
||||||
|
variant="standard"
|
||||||
|
value={gameName}
|
||||||
|
onChange={handleNameChange}/>
|
||||||
|
|
||||||
|
<DateTimePicker
|
||||||
|
label="Gamenight date and time"
|
||||||
|
variant="standard"
|
||||||
|
value={date}
|
||||||
|
onChange={onDateChange}
|
||||||
|
inputFormat="dd-MM-yyyy HH:mm"
|
||||||
|
renderInput={(params) => <TextField {...params} />}/>
|
||||||
|
|
||||||
|
<GameAdder games={props.games} onChange={onGamesListChange}/>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="success"
|
||||||
|
onClick={handleAddGamenight}>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
onClick={() => setExpanded(false)}>
|
||||||
|
Discard
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</LocalizationProvider>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
aria-label="add"
|
||||||
|
color="success"
|
||||||
|
onClick={(e) => setExpanded(true)}>
|
||||||
|
<AddIcon />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddGameNight
|
190
frontend/src/components/AdminPanel.jsx
Normal file
190
frontend/src/components/AdminPanel.jsx
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import {useState, useEffect, useCallback} from 'react';
|
||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
import Table from '@mui/material/Table';
|
||||||
|
import TableBody from '@mui/material/TableBody';
|
||||||
|
import TableCell from '@mui/material/TableCell';
|
||||||
|
import TableContainer from '@mui/material/TableContainer';
|
||||||
|
import TableHead from '@mui/material/TableHead';
|
||||||
|
import TablePagination from '@mui/material/TablePagination';
|
||||||
|
import TableRow from '@mui/material/TableRow';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import { DateTimePicker } from '@mui/x-date-pickers';
|
||||||
|
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
||||||
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||||
|
|
||||||
|
import moment from 'moment';
|
||||||
|
import {get_registration_tokens, add_registration_token, delete_registration_token, unpack_api_result} from '../api/Api';
|
||||||
|
|
||||||
|
function AdminPanel(props) {
|
||||||
|
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||||
|
const [registrationTokens, setRegistrationTokens] = useState([]);
|
||||||
|
const [expires, setExpires] = useState(null);
|
||||||
|
const [isSingleUse, setIsSingleUse] = useState(false);
|
||||||
|
|
||||||
|
const handleChangePage = (event, newPage) => {
|
||||||
|
setPage(newPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeRowsPerPage = (event) => {
|
||||||
|
setRowsPerPage(+event.target.value);
|
||||||
|
setPage(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const refetchTokens = useCallback(() => {
|
||||||
|
if(props.user !== null) {
|
||||||
|
unpack_api_result(get_registration_tokens(props.user.jwt), props.setFlash)
|
||||||
|
.then(result => {
|
||||||
|
if(result !== undefined) {
|
||||||
|
setRegistrationTokens(result.registration_tokens);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [props.setFlash, props.user]);
|
||||||
|
|
||||||
|
const deleteToken = (id) => {
|
||||||
|
if(props.user !== null) {
|
||||||
|
unpack_api_result(delete_registration_token(props.user.jwt, id), props.setFlash)
|
||||||
|
.then(result => {
|
||||||
|
if(result !== undefined) {
|
||||||
|
refetchTokens();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddToken = () => {
|
||||||
|
let input = {
|
||||||
|
single_use: isSingleUse,
|
||||||
|
expires: expires,
|
||||||
|
}
|
||||||
|
|
||||||
|
if(props.user !== null) {
|
||||||
|
unpack_api_result(add_registration_token(props.user.jwt, input), props.setFlash)
|
||||||
|
.then(result => {
|
||||||
|
if(result !== undefined) {
|
||||||
|
refetchTokens();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refetchTokens()
|
||||||
|
}, [refetchTokens])
|
||||||
|
|
||||||
|
let columns = [
|
||||||
|
{
|
||||||
|
id: 'single_use',
|
||||||
|
label: 'Single Use',
|
||||||
|
minWidth: 30,
|
||||||
|
format: value => (value ? "Yes" : "No")
|
||||||
|
},
|
||||||
|
{ id: 'token', label: 'Token', minwidht: 300},
|
||||||
|
{
|
||||||
|
id: 'expires',
|
||||||
|
label: 'Expires',
|
||||||
|
minwidth: 200,
|
||||||
|
format: value => (moment(value).format('LL HH:mm'))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'delete_button',
|
||||||
|
label: '',
|
||||||
|
minwidth: 20,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||||
|
<div className="Add-GameNight">
|
||||||
|
<form autoComplete="off" onSubmit={e => { e.preventDefault(); }}>
|
||||||
|
<DateTimePicker
|
||||||
|
label="Token expires at"
|
||||||
|
variant="standard"
|
||||||
|
value={expires}
|
||||||
|
onChange={setExpires}
|
||||||
|
inputFormat="dd-MM-yyyy HH:mm"
|
||||||
|
renderInput={(params) => <TextField {...params} />}/>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label="Single use"
|
||||||
|
value={isSingleUse}
|
||||||
|
onChange={(e) => setIsSingleUse(e.target.checked)}/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="success"
|
||||||
|
onClick={handleAddToken}>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</LocalizationProvider>
|
||||||
|
|
||||||
|
<TableContainer sx={{ maxHeight: 440 }}>
|
||||||
|
<Table stickyHeader>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<TableCell
|
||||||
|
key={column.id}
|
||||||
|
align={column.align}
|
||||||
|
style={{ minWidth: column.minWidth }}
|
||||||
|
>
|
||||||
|
{column.label}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{registrationTokens
|
||||||
|
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||||
|
.map((row) => {
|
||||||
|
return (
|
||||||
|
<TableRow hover role="checkbox" tabIndex={-1} key={row.code}>
|
||||||
|
{columns.map((column) => {
|
||||||
|
const value = row[column.id];
|
||||||
|
return (
|
||||||
|
<TableCell key={column.id} align={column.align}>
|
||||||
|
{column.format
|
||||||
|
? column.format(value)
|
||||||
|
: value}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<TableCell>
|
||||||
|
<IconButton
|
||||||
|
edge="end"
|
||||||
|
color="error"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteToken(row.id)
|
||||||
|
}}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
{registrationTokens.length > rowsPerPage && <TablePagination
|
||||||
|
rowsPerPageOptions={[10, 25, 100]}
|
||||||
|
component="div"
|
||||||
|
count={registrationTokens.length}
|
||||||
|
rowsPerPage={rowsPerPage}
|
||||||
|
page={page}
|
||||||
|
onPageChange={handleChangePage}
|
||||||
|
onRowsPerPageChange={handleChangeRowsPerPage}/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminPanel
|
60
frontend/src/components/GameAdder.jsx
Normal file
60
frontend/src/components/GameAdder.jsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Autocomplete from '@mui/material/Autocomplete';
|
||||||
|
|
||||||
|
export default function GameAdder(props) {
|
||||||
|
|
||||||
|
const [value, setValue] = React.useState([]);
|
||||||
|
|
||||||
|
const emptyUuid = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
id="tags-filled"
|
||||||
|
options={props.games}
|
||||||
|
value={value}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
freeSolo
|
||||||
|
selectOnFocus
|
||||||
|
renderTags={(value, getTagProps) =>
|
||||||
|
value.map((option, index) => (
|
||||||
|
<Chip variant="outlined" label={option.name} {...getTagProps({ index })} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
variant="filled"
|
||||||
|
label="What games do you like to play?"
|
||||||
|
placeholder="monopoly"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
onChange={(event, newValue) => {
|
||||||
|
newValue = newValue.map(option => {
|
||||||
|
if (typeof option === 'string') {
|
||||||
|
var match = props.games.find(g => g.name.toLowerCase() === option.toLowerCase());
|
||||||
|
if(match !== undefined) {
|
||||||
|
return match
|
||||||
|
} else {
|
||||||
|
return {id: emptyUuid, name: option};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
newValue = newValue.filter((value, index, self) =>
|
||||||
|
index === self.findIndex((t) => (
|
||||||
|
t.id === value.id && t.name === value.name
|
||||||
|
))
|
||||||
|
);
|
||||||
|
setValue(newValue);
|
||||||
|
props.onChange(newValue);
|
||||||
|
}}
|
||||||
|
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
139
frontend/src/components/Gamenight.jsx
Normal file
139
frontend/src/components/Gamenight.jsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
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';
|
||||||
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
|
||||||
|
import moment from 'moment';
|
||||||
|
import {unpack_api_result, get_gamenight, patch_gamenight} from '../api/Api';
|
||||||
|
|
||||||
|
function Gamenight(props) {
|
||||||
|
|
||||||
|
const dense = true;
|
||||||
|
const [gamenight, setGamenight] = useState(null);
|
||||||
|
|
||||||
|
const fetchGamenight = useCallback(() => {
|
||||||
|
if (props.user !== null) {
|
||||||
|
unpack_api_result(get_gamenight(props.gamenightId, props.user.jwt), props.setFlash)
|
||||||
|
.then(result => {
|
||||||
|
if(result !== undefined) {
|
||||||
|
setGamenight(result.gamenight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [props.gamenightId, props.user, props.setFlash]);
|
||||||
|
|
||||||
|
useEffect(fetchGamenight, [fetchGamenight]);
|
||||||
|
|
||||||
|
let games = gamenight?.game_list.map(g =>
|
||||||
|
(
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText
|
||||||
|
primary={g.name}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const participants = gamenight?.participants.map(p =>
|
||||||
|
(
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText
|
||||||
|
primary={p.username}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const Join = () => {
|
||||||
|
const input = {
|
||||||
|
action: 'AddParticipant'
|
||||||
|
};
|
||||||
|
|
||||||
|
unpack_api_result(patch_gamenight(gamenight.id, input, props.user.jwt), props.setFlash)
|
||||||
|
.then(result => {
|
||||||
|
if(result !== undefined) {
|
||||||
|
fetchGamenight();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const Leave = () => {
|
||||||
|
const input = {
|
||||||
|
action: 'RemoveParticipant',
|
||||||
|
};
|
||||||
|
|
||||||
|
unpack_api_result(patch_gamenight(gamenight.id, input, props.user.jwt), props.setFlash)
|
||||||
|
.then(result => {
|
||||||
|
if(result !== undefined) {
|
||||||
|
fetchGamenight();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let join_or_leave_button;
|
||||||
|
if(gamenight?.participants.find(p => p.id === props.user.id) === undefined) {
|
||||||
|
join_or_leave_button = (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="success"
|
||||||
|
onClick={Join}>
|
||||||
|
Join
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
join_or_leave_button = (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
onClick={Leave}>
|
||||||
|
Leave
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<IconButton
|
||||||
|
aria-label="back"
|
||||||
|
onClick={(e) => props.onDismis()}>
|
||||||
|
<ArrowBackIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Typography type="h3">
|
||||||
|
{gamenight?.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography type="body1">
|
||||||
|
When: {moment(gamenight?.datetime).format('LL HH:mm')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<List
|
||||||
|
dense={dense}
|
||||||
|
aria-labelledby="games-subheader"
|
||||||
|
subheader={
|
||||||
|
<ListSubheader component="div" id="games-subheader">
|
||||||
|
Games:
|
||||||
|
</ListSubheader>
|
||||||
|
}>
|
||||||
|
{games}
|
||||||
|
</List>
|
||||||
|
<List
|
||||||
|
dense={dense}
|
||||||
|
aria-labelledby="participants-subheader"
|
||||||
|
subheader={
|
||||||
|
<ListSubheader component="div" id="participants-subheader">
|
||||||
|
Participants:
|
||||||
|
</ListSubheader>
|
||||||
|
}>
|
||||||
|
{participants}
|
||||||
|
</List>
|
||||||
|
{join_or_leave_button}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Gamenight
|
79
frontend/src/components/Gamenights.jsx
Normal file
79
frontend/src/components/Gamenights.jsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import ListItemAvatar from '@mui/material/ListItemAvatar';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import GamesIcon from '@mui/icons-material/Games';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
|
||||||
|
import AddGameNight from './AddGameNight';
|
||||||
|
import {delete_gamenight, unpack_api_result} from '../api/Api';
|
||||||
|
|
||||||
|
function Gamenights(props) {
|
||||||
|
const dense = true;
|
||||||
|
|
||||||
|
const DeleteGamenight = (game_id) => {
|
||||||
|
if (props.user !== null) {
|
||||||
|
const input = { game_id: game_id };
|
||||||
|
unpack_api_result(delete_gamenight(input, props.user.jwt), props.setFlash)
|
||||||
|
.then(result => {
|
||||||
|
if(result !== undefined) {
|
||||||
|
console.log("hello?");
|
||||||
|
props.refetchGamenights();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let gamenights = props.gamenights.map(g => {
|
||||||
|
let secondaryAction;
|
||||||
|
if(props.user.id === g.owner_id || props.user.role === 'Admin') {
|
||||||
|
secondaryAction = (
|
||||||
|
<IconButton
|
||||||
|
edge="end"
|
||||||
|
aria-label="delete"
|
||||||
|
color="error"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
DeleteGamenight(g.id)
|
||||||
|
}}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
component="nav"
|
||||||
|
onClick={(e) => props.onSelectGamenight(g)}
|
||||||
|
secondaryAction={
|
||||||
|
secondaryAction
|
||||||
|
}>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<GamesIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={g.name}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AddGameNight
|
||||||
|
user={props.user}
|
||||||
|
games={props.games}
|
||||||
|
setFlash={props.setFlash}
|
||||||
|
refetchGamenights={props.refetchGamenights} />
|
||||||
|
<List dense={dense}>
|
||||||
|
{gamenights}
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Gamenights
|
42
frontend/src/components/Login.jsx
Normal file
42
frontend/src/components/Login.jsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
function Login(props) {
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
const handleUsernameChange = (event) => {
|
||||||
|
setUsername(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordChange = (event) => {
|
||||||
|
setPassword(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = (event) => {
|
||||||
|
props.onChange({ username: username, password: password });
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="Login-Component">
|
||||||
|
<form onSubmit={handleLogin}>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Login</legend>
|
||||||
|
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input id="username" name="username" type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={handleUsernameChange} />
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input id="password" name="password" type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={handlePasswordChange} />
|
||||||
|
|
||||||
|
<input type="submit" value="Submit" />
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login
|
79
frontend/src/components/MenuBar.jsx
Normal file
79
frontend/src/components/MenuBar.jsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import AppBar from '@mui/material/AppBar';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
|
||||||
|
function MenuBar(props) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let userButton = null;
|
||||||
|
let logoutButton = null;
|
||||||
|
let adminPanelButton = null;
|
||||||
|
let registerButton = null;
|
||||||
|
if (props.user !== null) {
|
||||||
|
userButton = (
|
||||||
|
<Button
|
||||||
|
color="inherit"
|
||||||
|
onClick={props.onUser}>
|
||||||
|
{props.user.username}
|
||||||
|
</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 (
|
||||||
|
<AppBar position="static">
|
||||||
|
<Toolbar>
|
||||||
|
<IconButton
|
||||||
|
size="large"
|
||||||
|
edge="start"
|
||||||
|
color="inherit"
|
||||||
|
aria-label="menu"
|
||||||
|
sx={{ mr: 2 }}>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Typography
|
||||||
|
style={{cursor:'pointer'}}
|
||||||
|
variant="h6"
|
||||||
|
component="div"
|
||||||
|
sx={{ flexGrow: 1 }}
|
||||||
|
onClick={props.onReset}>
|
||||||
|
Gamenight!
|
||||||
|
</Typography>
|
||||||
|
{userButton !== null && userButton}
|
||||||
|
{registerButton !== null && registerButton}
|
||||||
|
{adminPanelButton !== null && adminPanelButton}
|
||||||
|
{logoutButton !== null && logoutButton}
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MenuBar;
|
98
frontend/src/components/Register.jsx
Normal file
98
frontend/src/components/Register.jsx
Normal 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;
|
13
frontend/src/index.css
Normal file
13
frontend/src/index.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
17
frontend/src/index.js
Normal file
17
frontend/src/index.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
|
reportWebVitals();
|
1
frontend/src/logo.svg
Normal file
1
frontend/src/logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
After Width: | Height: | Size: 2.6 KiB |
13
frontend/src/reportWebVitals.js
Normal file
13
frontend/src/reportWebVitals.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const reportWebVitals = onPerfEntry => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reportWebVitals;
|
5
frontend/src/setupTests.js
Normal file
5
frontend/src/setupTests.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||||
|
// allows you to do things like:
|
||||||
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import '@testing-library/jest-dom';
|
9
gamenight-api-client-rs/.gitignore
vendored
9
gamenight-api-client-rs/.gitignore
vendored
@ -1,9 +0,0 @@
|
|||||||
/target/
|
|
||||||
git_push.sh
|
|
||||||
**/*.rs.bk
|
|
||||||
Cargo.lock
|
|
||||||
docs
|
|
||||||
src
|
|
||||||
.travis.yml
|
|
||||||
.openapi-generator
|
|
||||||
.openapi-generator-ignore
|
|
@ -1,14 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "gamenight-api-client-rs"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["dennis@brentj.es"]
|
|
||||||
description = "Api specifaction for a Gamenight server"
|
|
||||||
license = "MIT"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
serde = { version = "^1.0", features = ["derive"] }
|
|
||||||
serde_json = "^1.0"
|
|
||||||
serde_repr = "^0.1"
|
|
||||||
url = "^2.5"
|
|
||||||
reqwest = { version = "^0.12", default-features = false, features = ["json", "multipart"] }
|
|
@ -1,60 +0,0 @@
|
|||||||
# Rust API client for gamenight-api-client-rs
|
|
||||||
|
|
||||||
Api specifaction for a Gamenight server
|
|
||||||
|
|
||||||
For more information, please visit [https://brentj.es](https://brentj.es)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client.
|
|
||||||
|
|
||||||
- API version: 1.0
|
|
||||||
- Package version: 0.1.0
|
|
||||||
- Generator version: 7.13.0
|
|
||||||
- Build package: `org.openapitools.codegen.languages.RustClientCodegen`
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Put the package under your project folder in a directory named `gamenight-api-client-rs` and add the following to `Cargo.toml` under `[dependencies]`:
|
|
||||||
|
|
||||||
```
|
|
||||||
gamenight-api-client-rs = { path = "./gamenight-api-client-rs" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Documentation for API Endpoints
|
|
||||||
|
|
||||||
All URIs are relative to *http://localhost:8080*
|
|
||||||
|
|
||||||
Class | Method | HTTP request | Description
|
|
||||||
------------ | ------------- | ------------- | -------------
|
|
||||||
*DefaultApi* | [**get_gamenight**](docs/DefaultApi.md#get_gamenight) | **GET** /gamenight |
|
|
||||||
*DefaultApi* | [**get_gamenights**](docs/DefaultApi.md#get_gamenights) | **GET** /gamenights | Your GET endpoint
|
|
||||||
*DefaultApi* | [**get_token**](docs/DefaultApi.md#get_token) | **GET** /token |
|
|
||||||
*DefaultApi* | [**post_gamenight**](docs/DefaultApi.md#post_gamenight) | **POST** /gamenight |
|
|
||||||
*DefaultApi* | [**post_register**](docs/DefaultApi.md#post_register) | **POST** /user |
|
|
||||||
*DefaultApi* | [**user_get**](docs/DefaultApi.md#user_get) | **GET** /user |
|
|
||||||
|
|
||||||
|
|
||||||
## Documentation For Models
|
|
||||||
|
|
||||||
- [AddGamenightRequestBody](docs/AddGamenightRequestBody.md)
|
|
||||||
- [Failure](docs/Failure.md)
|
|
||||||
- [Gamenight](docs/Gamenight.md)
|
|
||||||
- [GetGamenightRequest](docs/GetGamenightRequest.md)
|
|
||||||
- [GetToken401Response](docs/GetToken401Response.md)
|
|
||||||
- [Login](docs/Login.md)
|
|
||||||
- [Registration](docs/Registration.md)
|
|
||||||
- [Token](docs/Token.md)
|
|
||||||
- [User](docs/User.md)
|
|
||||||
|
|
||||||
|
|
||||||
To get access to the crate's generated documentation, use:
|
|
||||||
|
|
||||||
```
|
|
||||||
cargo doc --open
|
|
||||||
```
|
|
||||||
|
|
||||||
## Author
|
|
||||||
|
|
||||||
dennis@brentj.es
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
|||||||
use std::process::Command;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let _ =
|
|
||||||
Command::new("openapi-generator")
|
|
||||||
.args(["generate", "-i", "../backend-actix/gamenight-api.yaml", "-g", "rust", "--additional-properties=withSeparateModelsAndApi=true,modelPackage=gamenight_model,apiPackage=gamenight_api,packageName=gamenight-api-client-rs,packageVersion=0.1.0"])
|
|
||||||
.output()
|
|
||||||
.expect("Failed to generate models sources for the gamenight API");
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
|
|
||||||
#
|
|
||||||
# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com"
|
|
||||||
|
|
||||||
git_user_id=$1
|
|
||||||
git_repo_id=$2
|
|
||||||
release_note=$3
|
|
||||||
git_host=$4
|
|
||||||
|
|
||||||
if [ "$git_host" = "" ]; then
|
|
||||||
git_host="github.com"
|
|
||||||
echo "[INFO] No command line input provided. Set \$git_host to $git_host"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$git_user_id" = "" ]; then
|
|
||||||
git_user_id="GIT_USER_ID"
|
|
||||||
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$git_repo_id" = "" ]; then
|
|
||||||
git_repo_id="GIT_REPO_ID"
|
|
||||||
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$release_note" = "" ]; then
|
|
||||||
release_note="Minor update"
|
|
||||||
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Initialize the local directory as a Git repository
|
|
||||||
git init
|
|
||||||
|
|
||||||
# Adds the files in the local repository and stages them for commit.
|
|
||||||
git add .
|
|
||||||
|
|
||||||
# Commits the tracked changes and prepares them to be pushed to a remote repository.
|
|
||||||
git commit -m "$release_note"
|
|
||||||
|
|
||||||
# Sets the new remote
|
|
||||||
git_remote=$(git remote)
|
|
||||||
if [ "$git_remote" = "" ]; then # git remote not defined
|
|
||||||
|
|
||||||
if [ "$GIT_TOKEN" = "" ]; then
|
|
||||||
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
|
|
||||||
git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git
|
|
||||||
else
|
|
||||||
git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git
|
|
||||||
fi
|
|
||||||
|
|
||||||
fi
|
|
||||||
|
|
||||||
git pull origin master
|
|
||||||
|
|
||||||
# Pushes (Forces) the changes in the local repository up to the remote repository
|
|
||||||
echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git"
|
|
||||||
git push origin master 2>&1 | grep -v 'To https'
|
|
1
gamenight-cli/.gitignore
vendored
1
gamenight-cli/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
target
|
|
1503
gamenight-cli/Cargo.lock
generated
1503
gamenight-cli/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,12 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "gamenight-cli"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
gamenight-api-client-rs = { path = "../gamenight-api-client-rs" }
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
inquire = { version = "0.7.5", features = ["date"] }
|
|
||||||
async-trait = "0.1"
|
|
||||||
dyn-clone = "1.0"
|
|
||||||
chrono = "0.4"
|
|
@ -1,27 +0,0 @@
|
|||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
use chrono::{DateTime, Local};
|
|
||||||
use gamenight_api_client_rs::models;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Gamenight {
|
|
||||||
pub name: String,
|
|
||||||
pub start_time: DateTime<Local>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<models::Gamenight> for Gamenight {
|
|
||||||
fn from(value: models::Gamenight) -> Self {
|
|
||||||
Self {
|
|
||||||
name: value.name,
|
|
||||||
start_time: DateTime::parse_from_rfc3339(&value.datetime).unwrap().into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Display for Gamenight {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
writeln!(f, r#"
|
|
||||||
Name: {}
|
|
||||||
When: {}
|
|
||||||
"#, self.name, self.start_time.format("%d-%m-%Y %H:%M"))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
pub mod gamenight;
|
|
@ -1,49 +0,0 @@
|
|||||||
|
|
||||||
use gamenight_api_client_rs::{apis::default_api::post_gamenight, models};
|
|
||||||
use inquire::{CustomType, DateSelect, Text};
|
|
||||||
use chrono::{self, Local, NaiveTime};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct AddGamenight {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AddGamenight {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<'a> Flow<'a> for AddGamenight {
|
|
||||||
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
|
|
||||||
let mut add_gamenight = models::AddGamenightRequestBody::new();
|
|
||||||
|
|
||||||
add_gamenight.name = Some(Text::new("What should we call your gamenight")
|
|
||||||
.prompt()?);
|
|
||||||
let naive_date = DateSelect::new("When is your gamenight")
|
|
||||||
.prompt()?;
|
|
||||||
let naive_time = CustomType::<NaiveTime>::new("At What time")
|
|
||||||
.prompt()?;
|
|
||||||
add_gamenight.datetime = Some(naive_date
|
|
||||||
.and_time(naive_time)
|
|
||||||
.and_local_timezone(Local)
|
|
||||||
.earliest()
|
|
||||||
.unwrap()
|
|
||||||
.to_utc()
|
|
||||||
.to_rfc3339());
|
|
||||||
|
|
||||||
post_gamenight(&state.configuration, Some(add_gamenight)).await?;
|
|
||||||
|
|
||||||
Ok((FlowOutcome::Successful, state))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for AddGamenight {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "Add Gamenight.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Exit {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Exit {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<'a> Flow<'a> for Exit {
|
|
||||||
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
|
|
||||||
Ok((FlowOutcome::Abort, state))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Exit {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "Exit")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
|
|
||||||
use gamenight_api_client_rs::apis::default_api::get_gamenights;
|
|
||||||
use inquire::Select;
|
|
||||||
|
|
||||||
use crate::flows::view_gamenight::ViewGamenight;
|
|
||||||
|
|
||||||
use super::{exit::Exit, *};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ListGamenights {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ListGamenights {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<'a> Flow<'a> for ListGamenights {
|
|
||||||
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
|
|
||||||
let response = get_gamenights(&state.configuration).await?;
|
|
||||||
|
|
||||||
let mut view_flows = response.into_iter().map(|gamenight| -> Box<dyn Flow<'a> + Send> {
|
|
||||||
Box::new(ViewGamenight::new(gamenight.into()))
|
|
||||||
}).collect::<Vec<Box<dyn Flow<'a> + Send>>>();
|
|
||||||
view_flows.push(Box::new(Exit::new()));
|
|
||||||
|
|
||||||
let choice = Select::new("What gamenight would you like to view?", view_flows)
|
|
||||||
.prompt_skippable()?;
|
|
||||||
|
|
||||||
handle_choice_option(&choice, self, state).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for ListGamenights {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "List all gamenights")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
use gamenight_api_client_rs::{apis::{configuration::Configuration, default_api::get_token}, models};
|
|
||||||
use inquire::{Password, Text};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Login {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Login {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<'a> Flow<'a> for Login {
|
|
||||||
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
|
|
||||||
let configuration = Configuration::new();
|
|
||||||
|
|
||||||
let username = Text::new("What is your login?").prompt()?;
|
|
||||||
let password = Password::new("what is your password?")
|
|
||||||
.without_confirmation()
|
|
||||||
.prompt()?;
|
|
||||||
|
|
||||||
let login = models::Login::new(username, password);
|
|
||||||
|
|
||||||
let result = get_token(&configuration, Some(login)).await?;
|
|
||||||
if let Some(token) = result.jwt_token {
|
|
||||||
state.configuration.bearer_access_token = Some(token);
|
|
||||||
Ok((FlowOutcome::Successful, state))
|
|
||||||
} else {
|
|
||||||
Err(FlowError{error: "Unexpected response".to_string()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Login {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "Login")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
|
|
||||||
use super::{exit::Exit, add_gamenight::AddGamenight, list_gamenights::ListGamenights, login::Login, main_menu::MainMenu, *};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Main {
|
|
||||||
login: Box<dyn for<'a> Flow<'a>>,
|
|
||||||
main_menu: MainMenu
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Main {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let mut main_menu = MainMenu::new();
|
|
||||||
main_menu.menu.push(Box::new(ListGamenights::new()));
|
|
||||||
main_menu.menu.push(Box::new(AddGamenight::new()));
|
|
||||||
main_menu.menu.push(Box::new(Exit::new()));
|
|
||||||
Self {
|
|
||||||
login: Box::new(Login::new()),
|
|
||||||
main_menu
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Main {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<'a> Flow<'a> for Main {
|
|
||||||
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
|
|
||||||
let (_outcome, state) = self.login.run(state).await?;
|
|
||||||
let (_outcome, state) = self.main_menu.run(state).await?;
|
|
||||||
Ok((FlowOutcome::Successful, state))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Main {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "main")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
|
|
||||||
use inquire::{ui::RenderConfig, Select};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct MainMenu {
|
|
||||||
pub menu: Vec<Box<dyn for<'a> Flow<'a> + Send>>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MainMenu {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
MainMenu {
|
|
||||||
menu: vec![]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl Send for MainMenu {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<'a> Flow<'a> for MainMenu {
|
|
||||||
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
|
|
||||||
let choice = Select::new("What would you like to do?", self.menu.clone())
|
|
||||||
.with_help_message("Select the action you want to take or quit the program")
|
|
||||||
.with_render_config(RenderConfig {
|
|
||||||
option_index_prefix: inquire::ui::IndexPrefix::Simple,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.prompt_skippable()?;
|
|
||||||
|
|
||||||
handle_choice_option(&choice, self, state).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for MainMenu {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "Main menu")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use chrono::ParseError;
|
|
||||||
use gamenight_api_client_rs::apis::configuration::Configuration;
|
|
||||||
use inquire::InquireError;
|
|
||||||
use dyn_clone::DynClone;
|
|
||||||
|
|
||||||
pub mod main;
|
|
||||||
mod login;
|
|
||||||
mod main_menu;
|
|
||||||
mod exit;
|
|
||||||
mod list_gamenights;
|
|
||||||
mod add_gamenight;
|
|
||||||
mod view_gamenight;
|
|
||||||
|
|
||||||
pub struct GamenightState {
|
|
||||||
configuration: Configuration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GamenightState {
|
|
||||||
pub fn new() -> Self{
|
|
||||||
Self {
|
|
||||||
configuration: Configuration::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for GamenightState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FlowError {
|
|
||||||
pub error: String
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<InquireError> for FlowError {
|
|
||||||
fn from(value: InquireError) -> Self {
|
|
||||||
Self {
|
|
||||||
error: value.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> From<gamenight_api_client_rs::apis::Error<T>> for FlowError {
|
|
||||||
fn from(value: gamenight_api_client_rs::apis::Error<T>) -> Self {
|
|
||||||
Self {
|
|
||||||
error: value.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ParseError> for FlowError {
|
|
||||||
fn from(value: ParseError) -> Self {
|
|
||||||
Self {
|
|
||||||
error: value.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
pub enum FlowOutcome {
|
|
||||||
Successful,
|
|
||||||
Bool(bool),
|
|
||||||
String(String),
|
|
||||||
Abort
|
|
||||||
}
|
|
||||||
|
|
||||||
type FlowResult<'a> = Result<(FlowOutcome, &'a mut GamenightState), FlowError>;
|
|
||||||
|
|
||||||
dyn_clone::clone_trait_object!(for<'a> Flow<'a>);
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait Flow<'a>: Sync + DynClone + Send + Display {
|
|
||||||
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a>;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_choice<'a>(choice: &Box<dyn Flow<'a> + Send>, flow: &dyn Flow<'a>, state: &'a mut GamenightState) -> FlowResult<'a> {
|
|
||||||
let (outcome, new_state) = choice.run(state).await?;
|
|
||||||
|
|
||||||
if outcome == FlowOutcome::Abort {
|
|
||||||
Ok((FlowOutcome::Successful, new_state))
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
flow.run(new_state).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_choice_option<'a>(choice: &Option<Box<dyn Flow<'a> + Send>>, flow: &dyn Flow<'a>, state: &'a mut GamenightState) -> FlowResult<'a> {
|
|
||||||
if let Some(choice) = choice {
|
|
||||||
handle_choice(choice, flow, state).await
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Ok((FlowOutcome::Abort, state))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
|
|
||||||
use inquire::Select;
|
|
||||||
|
|
||||||
use crate::{domain::gamenight::Gamenight, flows::exit::Exit};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ViewGamenight {
|
|
||||||
gamenight: Gamenight
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ViewGamenight {
|
|
||||||
pub fn new(gamenight: Gamenight) -> Self {
|
|
||||||
Self {
|
|
||||||
gamenight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<'a> Flow<'a> for ViewGamenight {
|
|
||||||
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
|
|
||||||
|
|
||||||
print!("{}", self.gamenight);
|
|
||||||
let options: Vec<Box<dyn Flow<'a> + Send>> = vec![
|
|
||||||
Box::new(Exit::new())
|
|
||||||
];
|
|
||||||
let choice = Select::new("What do you want to do:", options)
|
|
||||||
.prompt_skippable()?;
|
|
||||||
|
|
||||||
handle_choice_option(&choice, self, state).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for ViewGamenight {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{} {}", self.gamenight.name, self.gamenight.start_time.format("%d-%m-%Y %H:%M"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
|||||||
pub mod flows;
|
|
||||||
pub mod domain;
|
|
||||||
|
|
||||||
use flows::{main::Main, Flow, GamenightState};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let mut state = GamenightState::new();
|
|
||||||
let mainflow = Main::new();
|
|
||||||
if let Err(x) = mainflow.run(&mut state).await {
|
|
||||||
println!("{}", x.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
1
gamenight-database/.gitignore
vendored
1
gamenight-database/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
target
|
|
@ -1,15 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "gamenight-database"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
diesel = { version = "2.0", features = ["postgres", "r2d2", "uuid", "chrono"] }
|
|
||||||
diesel-derive-enum = { version = "2.0", features = ["postgres"] }
|
|
||||||
diesel_migrations = "2.0"
|
|
||||||
argon2 = { version = "0.5", features = ["std"] }
|
|
||||||
uuid = { version = "1.17.0", features = ["serde", "v4"] }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
|
||||||
rand_core = "0.9"
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
|||||||
[print_schema]
|
|
||||||
file = "src/schema/schema.rs"
|
|
@ -1,13 +0,0 @@
|
|||||||
pub struct DatabaseError(pub String);
|
|
||||||
|
|
||||||
impl From<diesel::result::Error> for DatabaseError {
|
|
||||||
fn from(value: diesel::result::Error) -> Self {
|
|
||||||
DatabaseError(value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<argon2::password_hash::Error> for DatabaseError {
|
|
||||||
fn from(value: argon2::password_hash::Error) -> Self {
|
|
||||||
DatabaseError(value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
use chrono::{DateTime, Utc};
|
|
||||||
use diesel::{Insertable, Queryable, PgConnection, RunQueryDsl, insert_into, QueryDsl};
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
use crate::schema::gamenight;
|
|
||||||
|
|
||||||
use super::error::DatabaseError;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
|
|
||||||
#[diesel(table_name = gamenight)]
|
|
||||||
pub struct Gamenight {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub name: String,
|
|
||||||
pub datetime: DateTime<Utc>,
|
|
||||||
pub owner_id: Uuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gamenights(conn: &mut PgConnection) -> Result<Vec::<Gamenight>, DatabaseError> {
|
|
||||||
Ok(gamenight::table.load::<Gamenight>(conn)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_gamenight(conn: &mut PgConnection, gamenight: Gamenight) -> Result<usize, DatabaseError> {
|
|
||||||
Ok(insert_into(gamenight::table).values(&gamenight).execute(conn)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_gamenight(conn: &mut PgConnection, id: Uuid) -> Result<Gamenight, DatabaseError> {
|
|
||||||
Ok(gamenight::table.find(id).first(conn)?)
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
use diesel::{ExpressionMethods, Insertable, QueryDsl, Queryable, RunQueryDsl};
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::schema::gamenight_participant;
|
|
||||||
|
|
||||||
use super::error::DatabaseError;
|
|
||||||
use super::DbConnection;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
|
|
||||||
#[diesel(belongs_to(Gamenight))]
|
|
||||||
#[diesel(belongs_to(User))]
|
|
||||||
#[diesel(table_name = gamenight_participant)]
|
|
||||||
pub struct GamenightParticipant {
|
|
||||||
pub gamenight_id: Uuid,
|
|
||||||
pub user_id: Uuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_participants(conn: &mut DbConnection, id: &Uuid) -> Result<Vec<Uuid>, DatabaseError> {
|
|
||||||
Ok(gamenight_participant::table
|
|
||||||
.filter(gamenight_participant::gamenight_id.eq(&id))
|
|
||||||
.select(gamenight_participant::user_id)
|
|
||||||
.get_results(conn)?)
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
pub mod user;
|
|
||||||
pub mod error;
|
|
||||||
pub mod schema;
|
|
||||||
pub mod gamenight;
|
|
||||||
pub mod gamenight_participants;
|
|
||||||
|
|
||||||
use diesel::r2d2::ConnectionManager;
|
|
||||||
use diesel::r2d2::ManageConnection;
|
|
||||||
use diesel::r2d2::Pool;
|
|
||||||
use diesel::r2d2::PooledConnection;
|
|
||||||
use diesel::PgConnection;
|
|
||||||
use diesel_migrations::embed_migrations;
|
|
||||||
use diesel_migrations::EmbeddedMigrations;
|
|
||||||
|
|
||||||
use diesel_migrations::MigrationHarness;
|
|
||||||
pub use user::login;
|
|
||||||
pub use user::register;
|
|
||||||
pub use gamenight::gamenights;
|
|
||||||
pub use gamenight_participants::get_participants;
|
|
||||||
|
|
||||||
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
|
|
||||||
pub type DbConnection = PgConnection;
|
|
||||||
pub type DbPool = Pool<ConnectionManager<DbConnection>>;
|
|
||||||
|
|
||||||
pub fn run_migration(conn: &mut DbConnection) {
|
|
||||||
conn.run_pending_migrations(MIGRATIONS).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_connection_pool(url: &str) -> DbPool {
|
|
||||||
let manager = ConnectionManager::<PgConnection>::new(url);
|
|
||||||
// Refer to the `r2d2` documentation for more methods to use
|
|
||||||
// when building a connection pool
|
|
||||||
Pool::builder()
|
|
||||||
.test_on_check_out(true)
|
|
||||||
.build(manager)
|
|
||||||
.expect("Could not build connection pool")
|
|
||||||
}
|
|
||||||
pub trait GetConnection<T> where T: ManageConnection {
|
|
||||||
fn get_conn(&self) -> PooledConnection<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: ManageConnection> GetConnection<T> for Pool<T> {
|
|
||||||
fn get_conn(&self) -> PooledConnection<T> {
|
|
||||||
self.get().expect("Couldn't get db connection from pool")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
// @generated automatically by Diesel CLI.
|
|
||||||
|
|
||||||
pub mod sql_types {
|
|
||||||
#[derive(diesel::sql_types::SqlType)]
|
|
||||||
#[diesel(postgres_type(name = "role"))]
|
|
||||||
pub struct Role;
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
gamenight (id) {
|
|
||||||
id -> Uuid,
|
|
||||||
name -> Varchar,
|
|
||||||
datetime -> Timestamptz,
|
|
||||||
owner_id -> Uuid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
gamenight_gamelist (gamenight_id, game_id) {
|
|
||||||
gamenight_id -> Uuid,
|
|
||||||
game_id -> Uuid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
gamenight_participant (gamenight_id, user_id) {
|
|
||||||
gamenight_id -> Uuid,
|
|
||||||
user_id -> Uuid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
known_games (id) {
|
|
||||||
id -> Uuid,
|
|
||||||
name -> Varchar,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
pwd (user_id) {
|
|
||||||
user_id -> Uuid,
|
|
||||||
password -> Varchar,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
registration_tokens (id) {
|
|
||||||
id -> Uuid,
|
|
||||||
#[max_length = 32]
|
|
||||||
token -> Bpchar,
|
|
||||||
single_use -> Bool,
|
|
||||||
expires -> Nullable<Timestamptz>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
use diesel::sql_types::*;
|
|
||||||
use super::sql_types::Role;
|
|
||||||
|
|
||||||
users (id) {
|
|
||||||
id -> Uuid,
|
|
||||||
username -> Varchar,
|
|
||||||
email -> Varchar,
|
|
||||||
role -> Role,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::joinable!(gamenight -> users (owner_id));
|
|
||||||
diesel::joinable!(gamenight_gamelist -> gamenight (gamenight_id));
|
|
||||||
diesel::joinable!(gamenight_gamelist -> known_games (game_id));
|
|
||||||
diesel::joinable!(gamenight_participant -> gamenight (gamenight_id));
|
|
||||||
diesel::joinable!(gamenight_participant -> users (user_id));
|
|
||||||
diesel::joinable!(pwd -> users (user_id));
|
|
||||||
|
|
||||||
diesel::allow_tables_to_appear_in_same_query!(
|
|
||||||
gamenight,
|
|
||||||
gamenight_gamelist,
|
|
||||||
gamenight_participant,
|
|
||||||
known_games,
|
|
||||||
pwd,
|
|
||||||
registration_tokens,
|
|
||||||
users,
|
|
||||||
);
|
|
@ -1,83 +0,0 @@
|
|||||||
// @generated automatically by Diesel CLI.
|
|
||||||
|
|
||||||
pub mod sql_types {
|
|
||||||
#[derive(diesel::sql_types::SqlType)]
|
|
||||||
#[diesel(postgres_type(name = "role"))]
|
|
||||||
pub struct Role;
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
gamenight (id) {
|
|
||||||
id -> Uuid,
|
|
||||||
name -> Varchar,
|
|
||||||
datetime -> Timestamptz,
|
|
||||||
owner_id -> Uuid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
gamenight_gamelist (gamenight_id, game_id) {
|
|
||||||
gamenight_id -> Uuid,
|
|
||||||
game_id -> Uuid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
gamenight_participant (gamenight_id, user_id) {
|
|
||||||
gamenight_id -> Uuid,
|
|
||||||
user_id -> Uuid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
known_games (id) {
|
|
||||||
id -> Uuid,
|
|
||||||
name -> Varchar,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
pwd (user_id) {
|
|
||||||
user_id -> Uuid,
|
|
||||||
password -> Varchar,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
registration_tokens (id) {
|
|
||||||
id -> Uuid,
|
|
||||||
#[max_length = 32]
|
|
||||||
token -> Bpchar,
|
|
||||||
single_use -> Bool,
|
|
||||||
expires -> Nullable<Timestamptz>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
use diesel::sql_types::*;
|
|
||||||
use super::sql_types::Role;
|
|
||||||
|
|
||||||
users (id) {
|
|
||||||
id -> Uuid,
|
|
||||||
username -> Varchar,
|
|
||||||
email -> Varchar,
|
|
||||||
role -> Role,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::joinable!(gamenight -> users (owner_id));
|
|
||||||
diesel::joinable!(gamenight_gamelist -> gamenight (gamenight_id));
|
|
||||||
diesel::joinable!(gamenight_gamelist -> known_games (game_id));
|
|
||||||
diesel::joinable!(gamenight_participant -> gamenight (gamenight_id));
|
|
||||||
diesel::joinable!(gamenight_participant -> users (user_id));
|
|
||||||
diesel::joinable!(pwd -> users (user_id));
|
|
||||||
|
|
||||||
diesel::allow_tables_to_appear_in_same_query!(
|
|
||||||
gamenight,
|
|
||||||
gamenight_gamelist,
|
|
||||||
gamenight_participant,
|
|
||||||
known_games,
|
|
||||||
pwd,
|
|
||||||
registration_tokens,
|
|
||||||
users,
|
|
||||||
);
|
|
@ -1,140 +0,0 @@
|
|||||||
use argon2::password_hash::Salt;
|
|
||||||
use diesel::Connection;
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, Insertable, Queryable};
|
|
||||||
use diesel_derive_enum::DbEnum;
|
|
||||||
use argon2::password_hash::SaltString;
|
|
||||||
use argon2::PasswordHash;
|
|
||||||
use argon2::PasswordVerifier;
|
|
||||||
use argon2::Argon2;
|
|
||||||
use argon2::password_hash::PasswordHasher;
|
|
||||||
use argon2::password_hash::rand_core::OsRng;
|
|
||||||
use argon2::password_hash::rand_core::RngCore;
|
|
||||||
use crate::DbConnection;
|
|
||||||
|
|
||||||
use super::schema::{pwd, users};
|
|
||||||
pub use super::error::DatabaseError;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
|
|
||||||
#[diesel(table_name = pwd)]
|
|
||||||
struct Pwd {
|
|
||||||
user_id: Uuid,
|
|
||||||
password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
|
|
||||||
#[diesel(table_name = users)]
|
|
||||||
pub struct User {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub username: String,
|
|
||||||
pub email: String,
|
|
||||||
pub role: Role,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(DbEnum, Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
|
|
||||||
#[ExistingTypePath = "crate::schema::sql_types::Role"]
|
|
||||||
pub enum Role {
|
|
||||||
Admin,
|
|
||||||
User
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LoginUser {
|
|
||||||
pub username: String,
|
|
||||||
pub password: String
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct LoginResult {
|
|
||||||
pub result: bool,
|
|
||||||
pub user: Option<User>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct RegisterResult {
|
|
||||||
pub result: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct Register {
|
|
||||||
pub username: String,
|
|
||||||
pub email: String,
|
|
||||||
pub password: String
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn login(conn: &mut DbConnection, user: LoginUser) -> Result<Option<User>, DatabaseError> {
|
|
||||||
let id: Uuid = users::table
|
|
||||||
.filter(users::username.eq(&user.username))
|
|
||||||
.or_filter(users::email.eq(&user.username))
|
|
||||||
.select(users::id)
|
|
||||||
.first(conn)?;
|
|
||||||
|
|
||||||
let pwd: String = pwd::table
|
|
||||||
.filter(pwd::user_id.eq(id))
|
|
||||||
.select(pwd::password)
|
|
||||||
.first(conn)?;
|
|
||||||
|
|
||||||
let parsed_hash = PasswordHash::new(&pwd)?;
|
|
||||||
|
|
||||||
if Argon2::default()
|
|
||||||
.verify_password(user.password.as_bytes(), &parsed_hash)
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
Ok(Some(users::table.find(id).first(conn)?))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_user(conn: &mut DbConnection, id: Uuid) -> Result<User, DatabaseError> {
|
|
||||||
Ok(users::table.find(id).first(conn)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn register(conn: &mut DbConnection, register: Register) -> Result<(), DatabaseError> {
|
|
||||||
let mut bytes = [0u8; Salt::RECOMMENDED_LENGTH];
|
|
||||||
OsRng.try_fill_bytes(&mut bytes).unwrap();
|
|
||||||
let salt = SaltString::encode_b64(&bytes).unwrap();
|
|
||||||
let argon2 = Argon2::default();
|
|
||||||
|
|
||||||
let password_hash = argon2
|
|
||||||
.hash_password(register.password.as_bytes(), &salt)?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
conn.transaction(|c| {
|
|
||||||
let id = Uuid::new_v4();
|
|
||||||
|
|
||||||
diesel::insert_into(users::table)
|
|
||||||
.values(User {
|
|
||||||
id,
|
|
||||||
username: register.username,
|
|
||||||
email: register.email,
|
|
||||||
role: Role::User,
|
|
||||||
})
|
|
||||||
.execute(c)?;
|
|
||||||
|
|
||||||
diesel::insert_into(pwd::table)
|
|
||||||
.values(Pwd {
|
|
||||||
user_id: id,
|
|
||||||
password: password_hash,
|
|
||||||
})
|
|
||||||
.execute(c)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn count_users_with_username(conn: &mut DbConnection, username: &String) -> Result<i64, DatabaseError> {
|
|
||||||
Ok(users::table
|
|
||||||
.count()
|
|
||||||
.filter(users::username.eq(username))
|
|
||||||
.get_result::<i64>(conn)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn count_users_with_email(conn: &mut DbConnection, email: &String) -> Result<i64, DatabaseError> {
|
|
||||||
Ok(users::table
|
|
||||||
.count()
|
|
||||||
.filter(users::email.eq(email))
|
|
||||||
.get_result::<i64>(conn)?)
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user