Basic create user account

This commit is contained in:
Pierre HUBERT 2023-05-24 16:19:46 +02:00
parent 9912428fd6
commit 0bfdc305b1
15 changed files with 224 additions and 8 deletions

View File

@ -33,5 +33,5 @@ diesel migation run
> Note: You can access the database directly using this command:
>
> ```bash
> PGPASSWORD=pass psql -h localhost -p 5432 -U user -d bioclimsol
> PGPASSWORD=pass psql -h localhost -p 5432 -U user -d geneit
> ```

View File

@ -68,6 +68,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "actix-remote-ip"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7629b357d4705cf3f1e31f989f48ecd56027112f7d52dcf06dd96ee197065f8e"
dependencies = [
"actix-web",
"futures-util",
"log",
]
[[package]]
name = "actix-router"
version = "0.5.1"
@ -285,6 +296,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "anyhow"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "autocfg"
version = "1.1.0"
@ -584,6 +601,17 @@ version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
[[package]]
name = "futures-macro"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.16",
]
[[package]]
name = "futures-sink"
version = "0.3.28"
@ -603,16 +631,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
dependencies = [
"futures-core",
"futures-macro",
"futures-task",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "geneit_backend"
version = "0.1.0"
dependencies = [
"actix-remote-ip",
"actix-web",
"anyhow",
"clap",
"diesel",
"env_logger",

View File

@ -10,6 +10,8 @@ log = "0.4.17"
env_logger = "0.10.0"
clap = { version = "4.3.0", features = ["derive", "env"] }
lazy_static = "1.4.0"
anyhow = "1.0.71"
actix-web = "4.3.1"
diesel = { version = "2.0.4", features = ["postgres"] }
serde = { version = "1.0.163", features = ["derive"] }
actix-remote-ip = "0.1.0"

View File

@ -16,17 +16,25 @@ pub struct AppConfig {
#[clap(short, long, env)]
pub proxy_ip: Option<String>,
/// PostgreSQL connexion chain
#[clap(long, env, default_value = "postgres://localhost/geneit")]
db_chain: String,
/// PostgreSQL database host
#[clap(long, env, default_value = "localhost")]
db_host: String,
/// PostgreSQL database port
#[clap(long, env, default_value_t = 5432)]
db_port: u16,
/// PostgreSQL username
#[clap(long, env, default_value = "user")]
db_username: String,
/// PostgreSQL password
#[clap(long, env, default_value = "user")]
#[clap(long, env, default_value = "pass")]
db_password: String,
/// PostgreSQL database name
#[clap(long, env, default_value = "geneit")]
db_name: String,
}
lazy_static::lazy_static! {
@ -40,4 +48,12 @@ impl AppConfig {
pub fn get() -> &'static AppConfig {
&ARGS
}
/// Get full db connection chain
pub fn db_connection_chain(&self) -> String {
format!(
"postgres://{}:{}@{}:{}/{}",
self.db_username, self.db_password, self.db_host, self.db_port, self.db_name
)
}
}

View File

@ -8,6 +8,11 @@ impl SizeConstraint {
pub fn new(min: usize, max: usize) -> Self {
Self { min, max }
}
pub fn validate(&self, val: &str) -> bool {
let len = val.trim().len();
len >= self.min && len <= self.max
}
}
#[derive(Debug, Clone, serde::Serialize)]

View File

@ -1 +1,4 @@
//! # API controller
pub mod auth_controller;
pub mod config_controller;

View File

@ -0,0 +1,43 @@
use crate::constants::StaticConstraints;
use crate::services::users_service;
use actix_remote_ip::RemoteIP;
use actix_web::error::ErrorInternalServerError;
use actix_web::{web, HttpResponse};
#[derive(serde::Deserialize)]
pub struct CreateAccountBody {
name: String,
email: String,
}
/// Create a new account
pub async fn create_account(
_remote_ip: RemoteIP,
req: web::Json<CreateAccountBody>,
) -> actix_web::Result<HttpResponse> {
// TODO : rate limiting
// TODO : check if email is valid
// Check parameters
let constraints = StaticConstraints::default();
if !constraints.user_name_len.validate(&req.name) || !constraints.mail_len.validate(&req.email)
{
return Ok(HttpResponse::BadRequest().json("Size constraints were not respected!"));
}
// TODO : check the mail address
// Create the account
let user_id = users_service::create_account(&req.name, &req.email)
.await
.map_err(|e| {
log::error!("Failed to create user! {e}");
ErrorInternalServerError(e)
})?;
// TODO : trigger reset password (send mail)
// Account successfully created
Ok(HttpResponse::Created().finish())
}

View File

@ -0,0 +1,25 @@
//! # Database connection management
use crate::app_config::AppConfig;
use diesel::{Connection, PgConnection};
use std::cell::RefCell;
thread_local! {
static POSTGRES_CONNECTION: RefCell<Option<PgConnection>> = RefCell::new(None);
}
/// Execute a request on the database
pub fn execute<E, I>(cb: E) -> anyhow::Result<I>
where
E: FnOnce(&mut PgConnection) -> anyhow::Result<I>,
{
// Establish connection if required
if POSTGRES_CONNECTION.with(|i| i.borrow().is_none()) {
let database_url = AppConfig::get().db_connection_chain();
let conn = PgConnection::establish(&database_url)
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url));
POSTGRES_CONNECTION.with(|i| *i.borrow_mut() = Some(conn))
}
POSTGRES_CONNECTION.with(|i| cb(i.borrow_mut().as_mut().unwrap()))
}

