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
|
||||
|
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,30 +0,0 @@
|
||||
use std::{fs::{exists, read_dir, remove_dir_all, File}, io::Write, process::Command};
|
||||
|
||||
|
||||
fn main() {
|
||||
println!("cargo::rerun-if-changed=gamenight-api.yaml");
|
||||
|
||||
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,447 +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: []
|
||||
post:
|
||||
summary: ''
|
||||
operationId: post-token
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/TokenResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/FailureResponse'
|
||||
description: Refresh your JWT-token without logging in again.
|
||||
parameters: []
|
||||
security:
|
||||
- JWT-Auth: []
|
||||
/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: []
|
||||
security:
|
||||
- JWT-Auth: []
|
||||
get:
|
||||
description: 'Get a user from primary id'
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/GetUserRequest'
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/UserResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/FailureResponse'
|
||||
'404':
|
||||
$ref: '#/components/responses/FailureResponse'
|
||||
'422':
|
||||
$ref: '#/components/responses/FailureResponse'
|
||||
security:
|
||||
- JWT-Auth: []
|
||||
/gamenights:
|
||||
get:
|
||||
summary: Get a all gamenights
|
||||
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.
|
||||
/participants:
|
||||
get:
|
||||
summary: Get all participants for a gamenight
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/ParticipantsResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/FailureResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/FailureResponse'
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/GetParticipants'
|
||||
security:
|
||||
- JWT-Auth: []
|
||||
description: Retrieve the participants of a single gamenight by id.
|
||||
/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: []
|
||||
/join:
|
||||
post:
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'401':
|
||||
$ref: '#/components/responses/FailureResponse'
|
||||
'422':
|
||||
$ref: '#/components/responses/FailureResponse'
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/JoinGamenight'
|
||||
security:
|
||||
- JWT-Auth: []
|
||||
/leave:
|
||||
post:
|
||||
responses:
|
||||
'200':
|
||||
description: "OK"
|
||||
'401':
|
||||
$ref: '#/components/responses/FailureResponse'
|
||||
'422':
|
||||
$ref: '#/components/responses/FailureResponse'
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/LeaveGamenight'
|
||||
security:
|
||||
- JWT-Auth: []
|
||||
/games:
|
||||
get:
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/GamesResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/FailureResponse'
|
||||
'422':
|
||||
$ref: '#/components/responses/FailureResponse'
|
||||
security:
|
||||
- JWT-Auth: []
|
||||
/game:
|
||||
get:
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/GameResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/FailureResponse'
|
||||
'422':
|
||||
$ref: '#/components/responses/FailureResponse'
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/GetGameRequest'
|
||||
security:
|
||||
- JWT-Auth: []
|
||||
post:
|
||||
responses:
|
||||
'200':
|
||||
description: "OK"
|
||||
'401':
|
||||
$ref: '#/components/responses/FailureResponse'
|
||||
'422':
|
||||
$ref: '#/components/responses/FailureResponse'
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/AddGameRequest'
|
||||
security:
|
||||
- JWT-Auth: []
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Gamenight:
|
||||
title: Gamenight
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
datetime:
|
||||
type: string
|
||||
owner_id:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- datetime
|
||||
- owner_id
|
||||
Participants:
|
||||
title: participants
|
||||
type: object
|
||||
properties:
|
||||
participants:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- participants
|
||||
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
|
||||
UserId:
|
||||
title: UserId
|
||||
type: object
|
||||
properties:
|
||||
user_id:
|
||||
type: string
|
||||
required:
|
||||
- user_id
|
||||
AddGamenightRequestBody:
|
||||
title: AddGamenightRequestBody
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
datetime:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- datetime
|
||||
GamenightId:
|
||||
title: GamenightId
|
||||
type: object
|
||||
properties:
|
||||
gamenight_id:
|
||||
type: string
|
||||
required:
|
||||
- gamenight_id
|
||||
GameId:
|
||||
title: GameId
|
||||
type: object
|
||||
properties:
|
||||
game_id:
|
||||
type: string
|
||||
required:
|
||||
- game_id
|
||||
GetGamenightRequestBody:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- username
|
||||
Game:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
AddGameRequestBody:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
|
||||
requestBodies:
|
||||
LoginRequest:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Login'
|
||||
RegisterRequest:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Registration'
|
||||
GetUserRequest:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserId'
|
||||
AddGamenight:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AddGamenightRequestBody'
|
||||
GetGamenight:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GetGamenightRequestBody'
|
||||
GetParticipants:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GamenightId'
|
||||
JoinGamenight:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GamenightId'
|
||||
LeaveGamenight:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GamenightId'
|
||||
GetGameRequest:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GameId'
|
||||
AddGameRequest:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AddGameRequestBody'
|
||||
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'
|
||||
ParticipantsResponse:
|
||||
description: Response with a list of participant uuids
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Participants'
|
||||
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'
|
||||
GamesResponse:
|
||||
description: A list of Games in this gamenight instance.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Game'
|
||||
GameResponse:
|
||||
description: A game.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Game'
|
||||
securitySchemes:
|
||||
JWT-Auth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: ''
|
||||
|
@ -1,57 +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(refresh)
|
||||
.service(register)
|
||||
.service(gamenights)
|
||||
.service(gamenight_post)
|
||||
.service(gamenight_get)
|
||||
.service(get_user)
|
||||
.service(get_user_unauthenticated)
|
||||
.service(post_join_gamenight)
|
||||
.service(post_leave_gamenight)
|
||||
.service(get_get_participants)
|
||||
.service(get_games)
|
||||
.service(post_game)
|
||||
})
|
||||
.bind(("::1", 8080))?
|
||||
.run()
|
||||
.await
|
||||
}
|
@ -1,77 +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, User}, DbPool};
|
||||
|
||||
use super::error::ApiError;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
exp: i64,
|
||||
uid: Uuid
|
||||
}
|
||||
|
||||
pub struct AuthUser(pub User);
|
||||
|
||||
// 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(value)
|
||||
}
|
||||
}
|
||||
|
||||
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,39 +0,0 @@
|
||||
use actix_web::{get, http::header::ContentType, post, web, HttpResponse, Responder};
|
||||
use gamenight_database::{game::insert_game, DbPool, GetConnection};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{models::{add_game_request_body::AddGameRequestBody, game::Game}, request::{authorization::AuthUser, error::ApiError}};
|
||||
|
||||
#[get("/games")]
|
||||
pub async fn get_games(pool: web::Data<DbPool>, _user: AuthUser) -> Result<impl Responder, ApiError> {
|
||||
let mut conn = pool.get_conn();
|
||||
let games: Vec<gamenight_database::game::Game> = gamenight_database::games(&mut conn)?;
|
||||
let model: Vec<Game> = games.iter().map(|x| {
|
||||
Game {
|
||||
id: x.id.to_string(),
|
||||
name: x.name.clone(),
|
||||
}}
|
||||
).collect();
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
.body(serde_json::to_string(&model)?)
|
||||
)
|
||||
}
|
||||
|
||||
impl From<AddGameRequestBody> for gamenight_database::game::Game {
|
||||
fn from(value: AddGameRequestBody) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
name: value.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/game")]
|
||||
pub async fn post_game(pool: web::Data<DbPool>, _user: AuthUser, game_data: web::Json<AddGameRequestBody>) -> Result<impl Responder, ApiError> {
|
||||
let mut conn = pool.get_conn();
|
||||
insert_game(&mut conn, game_data.0.into())?;
|
||||
|
||||
Ok(HttpResponse::Ok())
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
use actix_web::{get, web, Responder, http::header::ContentType, HttpResponse, post};
|
||||
use chrono::{DateTime, ParseError};
|
||||
use uuid::Uuid;
|
||||
|
||||
use gamenight_database::{gamenight, DbPool, GetConnection};
|
||||
|
||||
use crate::{models::{add_gamenight_request_body::AddGamenightRequestBody, gamenight::Gamenight, get_gamenight_request_body::GetGamenightRequestBody}, request::authorization::AuthUser};
|
||||
use crate::request::error::ApiError;
|
||||
|
||||
|
||||
impl AddGamenightRequestBody {
|
||||
pub fn into_with_user(&self, user: AuthUser) -> Result<gamenight::Gamenight, ParseError> {
|
||||
Ok(gamenight::Gamenight {
|
||||
datetime: DateTime::parse_from_rfc3339(&self.datetime)?.with_timezone(&chrono::Utc),
|
||||
id: Uuid::new_v4(),
|
||||
name: self.name.clone(),
|
||||
owner_id: user.0.id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GetGamenightRequestBody> for Uuid {
|
||||
fn from(value: GetGamenightRequestBody) -> 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::Gamenight> = gamenight_database::gamenights(&mut conn)?;
|
||||
let model: Vec<Gamenight> = gamenights.iter().map(|x| {
|
||||
Gamenight {
|
||||
id: x.id.to_string(),
|
||||
name: x.name.clone(),
|
||||
datetime: x.datetime.to_rfc3339(),
|
||||
owner_id: x.owner_id.to_string()
|
||||
}}
|
||||
).collect();
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
.body(serde_json::to_string(&model)?)
|
||||
)
|
||||
}
|
||||
|
||||
#[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::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<GetGamenightRequestBody>) -> Result<impl Responder, ApiError> {
|
||||
let mut conn = pool.get_conn();
|
||||
let gamenight = gamenight::get_gamenight(&mut conn, gamenight_data.into_inner().into())?;
|
||||
let model = Gamenight{
|
||||
id: gamenight.id.to_string(),
|
||||
datetime: gamenight.datetime.to_rfc3339(),
|
||||
name: gamenight.name,
|
||||
owner_id: gamenight.owner_id.to_string(),
|
||||
};
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
.body(serde_json::to_string(&model)?))
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
use actix_web::{post, web, HttpResponse, Responder};
|
||||
use gamenight_database::{gamenight_participants::{delete_gamenight_participant, insert_gamenight_participant, GamenightParticipant}, DbPool, GetConnection};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{models::gamenight_id::GamenightId, request::{authorization::AuthUser, error::ApiError}};
|
||||
|
||||
#[post("/join")]
|
||||
pub async fn post_join_gamenight(pool: web::Data<DbPool>, user: AuthUser, gamenight_id: web::Json<GamenightId>) -> Result<impl Responder, ApiError> {
|
||||
web::block(move || -> Result<usize, ApiError> {
|
||||
let mut conn = pool.get_conn();
|
||||
Ok(insert_gamenight_participant(&mut conn, GamenightParticipant {
|
||||
gamenight_id: Uuid::parse_str(&gamenight_id.gamenight_id)?,
|
||||
user_id: user.0.id
|
||||
})?)
|
||||
}).await??;
|
||||
|
||||
Ok(HttpResponse::Ok())
|
||||
}
|
||||
|
||||
#[post("/leave")]
|
||||
pub async fn post_leave_gamenight(pool: web::Data<DbPool>, user: AuthUser, gamenight_id: web::Json<GamenightId>) -> Result<impl Responder, ApiError> {
|
||||
web::block(move || -> Result<usize, ApiError> {
|
||||
let mut conn = pool.get_conn();
|
||||
let participant = GamenightParticipant {
|
||||
gamenight_id: Uuid::parse_str(&gamenight_id.gamenight_id)?,
|
||||
user_id: user.0.id
|
||||
};
|
||||
println!("{:?}", participant);
|
||||
let x = delete_gamenight_participant(&mut conn, participant)?;
|
||||
|
||||
println!("Amount of deleted rows: {:?}", x);
|
||||
|
||||
Ok(x)
|
||||
}).await??;
|
||||
|
||||
Ok(HttpResponse::Ok())
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
|
||||
mod user_handlers;
|
||||
mod gamenight_handlers;
|
||||
mod error;
|
||||
mod authorization;
|
||||
mod join_gamenight;
|
||||
mod participant_handlers;
|
||||
mod game;
|
||||
|
||||
pub use user_handlers::login;
|
||||
pub use user_handlers::refresh;
|
||||
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;
|
||||
pub use join_gamenight::post_join_gamenight;
|
||||
pub use join_gamenight::post_leave_gamenight;
|
||||
pub use participant_handlers::get_get_participants;
|
||||
pub use game::get_games;
|
||||
pub use game::post_game;
|
@ -1,17 +0,0 @@
|
||||
use actix_web::{get, http::header::ContentType, web, HttpResponse, Responder};
|
||||
use gamenight_database::{DbPool, GetConnection};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{models::{gamenight_id::GamenightId, participants::Participants}, request::{authorization::AuthUser, error::ApiError}};
|
||||
|
||||
#[get("/participants")]
|
||||
pub async fn get_get_participants(pool: web::Data<DbPool>, _user: AuthUser, gamenight_info: web::Json<GamenightId>) -> Result<impl Responder, ApiError> {
|
||||
let mut conn = pool.get_conn();
|
||||
|
||||
let users = gamenight_database::get_participants(&mut conn, &Uuid::parse_str(&gamenight_info.into_inner().gamenight_id)?)?
|
||||
.iter().map(|x| x.to_string()).collect();
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
.body(serde_json::to_string(&Participants{participants: users})?))
|
||||
}
|
@ -1,165 +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::models::user_id::UserId;
|
||||
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("/token")]
|
||||
pub async fn refresh(user: AuthUser) -> Result<impl Responder, ApiError> {
|
||||
let new_token = get_token(&user.0)?;
|
||||
let response = Token{ jwt_token: Some(new_token) };
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
.body(serde_json::to_string(&response)?))
|
||||
}
|
||||
|
||||
#[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())
|
||||
}
|
||||
|
||||
impl From<gamenight_database::user::User> for User {
|
||||
fn from(value: gamenight_database::user::User) -> Self {
|
||||
Self {
|
||||
id: value.id.to_string(),
|
||||
username: value.username,
|
||||
email: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/user")]
|
||||
pub async fn get_user(pool: web::Data<DbPool>, _user: AuthUser, user_info: web::Json<UserId>) -> Result<impl Responder, ApiError> {
|
||||
let mut conn = pool.get_conn();
|
||||
|
||||
let user = gamenight_database::user::get_user(&mut conn, Uuid::parse_str(&user_info.into_inner().user_id)?)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
.body(serde_json::to_string(&user)?))
|
||||
}
|
||||
|
||||
#[get("/user")]
|
||||
pub async fn get_user_unauthenticated(_path: web::Path<UserId>) -> 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" }
|
||||
|
3176
backend-actix/Cargo.lock → backend/Cargo.lock
generated
3176
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"
|
@ -1,4 +1,4 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
|
||||
drop table gamenight;
|
||||
drop table game;
|
||||
drop table known_games;
|
@ -6,7 +6,7 @@ CREATE TABLE gamenight (
|
||||
datetime VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE game (
|
||||
CREATE TABLE known_games (
|
||||
id UUID NOT NULL PRIMARY KEY,
|
||||
name VARCHAR UNIQUE NOT NULL
|
||||
);
|
@ -1,6 +1,4 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
|
||||
drop table pwd;
|
||||
drop table client;
|
||||
|
||||
drop type Role;
|
||||
drop table users;
|
@ -1,16 +1,14 @@
|
||||
CREATE TYPE Role AS ENUM ('user', 'admin');
|
||||
|
||||
CREATE TABLE client (
|
||||
CREATE TABLE users (
|
||||
id UUID NOT NULL PRIMARY KEY,
|
||||
username VARCHAR UNIQUE NOT NULL,
|
||||
email VARCHAR UNIQUE NOT NULL,
|
||||
role Role NOT NULL
|
||||
role VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE pwd (
|
||||
user_id UUID NOT NULL PRIMARY KEY,
|
||||
password VARCHAR NOT NULL,
|
||||
CONSTRAINT FK_UserId FOREIGN KEY (user_id) REFERENCES client(id) ON DELETE CASCADE
|
||||
CONSTRAINT FK_UserId FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
--Initialize default admin user, with password "gamenight!"
|
||||
@ -19,7 +17,7 @@ DO $$
|
||||
DECLARE
|
||||
admin_uuid uuid = uuid_generate_v4();
|
||||
BEGIN
|
||||
INSERT INTO client (id, username, email, role)
|
||||
INSERT INTO users (id, username, email, role)
|
||||
values(admin_uuid, 'admin', '', 'admin');
|
||||
|
||||
insert INTO pwd (user_id, password)
|
@ -5,7 +5,7 @@ CREATE TABLE gamenight (
|
||||
name VARCHAR NOT NULL,
|
||||
datetime VARCHAR NOT NULL,
|
||||
owner_id UUID NOT NULL,
|
||||
CONSTRAINT FK_OwnerId FOREIGN KEY (owner_id) REFERENCES client(id) ON DELETE CASCADE
|
||||
CONSTRAINT FK_OwnerId FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
SET session_replication_role = 'replica';
|
@ -4,6 +4,6 @@ create table gamenight_gamelist (
|
||||
gamenight_id UUID NOT NULL,
|
||||
game_id UUID NOT NULL,
|
||||
CONSTRAINT FK_gamenight_id FOREIGN KEY (gamenight_id) REFERENCES gamenight(id) ON DELETE CASCADE,
|
||||
CONSTRAINT FK_game_id FOREIGN KEY (game_id) REFERENCES game(id) ON DELETE CASCADE,
|
||||
CONSTRAINT FK_game_id FOREIGN KEY (game_id) REFERENCES known_games(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY(gamenight_id, game_id)
|
||||
);
|
@ -1,3 +1,3 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
|
||||
drop table gamenight_participant;
|
||||
drop table gamenight_participants;
|
@ -1,9 +1,9 @@
|
||||
-- Your SQL goes here
|
||||
|
||||
create table gamenight_participant (
|
||||
create table gamenight_participants (
|
||||
gamenight_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_user_id FOREIGN KEY (user_id) REFERENCES client(id) ON DELETE CASCADE,
|
||||
CONSTRAINT FK_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY(gamenight_id, user_id)
|
||||
)
|
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:
|
||||
|
||||
db:
|
||||
container_name: pg_container
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_PASSWORD: root
|
||||
@ -11,6 +15,7 @@ services:
|
||||
pgadmin:
|
||||
container_name: pgadmin4_container
|
||||
image: dpage/pgadmin4
|
||||
restart: always
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: admin@admin.com
|
||||
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';
|
3
gamenight-api-client-rs/.gitignore
vendored
3
gamenight-api-client-rs/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
/target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
@ -1,23 +0,0 @@
|
||||
# OpenAPI Generator Ignore
|
||||
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
||||
|
||||
# Use this file to prevent files from being overwritten by the generator.
|
||||
# The patterns follow closely to .gitignore or .dockerignore.
|
||||
|
||||
# As an example, the C# client generator defines ApiClient.cs.
|
||||
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
||||
#ApiClient.cs
|
||||
|
||||
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
||||
#foo/*/qux
|
||||
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
||||
|
||||
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
||||
#foo/**/qux
|
||||
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
||||
|
||||
# You can also negate patterns with an exclamation (!).
|
||||
# For example, you can ignore all files in a docs folder with the file extension .md:
|
||||
#docs/*.md
|
||||
# Then explicitly reverse the ignore rule for a single file:
|
||||
#!docs/README.md
|
@ -1,39 +0,0 @@
|
||||
.gitignore
|
||||
.travis.yml
|
||||
Cargo.toml
|
||||
README.md
|
||||
docs/AddGameRequestBody.md
|
||||
docs/AddGamenightRequestBody.md
|
||||
docs/DefaultApi.md
|
||||
docs/Failure.md
|
||||
docs/Game.md
|
||||
docs/GameId.md
|
||||
docs/Gamenight.md
|
||||
docs/GamenightId.md
|
||||
docs/GetGamenightRequestBody.md
|
||||
docs/Login.md
|
||||
docs/Participants.md
|
||||
docs/Registration.md
|
||||
docs/Token.md
|
||||
docs/User.md
|
||||
docs/UserId.md
|
||||
git_push.sh
|
||||
src/apis/configuration.rs
|
||||
src/apis/default_api.rs
|
||||
src/apis/mod.rs
|
||||
src/lib.rs
|
||||
src/models/add_game_request_body.rs
|
||||
src/models/add_gamenight_request_body.rs
|
||||
src/models/failure.rs
|
||||
src/models/game.rs
|
||||
src/models/game_id.rs
|
||||
src/models/gamenight.rs
|
||||
src/models/gamenight_id.rs
|
||||
src/models/get_gamenight_request_body.rs
|
||||
src/models/login.rs
|
||||
src/models/mod.rs
|
||||
src/models/participants.rs
|
||||
src/models/registration.rs
|
||||
src/models/token.rs
|
||||
src/models/user.rs
|
||||
src/models/user_id.rs
|
@ -1 +0,0 @@
|
||||
7.13.0
|
@ -1 +0,0 @@
|
||||
language: rust
|
@ -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,72 +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* | [**game_get**](docs/DefaultApi.md#game_get) | **GET** /game |
|
||||
*DefaultApi* | [**game_post**](docs/DefaultApi.md#game_post) | **POST** /game |
|
||||
*DefaultApi* | [**games_get**](docs/DefaultApi.md#games_get) | **GET** /games |
|
||||
*DefaultApi* | [**get_gamenight**](docs/DefaultApi.md#get_gamenight) | **GET** /gamenight |
|
||||
*DefaultApi* | [**get_gamenights**](docs/DefaultApi.md#get_gamenights) | **GET** /gamenights | Get a all gamenights
|
||||
*DefaultApi* | [**get_token**](docs/DefaultApi.md#get_token) | **GET** /token |
|
||||
*DefaultApi* | [**join_post**](docs/DefaultApi.md#join_post) | **POST** /join |
|
||||
*DefaultApi* | [**leave_post**](docs/DefaultApi.md#leave_post) | **POST** /leave |
|
||||
*DefaultApi* | [**participants_get**](docs/DefaultApi.md#participants_get) | **GET** /participants | Get all participants for a gamenight
|
||||
*DefaultApi* | [**post_gamenight**](docs/DefaultApi.md#post_gamenight) | **POST** /gamenight |
|
||||
*DefaultApi* | [**post_register**](docs/DefaultApi.md#post_register) | **POST** /user |
|
||||
*DefaultApi* | [**post_token**](docs/DefaultApi.md#post_token) | **POST** /token |
|
||||
*DefaultApi* | [**user_get**](docs/DefaultApi.md#user_get) | **GET** /user |
|
||||
|
||||
|
||||
## Documentation For Models
|
||||
|
||||
- [AddGameRequestBody](docs/AddGameRequestBody.md)
|
||||
- [AddGamenightRequestBody](docs/AddGamenightRequestBody.md)
|
||||
- [Failure](docs/Failure.md)
|
||||
- [Game](docs/Game.md)
|
||||
- [GameId](docs/GameId.md)
|
||||
- [Gamenight](docs/Gamenight.md)
|
||||
- [GamenightId](docs/GamenightId.md)
|
||||
- [GetGamenightRequestBody](docs/GetGamenightRequestBody.md)
|
||||
- [Login](docs/Login.md)
|
||||
- [Participants](docs/Participants.md)
|
||||
- [Registration](docs/Registration.md)
|
||||
- [Token](docs/Token.md)
|
||||
- [User](docs/User.md)
|
||||
- [UserId](docs/UserId.md)
|
||||
|
||||
|
||||
To get access to the crate's generated documentation, use:
|
||||
|
||||
```
|
||||
cargo doc --open
|
||||
```
|
||||
|
||||
## Author
|
||||
|
||||
dennis@brentj.es
|
||||
|
@ -1,10 +0,0 @@
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
println!("cargo::rerun-if-changed=../backend-actix/gamenight-api.yaml");
|
||||
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,11 +0,0 @@
|
||||
# AddGame
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**name** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -1,11 +0,0 @@
|
||||
# AddGameRequestBody
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**name** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -1,12 +0,0 @@
|
||||
# AddGamenightRequestBody
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**name** | **String** | |
|
||||
**datetime** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -1,390 +0,0 @@
|
||||
# \DefaultApi
|
||||
|
||||
All URIs are relative to *http://localhost:8080*
|
||||
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**game_get**](DefaultApi.md#game_get) | **GET** /game |
|
||||
[**game_post**](DefaultApi.md#game_post) | **POST** /game |
|
||||
[**games_get**](DefaultApi.md#games_get) | **GET** /games |
|
||||
[**get_gamenight**](DefaultApi.md#get_gamenight) | **GET** /gamenight |
|
||||
[**get_gamenights**](DefaultApi.md#get_gamenights) | **GET** /gamenights | Get a all gamenights
|
||||
[**get_token**](DefaultApi.md#get_token) | **GET** /token |
|
||||
[**join_post**](DefaultApi.md#join_post) | **POST** /join |
|
||||
[**leave_post**](DefaultApi.md#leave_post) | **POST** /leave |
|
||||
[**participants_get**](DefaultApi.md#participants_get) | **GET** /participants | Get all participants for a gamenight
|
||||
[**post_gamenight**](DefaultApi.md#post_gamenight) | **POST** /gamenight |
|
||||
[**post_register**](DefaultApi.md#post_register) | **POST** /user |
|
||||
[**post_token**](DefaultApi.md#post_token) | **POST** /token |
|
||||
[**user_get**](DefaultApi.md#user_get) | **GET** /user |
|
||||
|
||||
|
||||
|
||||
## game_get
|
||||
|
||||
> models::Game game_get(game_id)
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Required | Notes
|
||||
------------- | ------------- | ------------- | ------------- | -------------
|
||||
**game_id** | Option<[**GameId**](GameId.md)> | | |
|
||||
|
||||
### Return type
|
||||
|
||||
[**models::Game**](Game.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[JWT-Auth](../README.md#JWT-Auth)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
## game_post
|
||||
|
||||
> game_post(add_game_request_body)
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Required | Notes
|
||||
------------- | ------------- | ------------- | ------------- | -------------
|
||||
**add_game_request_body** | Option<[**AddGameRequestBody**](AddGameRequestBody.md)> | | |
|
||||
|
||||
### Return type
|
||||
|
||||
(empty response body)
|
||||
|
||||
### Authorization
|
||||
|
||||
[JWT-Auth](../README.md#JWT-Auth)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
## games_get
|
||||
|
||||
> Vec<models::Game> games_get()
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**Vec<models::Game>**](Game.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[JWT-Auth](../README.md#JWT-Auth)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
## get_gamenight
|
||||
|
||||
> models::Gamenight get_gamenight(get_gamenight_request_body)
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Required | Notes
|
||||
------------- | ------------- | ------------- | ------------- | -------------
|
||||
**get_gamenight_request_body** | Option<[**GetGamenightRequestBody**](GetGamenightRequestBody.md)> | | |
|
||||
|
||||
### Return type
|
||||
|
||||
[**models::Gamenight**](Gamenight.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[JWT-Auth](../README.md#JWT-Auth)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
## get_gamenights
|
||||
|
||||
> Vec<models::Gamenight> get_gamenights()
|
||||
Get a all gamenights
|
||||
|
||||
Retrieve the list of gamenights on this gamenight server. Requires authorization.
|
||||
|
||||
### Parameters
|
||||
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**Vec<models::Gamenight>**](Gamenight.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[JWT-Auth](../README.md#JWT-Auth)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
## get_token
|
||||
|
||||
> models::Token get_token(login)
|
||||
|
||||
|
||||
Submit your credentials to get a JWT-token to use with the rest of the api.
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Required | Notes
|
||||
------------- | ------------- | ------------- | ------------- | -------------
|
||||
**login** | Option<[**Login**](Login.md)> | | |
|
||||
|
||||
### Return type
|
||||
|
||||
[**models::Token**](Token.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
## join_post
|
||||
|
||||
> join_post(gamenight_id)
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Required | Notes
|
||||
------------- | ------------- | ------------- | ------------- | -------------
|
||||
**gamenight_id** | Option<[**GamenightId**](GamenightId.md)> | | |
|
||||
|
||||
### Return type
|
||||
|
||||
(empty response body)
|
||||
|
||||
### Authorization
|
||||
|
||||
[JWT-Auth](../README.md#JWT-Auth)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
## leave_post
|
||||
|
||||
> leave_post(gamenight_id)
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Required | Notes
|
||||
------------- | ------------- | ------------- | ------------- | -------------
|
||||
**gamenight_id** | Option<[**GamenightId**](GamenightId.md)> | | |
|
||||
|
||||
### Return type
|
||||
|
||||
(empty response body)
|
||||
|
||||
### Authorization
|
||||
|
||||
[JWT-Auth](../README.md#JWT-Auth)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
## participants_get
|
||||
|
||||
> models::Participants participants_get(gamenight_id)
|
||||
Get all participants for a gamenight
|
||||
|
||||
Retrieve the participants of a single gamenight by id.
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Required | Notes
|
||||
------------- | ------------- | ------------- | ------------- | -------------
|
||||
**gamenight_id** | Option<[**GamenightId**](GamenightId.md)> | | |
|
||||
|
||||
### Return type
|
||||
|
||||
[**models::Participants**](Participants.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[JWT-Auth](../README.md#JWT-Auth)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
## post_gamenight
|
||||
|
||||
> post_gamenight(add_gamenight_request_body)
|
||||
|
||||
|
||||
Add a gamenight by providing a name and a date, only available when providing an JWT token.
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Required | Notes
|
||||
------------- | ------------- | ------------- | ------------- | -------------
|
||||
**add_gamenight_request_body** | Option<[**AddGamenightRequestBody**](AddGamenightRequestBody.md)> | | |
|
||||
|
||||
### Return type
|
||||
|
||||
(empty response body)
|
||||
|
||||
### Authorization
|
||||
|
||||
[JWT-Auth](../README.md#JWT-Auth)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
## post_register
|
||||
|
||||
> post_register(registration)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
Name | Type | Description | Required | Notes
|
||||
------------- | ------------- | ------------- | ------------- | -------------
|
||||
**registration** | Option<[**Registration**](Registration.md)> | | |
|
||||
|
||||
### Return type
|
||||
|
||||
(empty response body)
|
||||
|
||||
### Authorization
|
||||
|
||||
[JWT-Auth](../README.md#JWT-Auth)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
## post_token
|
||||
|
||||
> models::Token post_token()
|
||||
|
||||
|
||||
Refresh your JWT-token without logging in again.
|
||||
|
||||
### Parameters
|
||||
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**models::Token**](Token.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[JWT-Auth](../README.md#JWT-Auth)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
## user_get
|
||||
|
||||
> models::User user_get(user_id)
|
||||
|
||||
|
||||
Get a user from primary id
|
||||
|
||||
### Parameters
|
||||
|
||||
|
||||
Name | Type | Description | Required | Notes
|
||||
------------- | ------------- | ------------- | ------------- | -------------
|
||||
**user_id** | Option<[**UserId**](UserId.md)> | | |
|
||||
|
||||
### Return type
|
||||
|
||||
[**models::User**](User.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[JWT-Auth](../README.md#JWT-Auth)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
@ -1,11 +0,0 @@
|
||||
# Failure
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**message** | Option<**String**> | | [optional]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -1,12 +0,0 @@
|
||||
# Game
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**id** | **String** | |
|
||||
**name** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -1,11 +0,0 @@
|
||||
# GameId
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**game_id** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -1,14 +0,0 @@
|
||||
# Gamenight
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**id** | **String** | |
|
||||
**name** | **String** | |
|
||||
**datetime** | **String** | |
|
||||
**owner_id** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -1,11 +0,0 @@
|
||||
# GamenightId
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**gamenight_id** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -1,11 +0,0 @@
|
||||
# GetGamenightRequest
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**id** | Option<**String**> | | [optional]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -1,11 +0,0 @@
|
||||
# GetGamenightRequestBody
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**id** | Option<**String**> | | [optional]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -1,11 +0,0 @@
|
||||
# GetToken401Response
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**message** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -1,12 +0,0 @@
|
||||
# Login
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**username** | **String** | |
|
||||
**password** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -1,11 +0,0 @@
|
||||
# Participants
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**participants** | **Vec<String>** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -1,15 +0,0 @@
|
||||
# Registration
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**username** | **String** | |
|
||||
**email** | **String** | |
|
||||
**password** | **String** | |
|
||||
**password_repeat** | **String** | |
|
||||
**registration_token** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -1,11 +0,0 @@
|
||||
# Token
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**jwt_token** | Option<**String**> | | [optional]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -1,13 +0,0 @@
|
||||
# User
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**id** | **String** | |
|
||||
**username** | **String** | |
|
||||
**email** | Option<**String**> | | [optional]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -1,11 +0,0 @@
|
||||
# UserId
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**user_id** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
@ -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'
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user