Added a user system with no proper user validation but working authorisation. #1
@ -1,36 +1,36 @@
|
||||
use validator::ValidateArgs;
|
||||
use crate::AppConfig;
|
||||
use rocket::request::Outcome;
|
||||
use jsonwebtoken::decode;
|
||||
use crate::schema;
|
||||
use crate::schema::DbConn;
|
||||
use crate::AppConfig;
|
||||
use chrono::Utc;
|
||||
use jsonwebtoken::decode;
|
||||
use jsonwebtoken::encode;
|
||||
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 jsonwebtoken::{EncodingKey, Header};
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{self, Request, FromRequest};
|
||||
use rocket::outcome::Outcome::{Success, Failure};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use rocket::outcome::Outcome::{Failure, Success};
|
||||
use rocket::request::Outcome;
|
||||
use rocket::request::{self, FromRequest, Request};
|
||||
use rocket::serde::json::{json, Json, Value};
|
||||
use rocket::State;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use validator::ValidateArgs;
|
||||
|
||||
pub struct Referer(String);
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ReferrerError {
|
||||
Missing,
|
||||
MoreThanOne
|
||||
MoreThanOne,
|
||||
}
|
||||
|
||||
#[derive(Debug, Responder)]
|
||||
pub enum ApiResponseVariant {
|
||||
Status(Status),
|
||||
// Redirect(Redirect),
|
||||
// Redirect(Redirect),
|
||||
Value(Value),
|
||||
// Flash(Flash<Redirect>)
|
||||
// Flash(Flash<Redirect>)
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
@ -38,7 +38,7 @@ 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();
|
||||
let referers: Vec<_> = req.headers().get("Referer").collect();
|
||||
match referers.len() {
|
||||
0 => Failure((Status::BadRequest, ReferrerError::Missing)),
|
||||
1 => Success(Referer(referers[0].to_string())),
|
||||
@ -51,9 +51,9 @@ impl<'r> FromRequest<'r> for Referer {
|
||||
struct ApiResponse {
|
||||
result: Cow<'static, str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
message: Option::<Cow<'static, str>>,
|
||||
message: Option<Cow<'static, str>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
jwt: Option::<Cow<'static, str>>
|
||||
jwt: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
impl ApiResponse {
|
||||
@ -63,14 +63,14 @@ impl ApiResponse {
|
||||
const SUCCES: Self = Self {
|
||||
result: Self::SUCCES_RESULT,
|
||||
message: None,
|
||||
jwt: None
|
||||
jwt: None,
|
||||
};
|
||||
|
||||
fn error(message: String) -> Self {
|
||||
Self {
|
||||
result: Self::FAILURE_RESULT,
|
||||
message: Some(Cow::Owned(message)),
|
||||
jwt: None
|
||||
jwt: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,7 +78,7 @@ impl ApiResponse {
|
||||
Self {
|
||||
result: Self::SUCCES_RESULT,
|
||||
message: None,
|
||||
jwt: Some(Cow::Owned(jwt))
|
||||
jwt: Some(Cow::Owned(jwt)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -98,23 +98,40 @@ impl<'r> FromRequest<'r> for schema::User {
|
||||
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())))
|
||||
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())))
|
||||
return Outcome::Failure((
|
||||
Status::BadRequest,
|
||||
ApiError::RequestError("Invalid Authorization header.".to_string()),
|
||||
));
|
||||
};
|
||||
|
||||
let app_config = req.guard::<&State<AppConfig>>().await.unwrap().inner();
|
||||
Roflin marked this conversation as resolved
Lucus
commented
I think you can use a Request Guard (see https://api.rocket.rs/v0.5-rc/rocket/request/trait.FromRequest.html) to authenticate the user and role: For example, endpoints that require admin privileges could accept a non-optional I think you can use a Request Guard (see https://api.rocket.rs/v0.5-rc/rocket/request/trait.FromRequest.html) to authenticate the user and role: For example, endpoints that require admin privileges could accept a non-optional `Admin` struct containing a user id and the request guard that generates it would only return `Success` if the user is logged and has the admin role.
Lucus
commented
See also the examples under the header "Request-Local State" in the above link. See also the examples under the header "Request-Local State" in the above link.
Lucus
commented
Reading more carefully I see you're already doing this, just that you're accepting an Reading more carefully I see you're already doing this, just that you're accepting an `Option<User>` and then checking it's not `None` while you could accept a `User` and be sure.
|
||||
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()) {
|
||||
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())))
|
||||
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)
|
||||
return Outcome::Success(schema::get_user(conn, id).await);
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,38 +140,45 @@ pub async fn gamenights(conn: DbConn, user: Option<schema::User>) -> ApiResponse
|
||||
if user.is_some() {
|
||||
let gamenights = schema::get_all_gamenights(conn).await;
|
||||
ApiResponseVariant::Value(json!(gamenights))
|
||||
}
|
||||
else {
|
||||
} 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 {
|
||||
pub async fn gamenight_post_json(
|
||||
conn: DbConn,
|
||||
user: Option<schema::User>,
|
||||
gamenight_json: Json<schema::GameNightNoId>,
|
||||
yorick
commented
Value is not an error response Value is not an error response
Roflin
commented
We'll it is, it's an application level error, so it's a valid request and you will get a valid http response with an "Failure" result. So that's why it returns an actual Json value We'll it is, it's an application level error, so it's a valid request and you will get a valid http response with an "Failure" result. So that's why it returns an actual Json value
|
||||
) -> ApiResponseVariant {
|
||||
if user.is_some() {
|
||||
schema::insert_gamenight(conn, gamenight_json.into_inner()).await;
|
||||
ApiResponseVariant::Value(json!(ApiResponse::SUCCES))
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
ApiResponseVariant::Status(Status::Unauthorized)
|
||||
yorick
commented
Value is not an error response Value is not an error response
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/register", format = "application/json", data = "<register_json>")]
|
||||
pub async fn register_post_json(conn: DbConn, register_json: Json<schema::Register>) -> ApiResponseVariant {
|
||||
|
||||
pub async fn register_post_json(
|
||||
conn: DbConn,
|
||||
register_json: Json<schema::Register>,
|
||||
) -> ApiResponseVariant {
|
||||
let register = register_json.into_inner();
|
||||
let register_clone = register.clone();
|
||||
match conn.run(move |c| {
|
||||
register_clone.validate_args((c,c))
|
||||
}).await {
|
||||
match conn
|
||||
.run(move |c| register_clone.validate_args((c, c)))
|
||||
.await
|
||||
{
|
||||
Ok(()) => (),
|
||||
Err(error) => return ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string())))
|
||||
Err(error) => {
|
||||
return ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string())))
|
||||
}
|
||||
}
|
||||
|
||||
match schema::insert_user(conn, register).await {
|
||||
Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)),
|
||||
Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string())))
|
||||
Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,23 +190,36 @@ struct Claims {
|
||||
}
|
||||
|
||||
#[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 {
|
||||
|
||||
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) => {
|
||||
if !login_result.result {
|
||||
return ApiResponseVariant::Value(json!(ApiResponse::error(String::from(
|
||||
"username and password didn't match"
|
||||
))));
|
||||
}
|
||||
|
||||
let my_claims = Claims {
|
||||
exp: Utc::now().timestamp() + chrono::Duration::days(7).num_seconds(),
|
||||
uid: login_result.id.unwrap(),
|
||||
role: login_result.role.unwrap()
|
||||
role: login_result.role.unwrap(),
|
||||
};
|
||||
|
||||
let secret = &config.inner().jwt_secret;
|
||||
match encode(&Header::default(), &my_claims, &EncodingKey::from_secret(secret.as_bytes()))
|
||||
{
|
||||
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())))
|
||||
Err(error) => {
|
||||
ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,17 @@
|
||||
#[macro_use] extern crate rocket;
|
||||
#[macro_use] extern crate diesel_migrations;
|
||||
#[macro_use] extern crate diesel;
|
||||
#[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::{
|
||||
fairing::AdHoc,
|
||||
figment::{
|
||||
providers::{Env, Format, Serialized, Toml},
|
||||
Figment, Profile,
|
||||
},
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@ -12,12 +21,14 @@ mod site;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct AppConfig {
|
||||
jwt_secret: String
|
||||
jwt_secret: String,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
fn default() -> AppConfig {
|
||||
AppConfig { jwt_secret: String::from("secret") }
|
||||
AppConfig {
|
||||
jwt_secret: String::from("secret"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,13 +45,17 @@ fn rocket() -> _ {
|
||||
.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,
|
||||
.attach(site::make_cors())
|
||||
.mount("/", routes![site::index, site::files])
|
||||
.mount(
|
||||
"/api",
|
||||
routes![
|
||||
api::gamenights,
|
||||
api::gamenight_post_json,
|
||||
api::register_post_json,
|
||||
api::login_post_json
|
||||
]);
|
||||
],
|
||||
);
|
||||
|
||||
rocket
|
||||
}
|
||||
|
@ -1,24 +1,21 @@
|
||||
use diesel::dsl::count;
|
||||
use std::ops::Deref;
|
||||
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 crate::diesel::ExpressionMethods;
|
||||
use crate::diesel::QueryDsl;
|
||||
use argon2::password_hash::SaltString;
|
||||
use argon2::PasswordHash;
|
||||
use argon2::PasswordVerifier;
|
||||
use argon2::{
|
||||
password_hash::{rand_core::OsRng, PasswordHasher},
|
||||
Argon2,
|
||||
};
|
||||
use diesel::dsl::count;
|
||||
use diesel::RunQueryDsl;
|
||||
use diesel_derive_enum::DbEnum;
|
||||
use rocket::{Build, Rocket};
|
||||
use rocket_sync_db_pools::database;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Deref;
|
||||
use validator::{Validate, ValidationError};
|
||||
|
||||
#[database("gamenight_database")]
|
||||
@ -66,30 +63,25 @@ table! {
|
||||
}
|
||||
}
|
||||
|
||||
allow_tables_to_appear_in_same_query!(
|
||||
gamenight,
|
||||
known_games,
|
||||
);
|
||||
allow_tables_to_appear_in_same_query!(gamenight, known_games,);
|
||||
|
||||
pub enum DatabaseError {
|
||||
Hash(password_hash::Error),
|
||||
Query(String)
|
||||
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)
|
||||
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 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) -> () {
|
||||
@ -98,7 +90,8 @@ pub async fn insert_gamenight(conn: DbConn, new_gamenight: GameNightNoId) -> ()
|
||||
.values(new_gamenight)
|
||||
.execute(c)
|
||||
.unwrap()
|
||||
}).await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn insert_user(conn: DbConn, new_user: Register) -> Result<(), DatabaseError> {
|
||||
@ -108,28 +101,40 @@ pub async fn insert_user(conn: DbConn, new_user: Register) -> Result<(), Databas
|
||||
|
||||
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))
|
||||
Err(error) => return Err(DatabaseError::Hash(error)),
|
||||
};
|
||||
|
||||
match conn.run(move |c| {
|
||||
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)))
|
||||
.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)))
|
||||
let ids: Vec<i32> = match user::table
|
||||
.filter(
|
||||
user::username
|
||||
.eq(&new_user.username)
|
||||
.and(user::email.eq(&new_user.email)),
|
||||
)
|
||||
.select(user::id)
|
||||
Roflin marked this conversation as resolved
yorick
commented
called called `user_id` now
|
||||
.get_results(c) {
|
||||
.get_results(c)
|
||||
{
|
||||
Ok(id) => id,
|
||||
Err(e) => return Err(e)
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
diesel::insert_into(pwd::table)
|
||||
.values((pwd::id.eq(ids[0]), pwd::password.eq(&password_hash)))
|
||||
Roflin marked this conversation as resolved
Outdated
Lucus
commented
Wel mooi om de grote expression waar je hier op matcht even een naam te geven zodat de match leesbaar blijft. Wel mooi om de grote expression waar je hier op matcht even een naam te geven zodat de match leesbaar blijft.
|
||||
.execute(c)
|
||||
})
|
||||
}).await {
|
||||
})
|
||||
.await
|
||||
{
|
||||
Err(e) => Err(DatabaseError::Query(e.to_string())),
|
||||
_ => Ok(()),
|
||||
}
|
||||
@ -137,82 +142,93 @@ pub async fn insert_user(conn: DbConn, new_user: Register) -> Result<(), Databas
|
||||
|
||||
pub async fn login(conn: DbConn, login: Login) -> Result<LoginResult, DatabaseError> {
|
||||
conn.run(move |c| -> Result<LoginResult, DatabaseError> {
|
||||
let id : i32 = match user::table
|
||||
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) {
|
||||
.get_results(c)
|
||||
{
|
||||
Ok(id) => id[0],
|
||||
Err(error) => return Err(DatabaseError::Query(error.to_string()))
|
||||
Err(error) => return Err(DatabaseError::Query(error.to_string())),
|
||||
Roflin marked this conversation as resolved
Outdated
yorick
commented
generates a panic if the user does not exist generates a panic if the user does not exist
|
||||
};
|
||||
|
||||
let pwd : String = match pwd::table
|
||||
let pwd: String = match pwd::table
|
||||
.filter(pwd::id.eq(id))
|
||||
.select(pwd::password)
|
||||
.get_results::<String>(c) {
|
||||
.get_results::<String>(c)
|
||||
{
|
||||
Ok(pwd) => pwd[0].clone(),
|
||||
Err(error) => return Err(DatabaseError::Query(error.to_string()))
|
||||
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))
|
||||
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
|
||||
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) {
|
||||
.get_results::<Role>(c)
|
||||
{
|
||||
Ok(role) => role[0].clone(),
|
||||
Err(error) => return Err(DatabaseError::Query(error.to_string()))
|
||||
Err(error) => return Err(DatabaseError::Query(error.to_string())),
|
||||
};
|
||||
|
||||
Ok(LoginResult {
|
||||
result: true,
|
||||
id: Some(id),
|
||||
role: Some(role)
|
||||
role: Some(role),
|
||||
})
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
Ok(LoginResult {
|
||||
result: false,
|
||||
id: None,
|
||||
role: None,
|
||||
})
|
||||
}
|
||||
}).await
|
||||
})
|
||||
.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
|
||||
conn.run(move |c| user::table.filter(user::id.eq(id)).first(c).unwrap())
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn unique_username(username: &String, conn: &diesel::SqliteConnection) -> Result<(), ValidationError> {
|
||||
pub fn unique_username(
|
||||
username: &String,
|
||||
conn: &diesel::SqliteConnection,
|
||||
) -> Result<(), ValidationError> {
|
||||
match user::table
|
||||
.select(count(user::username))
|
||||
.filter(user::username.eq(username))
|
||||
.execute(conn) {
|
||||
.execute(conn)
|
||||
{
|
||||
Ok(0) => Ok(()),
|
||||
Ok(_) => Err(ValidationError::new("User already exists")),
|
||||
Err(_) => Err(ValidationError::new("Database error while validating user"))
|
||||
Err(_) => Err(ValidationError::new("Database error while validating user")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unique_email(email: &String, conn: &diesel::SqliteConnection) -> Result<(), ValidationError> {
|
||||
pub fn unique_email(
|
||||
email: &String,
|
||||
conn: &diesel::SqliteConnection,
|
||||
) -> Result<(), ValidationError> {
|
||||
match user::table
|
||||
.select(count(user::email))
|
||||
.filter(user::email.eq(email))
|
||||
.execute(conn) {
|
||||
.execute(conn)
|
||||
{
|
||||
Ok(0) => Ok(()),
|
||||
Ok(_) => Err(ValidationError::new("email already exists")),
|
||||
Err(_) => Err(ValidationError::new("Database error while validating email"))
|
||||
Err(_) => Err(ValidationError::new(
|
||||
"Database error while validating email",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,7 +239,9 @@ pub async fn run_migrations(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
embed_migrations!();
|
||||
|
||||
let conn = DbConn::get_one(&rocket).await.expect("database connection");
|
||||
conn.run(|c| embedded_migrations::run(c)).await.expect("can run migrations");
|
||||
conn.run(|c| embedded_migrations::run(c))
|
||||
.await
|
||||
.expect("can run migrations");
|
||||
|
||||
rocket
|
||||
}
|
||||
@ -235,45 +253,51 @@ pub enum Role {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
|
||||
#[table_name="user"]
|
||||
#[table_name = "user"]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub role: Role
|
||||
pub role: Role,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, FromForm, Insertable)]
|
||||
#[table_name="known_games"]
|
||||
#[table_name = "known_games"]
|
||||
pub struct GameNoId {
|
||||
pub game : String,
|
||||
pub game: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, FromForm, Queryable)]
|
||||
pub struct Game {
|
||||
pub id: i32,
|
||||
pub game : String,
|
||||
pub game: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, FromForm, Insertable)]
|
||||
#[table_name="gamenight"]
|
||||
#[table_name = "gamenight"]
|
||||
pub struct GameNightNoId {
|
||||
pub game : String,
|
||||
pub datetime : String,
|
||||
pub game: String,
|
||||
pub datetime: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, FromForm, Queryable)]
|
||||
pub struct GameNight {
|
||||
pub id: i32,
|
||||
pub game : String,
|
||||
pub datetime : String,
|
||||
pub game: String,
|
||||
pub datetime: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
|
||||
pub struct Register {
|
||||
#[validate(length(min = 1), custom( function = "unique_username", arg = "&'v_a diesel::SqliteConnection"))]
|
||||
#[validate(
|
||||
length(min = 1),
|
||||
custom(function = "unique_username", arg = "&'v_a diesel::SqliteConnection")
|
||||
)]
|
||||
pub username: String,
|
||||
#[validate(email, custom( function = "unique_email", arg = "&'v_a diesel::SqliteConnection"))]
|
||||
#[validate(
|
||||
email,
|
||||
custom(function = "unique_email", arg = "&'v_a diesel::SqliteConnection")
|
||||
)]
|
||||
pub email: String,
|
||||
#[validate(length(min = 10), must_match = "password_repeat")]
|
||||
pub password: String,
|
||||
@ -283,12 +307,12 @@ pub struct Register {
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Login {
|
||||
pub username: String,
|
||||
pub password: String
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LoginResult {
|
||||
pub result: bool,
|
||||
pub id: Option<i32>,
|
||||
pub role: Option<Role>
|
||||
pub role: Option<Role>,
|
||||
}
|
||||
|
@ -1,88 +1,42 @@
|
||||
use std::borrow::Cow;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use rocket_dyn_templates::Template;
|
||||
use rocket::response::{Redirect};
|
||||
use rocket::request::{FlashMessage};
|
||||
use crate::schema;
|
||||
use rocket::fs::NamedFile;
|
||||
use rocket::http::Method;
|
||||
use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors, CorsOptions};
|
||||
Roflin marked this conversation as resolved
Outdated
yorick
commented
| ^^^^^^^^^^^ use of undeclared crate or module | ^^^^^^^^^^^ use of undeclared crate or module `rocket_cors`
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct FlashData {
|
||||
has_data: bool,
|
||||
kind: Cow<'static, str>,
|
||||
message: Cow<'static, str>
|
||||
pub fn make_cors() -> Cors {
|
||||
let allowed_origins = AllowedOrigins::some_exact(&[
|
||||
// 4.
|
||||
//CHANGE THESE TO MATCH YOUR PORTS
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
"http://localhost:8000",
|
||||
"http://0.0.0.0:8000",
|
||||
]);
|
||||
CorsOptions {
|
||||
// 5.
|
||||
allowed_origins,
|
||||
allowed_methods: vec![Method::Get].into_iter().map(From::from).collect(), // 1.
|
||||
allowed_headers: AllowedHeaders::some(&[
|
||||
"Authorization",
|
||||
"Accept",
|
||||
"Access-Control-Allow-Origin", // 6.
|
||||
]),
|
||||
allow_credentials: true,
|
||||
..Default::default()
|
||||
}
|
||||
.to_cors()
|
||||
.expect("error while building CORS")
|
||||
}
|
||||
|
||||
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("/<file..>")]
|
||||
pub async fn files(file: PathBuf) -> Option<NamedFile> {
|
||||
NamedFile::open(Path::new("../frontend/build/").join(file))
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[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)
|
||||
pub async fn index() -> io::Result<NamedFile> {
|
||||
NamedFile::open("../frontend/build/index.html").await
|
||||
}
|
You can probably use a
Result<Value, Status>
for most endpoints and avoid a custom enum. I also recommend usingjson::Value
qualified like that becauseValue
by itself is not very descriptive.True, but in the future we might want to return a status on a non error condition, or return a Redirect, I understand it is a bit overkill now, but in a previous iteration I was also returning Redirects and then this becomes a nice solution imho.