forked from Roflin/gamenight
Compare commits
55 Commits
Author | SHA1 | Date | |
---|---|---|---|
d1832bc794 | |||
f1d23cb495 | |||
156be1821a | |||
6950ac62e8 | |||
597a960bf1 | |||
|
3f7ed03973 | ||
c994321576 | |||
4e26d3cdcb | |||
fce0ebd76b | |||
6699dcf392 | |||
db25dc0aed | |||
02913c7b52 | |||
|
9e84a62c41 | ||
22f05c00c1 | |||
b2aba31264 | |||
|
1296f363af | ||
|
70ae15f655 | ||
3509a70a6a | |||
217e5ee64b | |||
5216f55a14 | |||
534e6867d8 | |||
1c8110cdb0 | |||
d961896242 | |||
7741c1dbae | |||
65d2dece55 | |||
34737bfb6b | |||
5ace39d820 | |||
b7f981e3a6 | |||
f7f9f7456b | |||
cfaa6ebdb1 | |||
8a318e877f | |||
bcfcf66df5 | |||
83a0b5ad9d | |||
9de8ffaa2d | |||
102a3e6082 | |||
639405bf9f | |||
2ba2026e21 | |||
86cdbedd41 | |||
836a4ab59f | |||
5c27be0191 | |||
1a6ead4760 | |||
5ffeea6553 | |||
cc26aed9a5 | |||
92e0257e74 | |||
0a214ca388 | |||
2cfaf2b4cc | |||
bf796201bf | |||
56d0889963 | |||
d80f705b5d | |||
aab60dcc11 | |||
b5e9420c1f | |||
5f73d556c6 | |||
81e65b1619 | |||
df8b553345 | |||
af0dcee159 |
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,4 +1,4 @@
|
|||||||
/target
|
/target/
|
||||||
|
**/*.rs.bk
|
||||||
|
Cargo.lock
|
||||||
.vscode
|
.vscode
|
||||||
Rocket.toml
|
|
||||||
*.sqlite
|
|
||||||
|
2019
Cargo.lock
generated
2019
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
18
Cargo.toml
18
Cargo.toml
@ -1,18 +0,0 @@
|
|||||||
[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.1", features = ["default", "json"] }
|
|
||||||
libsqlite3-sys = { version = ">=0.8.0, <0.19.0", features = ["bundled"] }
|
|
||||||
rocket_sync_db_pools = { version = "0.1.0-rc.1", features = ["diesel_sqlite_pool"] }
|
|
||||||
diesel = { version = "1.4.8", features = ["sqlite"] }
|
|
||||||
diesel_migrations = "1.4.0"
|
|
||||||
rocket_dyn_templates = { version = "0.1.0-rc.1", features = ["handlebars"] }
|
|
||||||
chrono = "0.4.19"
|
|
||||||
serde = "1.0.136"
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
|||||||
#Copy this file over to Rocket.toml after changing all relevant values.
|
|
||||||
|
|
||||||
[global.databases]
|
|
||||||
gamenight_database = { url = "gamenight.sqlite" }
|
|
3
backend-actix/.gitignore
vendored
Normal file
3
backend-actix/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
target
|
||||||
|
src/models/
|
||||||
|
docs/
|
2417
backend-actix/Cargo.lock
generated
Normal file
2417
backend-actix/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
backend-actix/Cargo.toml
Normal file
20
backend-actix/Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
[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"
|
29
backend-actix/build.rs
Normal file
29
backend-actix/build.rs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
use std::{fs::{exists, read_dir, remove_dir_all, File}, io::Write, process::Command};
|
||||||
|
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
|
||||||
|
if exists("src/models").unwrap() {
|
||||||
|
remove_dir_all("src/models").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ =
|
||||||
|
Command::new("openapi-generator")
|
||||||
|
.args(["generate", "-i", "gamenight-api.yaml", "-g", "rust", "--global-property", "models"])
|
||||||
|
.output()
|
||||||
|
.expect("Failed to generate models sources for the gamenight API");
|
||||||
|
|
||||||
|
let mut file = File::create("src/models/mod.rs").unwrap();
|
||||||
|
let paths = read_dir("./src/models").unwrap();
|
||||||
|
for path in paths {
|
||||||
|
let path = path.unwrap();
|
||||||
|
let path = path.path();
|
||||||
|
let stem = path.file_stem().unwrap();
|
||||||
|
if stem == "mod" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = format!("pub mod {};\n", stem.to_str().unwrap());
|
||||||
|
let _ = file.write(line.as_bytes()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
249
backend-actix/gamenight-api.yaml
Normal file
249
backend-actix/gamenight-api.yaml
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
openapi: 3.0.0
|
||||||
|
info:
|
||||||
|
title: Gamenight
|
||||||
|
version: '1.0'
|
||||||
|
contact:
|
||||||
|
name: Dennis Brentjes
|
||||||
|
email: dennis@brentj.es
|
||||||
|
url: 'https://brentj.es'
|
||||||
|
description: Api specifaction for a Gamenight server
|
||||||
|
license:
|
||||||
|
name: MIT
|
||||||
|
servers:
|
||||||
|
- url: 'http://localhost:8080'
|
||||||
|
description: Gamenight
|
||||||
|
paths:
|
||||||
|
/token:
|
||||||
|
get:
|
||||||
|
summary: ''
|
||||||
|
operationId: get-token
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
$ref: '#/components/responses/TokenResponse'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/FailureResponse'
|
||||||
|
requestBody:
|
||||||
|
$ref: '#/components/requestBodies/LoginRequest'
|
||||||
|
description: Submit your credentials to get a JWT-token to use with the rest of the api.
|
||||||
|
parameters: []
|
||||||
|
/user:
|
||||||
|
post:
|
||||||
|
summary: ''
|
||||||
|
operationId: post-register
|
||||||
|
requestBody:
|
||||||
|
$ref: '#/components/requestBodies/RegisterRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ''
|
||||||
|
'422':
|
||||||
|
$ref: '#/components/responses/FailureResponse'
|
||||||
|
description: 'Create a new user given a registration token and user information, username and email must be unique, and password and password_repeat must match.'
|
||||||
|
parameters: []
|
||||||
|
get:
|
||||||
|
description: 'Get a user from primary id'
|
||||||
|
parameters: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
$ref: '#/components/responses/UserResponse'
|
||||||
|
/gamenights:
|
||||||
|
get:
|
||||||
|
summary: Your GET endpoint
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
$ref: '#/components/responses/GamenightsResponse'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/FailureResponse'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/FailureResponse'
|
||||||
|
operationId: get-gamenights
|
||||||
|
security:
|
||||||
|
- JWT-Auth: []
|
||||||
|
description: Retrieve the list of gamenights on this gamenight server. Requires authorization.
|
||||||
|
/gamenight:
|
||||||
|
post:
|
||||||
|
summary: ''
|
||||||
|
operationId: post-gamenight
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/FailureResponse'
|
||||||
|
'422':
|
||||||
|
$ref: '#/components/responses/FailureResponse'
|
||||||
|
security:
|
||||||
|
- JWT-Auth: []
|
||||||
|
requestBody:
|
||||||
|
$ref: '#/components/requestBodies/AddGamenight'
|
||||||
|
description: 'Add a gamenight by providing a name and a date, only available when providing an JWT token.'
|
||||||
|
get:
|
||||||
|
summary: ''
|
||||||
|
operationId: get-gamenight
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
$ref: '#/components/responses/GamenightResponse'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/FailureResponse'
|
||||||
|
'422':
|
||||||
|
$ref: '#/components/responses/FailureResponse'
|
||||||
|
requestBody:
|
||||||
|
$ref: '#/components/requestBodies/GetGamenight'
|
||||||
|
security:
|
||||||
|
- JWT-Auth: []
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
Gamenight:
|
||||||
|
title: Gamenight
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
datetime:
|
||||||
|
type: string
|
||||||
|
owner_id:
|
||||||
|
type: string
|
||||||
|
participants:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- datetime
|
||||||
|
- owner_id
|
||||||
|
Failure:
|
||||||
|
title: Failure
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
description: 'Failure Reason'
|
||||||
|
Token:
|
||||||
|
title: Token
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
jwt_token:
|
||||||
|
type: string
|
||||||
|
Login:
|
||||||
|
title: Login
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
- password
|
||||||
|
Registration:
|
||||||
|
title: Registration
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
password_repeat:
|
||||||
|
type: string
|
||||||
|
registration_token:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
- email
|
||||||
|
- password
|
||||||
|
- password_repeat
|
||||||
|
- registration_token
|
||||||
|
AddGamenightRequestBody:
|
||||||
|
title: AddGamenightRequestBody
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
datetime:
|
||||||
|
type: string
|
||||||
|
User:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
requestBodies:
|
||||||
|
LoginRequest:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Login'
|
||||||
|
RegisterRequest:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Registration'
|
||||||
|
AddGamenight:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AddGamenightRequestBody'
|
||||||
|
GetGamenight:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
TokenResponse:
|
||||||
|
description: Example response
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Token'
|
||||||
|
FailureResponse:
|
||||||
|
description: Example response
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Failure'
|
||||||
|
application/xml:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- message
|
||||||
|
GamenightsResponse:
|
||||||
|
description: Example response
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Gamenight'
|
||||||
|
GamenightResponse:
|
||||||
|
description: A gamenight being hosted
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Gamenight'
|
||||||
|
UserResponse:
|
||||||
|
description: A user in the gamenight system
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
securitySchemes:
|
||||||
|
JWT-Auth:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
bearerFormat: JWT
|
||||||
|
description: ''
|
||||||
|
|
51
backend-actix/src/main.rs
Normal file
51
backend-actix/src/main.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#[allow(unused_imports)]
|
||||||
|
pub mod models;
|
||||||
|
pub mod request;
|
||||||
|
|
||||||
|
use actix_cors::Cors;
|
||||||
|
use actix_web::middleware::Logger;
|
||||||
|
use actix_web::HttpServer;
|
||||||
|
use actix_web::App;
|
||||||
|
use actix_web::http;
|
||||||
|
use actix_web::web;
|
||||||
|
use request::{*, login, register, gamenights};
|
||||||
|
use tracing_actix_web::TracingLogger;
|
||||||
|
use gamenight_database::*;
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
let url = "postgres://root:root@127.0.0.1/gamenight";
|
||||||
|
|
||||||
|
let pool = get_connection_pool(url);
|
||||||
|
|
||||||
|
let mut conn = pool.get_conn();
|
||||||
|
run_migration(&mut conn);
|
||||||
|
|
||||||
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
let cors = Cors::default()
|
||||||
|
.allowed_origin("0.0.0.0")
|
||||||
|
.allowed_origin_fn(|_origin, _req_head| { true })
|
||||||
|
.allowed_methods(vec!["GET", "POST"])
|
||||||
|
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
|
||||||
|
.allowed_header(http::header::CONTENT_TYPE)
|
||||||
|
.max_age(3600);
|
||||||
|
|
||||||
|
App::new()
|
||||||
|
.wrap(cors)
|
||||||
|
.wrap(Logger::default())
|
||||||
|
.wrap(TracingLogger::default())
|
||||||
|
.app_data(web::Data::new(pool.clone()))
|
||||||
|
.service(login)
|
||||||
|
.service(register)
|
||||||
|
.service(gamenights)
|
||||||
|
.service(gamenight_post)
|
||||||
|
.service(gamenight_get)
|
||||||
|
.service(get_user)
|
||||||
|
.service(get_user_unauthenticated)
|
||||||
|
})
|
||||||
|
.bind(("::1", 8080))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
80
backend-actix/src/request/authorization.rs
Normal file
80
backend-actix/src/request/authorization.rs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
use std::future::{Ready, ready};
|
||||||
|
|
||||||
|
use actix_web::{FromRequest, http, HttpRequest, dev::Payload, web::Data};
|
||||||
|
use chrono::Utc;
|
||||||
|
use jsonwebtoken::{encode, Header, EncodingKey, decode, DecodingKey, Validation};
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use gamenight_database::{user::{get_user, Role, User}, DbPool};
|
||||||
|
|
||||||
|
use super::error::ApiError;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Claims {
|
||||||
|
exp: i64,
|
||||||
|
uid: Uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AuthUser {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub username: String,
|
||||||
|
pub email: String,
|
||||||
|
pub role: Role,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<User> for AuthUser {
|
||||||
|
fn from(value: User) -> Self {
|
||||||
|
Self{
|
||||||
|
id: value.id,
|
||||||
|
username: value.username,
|
||||||
|
email: value.email,
|
||||||
|
role: value.role,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_claims(req: &HttpRequest) -> Result<Claims, ApiError> {
|
||||||
|
let token = req.headers()
|
||||||
|
.get(http::header::AUTHORIZATION)
|
||||||
|
.map(|h| h.to_str().unwrap().split_at(7).1.to_string());
|
||||||
|
|
||||||
|
let token = token.ok_or(ApiError{
|
||||||
|
status: 400,
|
||||||
|
message: "JWT-token was not specified in the Authorization header as Bearer: token".to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let secret = "secret";
|
||||||
|
Ok(decode::<Claims>(token.as_str(), &DecodingKey::from_secret(secret.as_bytes()), &Validation::default())?.claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_token(user: &User) -> Result<String, ApiError> {
|
||||||
|
let claims = Claims {
|
||||||
|
exp: Utc::now().timestamp() + chrono::Duration::days(7).num_seconds(),
|
||||||
|
uid: user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let secret = "secret";
|
||||||
|
Ok(encode(
|
||||||
|
&Header::default(),
|
||||||
|
&claims,
|
||||||
|
&EncodingKey::from_secret(secret.as_bytes()))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRequest for AuthUser {
|
||||||
|
type Error = ApiError;
|
||||||
|
type Future = Ready<Result<Self, Self::Error>>;
|
||||||
|
|
||||||
|
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
|
||||||
|
ready(
|
||||||
|
(|| -> Result<AuthUser, ApiError>{
|
||||||
|
let pool = req.app_data::<Data<DbPool>>().expect("No database configured");
|
||||||
|
let mut conn = pool.get().expect("couldn't get db connection from pool");
|
||||||
|
let uid = get_claims(req)?.uid;
|
||||||
|
let user = get_user(&mut conn, uid)?;
|
||||||
|
|
||||||
|
Ok(user.into())
|
||||||
|
})()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
91
backend-actix/src/request/error.rs
Normal file
91
backend-actix/src/request/error.rs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
backend-actix/src/request/gamenight_handlers.rs
Normal file
66
backend-actix/src/request/gamenight_handlers.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
use actix_web::{get, web, Responder, http::header::ContentType, HttpResponse, post};
|
||||||
|
use chrono::{DateTime, ParseError};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use gamenight_database::{gamenight::Gamenight, DbPool, GetConnection};
|
||||||
|
|
||||||
|
use crate::{models::{gamenight, add_gamenight_request_body::AddGamenightRequestBody, get_gamenight_request::GetGamenightRequest}, request::authorization::AuthUser};
|
||||||
|
use crate::request::error::ApiError;
|
||||||
|
|
||||||
|
|
||||||
|
impl AddGamenightRequestBody {
|
||||||
|
pub fn into_with_user(&self, user: AuthUser) -> Result<Gamenight, ParseError> {
|
||||||
|
Ok(Gamenight {
|
||||||
|
datetime: DateTime::parse_from_rfc3339(&self.clone().datetime.unwrap())?.with_timezone(&chrono::Utc),
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
name: self.clone().name.unwrap().clone(),
|
||||||
|
owner_id: user.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GetGamenightRequest> for Uuid {
|
||||||
|
fn from(value: GetGamenightRequest) -> Self {
|
||||||
|
Uuid::parse_str(value.id.unwrap().as_str()).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/gamenights")]
|
||||||
|
pub async fn gamenights(pool: web::Data<DbPool>, _user: AuthUser) -> Result<impl Responder, ApiError> {
|
||||||
|
let mut conn = pool.get_conn();
|
||||||
|
let gamenights: Vec<gamenight_database::gamenight::Gamenight> = gamenight_database::gamenights(&mut conn)?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type(ContentType::json())
|
||||||
|
.body(serde_json::to_string(&gamenights)?)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/gamenight")]
|
||||||
|
pub async fn gamenight_post(pool: web::Data<DbPool>, user: AuthUser, gamenight_data: web::Json<AddGamenightRequestBody>) -> Result<impl Responder, ApiError> {
|
||||||
|
let mut conn = pool.get_conn();
|
||||||
|
|
||||||
|
gamenight_database::gamenight::add_gamenight(&mut conn, gamenight_data.into_with_user(user)?)?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/gamenight")]
|
||||||
|
pub async fn gamenight_get(pool: web::Data<DbPool>, _user: AuthUser, gamenight_data: web::Json<GetGamenightRequest>) -> Result<impl Responder, ApiError> {
|
||||||
|
let mut conn = pool.get_conn();
|
||||||
|
|
||||||
|
let gamenight = gamenight_database::gamenight::get_gamenight(&mut conn, gamenight_data.into_inner().into())?;
|
||||||
|
let participants = gamenight_database::gamenight_participants::get_participants(&mut conn, &gamenight.id)?;
|
||||||
|
|
||||||
|
let model = gamenight::Gamenight{
|
||||||
|
id: gamenight.id.to_string(),
|
||||||
|
datetime: gamenight.datetime.to_rfc3339(),
|
||||||
|
name: gamenight.name,
|
||||||
|
owner_id: gamenight.owner_id.to_string(),
|
||||||
|
participants: Some(participants.iter().map(|x| {x.to_string()}).collect())
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type(ContentType::json())
|
||||||
|
.body(serde_json::to_string(&model)?))
|
||||||
|
}
|
13
backend-actix/src/request/mod.rs
Normal file
13
backend-actix/src/request/mod.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
mod user_handlers;
|
||||||
|
mod gamenight_handlers;
|
||||||
|
mod error;
|
||||||
|
mod authorization;
|
||||||
|
|
||||||
|
pub use user_handlers::login;
|
||||||
|
pub use user_handlers::register;
|
||||||
|
pub use gamenight_handlers::gamenights;
|
||||||
|
pub use gamenight_handlers::gamenight_post;
|
||||||
|
pub use gamenight_handlers::gamenight_get;
|
||||||
|
pub use user_handlers::get_user;
|
||||||
|
pub use user_handlers::get_user_unauthenticated;
|
161
backend-actix/src/request/user_handlers.rs
Normal file
161
backend-actix/src/request/user_handlers.rs
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
|
||||||
|
use actix_web::http::header::ContentType;
|
||||||
|
use actix_web::{get, post, web, HttpResponse, Responder};
|
||||||
|
use gamenight_database::user::{count_users_with_email, count_users_with_username};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use validator::{Validate, ValidateArgs, ValidationError};
|
||||||
|
use crate::models::login::Login;
|
||||||
|
use crate::models::registration::Registration;
|
||||||
|
use crate::models::token::Token;
|
||||||
|
use crate::models::user::User;
|
||||||
|
use crate::request::error::ApiError;
|
||||||
|
use crate::request::authorization::get_token;
|
||||||
|
use serde_json;
|
||||||
|
use gamenight_database::{DbPool, GetConnection};
|
||||||
|
|
||||||
|
use super::authorization::AuthUser;
|
||||||
|
|
||||||
|
impl From<Login> for gamenight_database::user::LoginUser {
|
||||||
|
fn from(val: Login) -> Self {
|
||||||
|
gamenight_database::user::LoginUser {
|
||||||
|
username: val.username,
|
||||||
|
password: val.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Registration> for gamenight_database::user::Register {
|
||||||
|
fn from(val: Registration) -> Self {
|
||||||
|
gamenight_database::user::Register {
|
||||||
|
email: val.email,
|
||||||
|
username: val.username,
|
||||||
|
password: val.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RegisterContext<'v_a> {
|
||||||
|
pub pool: &'v_a DbPool
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unique_username(username: &String, context: &RegisterContext) -> Result<(), ValidationError> {
|
||||||
|
let mut conn = context.pool.get_conn();
|
||||||
|
|
||||||
|
match count_users_with_username(&mut conn, username)
|
||||||
|
{
|
||||||
|
Ok(0) => Ok(()),
|
||||||
|
Ok(_) => Err(ValidationError::new("User already exists")),
|
||||||
|
Err(_) => Err(ValidationError::new("Database error while validating user")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unique_email(email: &String, context: &RegisterContext) -> Result<(), ValidationError> {
|
||||||
|
let mut conn = context.pool.get_conn();
|
||||||
|
|
||||||
|
match count_users_with_email(&mut conn, email)
|
||||||
|
{
|
||||||
|
Ok(0) => Ok(()),
|
||||||
|
Ok(_) => Err(ValidationError::new("email already exists")),
|
||||||
|
Err(_) => Err(ValidationError::new("Database error while validating email"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate)]
|
||||||
|
#[validate(context = RegisterContext::<'v_a>)]
|
||||||
|
pub struct ValidatableRegistration {
|
||||||
|
#[validate(
|
||||||
|
length(min = 1),
|
||||||
|
custom(function = "unique_username", use_context)
|
||||||
|
)]
|
||||||
|
pub username: String,
|
||||||
|
#[validate(
|
||||||
|
email,
|
||||||
|
custom(function = "unique_email", use_context)
|
||||||
|
)]
|
||||||
|
pub email: String,
|
||||||
|
#[validate(length(min = 10), must_match(other = "password_repeat", ))]
|
||||||
|
pub password: String,
|
||||||
|
pub password_repeat: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Registration> for ValidatableRegistration {
|
||||||
|
fn from(value: Registration) -> Self {
|
||||||
|
Self {
|
||||||
|
username: value.username,
|
||||||
|
email: value.email,
|
||||||
|
password: value.password,
|
||||||
|
password_repeat: value.password_repeat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/token")]
|
||||||
|
pub async fn login(pool: web::Data<DbPool>, login_data: web::Json<Login>) -> Result<impl Responder, ApiError> {
|
||||||
|
let data = login_data.into_inner();
|
||||||
|
|
||||||
|
if let Ok(Some(user)) = web::block(move || {
|
||||||
|
let mut conn = pool.get_conn();
|
||||||
|
gamenight_database::login(&mut conn, data.into())
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
let token = get_token(&user)?;
|
||||||
|
let response = Token{ jwt_token: Some(token) };
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type(ContentType::json())
|
||||||
|
.body(serde_json::to_string(&response)?)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Err(ApiError{status: 401, message: "User doesn't exist or password doesn't match".to_string()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/user")]
|
||||||
|
pub async fn register(pool: web::Data<DbPool>, register_data: web::Json<Registration>) -> Result<impl Responder, ApiError> {
|
||||||
|
web::block(move || -> Result<(), ApiError> {
|
||||||
|
let validatable_registration: ValidatableRegistration = register_data.clone().into();
|
||||||
|
validatable_registration.validate_with_args(&RegisterContext{pool: &pool})?;
|
||||||
|
let register_request = register_data.into_inner().into();
|
||||||
|
let mut conn = pool.get_conn();
|
||||||
|
gamenight_database::register(&mut conn, register_request)?;
|
||||||
|
Ok(())
|
||||||
|
}).await??;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct UserInfo {
|
||||||
|
pub uuid: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<gamenight_database::user::User> for User {
|
||||||
|
fn from(value: gamenight_database::user::User) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Some(value.id.to_string()),
|
||||||
|
username: value.username,
|
||||||
|
email: None,
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/user/{user_id}")]
|
||||||
|
pub async fn get_user(pool: web::Data<DbPool>, _user: AuthUser, path: web::Path<UserInfo>) -> Result<impl Responder, ApiError> {
|
||||||
|
let mut conn = pool.get_conn();
|
||||||
|
|
||||||
|
let user = gamenight_database::user::get_user(&mut conn, Uuid::parse_str(&path.uuid)?)?;
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type(ContentType::json())
|
||||||
|
.body(serde_json::to_string(&user)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/user/{user_id}")]
|
||||||
|
pub async fn get_user_unauthenticated(_path: web::Path<UserInfo>) -> Result<impl Responder, ApiError> {
|
||||||
|
Ok(HttpResponse::Forbidden())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
18
docker-compose.yml
Normal file
18
docker-compose.yml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
container_name: pg_container
|
||||||
|
image: postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: root
|
||||||
|
POSTGRES_PASSWORD: root
|
||||||
|
POSTGRES_DB: gamenight
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
pgadmin:
|
||||||
|
container_name: pgadmin4_container
|
||||||
|
image: dpage/pgadmin4
|
||||||
|
environment:
|
||||||
|
PGADMIN_DEFAULT_EMAIL: admin@admin.com
|
||||||
|
PGADMIN_DEFAULT_PASSWORD: root
|
||||||
|
ports:
|
||||||
|
- "5050:80"
|
9
gamenight-api-client-rs/.gitignore
vendored
Normal file
9
gamenight-api-client-rs/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/target/
|
||||||
|
git_push.sh
|
||||||
|
**/*.rs.bk
|
||||||
|
Cargo.lock
|
||||||
|
docs
|
||||||
|
src
|
||||||
|
.travis.yml
|
||||||
|
.openapi-generator
|
||||||
|
.openapi-generator-ignore
|
14
gamenight-api-client-rs/Cargo.toml
Normal file
14
gamenight-api-client-rs/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[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"] }
|
60
gamenight-api-client-rs/README.md
Normal file
60
gamenight-api-client-rs/README.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# Rust API client for gamenight-api-client-rs
|
||||||
|
|
||||||
|
Api specifaction for a Gamenight server
|
||||||
|
|
||||||
|
For more information, please visit [https://brentj.es](https://brentj.es)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client.
|
||||||
|
|
||||||
|
- API version: 1.0
|
||||||
|
- Package version: 0.1.0
|
||||||
|
- Generator version: 7.13.0
|
||||||
|
- Build package: `org.openapitools.codegen.languages.RustClientCodegen`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Put the package under your project folder in a directory named `gamenight-api-client-rs` and add the following to `Cargo.toml` under `[dependencies]`:
|
||||||
|
|
||||||
|
```
|
||||||
|
gamenight-api-client-rs = { path = "./gamenight-api-client-rs" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation for API Endpoints
|
||||||
|
|
||||||
|
All URIs are relative to *http://localhost:8080*
|
||||||
|
|
||||||
|
Class | Method | HTTP request | Description
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
*DefaultApi* | [**get_gamenight**](docs/DefaultApi.md#get_gamenight) | **GET** /gamenight |
|
||||||
|
*DefaultApi* | [**get_gamenights**](docs/DefaultApi.md#get_gamenights) | **GET** /gamenights | Your GET endpoint
|
||||||
|
*DefaultApi* | [**get_token**](docs/DefaultApi.md#get_token) | **GET** /token |
|
||||||
|
*DefaultApi* | [**post_gamenight**](docs/DefaultApi.md#post_gamenight) | **POST** /gamenight |
|
||||||
|
*DefaultApi* | [**post_register**](docs/DefaultApi.md#post_register) | **POST** /user |
|
||||||
|
*DefaultApi* | [**user_get**](docs/DefaultApi.md#user_get) | **GET** /user |
|
||||||
|
|
||||||
|
|
||||||
|
## Documentation For Models
|
||||||
|
|
||||||
|
- [AddGamenightRequestBody](docs/AddGamenightRequestBody.md)
|
||||||
|
- [Failure](docs/Failure.md)
|
||||||
|
- [Gamenight](docs/Gamenight.md)
|
||||||
|
- [GetGamenightRequest](docs/GetGamenightRequest.md)
|
||||||
|
- [GetToken401Response](docs/GetToken401Response.md)
|
||||||
|
- [Login](docs/Login.md)
|
||||||
|
- [Registration](docs/Registration.md)
|
||||||
|
- [Token](docs/Token.md)
|
||||||
|
- [User](docs/User.md)
|
||||||
|
|
||||||
|
|
||||||
|
To get access to the crate's generated documentation, use:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo doc --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
dennis@brentj.es
|
||||||
|
|
9
gamenight-api-client-rs/build.rs
Normal file
9
gamenight-api-client-rs/build.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let _ =
|
||||||
|
Command::new("openapi-generator")
|
||||||
|
.args(["generate", "-i", "../backend-actix/gamenight-api.yaml", "-g", "rust", "--additional-properties=withSeparateModelsAndApi=true,modelPackage=gamenight_model,apiPackage=gamenight_api,packageName=gamenight-api-client-rs,packageVersion=0.1.0"])
|
||||||
|
.output()
|
||||||
|
.expect("Failed to generate models sources for the gamenight API");
|
||||||
|
}
|
57
gamenight-api-client-rs/git_push.sh
Normal file
57
gamenight-api-client-rs/git_push.sh
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
|
||||||
|
#
|
||||||
|
# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com"
|
||||||
|
|
||||||
|
git_user_id=$1
|
||||||
|
git_repo_id=$2
|
||||||
|
release_note=$3
|
||||||
|
git_host=$4
|
||||||
|
|
||||||
|
if [ "$git_host" = "" ]; then
|
||||||
|
git_host="github.com"
|
||||||
|
echo "[INFO] No command line input provided. Set \$git_host to $git_host"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$git_user_id" = "" ]; then
|
||||||
|
git_user_id="GIT_USER_ID"
|
||||||
|
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$git_repo_id" = "" ]; then
|
||||||
|
git_repo_id="GIT_REPO_ID"
|
||||||
|
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$release_note" = "" ]; then
|
||||||
|
release_note="Minor update"
|
||||||
|
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialize the local directory as a Git repository
|
||||||
|
git init
|
||||||
|
|
||||||
|
# Adds the files in the local repository and stages them for commit.
|
||||||
|
git add .
|
||||||
|
|
||||||
|
# Commits the tracked changes and prepares them to be pushed to a remote repository.
|
||||||
|
git commit -m "$release_note"
|
||||||
|
|
||||||
|
# Sets the new remote
|
||||||
|
git_remote=$(git remote)
|
||||||
|
if [ "$git_remote" = "" ]; then # git remote not defined
|
||||||
|
|
||||||
|
if [ "$GIT_TOKEN" = "" ]; then
|
||||||
|
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
|
||||||
|
git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git
|
||||||
|
else
|
||||||
|
git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
git pull origin master
|
||||||
|
|
||||||
|
# Pushes (Forces) the changes in the local repository up to the remote repository
|
||||||
|
echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git"
|
||||||
|
git push origin master 2>&1 | grep -v 'To https'
|
1
gamenight-cli/.gitignore
vendored
Normal file
1
gamenight-cli/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
target
|
1503
gamenight-cli/Cargo.lock
generated
Normal file
1503
gamenight-cli/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
gamenight-cli/Cargo.toml
Normal file
12
gamenight-cli/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "gamenight-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gamenight-api-client-rs = { path = "../gamenight-api-client-rs" }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
inquire = { version = "0.7.5", features = ["date"] }
|
||||||
|
async-trait = "0.1"
|
||||||
|
dyn-clone = "1.0"
|
||||||
|
chrono = "0.4"
|
27
gamenight-cli/src/domain/gamenight.rs
Normal file
27
gamenight-cli/src/domain/gamenight.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
|
use gamenight_api_client_rs::models;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Gamenight {
|
||||||
|
pub name: String,
|
||||||
|
pub start_time: DateTime<Local>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<models::Gamenight> for Gamenight {
|
||||||
|
fn from(value: models::Gamenight) -> Self {
|
||||||
|
Self {
|
||||||
|
name: value.name,
|
||||||
|
start_time: DateTime::parse_from_rfc3339(&value.datetime).unwrap().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for Gamenight {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
writeln!(f, r#"
|
||||||
|
Name: {}
|
||||||
|
When: {}
|
||||||
|
"#, self.name, self.start_time.format("%d-%m-%Y %H:%M"))
|
||||||
|
}
|
||||||
|
}
|
1
gamenight-cli/src/domain/mod.rs
Normal file
1
gamenight-cli/src/domain/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod gamenight;
|
49
gamenight-cli/src/flows/add_gamenight.rs
Normal file
49
gamenight-cli/src/flows/add_gamenight.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
use gamenight_api_client_rs::{apis::default_api::post_gamenight, models};
|
||||||
|
use inquire::{CustomType, DateSelect, Text};
|
||||||
|
use chrono::{self, Local, NaiveTime};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AddGamenight {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddGamenight {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'a> Flow<'a> for AddGamenight {
|
||||||
|
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
|
||||||
|
let mut add_gamenight = models::AddGamenightRequestBody::new();
|
||||||
|
|
||||||
|
add_gamenight.name = Some(Text::new("What should we call your gamenight")
|
||||||
|
.prompt()?);
|
||||||
|
let naive_date = DateSelect::new("When is your gamenight")
|
||||||
|
.prompt()?;
|
||||||
|
let naive_time = CustomType::<NaiveTime>::new("At What time")
|
||||||
|
.prompt()?;
|
||||||
|
add_gamenight.datetime = Some(naive_date
|
||||||
|
.and_time(naive_time)
|
||||||
|
.and_local_timezone(Local)
|
||||||
|
.earliest()
|
||||||
|
.unwrap()
|
||||||
|
.to_utc()
|
||||||
|
.to_rfc3339());
|
||||||
|
|
||||||
|
post_gamenight(&state.configuration, Some(add_gamenight)).await?;
|
||||||
|
|
||||||
|
Ok((FlowOutcome::Successful, state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AddGamenight {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Add Gamenight.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
25
gamenight-cli/src/flows/exit.rs
Normal file
25
gamenight-cli/src/flows/exit.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Exit {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Exit {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'a> Flow<'a> for Exit {
|
||||||
|
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
|
||||||
|
Ok((FlowOutcome::Abort, state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Exit {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Exit")
|
||||||
|
}
|
||||||
|
}
|
42
gamenight-cli/src/flows/list_gamenights.rs
Normal file
42
gamenight-cli/src/flows/list_gamenights.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
use gamenight_api_client_rs::apis::default_api::get_gamenights;
|
||||||
|
use inquire::Select;
|
||||||
|
|
||||||
|
use crate::flows::view_gamenight::ViewGamenight;
|
||||||
|
|
||||||
|
use super::{exit::Exit, *};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ListGamenights {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListGamenights {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'a> Flow<'a> for ListGamenights {
|
||||||
|
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
|
||||||
|
let response = get_gamenights(&state.configuration).await?;
|
||||||
|
|
||||||
|
let mut view_flows = response.into_iter().map(|gamenight| -> Box<dyn Flow<'a> + Send> {
|
||||||
|
Box::new(ViewGamenight::new(gamenight.into()))
|
||||||
|
}).collect::<Vec<Box<dyn Flow<'a> + Send>>>();
|
||||||
|
view_flows.push(Box::new(Exit::new()));
|
||||||
|
|
||||||
|
let choice = Select::new("What gamenight would you like to view?", view_flows)
|
||||||
|
.prompt_skippable()?;
|
||||||
|
|
||||||
|
handle_choice_option(&choice, self, state).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ListGamenights {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "List all gamenights")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
44
gamenight-cli/src/flows/login.rs
Normal file
44
gamenight-cli/src/flows/login.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use gamenight_api_client_rs::{apis::{configuration::Configuration, default_api::get_token}, models};
|
||||||
|
use inquire::{Password, Text};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Login {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Login {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'a> Flow<'a> for Login {
|
||||||
|
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
|
||||||
|
let configuration = Configuration::new();
|
||||||
|
|
||||||
|
let username = Text::new("What is your login?").prompt()?;
|
||||||
|
let password = Password::new("what is your password?")
|
||||||
|
.without_confirmation()
|
||||||
|
.prompt()?;
|
||||||
|
|
||||||
|
let login = models::Login::new(username, password);
|
||||||
|
|
||||||
|
let result = get_token(&configuration, Some(login)).await?;
|
||||||
|
if let Some(token) = result.jwt_token {
|
||||||
|
state.configuration.bearer_access_token = Some(token);
|
||||||
|
Ok((FlowOutcome::Successful, state))
|
||||||
|
} else {
|
||||||
|
Err(FlowError{error: "Unexpected response".to_string()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Login {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Login")
|
||||||
|
}
|
||||||
|
}
|
42
gamenight-cli/src/flows/main.rs
Normal file
42
gamenight-cli/src/flows/main.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
use super::{exit::Exit, add_gamenight::AddGamenight, list_gamenights::ListGamenights, login::Login, main_menu::MainMenu, *};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Main {
|
||||||
|
login: Box<dyn for<'a> Flow<'a>>,
|
||||||
|
main_menu: MainMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Main {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut main_menu = MainMenu::new();
|
||||||
|
main_menu.menu.push(Box::new(ListGamenights::new()));
|
||||||
|
main_menu.menu.push(Box::new(AddGamenight::new()));
|
||||||
|
main_menu.menu.push(Box::new(Exit::new()));
|
||||||
|
Self {
|
||||||
|
login: Box::new(Login::new()),
|
||||||
|
main_menu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Main {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'a> Flow<'a> for Main {
|
||||||
|
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
|
||||||
|
let (_outcome, state) = self.login.run(state).await?;
|
||||||
|
let (_outcome, state) = self.main_menu.run(state).await?;
|
||||||
|
Ok((FlowOutcome::Successful, state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Main {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "main")
|
||||||
|
}
|
||||||
|
}
|
42
gamenight-cli/src/flows/main_menu.rs
Normal file
42
gamenight-cli/src/flows/main_menu.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
use inquire::{ui::RenderConfig, Select};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MainMenu {
|
||||||
|
pub menu: Vec<Box<dyn for<'a> Flow<'a> + Send>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MainMenu {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
MainMenu {
|
||||||
|
menu: vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for MainMenu {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'a> Flow<'a> for MainMenu {
|
||||||
|
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
|
||||||
|
let choice = Select::new("What would you like to do?", self.menu.clone())
|
||||||
|
.with_help_message("Select the action you want to take or quit the program")
|
||||||
|
.with_render_config(RenderConfig {
|
||||||
|
option_index_prefix: inquire::ui::IndexPrefix::Simple,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.prompt_skippable()?;
|
||||||
|
|
||||||
|
handle_choice_option(&choice, self, state).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for MainMenu {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Main menu")
|
||||||
|
}
|
||||||
|
}
|
99
gamenight-cli/src/flows/mod.rs
Normal file
99
gamenight-cli/src/flows/mod.rs
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use chrono::ParseError;
|
||||||
|
use gamenight_api_client_rs::apis::configuration::Configuration;
|
||||||
|
use inquire::InquireError;
|
||||||
|
use dyn_clone::DynClone;
|
||||||
|
|
||||||
|
pub mod main;
|
||||||
|
mod login;
|
||||||
|
mod main_menu;
|
||||||
|
mod exit;
|
||||||
|
mod list_gamenights;
|
||||||
|
mod add_gamenight;
|
||||||
|
mod view_gamenight;
|
||||||
|
|
||||||
|
pub struct GamenightState {
|
||||||
|
configuration: Configuration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GamenightState {
|
||||||
|
pub fn new() -> Self{
|
||||||
|
Self {
|
||||||
|
configuration: Configuration::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GamenightState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FlowError {
|
||||||
|
pub error: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<InquireError> for FlowError {
|
||||||
|
fn from(value: InquireError) -> Self {
|
||||||
|
Self {
|
||||||
|
error: value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<gamenight_api_client_rs::apis::Error<T>> for FlowError {
|
||||||
|
fn from(value: gamenight_api_client_rs::apis::Error<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
error: value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParseError> for FlowError {
|
||||||
|
fn from(value: ParseError) -> Self {
|
||||||
|
Self {
|
||||||
|
error: value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum FlowOutcome {
|
||||||
|
Successful,
|
||||||
|
Bool(bool),
|
||||||
|
String(String),
|
||||||
|
Abort
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlowResult<'a> = Result<(FlowOutcome, &'a mut GamenightState), FlowError>;
|
||||||
|
|
||||||
|
dyn_clone::clone_trait_object!(for<'a> Flow<'a>);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Flow<'a>: Sync + DynClone + Send + Display {
|
||||||
|
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_choice<'a>(choice: &Box<dyn Flow<'a> + Send>, flow: &dyn Flow<'a>, state: &'a mut GamenightState) -> FlowResult<'a> {
|
||||||
|
let (outcome, new_state) = choice.run(state).await?;
|
||||||
|
|
||||||
|
if outcome == FlowOutcome::Abort {
|
||||||
|
Ok((FlowOutcome::Successful, new_state))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
flow.run(new_state).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_choice_option<'a>(choice: &Option<Box<dyn Flow<'a> + Send>>, flow: &dyn Flow<'a>, state: &'a mut GamenightState) -> FlowResult<'a> {
|
||||||
|
if let Some(choice) = choice {
|
||||||
|
handle_choice(choice, flow, state).await
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Ok((FlowOutcome::Abort, state))
|
||||||
|
}
|
||||||
|
}
|
41
gamenight-cli/src/flows/view_gamenight.rs
Normal file
41
gamenight-cli/src/flows/view_gamenight.rs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
use inquire::Select;
|
||||||
|
|
||||||
|
use crate::{domain::gamenight::Gamenight, flows::exit::Exit};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ViewGamenight {
|
||||||
|
gamenight: Gamenight
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViewGamenight {
|
||||||
|
pub fn new(gamenight: Gamenight) -> Self {
|
||||||
|
Self {
|
||||||
|
gamenight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'a> Flow<'a> for ViewGamenight {
|
||||||
|
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
|
||||||
|
|
||||||
|
print!("{}", self.gamenight);
|
||||||
|
let options: Vec<Box<dyn Flow<'a> + Send>> = vec![
|
||||||
|
Box::new(Exit::new())
|
||||||
|
];
|
||||||
|
let choice = Select::new("What do you want to do:", options)
|
||||||
|
.prompt_skippable()?;
|
||||||
|
|
||||||
|
handle_choice_option(&choice, self, state).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ViewGamenight {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{} {}", self.gamenight.name, self.gamenight.start_time.format("%d-%m-%Y %H:%M"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
15
gamenight-cli/src/main.rs
Normal file
15
gamenight-cli/src/main.rs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
pub mod flows;
|
||||||
|
pub mod domain;
|
||||||
|
|
||||||
|
use flows::{main::Main, Flow, GamenightState};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let mut state = GamenightState::new();
|
||||||
|
let mainflow = Main::new();
|
||||||
|
if let Err(x) = mainflow.run(&mut state).await {
|
||||||
|
println!("{}", x.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
1
gamenight-database/.gitignore
vendored
Normal file
1
gamenight-database/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
target
|
15
gamenight-database/Cargo.toml
Normal file
15
gamenight-database/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "gamenight-database"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
diesel = { version = "2.0", features = ["postgres", "r2d2", "uuid", "chrono"] }
|
||||||
|
diesel-derive-enum = { version = "2.0", features = ["postgres"] }
|
||||||
|
diesel_migrations = "2.0"
|
||||||
|
argon2 = { version = "0.5", features = ["std"] }
|
||||||
|
uuid = { version = "1.17.0", features = ["serde", "v4"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
rand_core = "0.9"
|
||||||
|
|
2
gamenight-database/diesel.toml
Normal file
2
gamenight-database/diesel.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[print_schema]
|
||||||
|
file = "src/schema/schema.rs"
|
@ -0,0 +1,12 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
|
||||||
|
CREATE TABLE gamenight (
|
||||||
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
datetime VARCHAR NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE known_games (
|
||||||
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
name VARCHAR UNIQUE NOT NULL
|
||||||
|
);
|
@ -0,0 +1,6 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
|
||||||
|
drop table pwd;
|
||||||
|
drop table users;
|
||||||
|
|
||||||
|
drop type Role;
|
@ -0,0 +1,28 @@
|
|||||||
|
CREATE TYPE Role AS ENUM ('user', 'admin');
|
||||||
|
|
||||||
|
CREATE TABLE users (
|
||||||
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
username VARCHAR UNIQUE NOT NULL,
|
||||||
|
email VARCHAR UNIQUE NOT NULL,
|
||||||
|
role Role NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE pwd (
|
||||||
|
user_id UUID NOT NULL PRIMARY KEY,
|
||||||
|
password VARCHAR NOT NULL,
|
||||||
|
CONSTRAINT FK_UserId FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
--Initialize default admin user, with password "gamenight!"
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
admin_uuid uuid = uuid_generate_v4();
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO users (id, username, email, role)
|
||||||
|
values(admin_uuid, 'admin', '', 'admin');
|
||||||
|
|
||||||
|
insert INTO pwd (user_id, password)
|
||||||
|
values(admin_uuid, '$argon2id$v=19$m=4096,t=3,p=1$zEdUjCAnZqd8DziYWzlFHw$YBLQhKvYIZBY43B8zM6hyBvLKuqTeh0EM5pKOfbWQSI');
|
||||||
|
END $$;
|
||||||
|
|
@ -0,0 +1,4 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
|
||||||
|
ALTER TABLE gamenight
|
||||||
|
DROP COLUMN owner_id;
|
@ -0,0 +1,19 @@
|
|||||||
|
ALTER TABLE gamenight RENAME TO _gamenight_old;
|
||||||
|
|
||||||
|
CREATE TABLE gamenight (
|
||||||
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
datetime VARCHAR NOT NULL,
|
||||||
|
owner_id UUID NOT NULL,
|
||||||
|
CONSTRAINT FK_OwnerId FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
SET session_replication_role = 'replica';
|
||||||
|
|
||||||
|
INSERT INTO gamenight (id, name, datetime, owner_id)
|
||||||
|
select id, name, datetime, '00000000-0000-0000-0000-000000000000'
|
||||||
|
FROM _gamenight_old;
|
||||||
|
|
||||||
|
drop table _gamenight_old;
|
||||||
|
|
||||||
|
SET session_replication_role = 'origin';
|
@ -0,0 +1,3 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
|
||||||
|
drop table gamenight_gamelist;
|
@ -0,0 +1,9 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
|
||||||
|
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 known_games(id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY(gamenight_id, game_id)
|
||||||
|
);
|
@ -0,0 +1,3 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
|
||||||
|
drop table gamenight_participant;
|
@ -0,0 +1,9 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
|
||||||
|
create table gamenight_participant (
|
||||||
|
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 users(id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY(gamenight_id, user_id)
|
||||||
|
)
|
@ -0,0 +1,6 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
|
||||||
|
drop table registration_tokens;
|
||||||
|
|
||||||
|
ALTER TABLE gamenight
|
||||||
|
ALTER datetime TYPE VARCHAR;
|
@ -0,0 +1,11 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
|
||||||
|
create table registration_tokens (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
token CHARACTER(32) NOT NULL,
|
||||||
|
single_use BOOLEAN NOT NULL,
|
||||||
|
expires TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE gamenight
|
||||||
|
ALTER datetime TYPE TIMESTAMPTZ using datetime::timestamp;
|
13
gamenight-database/src/error.rs
Normal file
13
gamenight-database/src/error.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
pub struct DatabaseError(pub String);
|
||||||
|
|
||||||
|
impl From<diesel::result::Error> for DatabaseError {
|
||||||
|
fn from(value: diesel::result::Error) -> Self {
|
||||||
|
DatabaseError(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<argon2::password_hash::Error> for DatabaseError {
|
||||||
|
fn from(value: argon2::password_hash::Error) -> Self {
|
||||||
|
DatabaseError(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
28
gamenight-database/src/gamenight.rs
Normal file
28
gamenight-database/src/gamenight.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use diesel::{Insertable, Queryable, PgConnection, RunQueryDsl, insert_into, QueryDsl};
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::schema::gamenight;
|
||||||
|
|
||||||
|
use super::error::DatabaseError;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
|
||||||
|
#[diesel(table_name = gamenight)]
|
||||||
|
pub struct Gamenight {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub datetime: DateTime<Utc>,
|
||||||
|
pub owner_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gamenights(conn: &mut PgConnection) -> Result<Vec::<Gamenight>, DatabaseError> {
|
||||||
|
Ok(gamenight::table.load::<Gamenight>(conn)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_gamenight(conn: &mut PgConnection, gamenight: Gamenight) -> Result<usize, DatabaseError> {
|
||||||
|
Ok(insert_into(gamenight::table).values(&gamenight).execute(conn)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_gamenight(conn: &mut PgConnection, id: Uuid) -> Result<Gamenight, DatabaseError> {
|
||||||
|
Ok(gamenight::table.find(id).first(conn)?)
|
||||||
|
}
|
24
gamenight-database/src/gamenight_participants.rs
Normal file
24
gamenight-database/src/gamenight_participants.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
use diesel::{ExpressionMethods, Insertable, QueryDsl, Queryable, RunQueryDsl};
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::schema::gamenight_participant;
|
||||||
|
|
||||||
|
use super::error::DatabaseError;
|
||||||
|
use super::DbConnection;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
|
||||||
|
#[diesel(belongs_to(Gamenight))]
|
||||||
|
#[diesel(belongs_to(User))]
|
||||||
|
#[diesel(table_name = gamenight_participant)]
|
||||||
|
pub struct GamenightParticipant {
|
||||||
|
pub gamenight_id: Uuid,
|
||||||
|
pub user_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_participants(conn: &mut DbConnection, id: &Uuid) -> Result<Vec<Uuid>, DatabaseError> {
|
||||||
|
Ok(gamenight_participant::table
|
||||||
|
.filter(gamenight_participant::gamenight_id.eq(&id))
|
||||||
|
.select(gamenight_participant::user_id)
|
||||||
|
.get_results(conn)?)
|
||||||
|
}
|
46
gamenight-database/src/lib.rs
Normal file
46
gamenight-database/src/lib.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
pub mod user;
|
||||||
|
pub mod error;
|
||||||
|
pub mod schema;
|
||||||
|
pub mod gamenight;
|
||||||
|
pub mod gamenight_participants;
|
||||||
|
|
||||||
|
use diesel::r2d2::ConnectionManager;
|
||||||
|
use diesel::r2d2::ManageConnection;
|
||||||
|
use diesel::r2d2::Pool;
|
||||||
|
use diesel::r2d2::PooledConnection;
|
||||||
|
use diesel::PgConnection;
|
||||||
|
use diesel_migrations::embed_migrations;
|
||||||
|
use diesel_migrations::EmbeddedMigrations;
|
||||||
|
|
||||||
|
use diesel_migrations::MigrationHarness;
|
||||||
|
pub use user::login;
|
||||||
|
pub use user::register;
|
||||||
|
pub use gamenight::gamenights;
|
||||||
|
pub use gamenight_participants::get_participants;
|
||||||
|
|
||||||
|
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
|
||||||
|
pub type DbConnection = PgConnection;
|
||||||
|
pub type DbPool = Pool<ConnectionManager<DbConnection>>;
|
||||||
|
|
||||||
|
pub fn run_migration(conn: &mut DbConnection) {
|
||||||
|
conn.run_pending_migrations(MIGRATIONS).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_connection_pool(url: &str) -> DbPool {
|
||||||
|
let manager = ConnectionManager::<PgConnection>::new(url);
|
||||||
|
// Refer to the `r2d2` documentation for more methods to use
|
||||||
|
// when building a connection pool
|
||||||
|
Pool::builder()
|
||||||
|
.test_on_check_out(true)
|
||||||
|
.build(manager)
|
||||||
|
.expect("Could not build connection pool")
|
||||||
|
}
|
||||||
|
pub trait GetConnection<T> where T: ManageConnection {
|
||||||
|
fn get_conn(&self) -> PooledConnection<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ManageConnection> GetConnection<T> for Pool<T> {
|
||||||
|
fn get_conn(&self) -> PooledConnection<T> {
|
||||||
|
self.get().expect("Couldn't get db connection from pool")
|
||||||
|
}
|
||||||
|
}
|
83
gamenight-database/src/schema.rs
Normal file
83
gamenight-database/src/schema.rs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// @generated automatically by Diesel CLI.
|
||||||
|
|
||||||
|
pub mod sql_types {
|
||||||
|
#[derive(diesel::sql_types::SqlType)]
|
||||||
|
#[diesel(postgres_type(name = "role"))]
|
||||||
|
pub struct Role;
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
gamenight (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
name -> Varchar,
|
||||||
|
datetime -> Timestamptz,
|
||||||
|
owner_id -> Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
gamenight_gamelist (gamenight_id, game_id) {
|
||||||
|
gamenight_id -> Uuid,
|
||||||
|
game_id -> Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
gamenight_participant (gamenight_id, user_id) {
|
||||||
|
gamenight_id -> Uuid,
|
||||||
|
user_id -> Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
known_games (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
name -> Varchar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
pwd (user_id) {
|
||||||
|
user_id -> Uuid,
|
||||||
|
password -> Varchar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
registration_tokens (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
#[max_length = 32]
|
||||||
|
token -> Bpchar,
|
||||||
|
single_use -> Bool,
|
||||||
|
expires -> Nullable<Timestamptz>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use super::sql_types::Role;
|
||||||
|
|
||||||
|
users (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
username -> Varchar,
|
||||||
|
email -> Varchar,
|
||||||
|
role -> Role,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::joinable!(gamenight -> users (owner_id));
|
||||||
|
diesel::joinable!(gamenight_gamelist -> gamenight (gamenight_id));
|
||||||
|
diesel::joinable!(gamenight_gamelist -> known_games (game_id));
|
||||||
|
diesel::joinable!(gamenight_participant -> gamenight (gamenight_id));
|
||||||
|
diesel::joinable!(gamenight_participant -> users (user_id));
|
||||||
|
diesel::joinable!(pwd -> users (user_id));
|
||||||
|
|
||||||
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
|
gamenight,
|
||||||
|
gamenight_gamelist,
|
||||||
|
gamenight_participant,
|
||||||
|
known_games,
|
||||||
|
pwd,
|
||||||
|
registration_tokens,
|
||||||
|
users,
|
||||||
|
);
|
83
gamenight-database/src/schema/schema.rs
Normal file
83
gamenight-database/src/schema/schema.rs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// @generated automatically by Diesel CLI.
|
||||||
|
|
||||||
|
pub mod sql_types {
|
||||||
|
#[derive(diesel::sql_types::SqlType)]
|
||||||
|
#[diesel(postgres_type(name = "role"))]
|
||||||
|
pub struct Role;
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
gamenight (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
name -> Varchar,
|
||||||
|
datetime -> Timestamptz,
|
||||||
|
owner_id -> Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
gamenight_gamelist (gamenight_id, game_id) {
|
||||||
|
gamenight_id -> Uuid,
|
||||||
|
game_id -> Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
gamenight_participant (gamenight_id, user_id) {
|
||||||
|
gamenight_id -> Uuid,
|
||||||
|
user_id -> Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
known_games (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
name -> Varchar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
pwd (user_id) {
|
||||||
|
user_id -> Uuid,
|
||||||
|
password -> Varchar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
registration_tokens (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
#[max_length = 32]
|
||||||
|
token -> Bpchar,
|
||||||
|
single_use -> Bool,
|
||||||
|
expires -> Nullable<Timestamptz>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use super::sql_types::Role;
|
||||||
|
|
||||||
|
users (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
username -> Varchar,
|
||||||
|
email -> Varchar,
|
||||||
|
role -> Role,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::joinable!(gamenight -> users (owner_id));
|
||||||
|
diesel::joinable!(gamenight_gamelist -> gamenight (gamenight_id));
|
||||||
|
diesel::joinable!(gamenight_gamelist -> known_games (game_id));
|
||||||
|
diesel::joinable!(gamenight_participant -> gamenight (gamenight_id));
|
||||||
|
diesel::joinable!(gamenight_participant -> users (user_id));
|
||||||
|
diesel::joinable!(pwd -> users (user_id));
|
||||||
|
|
||||||
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
|
gamenight,
|
||||||
|
gamenight_gamelist,
|
||||||
|
gamenight_participant,
|
||||||
|
known_games,
|
||||||
|
pwd,
|
||||||
|
registration_tokens,
|
||||||
|
users,
|
||||||
|
);
|
140
gamenight-database/src/user.rs
Normal file
140
gamenight-database/src/user.rs
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
use argon2::password_hash::Salt;
|
||||||
|
use diesel::Connection;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, Insertable, Queryable};
|
||||||
|
use diesel_derive_enum::DbEnum;
|
||||||
|
use argon2::password_hash::SaltString;
|
||||||
|
use argon2::PasswordHash;
|
||||||
|
use argon2::PasswordVerifier;
|
||||||
|
use argon2::Argon2;
|
||||||
|
use argon2::password_hash::PasswordHasher;
|
||||||
|
use argon2::password_hash::rand_core::OsRng;
|
||||||
|
use argon2::password_hash::rand_core::RngCore;
|
||||||
|
use crate::DbConnection;
|
||||||
|
|
||||||
|
use super::schema::{pwd, users};
|
||||||
|
pub use super::error::DatabaseError;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
|
||||||
|
#[diesel(table_name = pwd)]
|
||||||
|
struct Pwd {
|
||||||
|
user_id: Uuid,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
|
||||||
|
#[diesel(table_name = users)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub username: String,
|
||||||
|
pub email: String,
|
||||||
|
pub role: Role,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DbEnum, Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
|
||||||
|
#[ExistingTypePath = "crate::schema::sql_types::Role"]
|
||||||
|
pub enum Role {
|
||||||
|
Admin,
|
||||||
|
User
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LoginUser {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct LoginResult {
|
||||||
|
pub result: bool,
|
||||||
|
pub user: Option<User>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct RegisterResult {
|
||||||
|
pub result: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Register {
|
||||||
|
pub username: String,
|
||||||
|
pub email: String,
|
||||||
|
pub password: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn login(conn: &mut DbConnection, user: LoginUser) -> Result<Option<User>, DatabaseError> {
|
||||||
|
let id: Uuid = users::table
|
||||||
|
.filter(users::username.eq(&user.username))
|
||||||
|
.or_filter(users::email.eq(&user.username))
|
||||||
|
.select(users::id)
|
||||||
|
.first(conn)?;
|
||||||
|
|
||||||
|
let pwd: String = pwd::table
|
||||||
|
.filter(pwd::user_id.eq(id))
|
||||||
|
.select(pwd::password)
|
||||||
|
.first(conn)?;
|
||||||
|
|
||||||
|
let parsed_hash = PasswordHash::new(&pwd)?;
|
||||||
|
|
||||||
|
if Argon2::default()
|
||||||
|
.verify_password(user.password.as_bytes(), &parsed_hash)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
Ok(Some(users::table.find(id).first(conn)?))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_user(conn: &mut DbConnection, id: Uuid) -> Result<User, DatabaseError> {
|
||||||
|
Ok(users::table.find(id).first(conn)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(conn: &mut DbConnection, register: Register) -> Result<(), DatabaseError> {
|
||||||
|
let mut bytes = [0u8; Salt::RECOMMENDED_LENGTH];
|
||||||
|
OsRng.try_fill_bytes(&mut bytes).unwrap();
|
||||||
|
let salt = SaltString::encode_b64(&bytes).unwrap();
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
|
||||||
|
let password_hash = argon2
|
||||||
|
.hash_password(register.password.as_bytes(), &salt)?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
conn.transaction(|c| {
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
|
||||||
|
diesel::insert_into(users::table)
|
||||||
|
.values(User {
|
||||||
|
id,
|
||||||
|
username: register.username,
|
||||||
|
email: register.email,
|
||||||
|
role: Role::User,
|
||||||
|
})
|
||||||
|
.execute(c)?;
|
||||||
|
|
||||||
|
diesel::insert_into(pwd::table)
|
||||||
|
.values(Pwd {
|
||||||
|
user_id: id,
|
||||||
|
password: password_hash,
|
||||||
|
})
|
||||||
|
.execute(c)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count_users_with_username(conn: &mut DbConnection, username: &String) -> Result<i64, DatabaseError> {
|
||||||
|
Ok(users::table
|
||||||
|
.count()
|
||||||
|
.filter(users::username.eq(username))
|
||||||
|
.get_result::<i64>(conn)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count_users_with_email(conn: &mut DbConnection, email: &String) -> Result<i64, DatabaseError> {
|
||||||
|
Ok(users::table
|
||||||
|
.count()
|
||||||
|
.filter(users::email.eq(email))
|
||||||
|
.get_result::<i64>(conn)?)
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
-- Your SQL goes here
|
|
||||||
|
|
||||||
CREATE TABLE gamenight (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
game text TEXT NOT NULL,
|
|
||||||
datetime TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE known_games (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
game TEXT UNIQUE NOT NULL
|
|
||||||
);
|
|
58
src/api.rs
58
src/api.rs
@ -1,58 +0,0 @@
|
|||||||
use crate::schema;
|
|
||||||
use rocket::form::Form;
|
|
||||||
use rocket::serde::json::{Json, json, Value};
|
|
||||||
use rocket::http::Status;
|
|
||||||
use rocket::request::{self, Request, FromRequest};
|
|
||||||
use rocket::outcome::Outcome::{Success, Failure};
|
|
||||||
use rocket::response::{Redirect, Flash};
|
|
||||||
|
|
||||||
pub struct Referer(String);
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ReferrerError {
|
|
||||||
Missing,
|
|
||||||
MoreThanOne
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Responder)]
|
|
||||||
pub enum ApiResponse {
|
|
||||||
Status(Status),
|
|
||||||
Redirect(Redirect),
|
|
||||||
Value(Value),
|
|
||||||
Flash(Flash<Redirect>)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
|
||||||
impl<'r> FromRequest<'r> for Referer {
|
|
||||||
type Error = ReferrerError;
|
|
||||||
|
|
||||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
||||||
let referers : Vec<_> = req.headers().get("Referer").collect();
|
|
||||||
match referers.len() {
|
|
||||||
0 => Failure((Status::BadRequest, ReferrerError::Missing)),
|
|
||||||
1 => Success(Referer(referers[0].to_string())),
|
|
||||||
_ => Failure((Status::BadRequest, ReferrerError::MoreThanOne)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/gamenights")]
|
|
||||||
pub async fn gamenights(conn: schema::DbConn) -> ApiResponse {
|
|
||||||
let gamenights = schema::get_all_gamenights(conn).await;
|
|
||||||
ApiResponse::Value(json!(gamenights))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/gamenight", format = "application/json", data = "<gamenight_json>")]
|
|
||||||
pub async fn gamenight_post_json(conn: schema::DbConn, gamenight_json: Json<schema::GameNightNoId>) -> ApiResponse {
|
|
||||||
schema::insert_gamenight(conn, gamenight_json.into_inner()).await;
|
|
||||||
ApiResponse::Status(Status::Accepted)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/gamenight", format = "application/x-www-form-urlencoded", data = "<gamenight_form>")]
|
|
||||||
pub async fn gamenight_post_form(referer: Option<Referer>, conn: schema::DbConn, gamenight_form: Form<schema::GameNightNoId>) -> ApiResponse {
|
|
||||||
schema::insert_gamenight(conn, gamenight_form.into_inner()).await;
|
|
||||||
match referer {
|
|
||||||
None => ApiResponse::Status(Status::Accepted),
|
|
||||||
Some(referer) => ApiResponse::Flash(Flash::success(Redirect::to(referer.0), "Added Gamenight."))
|
|
||||||
}
|
|
||||||
}
|
|
20
src/main.rs
20
src/main.rs
@ -1,20 +0,0 @@
|
|||||||
#[macro_use] extern crate rocket;
|
|
||||||
#[macro_use] extern crate diesel_migrations;
|
|
||||||
#[macro_use] extern crate diesel;
|
|
||||||
|
|
||||||
use rocket::fairing::AdHoc;
|
|
||||||
use rocket_dyn_templates::Template;
|
|
||||||
|
|
||||||
mod api;
|
|
||||||
pub mod schema;
|
|
||||||
mod site;
|
|
||||||
|
|
||||||
#[launch]
|
|
||||||
fn rocket() -> _ {
|
|
||||||
rocket::build()
|
|
||||||
.attach(schema::DbConn::fairing())
|
|
||||||
.attach(Template::fairing())
|
|
||||||
.attach(AdHoc::on_ignite("Run Migrations", schema::run_migrations))
|
|
||||||
.mount("/", routes![site::index, site::gamenights, site::add_game_night])
|
|
||||||
.mount("/api", routes![api::gamenights, api::gamenight_post_form, api::gamenight_post_json])
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
use rocket_sync_db_pools::database;
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use rocket::{Rocket, Build};
|
|
||||||
use diesel::RunQueryDsl;
|
|
||||||
|
|
||||||
|
|
||||||
#[database("gamenight_database")]
|
|
||||||
pub struct DbConn(diesel::SqliteConnection);
|
|
||||||
|
|
||||||
table! {
|
|
||||||
gamenight (id) {
|
|
||||||
id -> Integer,
|
|
||||||
game -> Text,
|
|
||||||
datetime -> Text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
known_games (game) {
|
|
||||||
id -> Integer,
|
|
||||||
game -> Text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(
|
|
||||||
gamenight,
|
|
||||||
known_games,
|
|
||||||
);
|
|
||||||
|
|
||||||
pub async fn get_all_gamenights(conn: DbConn) -> Vec::<GameNight> {
|
|
||||||
conn.run(|c| {
|
|
||||||
gamenight::table.load::<GameNight>(c).unwrap()
|
|
||||||
}).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn insert_gamenight(conn: DbConn, new_gamenight: GameNightNoId) -> () {
|
|
||||||
conn.run(|c| {
|
|
||||||
diesel::insert_into(gamenight::table)
|
|
||||||
.values(new_gamenight)
|
|
||||||
.execute(c)
|
|
||||||
.unwrap()
|
|
||||||
}).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, FromForm, Insertable)]
|
|
||||||
#[table_name="known_games"]
|
|
||||||
pub struct GameNoId {
|
|
||||||
pub game : String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, FromForm, Queryable)]
|
|
||||||
pub struct Game {
|
|
||||||
pub id: i32,
|
|
||||||
pub game : String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, FromForm, Insertable)]
|
|
||||||
#[table_name="gamenight"]
|
|
||||||
pub struct GameNightNoId {
|
|
||||||
pub game : String,
|
|
||||||
pub datetime : String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, FromForm, Queryable)]
|
|
||||||
pub struct GameNight {
|
|
||||||
pub id: i32,
|
|
||||||
pub game : String,
|
|
||||||
pub datetime : String,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
57
src/site.rs
57
src/site.rs
@ -1,57 +0,0 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use rocket_dyn_templates::Template;
|
|
||||||
use rocket::response::{Redirect};
|
|
||||||
use rocket::request::{FlashMessage};
|
|
||||||
use crate::schema;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
struct FlashData {
|
|
||||||
has_data: bool,
|
|
||||||
kind: String,
|
|
||||||
message: String
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
struct GameNightsData {
|
|
||||||
gamenights: Vec::<schema::GameNight>,
|
|
||||||
flash: FlashData
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/gamenights")]
|
|
||||||
pub async fn gamenights(conn: schema::DbConn) -> Template {
|
|
||||||
let gamenights = schema::get_all_gamenights(conn).await;
|
|
||||||
|
|
||||||
let data = GameNightsData {
|
|
||||||
gamenights: gamenights,
|
|
||||||
flash: FlashData { has_data: false, message: "".to_string(), kind: "".to_string() }
|
|
||||||
};
|
|
||||||
|
|
||||||
Template::render("gamenights", &data)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/")]
|
|
||||||
pub async fn index() -> Redirect {
|
|
||||||
Redirect::to(uri!(gamenights))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
struct GameNightAddData {
|
|
||||||
post_url: String,
|
|
||||||
flash : FlashData
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/gamenight/add")]
|
|
||||||
pub async fn add_game_night(flash: Option<FlashMessage<'_>>) -> Template {
|
|
||||||
let flash_data = match flash {
|
|
||||||
None => FlashData { has_data: false, message: "".to_string(), kind: "".to_string() },
|
|
||||||
Some(flash) => FlashData { has_data: true, message: flash.message().to_string(), kind: flash.kind().to_string() }
|
|
||||||
};
|
|
||||||
|
|
||||||
let data = GameNightAddData {
|
|
||||||
post_url: "/api/gamenight".to_string(),
|
|
||||||
flash: flash_data
|
|
||||||
};
|
|
||||||
|
|
||||||
Template::render("gamenight_add", &data)
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
{{#if has_data}}
|
|
||||||
<div>
|
|
||||||
<p>{{kind}}: {{message}}</p>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
@ -1,16 +0,0 @@
|
|||||||
<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>
|
|
@ -1,14 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{{> flash flash }}
|
|
||||||
|
|
||||||
{{#each gamenights}}
|
|
||||||
<div>
|
|
||||||
<span>game: {{this.game}}</span>
|
|
||||||
<span>when: {{this.datetime}}</span>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Loading…
x
Reference in New Issue
Block a user