forked from Roflin/gamenight
Started reimplementation of the Rest api in actix-web
This commit is contained in:
parent
7741c1dbae
commit
d961896242
1
backend-actix/.gitignore
vendored
Normal file
1
backend-actix/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
target
|
1792
backend-actix/Cargo.lock
generated
Normal file
1792
backend-actix/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
backend-actix/Cargo.toml
Normal file
18
backend-actix/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[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]
|
||||||
|
actix-web = "4"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
uuid = { version = "1.3.0", features = ["serde", "v4"] }
|
||||||
|
diesel = { version = "2.0", features = ["postgres", "r2d2", "uuid"] }
|
||||||
|
diesel-derive-enum = { version = "2.0", features = ["postgres"] }
|
||||||
|
argon2 = "0.5"
|
||||||
|
chrono = "0.4"
|
||||||
|
jsonwebtoken = "8.1"
|
||||||
|
validator = { version = "0.16", features = ["derive"] }
|
35
backend-actix/src/main.rs
Normal file
35
backend-actix/src/main.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
pub mod request;
|
||||||
|
pub mod schema;
|
||||||
|
|
||||||
|
use actix_web::HttpServer;
|
||||||
|
use actix_web::App;
|
||||||
|
use actix_web::web;
|
||||||
|
use diesel::PgConnection;
|
||||||
|
use request::{login, register};
|
||||||
|
use diesel::r2d2::ConnectionManager;
|
||||||
|
use diesel::r2d2::Pool;
|
||||||
|
|
||||||
|
pub(crate) type DbPool = Pool<ConnectionManager<PgConnection>>;
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
|
||||||
|
let url = "postgres://root:root@localhost/gamenight";
|
||||||
|
let manager = ConnectionManager::<PgConnection>::new(url);
|
||||||
|
// Refer to the `r2d2` documentation for more methods to use
|
||||||
|
// when building a connection pool
|
||||||
|
let pool = Pool::builder()
|
||||||
|
.test_on_check_out(true)
|
||||||
|
.build(manager)
|
||||||
|
.expect("Could not build connection pool");
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.app_data(web::Data::new(pool.clone()))
|
||||||
|
.service(login)
|
||||||
|
.service(register)
|
||||||
|
})
|
||||||
|
.bind(("::1", 8080))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
60
backend-actix/src/request/error.rs
Normal file
60
backend-actix/src/request/error.rs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
use std::fmt::{Display, Formatter, Result};
|
||||||
|
use actix_web::{ResponseError, error::BlockingError};
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use validator::ValidationErrors;
|
||||||
|
|
||||||
|
use crate::schema::error::DatabaseError;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct ApiError {
|
||||||
|
pub error: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ApiError {
|
||||||
|
// This trait requires `fmt` with this exact signature.
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
write!(f, "{}", self.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseError for ApiError { }
|
||||||
|
|
||||||
|
impl From<DatabaseError> for ApiError {
|
||||||
|
fn from(value: DatabaseError) -> Self {
|
||||||
|
ApiError {
|
||||||
|
error: value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BlockingError> for ApiError {
|
||||||
|
fn from(value: BlockingError) -> Self {
|
||||||
|
ApiError {
|
||||||
|
error: value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for ApiError {
|
||||||
|
fn from(value: serde_json::Error) -> Self {
|
||||||
|
ApiError {
|
||||||
|
error: value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<jsonwebtoken::errors::Error> for ApiError {
|
||||||
|
fn from(value: jsonwebtoken::errors::Error) -> Self {
|
||||||
|
ApiError {
|
||||||
|
error: value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ValidationErrors> for ApiError {
|
||||||
|
fn from(value: ValidationErrors) -> Self {
|
||||||
|
ApiError {
|
||||||
|
error: value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
98
backend-actix/src/request/handler.rs
Normal file
98
backend-actix/src/request/handler.rs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
|
||||||
|
use actix_web::http::header::ContentType;
|
||||||
|
use actix_web::{web, post, HttpResponse, Responder};
|
||||||
|
use chrono::Utc;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use validator::ValidateArgs;
|
||||||
|
use crate::DbPool;
|
||||||
|
use crate::request::request_data::{Login, Register};
|
||||||
|
use crate::request::error::ApiError;
|
||||||
|
use crate::request::responses::LoginResponse;
|
||||||
|
use crate::schema::user::Role;
|
||||||
|
use crate::schema::{self};
|
||||||
|
use serde_json;
|
||||||
|
use jsonwebtoken::{encode, EncodingKey, Header};
|
||||||
|
|
||||||
|
impl Into<schema::user::LoginUser> for Login {
|
||||||
|
fn into(self) -> schema::user::LoginUser {
|
||||||
|
schema::user::LoginUser {
|
||||||
|
username: self.username,
|
||||||
|
password: self.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<schema::user::Register> for Register {
|
||||||
|
fn into(self) -> schema::user::Register {
|
||||||
|
schema::user::Register {
|
||||||
|
email: self.email,
|
||||||
|
username: self.username,
|
||||||
|
password: self.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct Claims {
|
||||||
|
exp: i64,
|
||||||
|
uid: Uuid,
|
||||||
|
role: Role,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/login")]
|
||||||
|
pub async fn login(pool: web::Data<DbPool>, login_data: web::Json<Login>) -> Result<impl Responder, ApiError> {
|
||||||
|
let data = login_data.into_inner();
|
||||||
|
|
||||||
|
let response = if let Some(user) = web::block(move || {
|
||||||
|
let mut conn = pool.get().expect("couldn't get db connection from pool");
|
||||||
|
schema::login(&mut conn, data.into())
|
||||||
|
})
|
||||||
|
.await??
|
||||||
|
{
|
||||||
|
let my_claims = Claims {
|
||||||
|
exp: Utc::now().timestamp() + chrono::Duration::days(7).num_seconds(),
|
||||||
|
uid: user.id,
|
||||||
|
role: user.role,
|
||||||
|
};
|
||||||
|
|
||||||
|
let secret = "secret";
|
||||||
|
let token = encode(
|
||||||
|
&Header::default(),
|
||||||
|
&my_claims,
|
||||||
|
&EncodingKey::from_secret(secret.as_bytes()))?;
|
||||||
|
|
||||||
|
LoginResponse::success(user.id, token)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LoginResponse::failure("User doesn't exist or password doesn't match".to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type(ContentType::json())
|
||||||
|
.body(serde_json::to_string(&response)?)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/register")]
|
||||||
|
pub async fn register(pool: web::Data<DbPool>, register_data: web::Json<Register>) -> Result<impl Responder, ApiError> {
|
||||||
|
let data1 = register_data.clone();
|
||||||
|
let data2 = register_data.clone();
|
||||||
|
|
||||||
|
let register_request : schema::user::Register = data2.into();
|
||||||
|
let mut conn1 = pool.get().expect("couldn't get db connection from pool");
|
||||||
|
let mut conn2 = pool.get().expect("couldn't get db connection from pool");
|
||||||
|
|
||||||
|
let _validation_result = web::block(move || {
|
||||||
|
data1.validate_args((&mut conn1, &mut conn2))
|
||||||
|
}).await??;
|
||||||
|
|
||||||
|
let mut conn3 = pool.get().expect("couldn't get db connection from pool");
|
||||||
|
let _register_result = web::block(move || {
|
||||||
|
schema::register(&mut conn3, register_request)
|
||||||
|
}).await??;
|
||||||
|
|
||||||
|
return Ok(HttpResponse::Ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
8
backend-actix/src/request/mod.rs
Normal file
8
backend-actix/src/request/mod.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
mod request_data;
|
||||||
|
mod responses;
|
||||||
|
mod handler;
|
||||||
|
mod error;
|
||||||
|
|
||||||
|
pub use handler::login;
|
||||||
|
pub use handler::register;
|
28
backend-actix/src/request/request_data.rs
Normal file
28
backend-actix/src/request/request_data.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
use crate::schema::user::{unique_email, unique_username};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Login {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate)]
|
||||||
|
pub struct Register {
|
||||||
|
#[validate(
|
||||||
|
length(min = 1),
|
||||||
|
custom(function = "unique_username", arg = "&'v_a mut diesel::PgConnection")
|
||||||
|
)]
|
||||||
|
pub username: String,
|
||||||
|
#[validate(
|
||||||
|
email,
|
||||||
|
custom(function = "unique_email", arg = "&'v_a mut diesel::PgConnection")
|
||||||
|
)]
|
||||||
|
#[validate(email)]
|
||||||
|
pub email: String,
|
||||||
|
#[validate(length(min = 10), must_match = "password_repeat")]
|
||||||
|
pub password: String,
|
||||||
|
pub password_repeat: String,
|
||||||
|
}
|
30
backend-actix/src/request/responses.rs
Normal file
30
backend-actix/src/request/responses.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct LoginResponse {
|
||||||
|
pub login_result: bool,
|
||||||
|
pub message: Option<String>,
|
||||||
|
pub user_id: Option<Uuid>,
|
||||||
|
pub jwt_token: Option<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoginResponse {
|
||||||
|
pub fn success(user_id: Uuid, token: String) -> Self {
|
||||||
|
Self {
|
||||||
|
login_result: true,
|
||||||
|
message: None,
|
||||||
|
user_id: Some(user_id),
|
||||||
|
jwt_token: Some(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn failure(message: String) -> Self {
|
||||||
|
Self {
|
||||||
|
login_result: false,
|
||||||
|
message: Some(message),
|
||||||
|
user_id: None,
|
||||||
|
jwt_token: None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
backend-actix/src/schema/error.rs
Normal file
13
backend-actix/src/schema/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())
|
||||||
|
}
|
||||||
|
}
|
5
backend-actix/src/schema/mod.rs
Normal file
5
backend-actix/src/schema/mod.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub mod user;
|
||||||
|
pub mod error;
|
||||||
|
|
||||||
|
pub use user::login;
|
||||||
|
pub use user::register;
|
159
backend-actix/src/schema/user.rs
Normal file
159
backend-actix/src/schema/user.rs
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
use diesel::Connection;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use diesel::{PgConnection, ExpressionMethods, QueryDsl, RunQueryDsl, table, Insertable, Queryable};
|
||||||
|
use diesel_derive_enum::DbEnum;
|
||||||
|
use argon2::password_hash::SaltString;
|
||||||
|
use argon2::PasswordHash;
|
||||||
|
use argon2::PasswordVerifier;
|
||||||
|
use argon2::{
|
||||||
|
password_hash::{rand_core::OsRng, PasswordHasher},
|
||||||
|
Argon2,
|
||||||
|
};
|
||||||
|
use validator::ValidationError;
|
||||||
|
|
||||||
|
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(Debug, Serialize, Deserialize, DbEnum, Clone, Copy, PartialEq)]
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
table! {
|
||||||
|
users(id) {
|
||||||
|
id -> diesel::sql_types::Uuid,
|
||||||
|
username -> VarChar,
|
||||||
|
email -> VarChar,
|
||||||
|
role -> crate::schema::user::RoleMapping,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
pwd(user_id) {
|
||||||
|
user_id -> diesel::sql_types::Uuid,
|
||||||
|
password -> VarChar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn login(conn: &mut PgConnection, 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 unique_username(username: &String, conn: &mut PgConnection) -> Result<(), ValidationError> {
|
||||||
|
match users::table
|
||||||
|
.count()
|
||||||
|
.filter(users::username.eq(username))
|
||||||
|
.get_result(conn)
|
||||||
|
{
|
||||||
|
Ok(0) => Ok(()),
|
||||||
|
Ok(_) => Err(ValidationError::new("User already exists")),
|
||||||
|
Err(_) => Err(ValidationError::new("Database error while validating user")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unique_email(email: &String, conn: &mut PgConnection) -> Result<(), ValidationError> {
|
||||||
|
match users::table
|
||||||
|
.count()
|
||||||
|
.filter(users::email.eq(email))
|
||||||
|
.get_result(conn)
|
||||||
|
{
|
||||||
|
Ok(0) => Ok(()),
|
||||||
|
Ok(_) => Err(ValidationError::new("email already exists")),
|
||||||
|
Err(_) => Err(ValidationError::new(
|
||||||
|
"Database error while validating email",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(conn: &mut PgConnection, register: Register) -> Result<(), DatabaseError> {
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
|
||||||
|
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: id.clone(),
|
||||||
|
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(())
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user