mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Feat/http server adapt (#1754)
This commit is contained in:
@ -8,8 +8,8 @@ edition = "2018"
|
||||
[dependencies]
|
||||
flowy-derive = { path = "../flowy-derive" }
|
||||
flowy-database = { path = "../flowy-database", optional = true }
|
||||
flowy-error = { path = "../flowy-error", features = ["db", "http_server"] }
|
||||
|
||||
flowy-error = { path = "../flowy-error", features = ["adaptor_database", "adaptor_dispatch", "adaptor_user"] }
|
||||
user-model = { path = "../../../shared-lib/user-model" }
|
||||
lib-infra = { path = "../../../shared-lib/lib-infra" }
|
||||
flowy-notification = { path = "../flowy-notification" }
|
||||
lib-dispatch = { path = "../lib-dispatch" }
|
||||
@ -28,21 +28,10 @@ parking_lot = "0.12.1"
|
||||
strum = "0.21"
|
||||
strum_macros = "0.21"
|
||||
tokio = { version = "1", features = ["rt"] }
|
||||
unicode-segmentation = "1.8"
|
||||
validator = "0.15"
|
||||
fancy-regex = "0.10.0"
|
||||
|
||||
[dev-dependencies]
|
||||
flowy-test = { path = "../flowy-test" }
|
||||
nanoid = "0.4.0"
|
||||
quickcheck = "1.0.3"
|
||||
quickcheck_macros = "0.9.1"
|
||||
fake = "2.4.3"
|
||||
claim = "0.4.0"
|
||||
futures = "0.3.15"
|
||||
serial_test = "0.5.1"
|
||||
rand_core = "0.6.3"
|
||||
rand = "0.8.5"
|
||||
|
||||
[features]
|
||||
rev-sqlite = ["flowy-database"]
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::entities::parser::{UserEmail, UserName, UserPassword};
|
||||
use crate::errors::ErrorCode;
|
||||
use flowy_derive::ProtoBuf;
|
||||
use std::convert::TryInto;
|
||||
use user_model::{SignInParams, SignUpParams, UserEmail, UserName, UserPassword};
|
||||
|
||||
#[derive(ProtoBuf, Default)]
|
||||
pub struct SignInPayloadPB {
|
||||
@ -15,33 +15,6 @@ pub struct SignInPayloadPB {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf, Debug)]
|
||||
pub struct SignInParams {
|
||||
#[pb(index = 1)]
|
||||
pub email: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub password: String,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, ProtoBuf, Clone)]
|
||||
pub struct SignInResponse {
|
||||
#[pb(index = 1)]
|
||||
pub user_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub name: String,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub email: String,
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
impl TryInto<SignInParams> for SignInPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
|
||||
@ -83,30 +56,3 @@ impl TryInto<SignUpParams> for SignUpPayloadPB {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Default, Debug)]
|
||||
pub struct SignUpParams {
|
||||
#[pb(index = 1)]
|
||||
pub email: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub name: String,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct SignUpResponse {
|
||||
#[pb(index = 1)]
|
||||
pub user_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub name: String,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub email: String,
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub token: String,
|
||||
}
|
||||
|
@ -3,6 +3,5 @@ pub use user_profile::*;
|
||||
pub use user_setting::*;
|
||||
|
||||
pub mod auth;
|
||||
pub mod parser;
|
||||
mod user_profile;
|
||||
mod user_setting;
|
||||
|
@ -1,14 +0,0 @@
|
||||
// https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
|
||||
mod user_email;
|
||||
mod user_icon;
|
||||
mod user_id;
|
||||
mod user_name;
|
||||
mod user_password;
|
||||
mod user_workspace;
|
||||
|
||||
pub use user_email::*;
|
||||
pub use user_icon::*;
|
||||
pub use user_id::*;
|
||||
pub use user_name::*;
|
||||
pub use user_password::*;
|
||||
pub use user_workspace::*;
|
@ -1,73 +0,0 @@
|
||||
use crate::errors::ErrorCode;
|
||||
use validator::validate_email;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserEmail(pub String);
|
||||
|
||||
impl UserEmail {
|
||||
pub fn parse(s: String) -> Result<UserEmail, ErrorCode> {
|
||||
if s.trim().is_empty() {
|
||||
return Err(ErrorCode::EmailIsEmpty);
|
||||
}
|
||||
|
||||
if validate_email(&s) {
|
||||
Ok(Self(s))
|
||||
} else {
|
||||
Err(ErrorCode::EmailFormatInvalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for UserEmail {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use claim::assert_err;
|
||||
use fake::{faker::internet::en::SafeEmail, Fake};
|
||||
use rand::prelude::StdRng;
|
||||
use rand_core::SeedableRng;
|
||||
|
||||
#[test]
|
||||
fn empty_string_is_rejected() {
|
||||
let email = "".to_string();
|
||||
assert_err!(UserEmail::parse(email));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_missing_at_symbol_is_rejected() {
|
||||
let email = "helloworld.com".to_string();
|
||||
assert_err!(UserEmail::parse(email));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_missing_subject_is_rejected() {
|
||||
let email = "@domain.com".to_string();
|
||||
assert_err!(UserEmail::parse(email));
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ValidEmailFixture(pub String);
|
||||
|
||||
impl quickcheck::Arbitrary for ValidEmailFixture {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
let mut rand_slice: [u8; 32] = [0; 32];
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in 0..32 {
|
||||
rand_slice[i] = u8::arbitrary(g);
|
||||
}
|
||||
let mut seed = StdRng::from_seed(rand_slice);
|
||||
let email = SafeEmail().fake_with_rng(&mut seed);
|
||||
Self(email)
|
||||
}
|
||||
}
|
||||
|
||||
#[quickcheck_macros::quickcheck]
|
||||
fn valid_emails_are_parsed_successfully(valid_email: ValidEmailFixture) -> bool {
|
||||
UserEmail::parse(valid_email.0).is_ok()
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
use crate::errors::ErrorCode;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserIcon(pub String);
|
||||
|
||||
impl UserIcon {
|
||||
pub fn parse(s: String) -> Result<UserIcon, ErrorCode> {
|
||||
Ok(Self(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for UserIcon {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
use crate::errors::ErrorCode;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserId(pub String);
|
||||
|
||||
impl UserId {
|
||||
pub fn parse(s: String) -> Result<UserId, ErrorCode> {
|
||||
let is_empty_or_whitespace = s.trim().is_empty();
|
||||
if is_empty_or_whitespace {
|
||||
return Err(ErrorCode::UserIdInvalid);
|
||||
}
|
||||
Ok(Self(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for UserId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
use crate::errors::ErrorCode;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserName(pub String);
|
||||
|
||||
impl UserName {
|
||||
pub fn parse(s: String) -> Result<UserName, ErrorCode> {
|
||||
let is_empty_or_whitespace = s.trim().is_empty();
|
||||
if is_empty_or_whitespace {
|
||||
return Err(ErrorCode::UserNameIsEmpty);
|
||||
}
|
||||
// A grapheme is defined by the Unicode standard as a "user-perceived"
|
||||
// character: `å` is a single grapheme, but it is composed of two characters
|
||||
// (`a` and `̊`).
|
||||
//
|
||||
// `graphemes` returns an iterator over the graphemes in the input `s`.
|
||||
// `true` specifies that we want to use the extended grapheme definition set,
|
||||
// the recommended one.
|
||||
let is_too_long = s.graphemes(true).count() > 256;
|
||||
if is_too_long {
|
||||
return Err(ErrorCode::UserNameTooLong);
|
||||
}
|
||||
|
||||
let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}'];
|
||||
let contains_forbidden_characters = s.chars().any(|g| forbidden_characters.contains(&g));
|
||||
|
||||
if contains_forbidden_characters {
|
||||
return Err(ErrorCode::UserNameContainForbiddenCharacters);
|
||||
}
|
||||
|
||||
Ok(Self(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for UserName {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::UserName;
|
||||
use claim::{assert_err, assert_ok};
|
||||
|
||||
#[test]
|
||||
fn a_256_grapheme_long_name_is_valid() {
|
||||
let name = "a̐".repeat(256);
|
||||
assert_ok!(UserName::parse(name));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_name_longer_than_256_graphemes_is_rejected() {
|
||||
let name = "a".repeat(257);
|
||||
assert_err!(UserName::parse(name));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace_only_names_are_rejected() {
|
||||
let name = " ".to_string();
|
||||
assert_err!(UserName::parse(name));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_string_is_rejected() {
|
||||
let name = "".to_string();
|
||||
assert_err!(UserName::parse(name));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_containing_an_invalid_character_are_rejected() {
|
||||
for name in &['/', '(', ')', '"', '<', '>', '\\', '{', '}'] {
|
||||
let name = name.to_string();
|
||||
assert_err!(UserName::parse(name));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_valid_name_is_parsed_successfully() {
|
||||
let name = "nathan".to_string();
|
||||
assert_ok!(UserName::parse(name));
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
use crate::errors::ErrorCode;
|
||||
use fancy_regex::Regex;
|
||||
use lazy_static::lazy_static;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserPassword(pub String);
|
||||
|
||||
impl UserPassword {
|
||||
pub fn parse(s: String) -> Result<UserPassword, ErrorCode> {
|
||||
if s.trim().is_empty() {
|
||||
return Err(ErrorCode::PasswordIsEmpty);
|
||||
}
|
||||
|
||||
if s.graphemes(true).count() > 100 {
|
||||
return Err(ErrorCode::PasswordTooLong);
|
||||
}
|
||||
|
||||
let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}'];
|
||||
let contains_forbidden_characters = s.chars().any(|g| forbidden_characters.contains(&g));
|
||||
if contains_forbidden_characters {
|
||||
return Err(ErrorCode::PasswordContainsForbidCharacters);
|
||||
}
|
||||
|
||||
if !validate_password(&s) {
|
||||
return Err(ErrorCode::PasswordFormatInvalid);
|
||||
}
|
||||
|
||||
Ok(Self(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for UserPassword {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
// Test it in https://regex101.com/
|
||||
// https://stackoverflow.com/questions/2370015/regular-expression-for-password-validation/2370045
|
||||
// Hell1!
|
||||
// [invalid, greater or equal to 6]
|
||||
// Hel1!
|
||||
//
|
||||
// Hello1!
|
||||
// [invalid, must include number]
|
||||
// Hello!
|
||||
//
|
||||
// Hello12!
|
||||
// [invalid must include upper case]
|
||||
// hello12!
|
||||
static ref PASSWORD: Regex = Regex::new("((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\\W]).{6,20})").unwrap();
|
||||
}
|
||||
|
||||
pub fn validate_password(password: &str) -> bool {
|
||||
match PASSWORD.is_match(password) {
|
||||
Ok(is_match) => is_match,
|
||||
Err(e) => {
|
||||
log::error!("validate_password fail: {:?}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
#[derive(Debug)]
|
||||
pub struct UserWorkspace(pub String);
|
||||
|
||||
impl UserWorkspace {
|
||||
pub fn parse(s: String) -> Result<UserWorkspace, String> {
|
||||
let is_empty_or_whitespace = s.trim().is_empty();
|
||||
if is_empty_or_whitespace {
|
||||
return Err("workspace id is empty or whitespace".to_string());
|
||||
}
|
||||
Ok(Self(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for UserWorkspace {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
use crate::errors::ErrorCode;
|
||||
use flowy_derive::ProtoBuf;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use crate::{
|
||||
entities::parser::{UserEmail, UserIcon, UserId, UserName, UserPassword},
|
||||
errors::ErrorCode,
|
||||
};
|
||||
use user_model::{UpdateUserProfileParams, UserEmail, UserIcon, UserId, UserName, UserPassword};
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct UserTokenPB {
|
||||
@ -83,56 +80,6 @@ impl UpdateUserProfilePayloadPB {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Default, Clone, Debug)]
|
||||
pub struct UpdateUserProfileParams {
|
||||
#[pb(index = 1)]
|
||||
pub id: String,
|
||||
|
||||
#[pb(index = 2, one_of)]
|
||||
pub name: Option<String>,
|
||||
|
||||
#[pb(index = 3, one_of)]
|
||||
pub email: Option<String>,
|
||||
|
||||
#[pb(index = 4, one_of)]
|
||||
pub password: Option<String>,
|
||||
|
||||
#[pb(index = 5, one_of)]
|
||||
pub icon_url: Option<String>,
|
||||
}
|
||||
|
||||
impl UpdateUserProfileParams {
|
||||
pub fn new(user_id: &str) -> Self {
|
||||
Self {
|
||||
id: user_id.to_owned(),
|
||||
name: None,
|
||||
email: None,
|
||||
password: None,
|
||||
icon_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(mut self, name: &str) -> Self {
|
||||
self.name = Some(name.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn email(mut self, email: &str) -> Self {
|
||||
self.email = Some(email.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn password(mut self, password: &str) -> Self {
|
||||
self.password = Some(password.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon_url(mut self, icon_url: &str) -> Self {
|
||||
self.icon_url = Some(icon_url.to_owned());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<UpdateUserProfileParams> for UpdateUserProfilePayloadPB {
|
||||
type Error = ErrorCode;
|
||||
|
||||
|
@ -1,10 +1,9 @@
|
||||
use crate::entities::{
|
||||
SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfilePB,
|
||||
};
|
||||
use crate::entities::UserProfilePB;
|
||||
use crate::{errors::FlowyError, handlers::*, services::UserSession};
|
||||
use lib_dispatch::prelude::*;
|
||||
use lib_infra::future::FutureResult;
|
||||
use std::sync::Arc;
|
||||
use user_model::{SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams};
|
||||
|
||||
pub fn init(user_session: Arc<UserSession>) -> AFPlugin {
|
||||
AFPlugin::new()
|
||||
|
@ -3,6 +3,7 @@ use crate::services::UserSession;
|
||||
use flowy_error::FlowyError;
|
||||
use lib_dispatch::prelude::*;
|
||||
use std::{convert::TryInto, sync::Arc};
|
||||
use user_model::{SignInParams, SignUpParams};
|
||||
|
||||
// tracing instrument 👉🏻 https://docs.rs/tracing/0.1.26/tracing/attr.instrument.html
|
||||
#[tracing::instrument(level = "debug", name = "sign_in", skip(data, session), fields(email = %data.email), err)]
|
||||
|
@ -1,11 +1,11 @@
|
||||
use crate::entities::{
|
||||
AppearanceSettingsPB, UpdateUserProfileParams, UpdateUserProfilePayloadPB, UserProfilePB, UserSettingPB,
|
||||
APPEARANCE_DEFAULT_THEME,
|
||||
AppearanceSettingsPB, UpdateUserProfilePayloadPB, UserProfilePB, UserSettingPB, APPEARANCE_DEFAULT_THEME,
|
||||
};
|
||||
use crate::{errors::FlowyError, services::UserSession};
|
||||
use flowy_database::kv::KV;
|
||||
use lib_dispatch::prelude::*;
|
||||
use std::{convert::TryInto, sync::Arc};
|
||||
use user_model::UpdateUserProfileParams;
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(session))]
|
||||
pub async fn init_user_handler(session: AFPluginState<Arc<UserSession>>) -> Result<(), FlowyError> {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::entities::{SignInResponse, SignUpResponse, UpdateUserProfileParams, UserProfilePB};
|
||||
use crate::entities::UserProfilePB;
|
||||
use flowy_database::ConnectionPool;
|
||||
use flowy_database::{schema::user_table, DBConnection, Database};
|
||||
use flowy_error::{ErrorCode, FlowyError};
|
||||
@ -6,6 +6,7 @@ use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use std::path::PathBuf;
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
use user_model::{SignInResponse, SignUpResponse, UpdateUserProfileParams};
|
||||
|
||||
pub struct UserDB {
|
||||
db_dir: String,
|
||||
@ -41,7 +42,7 @@ impl UserDB {
|
||||
|
||||
tracing::trace!("open user db {} at path: {}", user_id, dir);
|
||||
let db = flowy_database::init(&dir).map_err(|e| {
|
||||
log::error!("open user: {} db failed, {:?}", user_id, e);
|
||||
tracing::error!("open user: {} db failed, {:?}", user_id, e);
|
||||
FlowyError::internal().context(e)
|
||||
})?;
|
||||
let pool = db.get_pool();
|
||||
|
@ -1,6 +1,4 @@
|
||||
use crate::entities::{
|
||||
SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfilePB, UserSettingPB,
|
||||
};
|
||||
use crate::entities::{UserProfilePB, UserSettingPB};
|
||||
use crate::{
|
||||
errors::{ErrorCode, FlowyError},
|
||||
event_map::UserCloudService,
|
||||
@ -20,6 +18,7 @@ use flowy_database::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use user_model::{SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams};
|
||||
|
||||
pub struct UserSessionConfig {
|
||||
root_dir: String,
|
||||
@ -227,7 +226,7 @@ impl UserSession {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
// TODO: retry?
|
||||
log::error!("update user profile failed: {:?}", e);
|
||||
tracing::error!("update user profile failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -241,7 +240,7 @@ impl UserSession {
|
||||
let _ = tokio::spawn(async move {
|
||||
match server.sign_out(&token).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => log::error!("Sign out failed: {:?}", e),
|
||||
Err(e) => tracing::error!("Sign out failed: {:?}", e),
|
||||
}
|
||||
})
|
||||
.await;
|
||||
@ -339,7 +338,7 @@ impl std::convert::From<String> for Session {
|
||||
match serde_json::from_str(&s) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::error!("Deserialize string to Session failed: {:?}", e);
|
||||
tracing::error!("Deserialize string to Session failed: {:?}", e);
|
||||
Session::default()
|
||||
}
|
||||
}
|
||||
@ -350,7 +349,7 @@ impl std::convert::From<Session> for String {
|
||||
match serde_json::to_string(&session) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::error!("Serialize session to string failed: {:?}", e);
|
||||
tracing::error!("Serialize session to string failed: {:?}", e);
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user