summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSanto Cariotti <santo@dcariotti.me>2022-11-21 12:11:38 +0100
committerSanto Cariotti <santo@dcariotti.me>2022-11-21 12:11:38 +0100
commit23cf79911e20eac981a25dc1c2f839d37f98c296 (patch)
treee0ed1e287121bbdc2bbe1ac3aff11273f730c741
parent7d661b657bbc31062e90b1a9c2bd8666627c2e07 (diff)
Add fields for users
-rw-r--r--server/migrations/20220822142548_add-users-table.sql11
-rw-r--r--server/src/errors.rs10
-rw-r--r--server/src/models/auth.rs9
-rw-r--r--server/src/models/user.rs71
-rw-r--r--server/src/routes/auth.rs51
-rw-r--r--server/src/routes/user.rs17
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<bool>,
+ avatar: Option<String>,
}
/// 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<bool>,
-}
-
-/// 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<bool>,
+ pub avatar: Option<String>,
}
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<Vec<UserList>, AppError> {
let pool = unsafe { get_client() };
- let rows: Vec<UserList> = sqlx::query_as(r#"SELECT id, email, is_staff FROM users"#)
- .fetch_all(pool)
- .await?;
+ let rows: Vec<UserList> = 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<bool, AppError> {
+ 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<bool, AppError> {
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<T>(pub T);
+
+impl<T> IntoResponse for JsonCreate<T>
+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<UserCreate>) -> Result<Json<AuthBody>, AppError> {
- let user = User::new(payload.email, payload.password);
+async fn make_login(Json(payload): Json<LoginCredentials>) -> Result<Json<AuthBody>, 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<SignUpForm>) -> Result<Json<AuthBody>, AppError> {
+async fn signup(Json(payload): Json<SignUpForm>) -> Result<JsonCreate<AuthBody>, 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<SignUpForm>) -> Result<Json<AuthBody>, 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<Json<Vec<UserList>>, AppError> {
Ok(Json(users))
}
-/// Create an user. Checks Authorization token
-async fn create_user(
- Json(payload): Json<UserCreate>,
- _: Claims,
-) -> Result<Json<UserList>, 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<Json<UserList>, 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())),
}
}