From 23cf79911e20eac981a25dc1c2f839d37f98c296 Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Mon, 21 Nov 2022 12:11:38 +0100 Subject: Add fields for users --- .../migrations/20220822142548_add-users-table.sql | 11 ++-- server/src/errors.rs | 10 +-- server/src/models/auth.rs | 9 +++ server/src/models/user.rs | 71 +++++++++++++++------- server/src/routes/auth.rs | 51 +++++++++++++--- server/src/routes/user.rs | 17 +----- 6 files changed, 114 insertions(+), 55 deletions(-) diff --git a/server/migrations/20220822142548_add-users-table.sql b/server/migrations/20220822142548_add-users-table.sql index 4798d7e..c9571ec 100644 --- a/server/migrations/20220822142548_add-users-table.sql +++ b/server/migrations/20220822142548_add-users-table.sql @@ -1,6 +1,9 @@ create table users ( - id serial unique, - email varchar(100) unique not null, - password varchar(100) not null, - is_staff boolean default false + id SERIAL PRIMARY KEY, + name VARCHAR(100), + email VARCHAR(100) UNIQUE NOT NULL, + username VARCHAR(100) UNIQUE NOT NULL, + password VARCHAR(100) NOT NULL, + avatar VARCHAR, + is_staff BOOLEAN DEFAULT false ); diff --git a/server/src/errors.rs b/server/src/errors.rs index e541eda..72eb837 100644 --- a/server/src/errors.rs +++ b/server/src/errors.rs @@ -7,14 +7,12 @@ use serde_json::json; /// All errors raised by the web app pub enum AppError { - /// Generic error, never called yet - Generic, /// Database error Database, /// Generic bad request. It is handled with a message value BadRequest(String), /// Not found error - NotFound, + NotFound(String), /// Raised when a token is not good created TokenCreation, /// Raised when a passed token is not valid @@ -30,16 +28,12 @@ impl IntoResponse for AppError { /// ``` fn into_response(self) -> Response { let (status, error_message) = match self { - AppError::Generic => ( - StatusCode::INTERNAL_SERVER_ERROR, - "Generic error, can't find why".to_string(), - ), AppError::Database => ( StatusCode::INTERNAL_SERVER_ERROR, "Error with database connection".to_string(), ), AppError::BadRequest(value) => (StatusCode::BAD_REQUEST, value), - AppError::NotFound => (StatusCode::NOT_FOUND, "Element not found".to_string()), + AppError::NotFound(value) => (StatusCode::NOT_FOUND, value), AppError::TokenCreation => ( StatusCode::INTERNAL_SERVER_ERROR, "Token creation error".to_string(), diff --git a/server/src/models/auth.rs b/server/src/models/auth.rs index f798eee..cf69f50 100644 --- a/server/src/models/auth.rs +++ b/server/src/models/auth.rs @@ -32,10 +32,19 @@ pub struct AuthBody { token_type: String, } +/// Payload used for login +#[derive(Deserialize)] +pub struct LoginCredentials { + pub username: String, + pub password: String, +} + /// Paylod used for user creation #[derive(Deserialize)] pub struct SignUpForm { + pub name: String, pub email: String, + pub username: String, pub password1: String, pub password2: String, } diff --git a/server/src/models/user.rs b/server/src/models/user.rs index ace4266..356b9a2 100644 --- a/server/src/models/user.rs +++ b/server/src/models/user.rs @@ -9,37 +9,42 @@ use validator::Validate; #[derive(Deserialize, Serialize, Validate, sqlx::FromRow)] pub struct User { id: i32, - #[validate(length(min = 1, message = "Can not be empty"))] + name: String, + #[validate(length(min = 4, message = "Can not be empty"))] email: String, + #[validate(length(min = 2, message = "Can not be empty"))] + username: String, #[validate(length(min = 8, message = "Must be min 8 chars length"))] password: String, is_staff: Option, + avatar: Option, } /// Response used to print a user (or a users list) -#[derive(Deserialize, Serialize, sqlx::FromRow)] +#[derive(Deserialize, Serialize, Validate, sqlx::FromRow)] pub struct UserList { // It is public because it used by `Claims` creation pub id: i32, - email: String, - is_staff: Option, -} - -/// Payload used for user creation -#[derive(Deserialize)] -pub struct UserCreate { + pub name: String, + #[validate(length(min = 4, message = "Can not be empty"))] pub email: String, - pub password: String, + #[validate(length(min = 2, message = "Can not be empty"))] + pub username: String, + pub is_staff: Option, + pub avatar: Option, } impl User { /// By default an user has id = 0. It is not created yet - pub fn new(email: String, password: String) -> Self { + pub fn new(name: String, email: String, username: String, password: String) -> Self { Self { id: 0, + name, email, + username, password, is_staff: Some(false), + avatar: None, } } @@ -54,12 +59,14 @@ impl User { let rec: UserList = sqlx::query_as( r#" - INSERT INTO users (email, password) - VALUES ( $1, $2 ) - RETURNING id, email, is_staff + INSERT INTO users (name, email, username, password) + VALUES ( $1, $2, $3, $4) + RETURNING id, name, email, username, is_staff, avatar "#, ) + .bind(user.name) .bind(user.email) + .bind(user.username) .bind(crypted_password) .fetch_one(pool) .await?; @@ -75,11 +82,11 @@ impl User { let rec: UserList = sqlx::query_as( r#" - SELECT id, email, is_staff FROM "users" - WHERE email = $1 AND password = $2 + SELECT id, name, email, username, is_staff, avatar FROM "users" + WHERE username = $1 AND password = $2 "#, ) - .bind(user.email) + .bind(user.username) .bind(crypted_password) .fetch_one(pool) .await?; @@ -93,7 +100,7 @@ impl User { let rec: UserList = sqlx::query_as( r#" - SELECT id, email, is_staff FROM "users" + SELECT id, name, email, username, is_staff, avatar FROM "users" WHERE id = $1 "#, ) @@ -107,13 +114,35 @@ impl User { /// List all users pub async fn list() -> Result, AppError> { let pool = unsafe { get_client() }; - let rows: Vec = sqlx::query_as(r#"SELECT id, email, is_staff FROM users"#) - .fetch_all(pool) - .await?; + let rows: Vec = sqlx::query_as( + r#"SELECT id, name, email, username, is_staff, avatar FROM users + ORDER BY id DESC + LIMIT $1 OFFSET $2 + "#, + ) + .fetch_all(pool) + .await?; Ok(rows) } + /// Prevent the "uniquess" Postgres fields check. Check if username has been taken + pub async fn username_has_taken(username: &String) -> Result { + let pool = unsafe { get_client() }; + let cursor = sqlx::query( + r#" + SELECT COUNT(id) as count FROM users WHERE username = $1 + "#, + ) + .bind(username) + .fetch_one(pool) + .await?; + + let count: i64 = cursor.try_get(0).unwrap(); + + Ok(count > 0) + } + /// Prevent the "uniquess" Postgres fields check. Check if email has been taken pub async fn email_has_taken(email: &String) -> Result { let pool = unsafe { get_client() }; diff --git a/server/src/routes/auth.rs b/server/src/routes/auth.rs index e3d7e4e..504d428 100644 --- a/server/src/routes/auth.rs +++ b/server/src/routes/auth.rs @@ -1,9 +1,15 @@ use crate::errors::AppError; use crate::models::{ - auth::{AuthBody, Claims, SignUpForm}, + auth::{AuthBody, Claims, LoginCredentials, SignUpForm}, user::*, }; -use axum::{routing::post, Json, Router}; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + routing::post, + Json, Router, +}; +use serde::Serialize; /// Create routes for `/v1/auth/` namespace pub fn create_route() -> Router { @@ -12,21 +18,39 @@ pub fn create_route() -> Router { .route("/signup", post(signup)) } +/// Extension of `Json` which returns the CREATED status code +pub struct JsonCreate(pub T); + +impl IntoResponse for JsonCreate +where + T: Serialize, +{ + fn into_response(self) -> Response { + (StatusCode::CREATED, Json(self.0)).into_response() + } +} + /// Make login. Check if a user with the email and password passed in request body exists into the /// database -async fn make_login(Json(payload): Json) -> Result, AppError> { - let user = User::new(payload.email, payload.password); +async fn make_login(Json(payload): Json) -> Result, AppError> { + let user = User::new( + String::new(), + String::new(), + payload.username, + payload.password, + ); match User::find(user).await { Ok(user) => { let claims = Claims::new(user.id); let token = claims.get_token()?; Ok(Json(AuthBody::new(token))) } - Err(_) => Err(AppError::NotFound), + Err(_) => Err(AppError::NotFound("User not found".to_string())), } } + /// Create a new user -async fn signup(Json(payload): Json) -> Result, AppError> { +async fn signup(Json(payload): Json) -> Result, AppError> { if payload.password1 != payload.password2 { return Err(AppError::BadRequest( "The inserted passwords do not match".to_string(), @@ -39,10 +63,21 @@ async fn signup(Json(payload): Json) -> Result, AppEr )); } - let user = User::new(payload.email, payload.password1); + if User::username_has_taken(&payload.username).await? { + return Err(AppError::BadRequest( + "An user with this username already exists".to_string(), + )); + } + + let user = User::new( + payload.name, + payload.email, + payload.username, + payload.password1, + ); let user = User::create(user).await?; let claims = Claims::new(user.id); let token = claims.get_token()?; - Ok(Json(AuthBody::new(token))) + Ok(JsonCreate(AuthBody::new(token))) } diff --git a/server/src/routes/user.rs b/server/src/routes/user.rs index 5733871..d0aa056 100644 --- a/server/src/routes/user.rs +++ b/server/src/routes/user.rs @@ -1,14 +1,14 @@ use crate::errors::AppError; use crate::models::{ auth::Claims, - user::{User, UserCreate, UserList}, + user::{User, UserList}, }; use axum::{routing::get, Json, Router}; /// Create routes for `/v1/users/` namespace pub fn create_route() -> Router { Router::new() - .route("/", get(list_users).post(create_user)) + .route("/", get(list_users)) .route("/me", get(get_user)) } @@ -19,21 +19,10 @@ async fn list_users(_: Claims) -> Result>, AppError> { Ok(Json(users)) } -/// Create an user. Checks Authorization token -async fn create_user( - Json(payload): Json, - _: Claims, -) -> Result, AppError> { - let user = User::new(payload.email, payload.password); - let user_new = User::create(user).await?; - - Ok(Json(user_new)) -} - /// Get the user from the `Authorization` header token async fn get_user(claims: Claims) -> Result, AppError> { match User::find_by_id(claims.user_id).await { Ok(user) => Ok(Json(user)), - Err(_) => Err(AppError::NotFound), + Err(_) => Err(AppError::NotFound("User not found".to_string())), } } -- cgit v1.2.3-18-g5258