瀏覽代碼

save user password with bcrypt

appflowy 3 年之前
父節點
當前提交
15f1267956

+ 14 - 0
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-user/sign_in.pb.dart

@@ -136,6 +136,7 @@ class SignInResponse extends $pb.GeneratedMessage {
     ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'uid')
     ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name')
     ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'email')
+    ..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'token')
     ..hasRequiredFields = false
   ;
 
@@ -144,6 +145,7 @@ class SignInResponse extends $pb.GeneratedMessage {
     $core.String? uid,
     $core.String? name,
     $core.String? email,
+    $core.String? token,
   }) {
     final _result = create();
     if (uid != null) {
@@ -155,6 +157,9 @@ class SignInResponse extends $pb.GeneratedMessage {
     if (email != null) {
       _result.email = email;
     }
+    if (token != null) {
+      _result.token = token;
+    }
     return _result;
   }
   factory SignInResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
@@ -204,5 +209,14 @@ class SignInResponse extends $pb.GeneratedMessage {
   $core.bool hasEmail() => $_has(2);
   @$pb.TagNumber(3)
   void clearEmail() => clearField(3);
+
+  @$pb.TagNumber(4)
+  $core.String get token => $_getSZ(3);
+  @$pb.TagNumber(4)
+  set token($core.String v) { $_setString(3, v); }
+  @$pb.TagNumber(4)
+  $core.bool hasToken() => $_has(3);
+  @$pb.TagNumber(4)
+  void clearToken() => clearField(4);
 }
 

+ 2 - 1
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-user/sign_in.pbjson.dart

@@ -37,8 +37,9 @@ const SignInResponse$json = const {
     const {'1': 'uid', '3': 1, '4': 1, '5': 9, '10': 'uid'},
     const {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'},
     const {'1': 'email', '3': 3, '4': 1, '5': 9, '10': 'email'},
+    const {'1': 'token', '3': 4, '4': 1, '5': 9, '10': 'token'},
   ],
 };
 
 /// Descriptor for `SignInResponse`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List signInResponseDescriptor = $convert.base64Decode('Cg5TaWduSW5SZXNwb25zZRIQCgN1aWQYASABKAlSA3VpZBISCgRuYW1lGAIgASgJUgRuYW1lEhQKBWVtYWlsGAMgASgJUgVlbWFpbA==');
+final $typed_data.Uint8List signInResponseDescriptor = $convert.base64Decode('Cg5TaWduSW5SZXNwb25zZRIQCgN1aWQYASABKAlSA3VpZBISCgRuYW1lGAIgASgJUgRuYW1lEhQKBWVtYWlsGAMgASgJUgVlbWFpbBIUCgV0b2tlbhgEIAEoCVIFdG9rZW4=');

+ 4 - 1
backend/Cargo.toml

@@ -14,6 +14,7 @@ actix-codec = "0.3"
 actix-web = "4.0.0-beta.8"
 actix-http = "3.0.0-beta.8"
 actix-web-actors = { version = "4.0.0-beta.6" }
+actix-identity = "0.4.0-beta.2"
 
 futures = "0.3.15"
 bytes = "1"
@@ -24,13 +25,15 @@ serde_json = "1.0"
 serde = { version = "1.0", features = ["derive"] }
 serde_repr = "0.1"
 serde-aux = "1.0.1"
-derive_more = {version = "0.99", features = ["display"]}
+derive_more = {version = "0.99"}
 protobuf = {version = "2.20.0"}
 uuid = { version = "0.8", features = ["serde", "v4"] }
 config = { version = "0.10.1", default-features = false, features = ["yaml"] }
 chrono = "0.4.19"
 anyhow = "1.0.40"
 thiserror = "1.0.24"
+bcrypt = "0.10"
+jsonwebtoken = "7.2"
 
 flowy-log = { path = "../rust-lib/flowy-log" }
 flowy-user = { path = "../rust-lib/flowy-user" }

+ 37 - 3
backend/src/application.rs

@@ -1,10 +1,16 @@
 use crate::{
-    config::{get_configuration, DatabaseSettings, Settings},
+    config::{
+        env::{domain, secret, use_https},
+        get_configuration,
+        DatabaseSettings,
+        Settings,
+    },
     context::AppContext,
     routers::*,
     ws_service::WSServer,
 };
 use actix::Actor;
+use actix_identity::{CookieIdentityPolicy, IdentityService};
 use actix_web::{dev::Server, middleware, web, web::Data, App, HttpServer, Scope};
 use sqlx::{postgres::PgPoolOptions, PgPool};
 use std::{net::TcpListener, sync::Arc};
@@ -34,10 +40,13 @@ pub fn run(listener: TcpListener, app_ctx: AppContext) -> Result<Server, std::io
     let AppContext { ws_server, pg_pool } = app_ctx;
     let ws_server = Data::new(ws_server);
     let pg_pool = Data::new(pg_pool);
+    let domain = domain();
+    let secret: String = secret();
 
     let server = HttpServer::new(move || {
         App::new()
             .wrap(middleware::Logger::default())
+            .wrap(identify_service(&domain, &secret))
             .app_data(web::JsonConfig::default().limit(4096))
             .service(ws_scope())
             .service(user_scope())
@@ -49,10 +58,24 @@ pub fn run(listener: TcpListener, app_ctx: AppContext) -> Result<Server, std::io
     Ok(server)
 }
 
-fn ws_scope() -> Scope { web::scope("/ws").service(ws::start_connection) }
+fn ws_scope() -> Scope { web::scope("/ws").service(ws_router::start_connection) }
 
 fn user_scope() -> Scope {
-    web::scope("/user").service(web::resource("/register").route(web::post().to(user::register)))
+    web::scope("/api")
+        // authentication
+        .service(web::resource("/auth")
+            .route(web::post().to(user_router::login_handler))
+            .route(web::delete().to(user_router::logout_handler))
+            .route(web::get().to(user_router::user_profile))
+        )
+        // password
+        .service(web::resource("/password_change")
+            .route(web::post().to(user_router::change_password))
+        )
+        // register
+        .service(web::resource("/register")
+            .route(web::post().to(user_router::register_handler))
+        )
 }
 
 async fn init_app_context(configuration: &Settings) -> AppContext {
@@ -69,6 +92,17 @@ async fn init_app_context(configuration: &Settings) -> AppContext {
     AppContext::new(ws_server, pg_pool)
 }
 
+pub fn identify_service(domain: &str, secret: &str) -> IdentityService<CookieIdentityPolicy> {
+    IdentityService::new(
+        CookieIdentityPolicy::new(secret.as_bytes())
+            .name("auth")
+            .path("/")
+            .domain(domain)
+            .max_age_secs(24 * 3600)
+            .secure(use_https()),
+    )
+}
+
 pub async fn get_connection_pool(configuration: &DatabaseSettings) -> Result<PgPool, sqlx::Error> {
     PgPoolOptions::new()
         .connect_timeout(std::time::Duration::from_secs(5))

+ 9 - 0
backend/src/config/env.rs

@@ -0,0 +1,9 @@
+use std::env;
+
+pub fn domain() -> String { env::var("DOMAIN").unwrap_or_else(|_| "localhost".to_string()) }
+
+pub fn jwt_secret() -> String { env::var("JWT_SECRET").unwrap_or_else(|_| "my secret".into()) }
+
+pub fn secret() -> String { env::var("SECRET_KEY").unwrap_or_else(|_| "0123".repeat(8)) }
+
+pub fn use_https() -> bool { false }

+ 1 - 0
backend/src/config/mod.rs

@@ -1,5 +1,6 @@
 mod configuration;
 mod const_define;
+pub mod env;
 
 pub use configuration::*;
 pub use const_define::*;

+ 2 - 1
backend/src/entities/mod.rs

@@ -1 +1,2 @@
-
+pub mod token;
+pub mod user;

+ 66 - 0
backend/src/entities/token.rs

@@ -0,0 +1,66 @@
+use crate::{
+    config::env::{domain, jwt_secret},
+    entities::user::User,
+};
+use chrono::{Duration, Local};
+use derive_more::{From, Into};
+use flowy_net::errors::{Code, ServerError};
+use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
+use serde::{Deserialize, Serialize};
+
+const DEFAULT_ALGORITHM: Algorithm = Algorithm::HS256;
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Claim {
+    // issuer
+    iss: String,
+    // subject
+    sub: String,
+    // issue at
+    iat: i64,
+    // expiry
+    exp: i64,
+    email: String,
+}
+
+impl Claim {
+    pub fn with_email(email: &str) -> Self {
+        let domain = domain();
+        Self {
+            iss: domain,
+            sub: "auth".to_string(),
+            email: email.to_string(),
+            iat: Local::now().timestamp(),
+            exp: (Local::now() + Duration::hours(24)).timestamp(),
+        }
+    }
+}
+
+// impl From<Claim> for User {
+//     fn from(claim: Claim) -> Self { Self { email: claim.email } }
+// }
+
+#[derive(From, Into)]
+pub struct Token(String);
+impl Token {
+    pub fn create_token(data: &User) -> Result<Self, ServerError> {
+        let claims = Claim::with_email(&data.email);
+        encode(
+            &Header::new(DEFAULT_ALGORITHM),
+            &claims,
+            &EncodingKey::from_secret(jwt_secret().as_ref()),
+        )
+        .map(Into::into)
+        .map_err(|err| ServerError::internal().with_msg(err))
+    }
+
+    pub fn decode_token(token: &Self) -> Result<Claim, ServerError> {
+        decode::<Claim>(
+            &token.0,
+            &DecodingKey::from_secret(jwt_secret().as_ref()),
+            &Validation::new(DEFAULT_ALGORITHM),
+        )
+        .map(|data| Ok(data.claims))
+        .map_err(|err| ServerError::unauthorized().with_msg(err))?
+    }
+}

+ 8 - 0
backend/src/entities/user.rs

@@ -0,0 +1,8 @@
+#[derive(Debug, Clone, sqlx::FromRow)]
+pub struct User {
+    pub(crate) id: uuid::Uuid,
+    pub(crate) email: String,
+    pub(crate) name: String,
+    pub(crate) create_time: i64,
+    pub(crate) password: String,
+}

+ 5 - 5
backend/src/routers/mod.rs

@@ -1,6 +1,6 @@
-mod helper;
-pub(crate) mod user;
-pub(crate) mod ws;
+pub(crate) mod user_router;
+mod utils;
+pub(crate) mod ws_router;
 
-pub use user::*;
-pub use ws::*;
+pub use user_router::*;
+pub use ws_router::*;

+ 0 - 25
backend/src/routers/user.rs

@@ -1,25 +0,0 @@
-use crate::routers::helper::parse_from_payload;
-use actix_web::{
-    web::{Data, Payload},
-    Error,
-    HttpRequest,
-    HttpResponse,
-};
-use flowy_net::response::*;
-use flowy_user::protobuf::SignUpParams;
-
-use crate::user_service::sign_up;
-use flowy_net::errors::ServerError;
-use sqlx::PgPool;
-use std::sync::Arc;
-
-pub async fn register(
-    _request: HttpRequest,
-    payload: Payload,
-    pool: Data<PgPool>,
-) -> Result<HttpResponse, ServerError> {
-    let params: SignUpParams = parse_from_payload(payload).await?;
-    let resp = sign_up(pool.get_ref(), params).await?;
-
-    Ok(resp.into())
-}

+ 57 - 0
backend/src/routers/user_router.rs

@@ -0,0 +1,57 @@
+use crate::routers::utils::parse_from_payload;
+use actix_web::{
+    web::{Data, Payload},
+    Error,
+    HttpRequest,
+    HttpResponse,
+};
+use flowy_net::response::*;
+use flowy_user::protobuf::{SignInParams, SignUpParams};
+
+use crate::user_service::auth_service::{register_user, sign_in};
+use actix_identity::Identity;
+use flowy_net::errors::ServerError;
+use sqlx::PgPool;
+use std::sync::Arc;
+
+pub async fn login_handler(
+    payload: Payload,
+    id: Identity,
+    pool: Data<PgPool>,
+) -> Result<HttpResponse, ServerError> {
+    let params: SignInParams = parse_from_payload(payload).await?;
+    let resp = sign_in(pool.get_ref(), params, id).await?;
+    Ok(resp.into())
+}
+
+pub async fn logout_handler(id: Identity) -> Result<HttpResponse, ServerError> {
+    id.forget();
+    Ok(HttpResponse::Ok().finish())
+}
+
+pub async fn user_profile(
+    request: HttpRequest,
+    payload: Payload,
+    pool: Data<PgPool>,
+) -> Result<HttpResponse, ServerError> {
+    unimplemented!()
+}
+
+pub async fn register_handler(
+    _request: HttpRequest,
+    payload: Payload,
+    pool: Data<PgPool>,
+) -> Result<HttpResponse, ServerError> {
+    let params: SignUpParams = parse_from_payload(payload).await?;
+    let resp = register_user(pool.get_ref(), params).await?;
+
+    Ok(resp.into())
+}
+
+pub async fn change_password(
+    request: HttpRequest,
+    payload: Payload,
+    pool: Data<PgPool>,
+) -> Result<HttpResponse, ServerError> {
+    unimplemented!()
+}

+ 5 - 2
backend/src/routers/helper.rs → backend/src/routers/utils.rs

@@ -1,6 +1,9 @@
 use crate::config::MAX_PAYLOAD_SIZE;
 use actix_web::web;
-use flowy_net::response::*;
+use flowy_net::{
+    errors::{Code, ServerError},
+    response::*,
+};
 use futures::StreamExt;
 use protobuf::{Message, ProtobufResult};
 
@@ -20,7 +23,7 @@ pub fn parse_from_bytes<T: Message>(bytes: &[u8]) -> Result<T, ServerError> {
 pub async fn poll_payload(mut payload: web::Payload) -> Result<web::BytesMut, ServerError> {
     let mut body = web::BytesMut::new();
     while let Some(chunk) = payload.next().await {
-        let chunk = chunk.map_err(ServerError::internal)?;
+        let chunk = chunk.map_err(|err| ServerError::internal().with_msg(err))?;
 
         if (body.len() + chunk.len()) > MAX_PAYLOAD_SIZE {
             return Err(ServerError {

+ 0 - 0
backend/src/routers/ws.rs → backend/src/routers/ws_router.rs


+ 0 - 73
backend/src/user_service/auth.rs

@@ -1,73 +0,0 @@
-use anyhow::Context;
-use chrono::Utc;
-use flowy_net::{errors::ServerError, response::FlowyResponse};
-use flowy_user::{entities::SignUpResponse, protobuf::SignUpParams};
-use sqlx::{Error, PgPool, Postgres, Transaction};
-use std::sync::Arc;
-
-pub async fn sign_up(pool: &PgPool, params: SignUpParams) -> Result<FlowyResponse, ServerError> {
-    let mut transaction = pool
-        .begin()
-        .await
-        .context("Failed to acquire a Postgres connection from the pool")?;
-
-    let _ = is_email_exist(&mut transaction, &params.email).await?;
-
-    let data = insert_user(&mut transaction, params)
-        .await
-        .context("Failed to insert user")?;
-
-    let response = FlowyResponse::success(data).context("Failed to generate response")?;
-
-    Ok(response)
-}
-
-async fn is_email_exist(
-    transaction: &mut Transaction<'_, Postgres>,
-    email: &str,
-) -> Result<(), ServerError> {
-    let result = sqlx::query!(
-        r#"SELECT email FROM user_table WHERE email = $1"#,
-        email.to_string()
-    )
-    .fetch_optional(transaction)
-    .await
-    .map_err(ServerError::internal)?;
-
-    match result {
-        Some(_) => Err(ServerError {
-            code: Code::EmailAlreadyExists,
-            msg: format!("{} already exists", email),
-        }),
-        None => Ok(()),
-    }
-}
-
-async fn insert_user(
-    transaction: &mut Transaction<'_, Postgres>,
-    params: SignUpParams,
-) -> Result<SignUpResponse, ServerError> {
-    let uuid = uuid::Uuid::new_v4();
-    let result = sqlx::query!(
-        r#"
-            INSERT INTO user_table (id, email, name, create_time, password)
-            VALUES ($1, $2, $3, $4, $5)
-        "#,
-        uuid,
-        params.email,
-        params.name,
-        Utc::now(),
-        "123".to_string()
-    )
-    .execute(transaction)
-    .await
-    .map_err(ServerError::internal)?;
-
-    let data = SignUpResponse {
-        uid: uuid.to_string(),
-        name: params.name,
-        email: params.email,
-    };
-
-    Ok(data)
-}

+ 132 - 0
backend/src/user_service/auth_service.rs

@@ -0,0 +1,132 @@
+use crate::{
+    entities::{token::Token, user::User},
+    user_service::utils::{hash_password, verify_password},
+};
+use actix_identity::Identity;
+use anyhow::Context;
+use chrono::Utc;
+use flowy_net::{
+    errors::{Code, ServerError},
+    response::FlowyResponse,
+};
+use flowy_user::{
+    entities::{SignInResponse, SignUpResponse},
+    protobuf::{SignInParams, SignUpParams},
+};
+use sqlx::{Error, PgPool, Postgres, Transaction};
+use std::sync::Arc;
+
+pub async fn sign_in(
+    pool: &PgPool,
+    params: SignInParams,
+    id: Identity,
+) -> Result<FlowyResponse, ServerError> {
+    let mut transaction = pool
+        .begin()
+        .await
+        .context("Failed to acquire a Postgres connection to sign in")?;
+    let user = read_user(&mut transaction, &params.email).await?;
+    transaction
+        .commit()
+        .await
+        .context("Failed to commit SQL transaction to sign in.")?;
+
+    match verify_password(&params.password, &user.password) {
+        Ok(true) => {
+            let token = Token::create_token(&user)?;
+            let data = SignInResponse {
+                uid: user.id.to_string(),
+                name: user.name,
+                email: user.email,
+                token: token.into(),
+            };
+            id.remember(data.token.clone());
+            FlowyResponse::success(data)
+        },
+        _ => Err(ServerError::passwordNotMatch()),
+    }
+}
+
+pub async fn register_user(
+    pool: &PgPool,
+    params: SignUpParams,
+) -> Result<FlowyResponse, ServerError> {
+    let mut transaction = pool
+        .begin()
+        .await
+        .context("Failed to acquire a Postgres connection to register user")?;
+
+    let _ = is_email_exist(&mut transaction, &params.email).await?;
+    let data = insert_user(&mut transaction, params)
+        .await
+        .context("Failed to insert user")?;
+
+    transaction
+        .commit()
+        .await
+        .context("Failed to commit SQL transaction to register user.")?;
+
+    FlowyResponse::success(data)
+}
+
+async fn is_email_exist(
+    transaction: &mut Transaction<'_, Postgres>,
+    email: &str,
+) -> Result<(), ServerError> {
+    let result = sqlx::query(r#"SELECT email FROM user_table WHERE email = $1"#)
+        .bind(email)
+        .fetch_optional(transaction)
+        .await
+        .map_err(|err| ServerError::internal().with_msg(err))?;
+
+    match result {
+        Some(_) => Err(ServerError {
+            code: Code::EmailAlreadyExists,
+            msg: format!("{} already exists", email),
+        }),
+        None => Ok(()),
+    }
+}
+
+async fn read_user(
+    transaction: &mut Transaction<'_, Postgres>,
+    email: &str,
+) -> Result<User, ServerError> {
+    let user = sqlx::query_as::<Postgres, User>("SELECT * FROM user_table WHERE email = $1")
+        .bind(email)
+        .fetch_one(transaction)
+        .await
+        .map_err(|err| ServerError::internal().with_msg(err))?;
+
+    Ok(user)
+}
+
+async fn insert_user(
+    transaction: &mut Transaction<'_, Postgres>,
+    params: SignUpParams,
+) -> Result<SignUpResponse, ServerError> {
+    let uuid = uuid::Uuid::new_v4();
+    let password = hash_password(&params.password)?;
+    let _ = sqlx::query!(
+        r#"
+            INSERT INTO user_table (id, email, name, create_time, password)
+            VALUES ($1, $2, $3, $4, $5)
+        "#,
+        uuid,
+        params.email,
+        params.name,
+        Utc::now(),
+        "123".to_string()
+    )
+    .execute(transaction)
+    .await
+    .map_err(|e| ServerError::internal().with_msg(e))?;
+
+    let data = SignUpResponse {
+        uid: uuid.to_string(),
+        name: params.name,
+        email: params.email,
+    };
+
+    Ok(data)
+}

+ 2 - 5
backend/src/user_service/mod.rs

@@ -1,5 +1,2 @@
-mod auth;
-
-pub use auth::*;
-
-pub fn uuid() -> String { uuid::Uuid::new_v4().to_string() }
+pub mod auth_service;
+pub mod utils;

+ 24 - 0
backend/src/user_service/utils.rs

@@ -0,0 +1,24 @@
+use bcrypt::{hash, verify, BcryptError, DEFAULT_COST};
+use flowy_net::errors::{Code, ServerError};
+use jsonwebtoken::Algorithm;
+
+pub fn uuid() -> String { uuid::Uuid::new_v4().to_string() }
+
+pub fn hash_password(plain: &str) -> Result<String, ServerError> {
+    let hashing_cost = std::env::var("HASH_COST")
+        .ok()
+        .and_then(|c| c.parse().ok())
+        .unwrap_or(DEFAULT_COST);
+
+    hash(plain, hashing_cost).map_err(|e| ServerError::internal().with_msg(e))
+}
+
+pub fn verify_password(source: &str, hash: &str) -> Result<bool, ServerError> {
+    match verify(source, hash) {
+        Ok(true) => Ok(true),
+        _ => Err(ServerError {
+            code: Code::PasswordNotMatch,
+            msg: "Username and password don't match".to_string(),
+        }),
+    }
+}

+ 0 - 1
rust-lib/flowy-derive/src/derive_cache/derive_cache.rs

@@ -64,7 +64,6 @@ pub fn category_from_str(type_str: &str) -> TypeCategory {
         | "EditorEvent"
         | "DocErrorCode"
         | "FFIStatusCode"
-        | "UserServerError"
         | "UserStatus"
         | "UserEvent"
         | "UserErrCode"

+ 1 - 1
rust-lib/flowy-net/src/config.rs

@@ -3,5 +3,5 @@ use lazy_static::lazy_static;
 pub const HOST: &'static str = "http://localhost:8000";
 
 lazy_static! {
-    pub static ref SIGN_UP_URL: String = format!("{}/user/register", HOST);
+    pub static ref SIGN_UP_URL: String = format!("{}/api/register", HOST);
 }

+ 36 - 8
rust-lib/flowy-net/src/errors.rs

@@ -14,9 +14,11 @@ pub struct ServerError {
 macro_rules! static_error {
     ($name:ident, $status:expr) => {
         #[allow(non_snake_case, missing_docs)]
-        pub fn $name<T: Debug>(error: T) -> ServerError {
-            let msg = format!("{:?}", error);
-            ServerError { code: $status, msg }
+        pub fn $name() -> ServerError {
+            ServerError {
+                code: $status,
+                msg: format!("{}", $status),
+            }
         }
     };
 }
@@ -25,6 +27,13 @@ impl ServerError {
     static_error!(internal, Code::InternalError);
     static_error!(http, Code::HttpError);
     static_error!(payload_none, Code::PayloadUnexpectedNone);
+    static_error!(unauthorized, Code::Unauthorized);
+    static_error!(passwordNotMatch, Code::PasswordNotMatch);
+
+    pub fn with_msg<T: Debug>(mut self, error: T) -> Self {
+        self.msg = format!("{:?}", error);
+        self
+    }
 }
 
 impl std::fmt::Display for ServerError {
@@ -43,28 +52,47 @@ impl std::convert::From<&ServerError> for FlowyResponse {
     }
 }
 
-#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug, Clone)]
+#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug, Clone, derive_more::Display)]
 #[repr(u16)]
 pub enum Code {
+    #[display(fmt = "Token is invalid")]
     InvalidToken       = 1,
-    Unauthorized       = 3,
-    PayloadOverflow    = 4,
-    PayloadSerdeFail   = 5,
-    PayloadUnexpectedNone = 6,
+    #[display(fmt = "Unauthorized")]
+    Unauthorized       = 2,
+    #[display(fmt = "Payload too large")]
+    PayloadOverflow    = 3,
+    #[display(fmt = "Payload deserialize failed")]
+    PayloadSerdeFail   = 4,
+    #[display(fmt = "Unexpected empty payload")]
+    PayloadUnexpectedNone = 5,
 
+    #[display(fmt = "Protobuf serde error")]
     ProtobufError      = 10,
+    #[display(fmt = "Json serde Error")]
     SerdeError         = 11,
 
+    #[display(fmt = "Email address already exists")]
     EmailAlreadyExists = 50,
 
+    #[display(fmt = "Username and password do not match")]
+    PasswordNotMatch   = 51,
+
+    #[display(fmt = "Connect refused")]
     ConnectRefused     = 100,
+
+    #[display(fmt = "Connection timeout")]
     ConnectTimeout     = 101,
+    #[display(fmt = "Connection closed")]
     ConnectClose       = 102,
+    #[display(fmt = "Connection canceled")]
     ConnectCancel      = 103,
 
+    #[display(fmt = "Sql error")]
     SqlError           = 200,
 
+    #[display(fmt = "Http request error")]
     HttpError          = 300,
 
+    #[display(fmt = "Internal error")]
     InternalError      = 1000,
 }

+ 7 - 9
rust-lib/flowy-net/src/request/request.rs

@@ -20,9 +20,9 @@ pub struct HttpRequestBuilder {
 }
 
 impl HttpRequestBuilder {
-    fn new() -> Self {
+    fn new(url: &str) -> Self {
         Self {
-            url: "".to_owned(),
+            url: url.to_owned(),
             body: None,
             response: None,
             method: Method::GET,
@@ -30,15 +30,13 @@ impl HttpRequestBuilder {
     }
 
     pub fn get(url: &str) -> Self {
-        let mut builder = Self::new();
-        builder.url = url.to_owned();
+        let mut builder = Self::new(url);
         builder.method = Method::GET;
         builder
     }
 
     pub fn post(url: &str) -> Self {
-        let mut builder = Self::new();
-        builder.url = url.to_owned();
+        let mut builder = Self::new(url);
         builder.method = Method::POST;
         builder
     }
@@ -54,11 +52,11 @@ impl HttpRequestBuilder {
 
     pub async fn send(mut self) -> Result<Self, ServerError> {
         let (tx, rx) = oneshot::channel::<Result<Response, _>>();
-        // reqwest client is not 'Sync' by channel is.
         let url = self.url.clone();
         let body = self.body.take();
         let method = self.method.clone();
 
+        // reqwest client is not 'Sync' by channel is.
         tokio::spawn(async move {
             let client = default_client();
             let mut builder = client.request(method, url);
@@ -85,7 +83,7 @@ impl HttpRequestBuilder {
         match data {
             None => {
                 let msg = format!("Request: {} receives unexpected empty body", self.url);
-                Err(ServerError::payload_none(msg))
+                Err(ServerError::payload_none().with_msg(msg))
             },
             Some(data) => Ok(T2::try_from(data)?),
         }
@@ -123,7 +121,7 @@ async fn get_response_data(original: Response) -> Result<Bytes, ServerError> {
             Some(error) => Err(error),
         }
     } else {
-        Err(ServerError::http(original))
+        Err(ServerError::http().with_msg(original))
     }
 }
 

+ 2 - 2
rust-lib/flowy-net/src/response/response.rs

@@ -32,7 +32,7 @@ impl std::convert::From<protobuf::ProtobufError> for ServerError {
 }
 
 impl std::convert::From<RecvError> for ServerError {
-    fn from(error: RecvError) -> Self { ServerError::internal(error) }
+    fn from(error: RecvError) -> Self { ServerError::internal().with_msg(error) }
 }
 
 impl std::convert::From<serde_json::Error> for ServerError {
@@ -46,7 +46,7 @@ impl std::convert::From<serde_json::Error> for ServerError {
 }
 
 impl std::convert::From<anyhow::Error> for ServerError {
-    fn from(error: anyhow::Error) -> Self { ServerError::internal(error) }
+    fn from(error: anyhow::Error) -> Self { ServerError::internal().with_msg(error) }
 }
 
 impl std::convert::From<reqwest::Error> for ServerError {

+ 3 - 0
rust-lib/flowy-user/src/entities/sign_in.rs

@@ -30,6 +30,9 @@ pub struct SignInResponse {
 
     #[pb(index = 3)]
     pub email: String,
+
+    #[pb(index = 4)]
+    pub token: String,
 }
 
 impl TryInto<SignInParams> for SignInRequest {

+ 72 - 26
rust-lib/flowy-user/src/protobuf/model/sign_in.rs

@@ -431,6 +431,7 @@ pub struct SignInResponse {
     pub uid: ::std::string::String,
     pub name: ::std::string::String,
     pub email: ::std::string::String,
+    pub token: ::std::string::String,
     // special fields
     pub unknown_fields: ::protobuf::UnknownFields,
     pub cached_size: ::protobuf::CachedSize,
@@ -524,6 +525,32 @@ impl SignInResponse {
     pub fn take_email(&mut self) -> ::std::string::String {
         ::std::mem::replace(&mut self.email, ::std::string::String::new())
     }
+
+    // string token = 4;
+
+
+    pub fn get_token(&self) -> &str {
+        &self.token
+    }
+    pub fn clear_token(&mut self) {
+        self.token.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_token(&mut self, v: ::std::string::String) {
+        self.token = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_token(&mut self) -> &mut ::std::string::String {
+        &mut self.token
+    }
+
+    // Take field
+    pub fn take_token(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.token, ::std::string::String::new())
+    }
 }
 
 impl ::protobuf::Message for SignInResponse {
@@ -544,6 +571,9 @@ impl ::protobuf::Message for SignInResponse {
                 3 => {
                     ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.email)?;
                 },
+                4 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.token)?;
+                },
                 _ => {
                     ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
                 },
@@ -565,6 +595,9 @@ impl ::protobuf::Message for SignInResponse {
         if !self.email.is_empty() {
             my_size += ::protobuf::rt::string_size(3, &self.email);
         }
+        if !self.token.is_empty() {
+            my_size += ::protobuf::rt::string_size(4, &self.token);
+        }
         my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
         self.cached_size.set(my_size);
         my_size
@@ -580,6 +613,9 @@ impl ::protobuf::Message for SignInResponse {
         if !self.email.is_empty() {
             os.write_string(3, &self.email)?;
         }
+        if !self.token.is_empty() {
+            os.write_string(4, &self.token)?;
+        }
         os.write_unknown_fields(self.get_unknown_fields())?;
         ::std::result::Result::Ok(())
     }
@@ -633,6 +669,11 @@ impl ::protobuf::Message for SignInResponse {
                 |m: &SignInResponse| { &m.email },
                 |m: &mut SignInResponse| { &mut m.email },
             ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "token",
+                |m: &SignInResponse| { &m.token },
+                |m: &mut SignInResponse| { &mut m.token },
+            ));
             ::protobuf::reflect::MessageDescriptor::new_pb_name::<SignInResponse>(
                 "SignInResponse",
                 fields,
@@ -652,6 +693,7 @@ impl ::protobuf::Clear for SignInResponse {
         self.uid.clear();
         self.name.clear();
         self.email.clear();
+        self.token.clear();
         self.unknown_fields.clear();
     }
 }
@@ -672,33 +714,37 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     \n\rsign_in.proto\"A\n\rSignInRequest\x12\x14\n\x05email\x18\x01\x20\x01\
     (\tR\x05email\x12\x1a\n\x08password\x18\x02\x20\x01(\tR\x08password\"@\n\
     \x0cSignInParams\x12\x14\n\x05email\x18\x01\x20\x01(\tR\x05email\x12\x1a\
-    \n\x08password\x18\x02\x20\x01(\tR\x08password\"L\n\x0eSignInResponse\
+    \n\x08password\x18\x02\x20\x01(\tR\x08password\"b\n\x0eSignInResponse\
     \x12\x10\n\x03uid\x18\x01\x20\x01(\tR\x03uid\x12\x12\n\x04name\x18\x02\
-    \x20\x01(\tR\x04name\x12\x14\n\x05email\x18\x03\x20\x01(\tR\x05emailJ\
-    \xdb\x03\n\x06\x12\x04\0\0\x0e\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\n\n\
-    \x02\x04\0\x12\x04\x02\0\x05\x01\n\n\n\x03\x04\0\x01\x12\x03\x02\x08\x15\
-    \n\x0b\n\x04\x04\0\x02\0\x12\x03\x03\x04\x15\n\x0c\n\x05\x04\0\x02\0\x05\
-    \x12\x03\x03\x04\n\n\x0c\n\x05\x04\0\x02\0\x01\x12\x03\x03\x0b\x10\n\x0c\
-    \n\x05\x04\0\x02\0\x03\x12\x03\x03\x13\x14\n\x0b\n\x04\x04\0\x02\x01\x12\
-    \x03\x04\x04\x18\n\x0c\n\x05\x04\0\x02\x01\x05\x12\x03\x04\x04\n\n\x0c\n\
-    \x05\x04\0\x02\x01\x01\x12\x03\x04\x0b\x13\n\x0c\n\x05\x04\0\x02\x01\x03\
-    \x12\x03\x04\x16\x17\n\n\n\x02\x04\x01\x12\x04\x06\0\t\x01\n\n\n\x03\x04\
-    \x01\x01\x12\x03\x06\x08\x14\n\x0b\n\x04\x04\x01\x02\0\x12\x03\x07\x04\
-    \x15\n\x0c\n\x05\x04\x01\x02\0\x05\x12\x03\x07\x04\n\n\x0c\n\x05\x04\x01\
-    \x02\0\x01\x12\x03\x07\x0b\x10\n\x0c\n\x05\x04\x01\x02\0\x03\x12\x03\x07\
-    \x13\x14\n\x0b\n\x04\x04\x01\x02\x01\x12\x03\x08\x04\x18\n\x0c\n\x05\x04\
-    \x01\x02\x01\x05\x12\x03\x08\x04\n\n\x0c\n\x05\x04\x01\x02\x01\x01\x12\
-    \x03\x08\x0b\x13\n\x0c\n\x05\x04\x01\x02\x01\x03\x12\x03\x08\x16\x17\n\n\
-    \n\x02\x04\x02\x12\x04\n\0\x0e\x01\n\n\n\x03\x04\x02\x01\x12\x03\n\x08\
-    \x16\n\x0b\n\x04\x04\x02\x02\0\x12\x03\x0b\x04\x13\n\x0c\n\x05\x04\x02\
-    \x02\0\x05\x12\x03\x0b\x04\n\n\x0c\n\x05\x04\x02\x02\0\x01\x12\x03\x0b\
-    \x0b\x0e\n\x0c\n\x05\x04\x02\x02\0\x03\x12\x03\x0b\x11\x12\n\x0b\n\x04\
-    \x04\x02\x02\x01\x12\x03\x0c\x04\x14\n\x0c\n\x05\x04\x02\x02\x01\x05\x12\
-    \x03\x0c\x04\n\n\x0c\n\x05\x04\x02\x02\x01\x01\x12\x03\x0c\x0b\x0f\n\x0c\
-    \n\x05\x04\x02\x02\x01\x03\x12\x03\x0c\x12\x13\n\x0b\n\x04\x04\x02\x02\
-    \x02\x12\x03\r\x04\x15\n\x0c\n\x05\x04\x02\x02\x02\x05\x12\x03\r\x04\n\n\
-    \x0c\n\x05\x04\x02\x02\x02\x01\x12\x03\r\x0b\x10\n\x0c\n\x05\x04\x02\x02\
-    \x02\x03\x12\x03\r\x13\x14b\x06proto3\
+    \x20\x01(\tR\x04name\x12\x14\n\x05email\x18\x03\x20\x01(\tR\x05email\x12\
+    \x14\n\x05token\x18\x04\x20\x01(\tR\x05tokenJ\x92\x04\n\x06\x12\x04\0\0\
+    \x0f\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\n\n\x02\x04\0\x12\x04\x02\0\
+    \x05\x01\n\n\n\x03\x04\0\x01\x12\x03\x02\x08\x15\n\x0b\n\x04\x04\0\x02\0\
+    \x12\x03\x03\x04\x15\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03\x03\x04\n\n\x0c\
+    \n\x05\x04\0\x02\0\x01\x12\x03\x03\x0b\x10\n\x0c\n\x05\x04\0\x02\0\x03\
+    \x12\x03\x03\x13\x14\n\x0b\n\x04\x04\0\x02\x01\x12\x03\x04\x04\x18\n\x0c\
+    \n\x05\x04\0\x02\x01\x05\x12\x03\x04\x04\n\n\x0c\n\x05\x04\0\x02\x01\x01\
+    \x12\x03\x04\x0b\x13\n\x0c\n\x05\x04\0\x02\x01\x03\x12\x03\x04\x16\x17\n\
+    \n\n\x02\x04\x01\x12\x04\x06\0\t\x01\n\n\n\x03\x04\x01\x01\x12\x03\x06\
+    \x08\x14\n\x0b\n\x04\x04\x01\x02\0\x12\x03\x07\x04\x15\n\x0c\n\x05\x04\
+    \x01\x02\0\x05\x12\x03\x07\x04\n\n\x0c\n\x05\x04\x01\x02\0\x01\x12\x03\
+    \x07\x0b\x10\n\x0c\n\x05\x04\x01\x02\0\x03\x12\x03\x07\x13\x14\n\x0b\n\
+    \x04\x04\x01\x02\x01\x12\x03\x08\x04\x18\n\x0c\n\x05\x04\x01\x02\x01\x05\
+    \x12\x03\x08\x04\n\n\x0c\n\x05\x04\x01\x02\x01\x01\x12\x03\x08\x0b\x13\n\
+    \x0c\n\x05\x04\x01\x02\x01\x03\x12\x03\x08\x16\x17\n\n\n\x02\x04\x02\x12\
+    \x04\n\0\x0f\x01\n\n\n\x03\x04\x02\x01\x12\x03\n\x08\x16\n\x0b\n\x04\x04\
+    \x02\x02\0\x12\x03\x0b\x04\x13\n\x0c\n\x05\x04\x02\x02\0\x05\x12\x03\x0b\
+    \x04\n\n\x0c\n\x05\x04\x02\x02\0\x01\x12\x03\x0b\x0b\x0e\n\x0c\n\x05\x04\
+    \x02\x02\0\x03\x12\x03\x0b\x11\x12\n\x0b\n\x04\x04\x02\x02\x01\x12\x03\
+    \x0c\x04\x14\n\x0c\n\x05\x04\x02\x02\x01\x05\x12\x03\x0c\x04\n\n\x0c\n\
+    \x05\x04\x02\x02\x01\x01\x12\x03\x0c\x0b\x0f\n\x0c\n\x05\x04\x02\x02\x01\
+    \x03\x12\x03\x0c\x12\x13\n\x0b\n\x04\x04\x02\x02\x02\x12\x03\r\x04\x15\n\
+    \x0c\n\x05\x04\x02\x02\x02\x05\x12\x03\r\x04\n\n\x0c\n\x05\x04\x02\x02\
+    \x02\x01\x12\x03\r\x0b\x10\n\x0c\n\x05\x04\x02\x02\x02\x03\x12\x03\r\x13\
+    \x14\n\x0b\n\x04\x04\x02\x02\x03\x12\x03\x0e\x04\x15\n\x0c\n\x05\x04\x02\
+    \x02\x03\x05\x12\x03\x0e\x04\n\n\x0c\n\x05\x04\x02\x02\x03\x01\x12\x03\
+    \x0e\x0b\x10\n\x0c\n\x05\x04\x02\x02\x03\x03\x12\x03\x0e\x13\x14b\x06pro\
+    to3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 1 - 0
rust-lib/flowy-user/src/protobuf/proto/sign_in.proto

@@ -12,4 +12,5 @@ message SignInResponse {
     string uid = 1;
     string name = 2;
     string email = 3;
+    string token = 4;
 }

+ 1 - 0
rust-lib/flowy-user/src/services/user/user_server.rs

@@ -81,6 +81,7 @@ impl UserServer for UserServerMock {
                 uid,
                 name: params.email.clone(),
                 email: params.email,
+                token: "".to_string(),
             })
         })
     }