AppFlowy-Cloud/src/component/auth/user.rs

381 lines
9.8 KiB
Rust

use crate::component::auth::{
compute_hash_password, internal_error, validate_credentials, AuthError, Credentials,
};
use crate::config::env::{domain, jwt_secret};
use crate::state::UserCache;
use crate::telemetry::spawn_blocking_with_tracing;
use actix_web::http::header::HeaderValue;
use actix_web::{FromRequest, HttpRequest};
use anyhow::Context;
use chrono::Utc;
use chrono::{Duration, Local};
use futures_util::future::{ready, Ready};
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use secrecy::{ExposeSecret, Secret, Zeroize};
use serde::{Deserialize, Serialize};
use sqlx::types::uuid;
use sqlx::{PgPool, Postgres, Transaction};
use std::sync::Arc;
use tokio::sync::RwLock;
pub async fn login(
pg_pool: PgPool,
cache: Arc<RwLock<UserCache>>,
email: String,
password: String,
) -> Result<(LoginResponse, Secret<Token>), AuthError> {
let credentials = Credentials {
email,
password: Secret::new(password),
};
match validate_credentials(credentials, &pg_pool).await {
Ok(uid) => {
let uid = uid.to_string();
let token = Token::create_token(&uid)?;
let logged_user = LoggedUser::new(uid.clone());
cache.write().await.authorized(logged_user);
Ok((
LoginResponse {
token: token.clone().into(),
uid,
},
Secret::new(token),
))
}
Err(err) => {
//
Err(err)
}
}
}
pub async fn logout(logged_user: LoggedUser, cache: Arc<RwLock<UserCache>>) {
cache.write().await.unauthorized(logged_user);
}
pub async fn register(
pg_pool: PgPool,
cache: Arc<RwLock<UserCache>>,
username: String,
email: String,
password: String,
) -> Result<RegisterResponse, AuthError> {
let mut transaction = pg_pool
.begin()
.await
.context("Failed to acquire a Postgres connection to register user")
.map_err(internal_error)?;
if is_email_exist(&mut transaction, email.as_ref())
.await
.map_err(internal_error)?
{
return Err(AuthError::UserAlreadyExist { email });
}
let uuid = uuid::Uuid::new_v4();
let token = Token::create_token(&uuid.to_string())?;
let password = compute_hash_password(password.as_bytes()).map_err(internal_error)?;
let _ = sqlx::query!(
r#"
INSERT INTO users (uid, email, username, create_time, password)
VALUES ($1, $2, $3, $4, $5)
"#,
uuid,
email,
username,
Utc::now(),
password.expose_secret(),
)
.execute(&mut transaction)
.await
.context("Save user to disk failed")
.map_err(internal_error)?;
transaction
.commit()
.await
.context("Failed to commit SQL transaction to register user.")
.map_err(internal_error)?;
let logged_user = LoggedUser::new(uuid.to_string());
cache.write().await.authorized(logged_user);
Ok(RegisterResponse {
token: token.into(),
})
}
pub async fn change_password(
pg_pool: PgPool,
logged_user: LoggedUser,
current_password: String,
new_password: String,
) -> Result<(), AuthError> {
let mut transaction = pg_pool
.begin()
.await
.context("Failed to acquire a Postgres connection to change password")
.map_err(internal_error)?;
let email = get_user_email(logged_user.expose_secret(), &mut transaction).await?;
// check password
let credentials = Credentials {
email,
password: Secret::new(current_password),
};
let _ = validate_credentials(credentials, &pg_pool).await?;
// Hash password
let new_hash_password =
spawn_blocking_with_tracing(move || compute_hash_password(new_password.as_bytes()))
.await
.context("Failed to hash password")??;
let uid =
uuid::Uuid::parse_str(logged_user.expose_secret()).map_err(|e| AuthError::InvalidUuid {
err: format!("{}", e),
})?;
// Save password to disk
let sql = "UPDATE users SET password = $1 where uid = $2";
let _ = sqlx::query(sql)
.bind(new_hash_password.expose_secret())
.bind(uid)
.execute(&mut transaction)
.await
.context("Failed to change user's password in the database.")?;
transaction
.commit()
.await
.context("Failed to commit SQL transaction to change user's password")
.map_err(internal_error)?;
Ok(())
}
pub async fn get_user_email(
uid: &str,
transaction: &mut Transaction<'_, Postgres>,
) -> Result<String, anyhow::Error> {
let uid = uuid::Uuid::parse_str(uid)?;
let row = sqlx::query!(
r#"
SELECT email
FROM users
WHERE uid = $1
"#,
uid,
)
.fetch_one(transaction)
.await
.context("Failed to retrieve the username`")?;
Ok(row.email)
}
/// TODO: cache this state in [State]
async fn is_email_exist(
transaction: &mut Transaction<'_, Postgres>,
email: &str,
) -> Result<bool, anyhow::Error> {
let result = sqlx::query(r#"SELECT email FROM users WHERE email = $1"#)
.bind(email)
.fetch_optional(transaction)
.await?;
Ok(result.is_some())
}
#[derive(Default, Deserialize, Debug)]
pub struct LoginRequest {
pub email: String,
pub password: String,
}
#[derive(Default, Serialize, Deserialize, Debug)]
pub struct LoginResponse {
pub token: String,
pub uid: String,
}
#[derive(Default, Deserialize, Debug)]
pub struct RegisterRequest {
pub email: String,
pub password: String,
pub name: String,
}
#[derive(Default, Serialize, Deserialize, Debug)]
pub struct RegisterResponse {
pub token: String,
}
#[derive(Default, Deserialize, Debug)]
pub struct ChangePasswordRequest {
pub current_password: String,
pub new_password: String,
pub new_password_confirm: String,
}
#[derive(Debug, Clone)]
pub struct LoggedUser {
uid: Secret<String>,
}
impl From<Claim> for LoggedUser {
fn from(c: Claim) -> Self {
Self {
uid: Secret::new(c.user_id()),
}
}
}
impl LoggedUser {
pub fn new(uid: String) -> Self {
Self {
uid: Secret::new(uid),
}
}
pub fn from_token(token: String) -> Result<Self, AuthError> {
let user: LoggedUser = Token::decode_token(&token.into())?.into();
Ok(user)
}
}
impl std::ops::Deref for LoggedUser {
type Target = Secret<String>;
fn deref(&self) -> &Self::Target {
&self.uid
}
}
impl FromRequest for LoggedUser {
type Error = AuthError;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut actix_http::Payload) -> Self::Future {
match Token::parser_from_request(req) {
Ok(token) => ready(LoggedUser::from_token(token.0)),
Err(err) => ready(Err(err)),
}
}
}
impl TryFrom<&HeaderValue> for LoggedUser {
type Error = AuthError;
fn try_from(header: &HeaderValue) -> Result<Self, Self::Error> {
match header.to_str() {
Ok(val) => LoggedUser::from_token(val.to_owned()),
Err(e) => {
tracing::error!("Header to string failed: {:?}", e);
Err(AuthError::Unauthorized)
}
}
}
}
pub const HEADER_TOKEN: &str = "token";
const DEFAULT_ALGORITHM: Algorithm = Algorithm::HS256;
pub const EXPIRED_DURATION_DAYS: i64 = 30;
#[derive(Debug, Serialize, Deserialize)]
pub struct Claim {
// issuer
iss: String,
// subject
sub: String,
// issue at
iat: i64,
// expiry
exp: i64,
uid: String,
}
impl Claim {
pub fn with_user_id(uid: &str) -> Self {
let domain = domain();
Self {
iss: domain,
sub: "auth".to_string(),
uid: uid.to_string(),
iat: Local::now().timestamp(),
exp: (Local::now() + Duration::days(EXPIRED_DURATION_DAYS)).timestamp(),
}
}
pub fn user_id(self) -> String {
self.uid
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct Token(pub String);
impl Zeroize for Token {
fn zeroize(&mut self) {
self.0.zeroize()
}
}
impl Token {
pub fn create_token(user_id: &str) -> Result<Self, AuthError> {
let claims = Claim::with_user_id(user_id);
encode(
&Header::new(DEFAULT_ALGORITHM),
&claims,
&EncodingKey::from_secret(jwt_secret().as_ref()),
)
.map(Into::into)
.context("Create user token failed")
.map_err(|err| AuthError::InternalError(err))
}
pub fn decode_token(token: &Self) -> Result<Claim, AuthError> {
decode::<Claim>(
&token.0,
&DecodingKey::from_secret(jwt_secret().as_ref()),
&Validation::new(DEFAULT_ALGORITHM),
)
.map(|data| Ok(data.claims))
.map_err(|_err| AuthError::Unauthorized)?
}
pub fn parser_from_request(request: &HttpRequest) -> Result<Self, AuthError> {
match request.headers().get(HEADER_TOKEN) {
Some(header) => match header.to_str() {
Ok(val) => Ok(Token(val.to_owned())),
Err(_) => Err(AuthError::Unauthorized),
},
None => Err(AuthError::Unauthorized),
}
}
}
impl From<String> for Token {
fn from(val: String) -> Self {
Self(val)
}
}
impl Into<String> for Token {
fn into(self) -> String {
self.0
}
}
impl FromRequest for Token {
type Error = AuthError;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut actix_http::Payload) -> Self::Future {
match Token::parser_from_request(req) {
Ok(token) => ready(Ok(token)),
Err(err) => ready(Err(err)),
}
}
}