Feat/http server adapt (#1754)

This commit is contained in:
Nathan.fooo
2023-01-30 11:11:19 +08:00
committed by GitHub
parent 000569a836
commit 0d8adaa921
301 changed files with 2181 additions and 1768 deletions

View File

@ -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"]

View File

@ -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,
}

View File

@ -3,6 +3,5 @@ pub use user_profile::*;
pub use user_setting::*;
pub mod auth;
pub mod parser;
mod user_profile;
mod user_setting;

View File

@ -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::*;

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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 = "".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));
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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;

View File

@ -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()

View File

@ -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)]

View File

@ -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> {

View File

@ -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();

View File

@ -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()
}
}