feat: add login test
This commit is contained in:
parent
508961d07d
commit
2429a41b2f
|
|
@ -1,19 +1,58 @@
|
||||||
{
|
{
|
||||||
"db": "PostgreSQL",
|
"db": "PostgreSQL",
|
||||||
"e8c487b4314c267f6da2667b95f6c8003fabc2461c10df2d6d39d081e74e167f": {
|
"1775f308cda63b61c0aaf4d8d1c7cf00f6bff5041e821575f65867c32d42f1b0": {
|
||||||
"query": "\n INSERT INTO user (uid, username, password, email,create_time)\n VALUES ($1, $2, $3, $4, $5)\n ",
|
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Left": [
|
"Left": [
|
||||||
"Uuid",
|
"Uuid",
|
||||||
"Text",
|
"Text",
|
||||||
"Text",
|
"Text",
|
||||||
"Text",
|
"Timestamptz",
|
||||||
"Timestamptz"
|
"Text"
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"nullable": []
|
},
|
||||||
}
|
"query": "\n INSERT INTO users (uid, email, username, create_time, password)\n VALUES ($1, $2, $3, $4, $5)\n "
|
||||||
|
},
|
||||||
|
"b188bb8915fa0fc8c5dfefa27f1b086b839633c3c4dc6e4991b43a608d46a170": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "uid",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "\n SELECT uid, password\n FROM users\n WHERE email = $1\n "
|
||||||
|
},
|
||||||
|
"b754835f0a1543fc32acbfee436f775e542036888e7a2051622efa435debb318": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text",
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "\n UPDATE users\n SET password= $1\n WHERE uid = $2\n "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::component::auth::{
|
use crate::component::auth::{
|
||||||
register_user, InputParamsError, LoginRequest, RegisterRequestParams,
|
login, register, InputParamsError, LoginRequest, RegisterRequestParams,
|
||||||
};
|
};
|
||||||
use crate::domain::{UserEmail, UserName, UserPassword};
|
use crate::domain::{UserEmail, UserName, UserPassword};
|
||||||
use crate::state::State;
|
use crate::state::State;
|
||||||
|
|
@ -10,21 +10,36 @@ use actix_web::{web, HttpResponse, Scope};
|
||||||
|
|
||||||
pub fn user_scope() -> Scope {
|
pub fn user_scope() -> Scope {
|
||||||
web::scope("/api/user")
|
web::scope("/api/user")
|
||||||
.service(web::resource("/login").route(web::post().to(login)))
|
.service(web::resource("/login").route(web::post().to(login_handler)))
|
||||||
.service(web::resource("/logout").route(web::get().to(logout)))
|
.service(web::resource("/logout").route(web::get().to(logout_handler)))
|
||||||
.service(web::resource("/register").route(web::post().to(register)))
|
.service(web::resource("/register").route(web::post().to(register_handler)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn login(id: Identity, req: Data<LoginRequest>, state: Data<State>) -> Result<HttpResponse> {
|
async fn login_handler(req: Json<LoginRequest>, state: Data<State>) -> Result<HttpResponse> {
|
||||||
todo!()
|
let params = req.into_inner();
|
||||||
|
let email = UserEmail::parse(params.email)
|
||||||
|
.map_err(|e| InputParamsError::InvalidEmail(e))?
|
||||||
|
.0;
|
||||||
|
let password = UserPassword::parse(params.password)
|
||||||
|
.map_err(|_| InputParamsError::InvalidPassword)?
|
||||||
|
.0;
|
||||||
|
let resp = login(state.pg_pool.clone(), state.cache.clone(), email, password).await?;
|
||||||
|
Ok(HttpResponse::Ok().json(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn logout(payload: Payload, id: Identity, state: Data<State>) -> Result<HttpResponse> {
|
async fn logout_handler(
|
||||||
|
payload: Payload,
|
||||||
|
id: Identity,
|
||||||
|
state: Data<State>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(state))]
|
#[tracing::instrument(level = "debug", skip(state))]
|
||||||
async fn register(req: Json<RegisterRequestParams>, state: Data<State>) -> Result<HttpResponse> {
|
async fn register_handler(
|
||||||
|
req: Json<RegisterRequestParams>,
|
||||||
|
state: Data<State>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
let params = req.into_inner();
|
let params = req.into_inner();
|
||||||
let name = UserName::parse(params.name)
|
let name = UserName::parse(params.name)
|
||||||
.map_err(|e| InputParamsError::InvalidName(e))?
|
.map_err(|e| InputParamsError::InvalidName(e))?
|
||||||
|
|
@ -36,7 +51,7 @@ async fn register(req: Json<RegisterRequestParams>, state: Data<State>) -> Resul
|
||||||
.map_err(|_| InputParamsError::InvalidPassword)?
|
.map_err(|_| InputParamsError::InvalidPassword)?
|
||||||
.0;
|
.0;
|
||||||
|
|
||||||
let resp = register_user(
|
let resp = register(
|
||||||
state.pg_pool.clone(),
|
state.pg_pool.clone(),
|
||||||
state.cache.clone(),
|
state.cache.clone(),
|
||||||
name,
|
name,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::component::auth::AuthError;
|
||||||
use crate::telemetry::spawn_blocking_with_tracing;
|
use crate::telemetry::spawn_blocking_with_tracing;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use argon2::password_hash::SaltString;
|
use argon2::password_hash::SaltString;
|
||||||
|
|
@ -6,7 +7,7 @@ use secrecy::{ExposeSecret, Secret};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
pub username: String,
|
pub email: String,
|
||||||
pub password: Secret<String>,
|
pub password: Secret<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -14,8 +15,8 @@ pub struct Credentials {
|
||||||
pub async fn validate_credentials(
|
pub async fn validate_credentials(
|
||||||
credentials: Credentials,
|
credentials: Credentials,
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
) -> Result<uuid::Uuid, anyhow::Error> {
|
) -> Result<uuid::Uuid, AuthError> {
|
||||||
let mut user_id = None;
|
let mut uid = None;
|
||||||
let mut expected_hash_password = Secret::new(
|
let mut expected_hash_password = Secret::new(
|
||||||
"$argon2id$v=19$m=15000,t=2,p=1$\
|
"$argon2id$v=19$m=15000,t=2,p=1$\
|
||||||
gZiV/M1gPc22ElAH/Jh1Hw$\
|
gZiV/M1gPc22ElAH/Jh1Hw$\
|
||||||
|
|
@ -23,10 +24,10 @@ pub async fn validate_credentials(
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some((stored_user_id, stored_hash_password)) =
|
if let Some((stored_uid, stored_hash_password)) =
|
||||||
get_stored_credentials(&credentials.username, pool).await?
|
get_stored_credentials(&credentials.email, pool).await?
|
||||||
{
|
{
|
||||||
user_id = Some(stored_user_id);
|
uid = Some(stored_uid);
|
||||||
expected_hash_password = stored_hash_password;
|
expected_hash_password = stored_hash_password;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,7 +37,8 @@ pub async fn validate_credentials(
|
||||||
.await
|
.await
|
||||||
.context("Failed to spawn blocking task.")??;
|
.context("Failed to spawn blocking task.")??;
|
||||||
|
|
||||||
user_id.ok_or_else(|| anyhow::anyhow!("Unknown username."))
|
uid.ok_or_else(|| anyhow::anyhow!("Unknown email."))
|
||||||
|
.map_err(AuthError::InvalidCredentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(name = "Change password", skip(password, pool))]
|
#[tracing::instrument(name = "Change password", skip(password, pool))]
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,56 @@
|
||||||
use crate::component::auth::{compute_hash_password, internal_error, AuthError};
|
use crate::component::auth::{
|
||||||
|
compute_hash_password, internal_error, validate_credentials, AuthError, Credentials,
|
||||||
|
};
|
||||||
use crate::config::env::{domain, jwt_secret};
|
use crate::config::env::{domain, jwt_secret};
|
||||||
use crate::state::Cache;
|
use crate::state::Cache;
|
||||||
use actix_web::http::header::HeaderValue;
|
use actix_web::http::header::HeaderValue;
|
||||||
use actix_web::{FromRequest, HttpRequest};
|
use actix_web::{FromRequest, HttpRequest};
|
||||||
use anyhow::Context;
|
use anyhow::{Context, Error};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use chrono::{Duration, Local};
|
use chrono::{Duration, Local};
|
||||||
use futures_util::future::{ready, Ready};
|
use futures_util::future::{ready, Ready};
|
||||||
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||||
|
use secrecy::Secret;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::types::uuid;
|
use sqlx::types::uuid;
|
||||||
use sqlx::{PgPool, Postgres, Transaction};
|
use sqlx::{PgPool, Postgres, Transaction};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub async fn register_user(
|
pub async fn login(
|
||||||
|
pg_pool: PgPool,
|
||||||
|
cache: Arc<RwLock<Cache>>,
|
||||||
|
email: String,
|
||||||
|
password: String,
|
||||||
|
) -> Result<LoginResponse, 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)?.into();
|
||||||
|
let logged_user = LoggedUser::new(uid);
|
||||||
|
cache.write().await.authorized(logged_user);
|
||||||
|
|
||||||
|
Ok(LoginResponse {
|
||||||
|
token,
|
||||||
|
user_id: uid,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn logout(logged_user: LoggedUser, cache: Arc<RwLock<Cache>>) -> Result<(), AuthError> {
|
||||||
|
cache.write().await.unauthorized(logged_user);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn register(
|
||||||
pg_pool: PgPool,
|
pg_pool: PgPool,
|
||||||
cache: Arc<RwLock<Cache>>,
|
cache: Arc<RwLock<Cache>>,
|
||||||
username: String,
|
username: String,
|
||||||
|
|
@ -87,6 +123,12 @@ pub struct LoginRequest {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct LoginResponse {
|
||||||
|
pub token: String,
|
||||||
|
pub user_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Debug)]
|
#[derive(Default, Serialize, Deserialize, Debug)]
|
||||||
pub struct RegisterRequestParams {
|
pub struct RegisterRequestParams {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
|
|
@ -101,20 +143,18 @@ pub struct RegisterResponse {
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Ord, PartialOrd)]
|
#[derive(Debug, PartialEq, Eq, Hash, Clone, Ord, PartialOrd)]
|
||||||
pub struct LoggedUser {
|
pub struct LoggedUser {
|
||||||
pub user_id: String,
|
pub uid: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::From<Claim> for LoggedUser {
|
impl std::convert::From<Claim> for LoggedUser {
|
||||||
fn from(c: Claim) -> Self {
|
fn from(c: Claim) -> Self {
|
||||||
Self {
|
Self { uid: c.user_id() }
|
||||||
user_id: c.user_id(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoggedUser {
|
impl LoggedUser {
|
||||||
pub fn new(user_id: String) -> Self {
|
pub fn new(user_id: String) -> Self {
|
||||||
Self { user_id }
|
Self { uid: user_id }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_token(token: String) -> Result<Self, AuthError> {
|
pub fn from_token(token: String) -> Result<Self, AuthError> {
|
||||||
|
|
@ -123,7 +163,7 @@ impl LoggedUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_uuid(&self) -> Result<uuid::Uuid, anyhow::Error> {
|
pub fn as_uuid(&self) -> Result<uuid::Uuid, anyhow::Error> {
|
||||||
let uuid = uuid::Uuid::parse_str(&self.user_id).context("Invalid uuid")?;
|
let uuid = uuid::Uuid::parse_str(&self.uid).context("Invalid uuid")?;
|
||||||
Ok(uuid)
|
Ok(uuid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::test_server::spawn_server;
|
use crate::test_server::spawn_server;
|
||||||
use appflowy_server::component::auth::RegisterResponse;
|
use appflowy_server::component::auth::RegisterResponse;
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
// curl -X POST --url http://0.0.0.0:8000/api/user/register --header 'content-type: application/json' --data '{"name":"fake name", "email":"fake@appflowy.io", "password":"Fake@123"}'
|
// curl -X POST --url http://0.0.0.0:8000/api/user/register --header 'content-type: application/json' --data '{"name":"fake name", "email":"fake@appflowy.io", "password":"Fake@123"}'
|
||||||
|
|
@ -14,3 +15,28 @@ async fn register_success() {
|
||||||
|
|
||||||
println!("{:?}", response);
|
println!("{:?}", response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn register_with_invalid_password() {
|
||||||
|
let server = spawn_server().await;
|
||||||
|
let http_resp = server.register("user 1", "fake@appflowy.io", "123").await;
|
||||||
|
assert_eq!(http_resp.status(), StatusCode::BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn register_with_invalid_name() {
|
||||||
|
let server = spawn_server().await;
|
||||||
|
let http_resp = server
|
||||||
|
.register("", "fake@appflowy.io", "FakePassword!123")
|
||||||
|
.await;
|
||||||
|
assert_eq!(http_resp.status(), StatusCode::BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn register_with_invalid_email() {
|
||||||
|
let server = spawn_server().await;
|
||||||
|
let http_resp = server
|
||||||
|
.register("me", "appflowy.io", "FakePassword!123")
|
||||||
|
.await;
|
||||||
|
assert_eq!(http_resp.status(), StatusCode::BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,13 @@ use sqlx::{Connection, Executor, PgConnection, PgPool};
|
||||||
|
|
||||||
// Ensure that the `tracing` stack is only initialised once using `once_cell`
|
// Ensure that the `tracing` stack is only initialised once using `once_cell`
|
||||||
static TRACING: Lazy<()> = Lazy::new(|| {
|
static TRACING: Lazy<()> = Lazy::new(|| {
|
||||||
let default_filter_level = "info".to_string();
|
let level = "trace".to_string();
|
||||||
|
let mut filters = vec![];
|
||||||
|
filters.push(format!("appflowy_server={}", level));
|
||||||
|
filters.push(format!("hyper={}", level));
|
||||||
|
|
||||||
let subscriber_name = "test".to_string();
|
let subscriber_name = "test".to_string();
|
||||||
let subscriber = get_subscriber(subscriber_name, default_filter_level, std::io::stdout);
|
let subscriber = get_subscriber(subscriber_name, filters.join(","), std::io::stdout);
|
||||||
init_subscriber(subscriber);
|
init_subscriber(subscriber);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -42,7 +46,6 @@ impl TestServer {
|
||||||
|
|
||||||
pub async fn spawn_server() -> TestServer {
|
pub async fn spawn_server() -> TestServer {
|
||||||
Lazy::force(&TRACING);
|
Lazy::force(&TRACING);
|
||||||
|
|
||||||
let database_name = Uuid::new_v4().to_string();
|
let database_name = Uuid::new_v4().to_string();
|
||||||
let config = {
|
let config = {
|
||||||
let mut config = get_configuration().expect("Failed to read configuration.");
|
let mut config = get_configuration().expect("Failed to read configuration.");
|
||||||
|
|
@ -58,18 +61,18 @@ pub async fn spawn_server() -> TestServer {
|
||||||
.await
|
.await
|
||||||
.expect("Failed to build application.");
|
.expect("Failed to build application.");
|
||||||
|
|
||||||
|
let port = application.port();
|
||||||
|
let address = format!("http://localhost:{}", port);
|
||||||
|
let _ = tokio::spawn(async {
|
||||||
|
let _ = application.run_until_stopped().await;
|
||||||
|
});
|
||||||
|
|
||||||
let api_client = reqwest::Client::builder()
|
let api_client = reqwest::Client::builder()
|
||||||
.redirect(reqwest::redirect::Policy::none())
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
.cookie_store(true)
|
.cookie_store(true)
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let port = application.port();
|
|
||||||
let address = format!("http://localhost:{}", port);
|
|
||||||
let _ = tokio::spawn(async {
|
|
||||||
let _ = application.run_until_stopped();
|
|
||||||
});
|
|
||||||
|
|
||||||
TestServer {
|
TestServer {
|
||||||
state,
|
state,
|
||||||
api_client,
|
api_client,
|
||||||
|
|
@ -100,3 +103,31 @@ async fn configure_database(config: &DatabaseSetting) -> PgPool {
|
||||||
|
|
||||||
connection_pool
|
connection_pool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub struct TestUser {
|
||||||
|
name: String,
|
||||||
|
pub email: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestUser {
|
||||||
|
pub fn generate() -> Self {
|
||||||
|
Self {
|
||||||
|
name: "Me".to_string(),
|
||||||
|
email: "me@appflowy.io".to_string(),
|
||||||
|
password: "HelloAppFlowy123".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn register(&self, test_server: &TestServer) {
|
||||||
|
let url = format!("{}/api/user/register", test_server.address);
|
||||||
|
test_server
|
||||||
|
.api_client
|
||||||
|
.post(&url)
|
||||||
|
.json(&self)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Fail to register user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue