Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
254 changes: 0 additions & 254 deletions backend/src/http/users.rs

This file was deleted.

51 changes: 51 additions & 0 deletions backend/src/http/users/create.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use crate::http::{ApiContext, Result};
use crate::metrics;
use axum::Json;
use axum::extract::State;

use crate::http::error::{Error, ResultExt};
use crate::http::extractor::AuthUser;
use uuid::Uuid;

use super::helpers::*;
use super::models::*;

pub async fn create_user(
State(ctx): State<ApiContext>,
Json(req): Json<UserBody<NewUser>>,
) -> Result<Json<UserBody<User>>> {
let password_hash = hash_password(req.user.password).await?;

metrics::observe_db_query();
let user_id = sqlx::query_scalar!(
r#"insert into "user" (username, email, password_hash) values ($1, $2, $3) returning user_id"#,
req.user.username,
req.user.email,
password_hash
)
.fetch_one(&ctx.db)
.await
.on_constraint("user_username_key", |_| {
Error::unprocessable_entity([("username", "username taken")])
})
.on_constraint("user_email_key", |_| {
Error::unprocessable_entity([("email", "email taken")])
})?;

metrics::record_user_created();

Ok(Json(UserBody {
user: User {
email: req.user.email,
token: AuthUser {
user_id,
session_id: Uuid::new_v4(),
}
.to_jwt(&ctx)
.await?,
username: req.user.username,
bio: "".to_string(),
image: None,
},
}))
}
31 changes: 31 additions & 0 deletions backend/src/http/users/get.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use crate::http::{ApiContext, Result};
use crate::metrics;
use axum::Json;
use axum::extract::State;

use crate::http::extractor::AuthUser;

use super::models::*;

pub async fn get_current_user(
auth_user: AuthUser,
State(ctx): State<ApiContext>,
) -> Result<Json<UserBody<User>>> {
metrics::observe_db_query();
let user = sqlx::query!(
r#"select email, username, bio, pfp_id from "user" where user_id = $1"#,
auth_user.user_id
)
.fetch_one(&ctx.db)
.await?;

Ok(Json(UserBody {
user: User {
email: user.email,
token: auth_user.to_jwt(&ctx).await?,
username: user.username,
bio: user.bio,
image: user.pfp_id,
},
}))
}
32 changes: 32 additions & 0 deletions backend/src/http/users/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use crate::http::Result;
use anyhow::Context;
use argon2::password_hash::SaltString;
use argon2::{Argon2, PasswordHash};

use crate::http::error::Error;

pub async fn hash_password(password: String) -> Result<String> {
Ok(tokio::task::spawn_blocking(move || -> Result<String> {
let salt = SaltString::generate(rand::thread_rng());
Ok(PasswordHash::generate(Argon2::default(), password, &salt)
.map_err(|e| anyhow::anyhow!("failed to generate password hash: {}", e))?
.to_string())
})
.await
.context("panic in generating password hash")??)
}

pub async fn verify_password(password: String, password_hash: String) -> Result<()> {
Ok(tokio::task::spawn_blocking(move || -> Result<()> {
let hash = PasswordHash::new(&password_hash)
.map_err(|e| anyhow::anyhow!("invalid password hash: {}", e))?;

hash.verify_password(&[&Argon2::default()], password)
.map_err(|e| match e {
argon2::password_hash::Error::Password => Error::Unauthorized,
_ => anyhow::anyhow!("failed to verify password hash: {}", e).into(),
})
})
.await
.context("panic in verifying password hash")??)
}
Loading