View File

@ -1,4 +1,10 @@
pub mod app_config;
pub mod constants;
pub mod controllers;
pub mod services;
pub mod utils;
// Diesel specific
pub mod db_connection;
pub mod models;
pub mod schema;

View File

@ -1,6 +1,8 @@
use actix_remote_ip::RemoteIPConfig;
use actix_web::middleware::Logger;
use actix_web::{web, App, HttpServer};
use geneit_backend::app_config::AppConfig;
use geneit_backend::controllers::config_controller;
use geneit_backend::controllers::{auth_controller, config_controller};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
@ -10,12 +12,21 @@ async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.wrap(Logger::default())
.app_data(web::Data::new(RemoteIPConfig {
proxy: AppConfig::get().proxy_ip.clone(),
}))
// Config controller
.route("/", web::get().to(config_controller::home))
.route(
"/config/static",
web::get().to(config_controller::static_config),
)
// Auth controller
.route(
"/auth/create_account",
web::post().to(auth_controller::create_account),
)
})
.bind(AppConfig::get().listen_address.as_str())?
.run()

View File

@ -0,0 +1,34 @@
use crate::schema::users;
use diesel::prelude::*;
/// User ID holder
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct UserID(i32);
#[derive(Queryable, Debug)]
pub struct User {
id: i32,
name: String,
email: String,
password: Option<String>,
reset_password_token: Option<String>,
time_create: i64,
time_gen_reset_token: i64,
time_activate: i64,
active: bool,
admin: bool,
}
impl User {
pub fn id(&self) -> UserID {
UserID(self.id)
}
}
#[derive(Insertable)]
#[diesel(table_name = users)]
pub struct NewUser<'a> {
pub name: &'a str,
pub email: &'a str,
pub time_create: i64,
}

View File

@ -0,0 +1,3 @@
//! # Backend services
pub mod users_service;

View File

@ -0,0 +1,22 @@
//! # Users service
use crate::db_connection;
use crate::models::{NewUser, User};
use crate::schema::users;
use crate::utils::time_utils::time;
use diesel::prelude::*;
/// Create a new account
pub async fn create_account(name: &str, email: &str) -> anyhow::Result<User> {
db_connection::execute(|conn| {
let res = diesel::insert_into(users::table)
.values(&NewUser {
name: name.trim(),
email: email.trim(),
time_create: time() as i64,
})
.get_result(conn)?;
Ok(res)
})
}

View File

@ -0,0 +1,3 @@
//! # App utilities
pub mod time_utils;

View File

@ -0,0 +1,11 @@
//! # Time utilities
use std::time::{SystemTime, UNIX_EPOCH};
/// Get the current time since epoch
pub fn time() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}