Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Password change, profile update API and Email whitelisting #59

Merged
merged 15 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions server/migrations/20240603072942_users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ create table users (
user_id uuid primary key default uuid_generate_v1mc(),
username text collate "case_insensitive" unique not null,
email text collate "case_insensitive" unique not null,
fullname text,
saifullah-talukder marked this conversation as resolved.
Show resolved Hide resolved
title text,
company text,
password_hash text,
access_token text,
created_at timestamptz not null default now(),
Expand Down
6 changes: 6 additions & 0 deletions server/migrations/20240614061840_whitelisted_emails.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Add migration script here
CREATE TABLE whitelisted_emails
(
email text collate "case_insensitive" primary key,
approved boolean not null default false
);
2 changes: 1 addition & 1 deletion server/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ pub mod models;
pub mod oauth2;
pub mod routes;
pub mod services;
pub mod utils;
pub(crate) mod sessions;
mod utils;
5 changes: 5 additions & 0 deletions server/src/auth/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,8 @@ mod tests {
assert!(dummy_verify_password(Secret::new("password")).is_ok());
}
}

pub struct WhitelistedEmail {
pub email: String,
pub approved: bool,
}
7 changes: 5 additions & 2 deletions server/src/auth/routes.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::auth::models::{AuthSession, Credentials, RegisterUserRequest};
use crate::auth::services::register;
use crate::auth::services;
use crate::auth::{OAuthCredentials, PasswordCredentials};
use crate::err::AppError;
use crate::startup::AppState;
Expand All @@ -20,7 +20,10 @@ async fn register_handler(
State(pool): State<PgPool>,
Form(request): Form<RegisterUserRequest>,
) -> crate::Result<impl IntoResponse> {
register(pool, request)
if !services::is_email_whitelisted(&pool, &request.email).await? {
return Err(eyre!("This email is not whitelisted!").into());
}
services::register(pool, request)
.await
.map(|user| (StatusCode::CREATED, Json(user)))
}
Expand Down
15 changes: 13 additions & 2 deletions server/src/auth/services.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use crate::auth::models::RegisterUserRequest;
use crate::auth::models;
use crate::auth::utils;
use crate::err::{AppError, ResultExt};
use crate::users::{User, UserRecord};
use color_eyre::eyre::eyre;
use sqlx::PgPool;

