Added a user system with no proper user validation but working authorisation.
This commit is contained in:
4
backend/.gitignore
vendored
Normal file
4
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/target
|
||||
.vscode
|
||||
app.toml
|
||||
*.sqlite
|
||||
2280
backend/Cargo.lock
generated
Normal file
2280
backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
backend/Cargo.toml
Normal file
23
backend/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[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"
|
||||
password-hash = "0.4"
|
||||
argon2 = "0.4"
|
||||
rand_core = { version = "0.6", features = ["std"] }
|
||||
diesel-derive-enum = { version = "1.1", features = ["sqlite"] }
|
||||
jsonwebtoken = "8.1"
|
||||
|
||||
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" }
|
||||
|
||||
4
backend/migrations/2022-03-19-191822_initial/down.sql
Normal file
4
backend/migrations/2022-03-19-191822_initial/down.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
|
||||
drop table gamenight;
|
||||
drop table known_games;
|
||||
12
backend/migrations/2022-03-19-191822_initial/up.sql
Normal file
12
backend/migrations/2022-03-19-191822_initial/up.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- 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
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
|
||||
drop table user;
|
||||
drop table pwd;
|
||||
11
backend/migrations/2022-04-17-175115_user-system/up.sql
Normal file
11
backend/migrations/2022-04-17-175115_user-system/up.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE user (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
role TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE pwd (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
password TEXT NOT NULL
|
||||
);
|
||||
30
backend/readme.md
Normal file
30
backend/readme.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Gamenight
|
||||
|
||||
Een online tooltje voor het organiseren van een \(board\)gamenight
|
||||
|
||||
Het doel, Je kan een GameNight organiseren, je geeft wanneer je zou willen spelen, je geeft aan waar je zin in hebt, welke games je hebt, en of je kan hosten. Mensen kunnen zich erbij klikken, hopelijk volgt er een spelletjes consensus, en go.
|
||||
|
||||
Ik wil iets maken wat georganiseerder is dan een mailthread en minder push bericht is dan een boargame appgroep.
|
||||
|
||||
Geplande features:
|
||||
* Account systeem zodat het niet allemaal publiek is.
|
||||
* Manier om een event toe te voegen.
|
||||
* Manier voor owners om een event te verwijderen.
|
||||
* Lijst aan upcoming events.
|
||||
* Lijst aan archived events
|
||||
* Manier om te koppelen aan je account welke games je hebt, zodat dit automatisch aangevult.wordt als je een party joined,
|
||||
* manier om comments te plaatsen op een event.
|
||||
* manier om een of meer spellen te selecten zodat mensen dit ook daadwerkelijk meenemen en niet iedereen alles hoeft mee te nemen #QOL
|
||||
* manier om recurring game avonden te plannen.
|
||||
|
||||
Meta features:
|
||||
* Api apart van de site ontwikkelen zodat je shit kan automagiseren, zelf push berichten kan fixen als je wil via de API.
|
||||
* Een beetje sexy website bouwen zodat hij op zijn minst bruikbaar is op je mobiel.
|
||||
|
||||
# Mee devven?
|
||||
|
||||
Graag!
|
||||
|
||||
Belangrijkste devding dat je moet weten is dat je diesel migrations kan genereren en invulling kan geven, deze database migraties worden automatisch uitgevoerd als je de binary daarna start, of je kan ze handmatig uitvoeren met de diesel executable zelf. Ik weet niet zeker of je diesel nog handmatig moet installeren, maar ik denk het wel `cargo install diesel`
|
||||
|
||||
database migration genereren: `diesel migration generate <descriptive name>`
|
||||
3
backend/requests/gamenights.sh
Normal file
3
backend/requests/gamenights.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
echo $JWT
|
||||
|
||||
curl -X GET -H "Authorization: Bearer ${JWT}" localhost:8000/api/gamenights
|
||||
1
backend/requests/login.sh
Normal file
1
backend/requests/login.sh
Normal 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
Normal file
1
backend/requests/register.sh
Normal file
@@ -0,0 +1 @@
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"username": "a", "email": "b", "password": "c", "password_repeat": "d"}' localhost:8000/api/register
|
||||
178
backend/src/api.rs
Normal file
178
backend/src/api.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use crate::AppConfig;
|
||||
use rocket::request::Outcome;
|
||||
use jsonwebtoken::decode;
|
||||
use crate::schema::DbConn;
|
||||
use jsonwebtoken::DecodingKey;
|
||||
use jsonwebtoken::Validation;
|
||||
use rocket::State;
|
||||
use chrono::Utc;
|
||||
use jsonwebtoken::{Header, EncodingKey};
|
||||
use crate::schema;
|
||||
use std::borrow::Cow;
|
||||
use jsonwebtoken::encode;
|
||||
use rocket::serde::json::{Json, json, Value};
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{self, Request, FromRequest};
|
||||
use rocket::outcome::Outcome::{Success, Failure};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
pub struct Referer(String);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ReferrerError {
|
||||
Missing,
|
||||
MoreThanOne
|
||||
}
|
||||
|
||||
#[derive(Debug, Responder)]
|
||||
pub enum ApiResponseVariant {
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct ApiResponse {
|
||||
result: Cow<'static, str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
message: Option::<Cow<'static, str>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
jwt: Option::<Cow<'static, str>>
|
||||
}
|
||||
|
||||
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,
|
||||
jwt: None
|
||||
};
|
||||
|
||||
fn error(message: String) -> Self {
|
||||
Self {
|
||||
result: Self::FAILURE_RESULT,
|
||||
message: Some(Cow::Owned(message)),
|
||||
jwt: None
|
||||
}
|
||||
}
|
||||
|
||||
fn login_response(jwt: String) -> Self {
|
||||
Self {
|
||||
result: Self::SUCCES_RESULT,
|
||||
message: None,
|
||||
jwt: Some(Cow::Owned(jwt))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ApiError {
|
||||
RequestError(String),
|
||||
}
|
||||
|
||||
const AUTH_HEADER: &str = "Authorization";
|
||||
const BEARER: &str = "Bearer ";
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for schema::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::Failure((Status::BadRequest, ApiError::RequestError("No authorization header found".to_string())))
|
||||
};
|
||||
|
||||
if !header.starts_with(BEARER) {
|
||||
return Outcome::Failure((Status::BadRequest, ApiError::RequestError("Invalid Authorization header.".to_string())))
|
||||
};
|
||||
|
||||
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(error) => return Outcome::Failure((Status::BadRequest, ApiError::RequestError(error.to_string())))
|
||||
};
|
||||
let id = token.claims.uid;
|
||||
|
||||
let conn = req.guard::<DbConn>().await.unwrap();
|
||||
return Outcome::Success(schema::get_user(conn, id).await)
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/gamenights")]
|
||||
pub async fn gamenights(conn: DbConn, user: Option<schema::User>) -> ApiResponseVariant {
|
||||
if user.is_some() {
|
||||
let gamenights = schema::get_all_gamenights(conn).await;
|
||||
ApiResponseVariant::Value(json!(gamenights))
|
||||
}
|
||||
else {
|
||||
ApiResponseVariant::Status(Status::Unauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/gamenight", format = "application/json", data = "<gamenight_json>")]
|
||||
pub async fn gamenight_post_json(conn: DbConn, user: Option<schema::User>, gamenight_json: Json<schema::GameNightNoId>) -> ApiResponseVariant {
|
||||
if user.is_some() {
|
||||
schema::insert_gamenight(conn, gamenight_json.into_inner()).await;
|
||||
ApiResponseVariant::Value(json!(ApiResponse::SUCCES))
|
||||
}
|
||||
else {
|
||||
ApiResponseVariant::Status(Status::Unauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/register", format = "application/json", data = "<register_json>")]
|
||||
pub async fn register_post_json(conn: DbConn, register_json: Json<schema::Register>) -> ApiResponseVariant {
|
||||
match schema::insert_user(conn, register_json.into_inner()).await {
|
||||
Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)),
|
||||
Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string())))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Claims {
|
||||
exp: i64,
|
||||
uid: i32,
|
||||
role: schema::Role,
|
||||
}
|
||||
|
||||
#[post("/login", format = "application/json", data = "<login_json>")]
|
||||
pub async fn login_post_json(conn: DbConn, config: &State<AppConfig>, login_json: Json<schema::Login>) -> ApiResponseVariant {
|
||||
|
||||
match schema::login(conn, login_json.into_inner()).await {
|
||||
Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
|
||||
Ok(login_result) => {
|
||||
|
||||
let my_claims = Claims {
|
||||
exp: Utc::now().timestamp() + chrono::Duration::days(7).num_seconds(),
|
||||
uid: login_result.id.unwrap(),
|
||||
role: login_result.role.unwrap()
|
||||
};
|
||||
|
||||
let secret = &config.inner().jwt_secret;
|
||||
match encode(&Header::default(), &my_claims, &EncodingKey::from_secret(secret.as_bytes()))
|
||||
{
|
||||
Ok(token) => ApiResponseVariant::Value(json!(ApiResponse::login_response(token))),
|
||||
Err(error) => ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
backend/src/main.rs
Normal file
46
backend/src/main.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
#[macro_use] extern crate rocket;
|
||||
#[macro_use] extern crate diesel_migrations;
|
||||
#[macro_use] extern crate diesel;
|
||||
|
||||
use rocket::{fairing::AdHoc, figment::{Figment, providers::{Serialized, Toml, Env, Format}, 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]
|
||||
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(AdHoc::on_ignite("Run Migrations", schema::run_migrations))
|
||||
.attach(AdHoc::config::<AppConfig>())
|
||||
.mount("/", routes![site::index, site::gamenights,
|
||||
site::add_game_night, site::register])
|
||||
.mount("/api", routes![
|
||||
api::gamenights, api::gamenight_post_json,
|
||||
api::register_post_json,
|
||||
api::login_post_json
|
||||
]);
|
||||
|
||||
rocket
|
||||
}
|
||||
258
backend/src/schema.rs
Normal file
258
backend/src/schema.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
use argon2::PasswordVerifier;
|
||||
use argon2::PasswordHash;
|
||||
use diesel_derive_enum::DbEnum;
|
||||
use crate::diesel::QueryDsl;
|
||||
use crate::diesel::BoolExpressionMethods;
|
||||
use crate::diesel::ExpressionMethods;
|
||||
use crate::diesel::Connection;
|
||||
use rocket_sync_db_pools::database;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use rocket::{Rocket, Build};
|
||||
use diesel::RunQueryDsl;
|
||||
use argon2::{
|
||||
password_hash::{
|
||||
rand_core::OsRng,
|
||||
PasswordHasher
|
||||
},
|
||||
Argon2
|
||||
};
|
||||
use argon2::password_hash::SaltString;
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::Integer;
|
||||
use diesel::sql_types::Text;
|
||||
use super::RoleMapping;
|
||||
user(id) {
|
||||
id -> Integer,
|
||||
username -> Text,
|
||||
email -> Text,
|
||||
role -> RoleMapping,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
pwd(id) {
|
||||
id -> Integer,
|
||||
password -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
allow_tables_to_appear_in_same_query!(
|
||||
gamenight,
|
||||
known_games,
|
||||
);
|
||||
|
||||
pub enum DatabaseError {
|
||||
Hash(password_hash::Error),
|
||||
Query(String)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 insert_user(conn: DbConn, new_user: Register) -> Result<(), DatabaseError> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
|
||||
let argon2 = Argon2::default();
|
||||
|
||||
let password_hash = match argon2.hash_password(new_user.password.as_bytes(), &salt) {
|
||||
Ok(hash) => hash.to_string(),
|
||||
Err(error) => return Err(DatabaseError::Hash(error))
|
||||
};
|
||||
|
||||
match conn.run(move |c| {
|
||||
c.transaction(|| {
|
||||
diesel::insert_into(user::table)
|
||||
.values((user::username.eq(&new_user.username), user::email.eq(&new_user.email), user::role.eq(Role::User)))
|
||||
.execute(c)?;
|
||||
|
||||
let ids : Vec::<i32> = match user::table
|
||||
.filter(user::username.eq(&new_user.username).and(user::email.eq(&new_user.email)))
|
||||
.select(user::id)
|
||||
.get_results(c) {
|
||||
Ok(id) => id,
|
||||
Err(e) => return Err(e)
|
||||
};
|
||||
|
||||
diesel::insert_into(pwd::table)
|
||||
.values((pwd::id.eq(ids[0]), pwd::password.eq(&password_hash)))
|
||||
.execute(c)
|
||||
})
|
||||
}).await {
|
||||
Err(e) => Err(DatabaseError::Query(e.to_string())),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn login(conn: DbConn, login: Login) -> Result<LoginResult, DatabaseError> {
|
||||
conn.run(move |c| -> Result<LoginResult, DatabaseError> {
|
||||
let id : i32 = match user::table
|
||||
.filter(user::username.eq(&login.username))
|
||||
.or_filter(user::email.eq(&login.username))
|
||||
.select(user::id)
|
||||
.get_results(c) {
|
||||
Ok(id) => id[0],
|
||||
Err(error) => return Err(DatabaseError::Query(error.to_string()))
|
||||
};
|
||||
|
||||
let pwd : String = match pwd::table
|
||||
.filter(pwd::id.eq(id))
|
||||
.select(pwd::password)
|
||||
.get_results::<String>(c) {
|
||||
Ok(pwd) => pwd[0].clone(),
|
||||
Err(error) => return Err(DatabaseError::Query(error.to_string()))
|
||||
};
|
||||
|
||||
let parsed_hash = match PasswordHash::new(&pwd) {
|
||||
Ok(hash) => hash,
|
||||
Err(error) => return Err(DatabaseError::Hash(error))
|
||||
};
|
||||
|
||||
if Argon2::default().verify_password(&login.password.as_bytes(), &parsed_hash).is_ok() {
|
||||
|
||||
let role : Role = match user::table
|
||||
.filter(user::id.eq(id))
|
||||
.select(user::role)
|
||||
.get_results::<Role>(c) {
|
||||
Ok(role) => role[0].clone(),
|
||||
Err(error) => return Err(DatabaseError::Query(error.to_string()))
|
||||
};
|
||||
|
||||
Ok(LoginResult {
|
||||
result: true,
|
||||
id: Some(id),
|
||||
role: Some(role)
|
||||
})
|
||||
}
|
||||
else {
|
||||
Ok(LoginResult {
|
||||
result: false,
|
||||
id: None,
|
||||
role: None,
|
||||
})
|
||||
}
|
||||
}).await
|
||||
}
|
||||
|
||||
pub async fn get_user(conn: DbConn, id: i32) -> User {
|
||||
conn.run(move |c| {
|
||||
user::table
|
||||
.filter(user::id.eq(id))
|
||||
.first(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(Debug, Serialize, Deserialize, DbEnum, Clone)]
|
||||
pub enum Role {
|
||||
Admin,
|
||||
User,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
|
||||
#[table_name="user"]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub role: Role
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Register {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub password_repeat: 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 id: Option<i32>,
|
||||
pub role: Option<Role>
|
||||
}
|
||||
88
backend/src/site.rs
Normal file
88
backend/src/site.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use std::borrow::Cow;
|
||||
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: Cow<'static, str>,
|
||||
message: Cow<'static, str>
|
||||
}
|
||||
|
||||
impl FlashData {
|
||||
const EMPTY: Self = Self { has_data: false, message: Cow::Borrowed(""), kind: Cow::Borrowed("") };
|
||||
}
|
||||
|
||||
#[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::EMPTY
|
||||
};
|
||||
|
||||
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::EMPTY,
|
||||
Some(flash) => FlashData {
|
||||
has_data: true,
|
||||
message: Cow::Owned(flash.message().to_string()),
|
||||
kind: Cow::Owned(flash.kind().to_string())
|
||||
}
|
||||
};
|
||||
|
||||
let data = GameNightAddData {
|
||||
post_url: "/api/gamenight".to_string(),
|
||||
flash: flash_data
|
||||
};
|
||||
|
||||
Template::render("gamenight_add", &data)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct RegisterData {
|
||||
flash : FlashData
|
||||
}
|
||||
|
||||
#[get("/register")]
|
||||
pub async fn register(flash: Option<FlashMessage<'_>>) -> Template {
|
||||
let flash_data = match flash {
|
||||
None => FlashData::EMPTY,
|
||||
Some(flash) => FlashData {
|
||||
has_data: true,
|
||||
message: Cow::Owned(flash.message().to_string()),
|
||||
kind: Cow::Owned(flash.kind().to_string())
|
||||
}
|
||||
};
|
||||
|
||||
let data = RegisterData {
|
||||
flash: flash_data
|
||||
};
|
||||
|
||||
Template::render("register", &data)
|
||||
}
|
||||
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
|
||||
Reference in New Issue
Block a user