backend/src/api/v1/auth/register.rs
Radical 8605b81e7b
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
style: cargo clippy && format
2025-05-24 01:09:17 +02:00

146 lines
4.2 KiB
Rust

use std::time::{SystemTime, UNIX_EPOCH};
use actix_web::{HttpResponse, post, web};
use argon2::{
PasswordHasher,
password_hash::{SaltString, rand_core::OsRng},
};
use diesel::{ExpressionMethods, dsl::insert_into};
use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::Response;
use crate::{
Data,
api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX},
error::Error,
schema::{
access_tokens::{self, dsl as adsl},
refresh_tokens::{self, dsl as rdsl},
users::{self, dsl as udsl},
},
utils::{generate_access_token, generate_refresh_token, refresh_token_cookie},
};
#[derive(Deserialize)]
struct AccountInformation {
identifier: String,
email: String,
password: String,
device_name: String,
}
#[derive(Serialize)]
struct ResponseError {
signups_enabled: bool,
gorb_id_valid: bool,
gorb_id_available: bool,
email_valid: bool,
email_available: bool,
password_hashed: bool,
password_minimum_length: bool,
password_special_characters: bool,
password_letters: bool,
password_numbers: bool,
}
impl Default for ResponseError {
fn default() -> Self {
Self {
signups_enabled: true,
gorb_id_valid: true,
gorb_id_available: true,
email_valid: true,
email_available: true,
password_hashed: true,
password_minimum_length: true,
password_special_characters: true,
password_letters: true,
password_numbers: true,
}
}
}
#[post("/register")]
pub async fn res(
account_information: web::Json<AccountInformation>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let uuid = Uuid::now_v7();
if !EMAIL_REGEX.is_match(&account_information.email) {
return Ok(HttpResponse::Forbidden().json(ResponseError {
email_valid: false,
..Default::default()
}));
}
if !USERNAME_REGEX.is_match(&account_information.identifier)
|| account_information.identifier.len() < 3
|| account_information.identifier.len() > 32
{
return Ok(HttpResponse::Forbidden().json(ResponseError {
gorb_id_valid: false,
..Default::default()
}));
}
if !PASSWORD_REGEX.is_match(&account_information.password) {
return Ok(HttpResponse::Forbidden().json(ResponseError {
password_hashed: false,
..Default::default()
}));
}
let salt = SaltString::generate(&mut OsRng);
if let Ok(hashed_password) = data
.argon2
.hash_password(account_information.password.as_bytes(), &salt)
{
let mut conn = data.pool.get().await?;
// TODO: Check security of this implementation
insert_into(users::table)
.values((
udsl::uuid.eq(uuid),
udsl::username.eq(&account_information.identifier),
udsl::password.eq(hashed_password.to_string()),
udsl::email.eq(&account_information.email),
))
.execute(&mut conn)
.await?;
let refresh_token = generate_refresh_token()?;
let access_token = generate_access_token()?;
let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64;
insert_into(refresh_tokens::table)
.values((
rdsl::token.eq(&refresh_token),
rdsl::uuid.eq(uuid),
rdsl::created_at.eq(current_time),
rdsl::device_name.eq(&account_information.device_name),
))
.execute(&mut conn)
.await?;
insert_into(access_tokens::table)
.values((
adsl::token.eq(&access_token),
adsl::refresh_token.eq(&refresh_token),
adsl::uuid.eq(uuid),
adsl::created_at.eq(current_time),
))
.execute(&mut conn)
.await?;
return Ok(HttpResponse::Ok()
.cookie(refresh_token_cookie(refresh_token))
.json(Response { access_token }));
}
Ok(HttpResponse::InternalServerError().finish())
}