#[tracing::instrument(level = "debug", ret, err)]
pub async fn register(pool: PgPool, request: RegisterUserRequest) -> crate::Result<UserRecord> {
pub async fn register(pool: PgPool, request: models::RegisterUserRequest) -> crate::Result<UserRecord> {
if let Some(password) = request.password {
let password_hash = utils::hash_password(password).await?;
let user = sqlx::query_as!(
Expand Down Expand Up @@ -47,3 +47,14 @@ pub async fn register(pool: PgPool, request: RegisterUserRequest) -> crate::Resu
}
Err(eyre!("Either password or access_token must be provided to create a user").into())
}

pub async fn is_email_whitelisted(pool: &PgPool, email: &String) -> crate::Result<bool> {
let whitelisted_email = sqlx::query_as!(models::WhitelistedEmail, "SELECT * FROM whitelisted_emails WHERE email = $1", email)
.fetch_one(pool)
.await;

match whitelisted_email {
Ok(whitelisted_email) => Ok(whitelisted_email.approved),
_ => Ok(false)
}
}
1 change: 1 addition & 0 deletions server/src/users/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pub use routes::*;
pub mod models;
pub mod routes;
pub mod selectors;
pub mod services;
25 changes: 25 additions & 0 deletions server/src/users/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ pub struct UserRecord {
pub user_id: uuid::Uuid,
pub email: String,
pub username: String,
pub fullname: Option<String>,
pub title: Option<String>,
pub company: Option<String>,
}

impl From<User> for UserRecord {
Expand All @@ -22,6 +25,9 @@ impl From<User> for UserRecord {
user_id: user.user_id,
email: user.email,
username: user.username,
fullname: user.fullname,
title: user.title,
company: user.company,
}
}
}
Expand All @@ -31,6 +37,9 @@ pub struct User {
pub user_id: uuid::Uuid,
pub email: String,
pub username: String,
pub fullname: Option<String>,
pub title: Option<String>,
pub company: Option<String>,
pub password_hash: Secret<Option<String>>,
pub access_token: Secret<Option<String>>,

Expand Down Expand Up @@ -81,3 +90,19 @@ impl AuthUser for User {
&[]
}
}

#[derive(Serialize, Deserialize, Debug)]
pub struct UpdatePasswordRequest {
pub old_password: Secret<String>,
pub new_password: Secret<String>,
}


#[derive(Serialize, Deserialize, Debug)]
pub struct UpdateProfileRequest {
pub username: Option<String>,
pub email: Option<String>,
pub fullname: Option<String>,
pub title: Option<String>,
pub company: Option<String>,
}
46 changes: 42 additions & 4 deletions server/src/users/routes.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
use crate::auth::AuthSession;
use crate::auth::utils::verify_user_password;
use crate::err::AppError;
use crate::startup::AppState;
use crate::users::UserRecord;
use axum::routing::get;
use axum::{Json, Router};
use crate::users::{User, UserRecord, models, services};
use color_eyre::eyre::eyre;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::routing::{get, patch};
use axum::extract::State;
use axum::{Form, Json, Router};
use sqlx::PgPool;

#[tracing::instrument(level = "debug", skip_all, ret, err(Debug))]
async fn get_user_handler(auth_session: AuthSession) -> crate::Result<Json<UserRecord>> {
Expand All @@ -15,6 +21,38 @@ async fn get_user_handler(auth_session: AuthSession) -> crate::Result<Json<UserR
Ok(Json(user))
}

#[tracing::instrument(level = "debug", skip_all, ret, err(Debug))]
async fn update_profile_handler(
State(pool): State<PgPool>,
user: User,
Json(update_profile_request): Json<models::UpdateProfileRequest>
) -> crate::Result<Json<UserRecord>> {
let user_id = user.user_id;
let updated_user = services::update_profile(&pool, &user_id, update_profile_request).await?;
saifullah-talukder marked this conversation as resolved.
Show resolved Hide resolved

Ok(Json(UserRecord::from(updated_user)))
}

#[tracing::instrument(level = "debug", skip_all, ret, err(Debug))]
async fn update_password_handler(
State(pool): State<PgPool>,
user: User,
Form(update_password_request): Form<models::UpdatePasswordRequest>
) -> crate::Result<impl IntoResponse> {
let user_id = user.user_id;
match verify_user_password(Some(user), update_password_request.old_password) {
Ok(Some(_user)) => {
saifullah-talukder marked this conversation as resolved.
Show resolved Hide resolved
services::update_password(&pool, &user_id, update_password_request.new_password).await?;
Ok((StatusCode::OK, ()))
},
_ => Err(eyre!("Failed to authenticate old password").into())

}
}

pub fn routes() -> Router<AppState> {
Router::new().route("/me", get(get_user_handler))
Router::new()
.route("/me", get(get_user_handler))
.route("/me", patch(update_profile_handler))
.route("/update-password", patch(update_password_handler))
}
70 changes: 70 additions & 0 deletions server/src/users/services.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use crate::secrets::Secret;
use crate::auth::utils::hash_password;
use crate::users::models;
use sqlx::PgPool;
use uuid::Uuid;

#[tracing::instrument(level = "debug", ret, err)]
pub async fn update_profile(
pool: &PgPool,
user_id: &Uuid,
update_profile_request: models::UpdateProfileRequest,
) -> crate::Result<models::User> {
let user = sqlx::query_as!(
models::User,
"
update users
set username = case
when $1::text is not null then $1::text
else username
end,
email = case
when $2::text is not null then $2::text
else email
end,
saifullah-talukder marked this conversation as resolved.
Show resolved Hide resolved
fullname = case
when $3::text is not null then $3::text
else fullname
end,
title = case
when $4::text is not null then $4::text
else title
end,
company = case
when $5::text is not null then $5::text
else company
end
where user_id = $6 returning *
",
update_profile_request.username,
update_profile_request.email,
update_profile_request.fullname,
update_profile_request.title,
update_profile_request.company,
user_id,
)
.fetch_one(pool)
.await?;

return Ok(user);
}


#[tracing::instrument(level = "debug", ret, err)]
pub async fn update_password(
pool: &PgPool,
user_id: &Uuid,
password: Secret<String>,
) -> crate::Result<()> {
let password_hash = hash_password(password).await?;
sqlx::query_as!(
User,
"update users set password_hash = $1 where user_id = $2",
password_hash.expose(),
user_id
)
.execute(pool)
.await?;

return Ok(());
}