mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-28 19:31:08 +00:00
registration and login working
This commit is contained in:
parent
89a71c7fcd
commit
8ca01dee07
25 changed files with 424 additions and 181 deletions
118
Cargo.lock
generated
118
Cargo.lock
generated
|
@ -233,6 +233,28 @@ dependencies = [
|
||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-extra"
|
||||||
|
version = "0.7.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a93e433be9382c737320af3924f7d5fc6f89c155cf2bf88949d8f5126fab283f"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"axum-core",
|
||||||
|
"bytes",
|
||||||
|
"cookie 0.17.0",
|
||||||
|
"futures-util",
|
||||||
|
"http 0.2.11",
|
||||||
|
"http-body 0.4.5",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"serde",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-macros"
|
name = "axum-macros"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
|
@ -539,6 +561,45 @@ dependencies = [
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie"
|
||||||
|
version = "0.16.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
"time",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
"time",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie_store"
|
||||||
|
version = "0.16.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d606d0fba62e13cf04db20536c05cb7f13673c161cb47a47a82b9b9e7d3f1daa"
|
||||||
|
dependencies = [
|
||||||
|
"cookie 0.16.2",
|
||||||
|
"idna 0.2.3",
|
||||||
|
"log",
|
||||||
|
"publicsuffix",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.3"
|
version = "0.9.3"
|
||||||
|
@ -1325,6 +1386,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
|
"axum-extra",
|
||||||
"axum-macros",
|
"axum-macros",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
@ -1351,6 +1413,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
@ -1365,6 +1428,27 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
|
||||||
|
dependencies = [
|
||||||
|
"matches",
|
||||||
|
"unicode-bidi",
|
||||||
|
"unicode-normalization",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-bidi",
|
||||||
|
"unicode-normalization",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
@ -1802,6 +1886,12 @@ dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matches"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
|
@ -2245,6 +2335,22 @@ dependencies = [
|
||||||
"yansi 1.0.0-rc.1",
|
"yansi 1.0.0-rc.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psl-types"
|
||||||
|
version = "2.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "publicsuffix"
|
||||||
|
version = "2.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457"
|
||||||
|
dependencies = [
|
||||||
|
"idna 0.3.0",
|
||||||
|
"psl-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pulldown-cmark"
|
name = "pulldown-cmark"
|
||||||
version = "0.9.3"
|
version = "0.9.3"
|
||||||
|
@ -2389,6 +2495,8 @@ checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.5",
|
"base64 0.21.5",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"cookie 0.16.2",
|
||||||
|
"cookie_store",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
@ -2949,9 +3057,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
|
checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"itoa",
|
"itoa",
|
||||||
|
@ -2969,9 +3077,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.15"
|
version = "0.2.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
|
checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
@ -3268,7 +3376,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
|
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna 0.5.0",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,6 +8,7 @@ default = ["ssr"]
|
||||||
ssr = [
|
ssr = [
|
||||||
"axum",
|
"axum",
|
||||||
"axum-macros",
|
"axum-macros",
|
||||||
|
"axum-extra",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel-derive-newtype",
|
"diesel-derive-newtype",
|
||||||
|
@ -28,6 +29,7 @@ anyhow = "1.0.75"
|
||||||
async-trait = "0.1.74"
|
async-trait = "0.1.74"
|
||||||
axum = { version = "0.6.20", optional = true }
|
axum = { version = "0.6.20", optional = true }
|
||||||
axum-macros = { version = "0.3.8", optional = true }
|
axum-macros = { version = "0.3.8", optional = true }
|
||||||
|
axum-extra = { version = "0.7.7", features = ["cookie"], optional = true }
|
||||||
leptos = "0.5.4"
|
leptos = "0.5.4"
|
||||||
leptos_meta = "0.5.4"
|
leptos_meta = "0.5.4"
|
||||||
leptos_router = "0.5.4"
|
leptos_router = "0.5.4"
|
||||||
|
@ -62,10 +64,11 @@ once_cell = "1.18.0"
|
||||||
wasm-bindgen = "0.2.89"
|
wasm-bindgen = "0.2.89"
|
||||||
console_error_panic_hook = "0.1.7"
|
console_error_panic_hook = "0.1.7"
|
||||||
console_log = "1.0.0"
|
console_log = "1.0.0"
|
||||||
|
time = "0.3.31"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = "1.4.0"
|
pretty_assertions = "1.4.0"
|
||||||
reqwest = "0.11.22"
|
reqwest = { version = "0.11.22", features = ["cookies"] }
|
||||||
|
|
||||||
[package.metadata.leptos]
|
[package.metadata.leptos]
|
||||||
output-name = "ibis"
|
output-name = "ibis"
|
||||||
|
|
|
@ -11,11 +11,11 @@ The Ibis is a [bird which is related to the Egyptian god of knowledge and scienc
|
||||||
|
|
||||||
You need to install [cargo](https://rustup.rs/) and [trunk](https://trunkrs.dev). Then run the following commands in separate terminals:
|
You need to install [cargo](https://rustup.rs/) and [trunk](https://trunkrs.dev). Then run the following commands in separate terminals:
|
||||||
```
|
```
|
||||||
# start backend
|
# start backend, with separate target folder to avoid rebuilds from arch change
|
||||||
cargo run
|
cargo run --target-dir target/backend
|
||||||
|
|
||||||
# start frontend
|
# start frontend, automatic rebuild on changes
|
||||||
trunk serve
|
trunk serve -w src/frontend/
|
||||||
```
|
```
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ use crate::backend::database::article::DbArticleForm;
|
||||||
use crate::backend::database::conflict::{ApiConflict, DbConflict, DbConflictForm};
|
use crate::backend::database::conflict::{ApiConflict, DbConflict, DbConflictForm};
|
||||||
use crate::backend::database::edit::DbEditForm;
|
use crate::backend::database::edit::DbEditForm;
|
||||||
use crate::backend::database::instance::DbInstance;
|
use crate::backend::database::instance::DbInstance;
|
||||||
use crate::backend::database::user::LocalUserView;
|
|
||||||
use crate::backend::database::MyDataHandle;
|
use crate::backend::database::MyDataHandle;
|
||||||
use crate::backend::error::MyResult;
|
use crate::backend::error::MyResult;
|
||||||
use crate::backend::federation::activities::create_article::CreateArticle;
|
use crate::backend::federation::activities::create_article::CreateArticle;
|
||||||
|
@ -10,6 +9,7 @@ use crate::backend::federation::activities::submit_article_update;
|
||||||
use crate::backend::utils::generate_article_version;
|
use crate::backend::utils::generate_article_version;
|
||||||
use crate::common::EditVersion;
|
use crate::common::EditVersion;
|
||||||
use crate::common::GetArticleData;
|
use crate::common::GetArticleData;
|
||||||
|
use crate::common::LocalUserView;
|
||||||
use crate::common::{ArticleView, DbArticle, DbEdit};
|
use crate::common::{ArticleView, DbArticle, DbEdit};
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
use activitypub_federation::fetch::object_id::ObjectId;
|
use activitypub_federation::fetch::object_id::ObjectId;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::backend::database::instance::{DbInstance, InstanceView};
|
use crate::backend::database::instance::{DbInstance, InstanceView};
|
||||||
use crate::backend::database::user::LocalUserView;
|
|
||||||
use crate::backend::database::MyDataHandle;
|
use crate::backend::database::MyDataHandle;
|
||||||
use crate::backend::error::MyResult;
|
use crate::backend::error::MyResult;
|
||||||
use crate::backend::federation::activities::follow::Follow;
|
use crate::backend::federation::activities::follow::Follow;
|
||||||
|
use crate::common::LocalUserView;
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
use axum::Extension;
|
use axum::Extension;
|
||||||
use axum::{Form, Json};
|
use axum::{Form, Json};
|
||||||
|
|
|
@ -2,15 +2,16 @@ use crate::backend::api::article::create_article;
|
||||||
use crate::backend::api::article::{edit_article, fork_article, get_article};
|
use crate::backend::api::article::{edit_article, fork_article, get_article};
|
||||||
use crate::backend::api::instance::follow_instance;
|
use crate::backend::api::instance::follow_instance;
|
||||||
use crate::backend::api::instance::get_local_instance;
|
use crate::backend::api::instance::get_local_instance;
|
||||||
use crate::backend::api::user::login_user;
|
use crate::backend::api::user::my_profile;
|
||||||
use crate::backend::api::user::register_user;
|
use crate::backend::api::user::register_user;
|
||||||
use crate::backend::api::user::validate;
|
use crate::backend::api::user::validate;
|
||||||
|
use crate::backend::api::user::{login_user, logout_user};
|
||||||
use crate::backend::database::conflict::{ApiConflict, DbConflict};
|
use crate::backend::database::conflict::{ApiConflict, DbConflict};
|
||||||
use crate::backend::database::instance::DbInstance;
|
use crate::backend::database::instance::DbInstance;
|
||||||
use crate::backend::database::user::LocalUserView;
|
|
||||||
use crate::backend::database::MyDataHandle;
|
use crate::backend::database::MyDataHandle;
|
||||||
use crate::backend::error::MyResult;
|
use crate::backend::error::MyResult;
|
||||||
use crate::common::DbEdit;
|
use crate::common::DbEdit;
|
||||||
|
use crate::common::LocalUserView;
|
||||||
use crate::common::{ArticleView, DbArticle};
|
use crate::common::{ArticleView, DbArticle};
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
use activitypub_federation::fetch::object_id::ObjectId;
|
use activitypub_federation::fetch::object_id::ObjectId;
|
||||||
|
@ -49,8 +50,10 @@ pub fn api_routes() -> Router {
|
||||||
.route("/instance", get(get_local_instance))
|
.route("/instance", get(get_local_instance))
|
||||||
.route("/instance/follow", post(follow_instance))
|
.route("/instance/follow", post(follow_instance))
|
||||||
.route("/search", get(search_article))
|
.route("/search", get(search_article))
|
||||||
.route("/user/register", post(register_user))
|
.route("/account/register", post(register_user))
|
||||||
.route("/user/login", post(login_user))
|
.route("/account/login", post(login_user))
|
||||||
|
.route("/account/my_profile", get(my_profile))
|
||||||
|
.route("/account/logout", get(logout_user))
|
||||||
.route_layer(middleware::from_fn(auth))
|
.route_layer(middleware::from_fn(auth))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use crate::backend::database::user::{DbLocalUser, DbPerson, LocalUserView};
|
|
||||||
use crate::backend::database::{read_jwt_secret, MyDataHandle};
|
use crate::backend::database::{read_jwt_secret, MyDataHandle};
|
||||||
use crate::backend::error::MyResult;
|
use crate::backend::error::MyResult;
|
||||||
use crate::common::{LoginResponse, LoginUserData, RegisterUserData};
|
use crate::common::{DbLocalUser, DbPerson, LocalUserView, LoginUserData, RegisterUserData};
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use axum::{Form, Json};
|
use axum::{Form, Json};
|
||||||
|
use axum_extra::extract::cookie::{Cookie, CookieJar, Expiration, SameSite};
|
||||||
use axum_macros::debug_handler;
|
use axum_macros::debug_handler;
|
||||||
use bcrypt::verify;
|
use bcrypt::verify;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
@ -13,6 +13,9 @@ use jsonwebtoken::Validation;
|
||||||
use jsonwebtoken::{decode, get_current_timestamp};
|
use jsonwebtoken::{decode, get_current_timestamp};
|
||||||
use jsonwebtoken::{encode, EncodingKey, Header};
|
use jsonwebtoken::{encode, EncodingKey, Header};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use time::{Duration, OffsetDateTime};
|
||||||
|
|
||||||
|
pub static AUTH_COOKIE: &str = "auth";
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct Claims {
|
struct Claims {
|
||||||
|
@ -26,22 +29,19 @@ struct Claims {
|
||||||
pub exp: u64,
|
pub exp: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_login_token(
|
fn generate_login_token(local_user: &DbLocalUser, data: &Data<MyDataHandle>) -> MyResult<String> {
|
||||||
local_user: DbLocalUser,
|
|
||||||
data: &Data<MyDataHandle>,
|
|
||||||
) -> MyResult<LoginResponse> {
|
|
||||||
let hostname = data.domain().to_string();
|
let hostname = data.domain().to_string();
|
||||||
let claims = Claims {
|
let claims = Claims {
|
||||||
sub: local_user.id.to_string(),
|
sub: local_user.id.to_string(),
|
||||||
iss: hostname,
|
iss: hostname,
|
||||||
iat: Utc::now().timestamp(),
|
iat: Utc::now().timestamp(),
|
||||||
exp: get_current_timestamp(),
|
exp: get_current_timestamp() + 60 * 60 * 24 * 365,
|
||||||
};
|
};
|
||||||
|
|
||||||
let secret = read_jwt_secret(data)?;
|
let secret = read_jwt_secret(data)?;
|
||||||
let key = EncodingKey::from_secret(secret.as_bytes());
|
let key = EncodingKey::from_secret(secret.as_bytes());
|
||||||
let jwt = encode(&Header::default(), &claims, &key)?;
|
let jwt = encode(&Header::default(), &claims, &key)?;
|
||||||
Ok(LoginResponse { jwt })
|
Ok(jwt)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn validate(jwt: &str, data: &Data<MyDataHandle>) -> MyResult<LocalUserView> {
|
pub async fn validate(jwt: &str, data: &Data<MyDataHandle>) -> MyResult<LocalUserView> {
|
||||||
|
@ -55,21 +55,58 @@ pub async fn validate(jwt: &str, data: &Data<MyDataHandle>) -> MyResult<LocalUse
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub(in crate::backend::api) async fn register_user(
|
pub(in crate::backend::api) async fn register_user(
|
||||||
data: Data<MyDataHandle>,
|
data: Data<MyDataHandle>,
|
||||||
|
jar: CookieJar,
|
||||||
Form(form): Form<RegisterUserData>,
|
Form(form): Form<RegisterUserData>,
|
||||||
) -> MyResult<Json<LoginResponse>> {
|
) -> MyResult<(CookieJar, Json<LocalUserView>)> {
|
||||||
let user = DbPerson::create_local(form.username, form.password, &data)?;
|
let user = DbPerson::create_local(form.username, form.password, &data)?;
|
||||||
Ok(Json(generate_login_token(user.local_user, &data)?))
|
let token = generate_login_token(&user.local_user, &data)?;
|
||||||
|
let jar = jar.add(create_cookie(token));
|
||||||
|
Ok((jar, Json(user)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub(in crate::backend::api) async fn login_user(
|
pub(in crate::backend::api) async fn login_user(
|
||||||
data: Data<MyDataHandle>,
|
data: Data<MyDataHandle>,
|
||||||
|
jar: CookieJar,
|
||||||
Form(form): Form<LoginUserData>,
|
Form(form): Form<LoginUserData>,
|
||||||
) -> MyResult<Json<LoginResponse>> {
|
) -> MyResult<(CookieJar, Json<LocalUserView>)> {
|
||||||
let user = DbPerson::read_local_from_name(&form.username, &data)?;
|
let user = DbPerson::read_local_from_name(&form.username, &data)?;
|
||||||
let valid = verify(&form.password, &user.local_user.password_encrypted)?;
|
let valid = verify(&form.password, &user.local_user.password_encrypted)?;
|
||||||
if !valid {
|
if !valid {
|
||||||
return Err(anyhow!("Invalid login").into());
|
return Err(anyhow!("Invalid login").into());
|
||||||
}
|
}
|
||||||
Ok(Json(generate_login_token(user.local_user, &data)?))
|
let token = generate_login_token(&user.local_user, &data)?;
|
||||||
|
let jar = jar.add(create_cookie(token));
|
||||||
|
Ok((jar, Json(user)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_cookie(jwt: String) -> Cookie<'static> {
|
||||||
|
Cookie::build(AUTH_COOKIE, jwt)
|
||||||
|
.domain("localhost")
|
||||||
|
.same_site(SameSite::Strict)
|
||||||
|
.path("/")
|
||||||
|
.http_only(true)
|
||||||
|
.expires(Expiration::DateTime(
|
||||||
|
OffsetDateTime::now_utc() + Duration::weeks(52),
|
||||||
|
))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
pub(in crate::backend::api) async fn my_profile(
|
||||||
|
data: Data<MyDataHandle>,
|
||||||
|
jar: CookieJar,
|
||||||
|
) -> MyResult<Json<LocalUserView>> {
|
||||||
|
let jwt = jar.get(AUTH_COOKIE).map(|c| c.value());
|
||||||
|
if let Some(jwt) = jwt {
|
||||||
|
Ok(Json(validate(jwt, &data).await?))
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("invalid/missing auth").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
pub(in crate::backend::api) async fn logout_user(jar: CookieJar) -> MyResult<CookieJar> {
|
||||||
|
let jar = jar.remove(Cookie::named(AUTH_COOKIE));
|
||||||
|
Ok(jar)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
use crate::backend::database::schema::conflict;
|
use crate::backend::database::schema::conflict;
|
||||||
use crate::backend::database::user::DbLocalUser;
|
|
||||||
use crate::backend::database::MyDataHandle;
|
use crate::backend::database::MyDataHandle;
|
||||||
use crate::backend::error::MyResult;
|
use crate::backend::error::MyResult;
|
||||||
use crate::backend::federation::activities::submit_article_update;
|
use crate::backend::federation::activities::submit_article_update;
|
||||||
use crate::backend::utils::generate_article_version;
|
use crate::backend::utils::generate_article_version;
|
||||||
use crate::common::DbArticle;
|
use crate::common::DbArticle;
|
||||||
use crate::common::DbEdit;
|
use crate::common::DbEdit;
|
||||||
|
use crate::common::DbLocalUser;
|
||||||
use crate::common::EditVersion;
|
use crate::common::EditVersion;
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
use activitypub_federation::fetch::object_id::ObjectId;
|
|
||||||
use diesel::ExpressionMethods;
|
use diesel::ExpressionMethods;
|
||||||
use diesel::{
|
use diesel::{
|
||||||
delete, insert_into, Identifiable, Insertable, PgConnection, QueryDsl, Queryable, RunQueryDsl,
|
delete, insert_into, Identifiable, Insertable, PgConnection, QueryDsl, Queryable, RunQueryDsl,
|
||||||
|
@ -76,9 +75,7 @@ impl DbConflict {
|
||||||
) -> MyResult<Option<ApiConflict>> {
|
) -> MyResult<Option<ApiConflict>> {
|
||||||
let article = DbArticle::read(self.article_id, &data.db_connection)?;
|
let article = DbArticle::read(self.article_id, &data.db_connection)?;
|
||||||
// Make sure to get latest version from origin so that all conflicts can be resolved
|
// Make sure to get latest version from origin so that all conflicts can be resolved
|
||||||
let original_article = ObjectId::parse(&article.ap_id)?
|
let original_article = article.ap_id.dereference_forced(data).await?;
|
||||||
.dereference_forced(data)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// create common ancestor version
|
// create common ancestor version
|
||||||
let edits = DbEdit::read_for_article(&original_article, &data.db_connection)?;
|
let edits = DbEdit::read_for_article(&original_article, &data.db_connection)?;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::backend::database::schema::{instance, instance_follow};
|
use crate::backend::database::schema::{instance, instance_follow};
|
||||||
use crate::backend::database::user::DbPerson;
|
|
||||||
use crate::backend::database::MyDataHandle;
|
use crate::backend::database::MyDataHandle;
|
||||||
use crate::backend::error::MyResult;
|
use crate::backend::error::MyResult;
|
||||||
use crate::backend::federation::objects::articles_collection::DbArticleCollection;
|
use crate::backend::federation::objects::articles_collection::DbArticleCollection;
|
||||||
|
use crate::common::DbPerson;
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
use activitypub_federation::fetch::collection_id::CollectionId;
|
use activitypub_federation::fetch::collection_id::CollectionId;
|
||||||
use activitypub_federation::fetch::object_id::ObjectId;
|
use activitypub_federation::fetch::object_id::ObjectId;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::backend::database::schema::{local_user, person};
|
use crate::backend::database::schema::{local_user, person};
|
||||||
use crate::backend::database::MyDataHandle;
|
use crate::backend::database::MyDataHandle;
|
||||||
use crate::backend::error::MyResult;
|
use crate::backend::error::MyResult;
|
||||||
|
use crate::common::{DbLocalUser, DbPerson, LocalUserView};
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
use activitypub_federation::fetch::object_id::ObjectId;
|
use activitypub_federation::fetch::object_id::ObjectId;
|
||||||
use activitypub_federation::http_signatures::generate_actor_keypair;
|
use activitypub_federation::http_signatures::generate_actor_keypair;
|
||||||
|
@ -9,30 +10,10 @@ use bcrypt::DEFAULT_COST;
|
||||||
use chrono::{DateTime, Local, Utc};
|
use chrono::{DateTime, Local, Utc};
|
||||||
use diesel::ExpressionMethods;
|
use diesel::ExpressionMethods;
|
||||||
use diesel::QueryDsl;
|
use diesel::QueryDsl;
|
||||||
use diesel::{
|
use diesel::{insert_into, AsChangeset, Insertable, PgConnection, RunQueryDsl};
|
||||||
insert_into, AsChangeset, Identifiable, Insertable, PgConnection, Queryable, RunQueryDsl,
|
|
||||||
Selectable,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable)]
|
|
||||||
#[diesel(check_for_backend(diesel::pg::Pg))]
|
|
||||||
pub struct LocalUserView {
|
|
||||||
pub person: DbPerson,
|
|
||||||
pub local_user: DbLocalUser,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A user with account registered on local instance.
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable, Selectable, Identifiable)]
|
|
||||||
#[diesel(table_name = local_user, check_for_backend(diesel::pg::Pg))]
|
|
||||||
pub struct DbLocalUser {
|
|
||||||
pub id: i32,
|
|
||||||
pub password_encrypted: String,
|
|
||||||
pub person_id: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Insertable, AsChangeset)]
|
#[derive(Debug, Clone, Insertable, AsChangeset)]
|
||||||
#[diesel(table_name = local_user, check_for_backend(diesel::pg::Pg))]
|
#[diesel(table_name = local_user, check_for_backend(diesel::pg::Pg))]
|
||||||
pub struct DbLocalUserForm {
|
pub struct DbLocalUserForm {
|
||||||
|
@ -40,23 +21,6 @@ pub struct DbLocalUserForm {
|
||||||
pub person_id: i32,
|
pub person_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Federation related data from a local or remote user.
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable, Selectable, Identifiable)]
|
|
||||||
#[diesel(table_name = person, check_for_backend(diesel::pg::Pg))]
|
|
||||||
pub struct DbPerson {
|
|
||||||
pub id: i32,
|
|
||||||
pub username: String,
|
|
||||||
pub ap_id: ObjectId<DbPerson>,
|
|
||||||
pub inbox_url: String,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub public_key: String,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub private_key: Option<String>,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub last_refreshed_at: DateTime<Utc>,
|
|
||||||
pub local: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Insertable, AsChangeset)]
|
#[derive(Debug, Clone, Insertable, AsChangeset)]
|
||||||
#[diesel(table_name = person, check_for_backend(diesel::pg::Pg))]
|
#[diesel(table_name = person, check_for_backend(diesel::pg::Pg))]
|
||||||
pub struct DbPersonForm {
|
pub struct DbPersonForm {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use crate::backend::database::instance::DbInstance;
|
use crate::backend::database::instance::DbInstance;
|
||||||
use crate::backend::database::user::DbPerson;
|
|
||||||
use crate::backend::error::MyResult;
|
use crate::backend::error::MyResult;
|
||||||
use crate::backend::federation::send_activity;
|
use crate::backend::federation::send_activity;
|
||||||
use crate::backend::{
|
use crate::backend::{
|
||||||
database::MyDataHandle, federation::activities::accept::Accept, generate_activity_id,
|
database::MyDataHandle, federation::activities::accept::Accept, generate_activity_id,
|
||||||
};
|
};
|
||||||
|
use crate::common::DbPerson;
|
||||||
use activitypub_federation::{
|
use activitypub_federation::{
|
||||||
config::Data,
|
config::Data,
|
||||||
fetch::object_id::ObjectId,
|
fetch::object_id::ObjectId,
|
||||||
|
|
|
@ -35,7 +35,7 @@ pub async fn submit_article_update(
|
||||||
id: -1,
|
id: -1,
|
||||||
creator_id,
|
creator_id,
|
||||||
hash: form.hash,
|
hash: form.hash,
|
||||||
ap_id: form.ap_id.to_string(),
|
ap_id: form.ap_id,
|
||||||
diff: form.diff,
|
diff: form.diff,
|
||||||
article_id: form.article_id,
|
article_id: form.article_id,
|
||||||
previous_version_id: form.previous_version_id,
|
previous_version_id: form.previous_version_id,
|
||||||
|
|
|
@ -49,7 +49,7 @@ impl Object for DbArticle {
|
||||||
let local_instance = DbInstance::read_local_instance(&data.db_connection)?;
|
let local_instance = DbInstance::read_local_instance(&data.db_connection)?;
|
||||||
Ok(ApubArticle {
|
Ok(ApubArticle {
|
||||||
kind: Default::default(),
|
kind: Default::default(),
|
||||||
id: ObjectId::parse(&self.ap_id)?,
|
id: self.ap_id.clone(),
|
||||||
attributed_to: local_instance.ap_id.clone(),
|
attributed_to: local_instance.ap_id.clone(),
|
||||||
to: vec![public(), local_instance.followers_url()?],
|
to: vec![public(), local_instance.followers_url()?],
|
||||||
edits: self.edits_id()?,
|
edits: self.edits_id()?,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::backend::database::edit::DbEditForm;
|
use crate::backend::database::edit::DbEditForm;
|
||||||
use crate::backend::database::user::DbPerson;
|
|
||||||
use crate::backend::database::MyDataHandle;
|
use crate::backend::database::MyDataHandle;
|
||||||
use crate::backend::error::Error;
|
use crate::backend::error::Error;
|
||||||
|
use crate::common::DbPerson;
|
||||||
use crate::common::EditVersion;
|
use crate::common::EditVersion;
|
||||||
use crate::common::{DbArticle, DbEdit};
|
use crate::common::{DbArticle, DbEdit};
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
|
@ -48,11 +48,11 @@ impl Object for DbEdit {
|
||||||
let creator = DbPerson::read(self.creator_id, data)?;
|
let creator = DbPerson::read(self.creator_id, data)?;
|
||||||
Ok(ApubEdit {
|
Ok(ApubEdit {
|
||||||
kind: PatchType::Patch,
|
kind: PatchType::Patch,
|
||||||
id: ObjectId::parse(&self.ap_id)?,
|
id: self.ap_id,
|
||||||
content: self.diff,
|
content: self.diff,
|
||||||
version: self.hash,
|
version: self.hash,
|
||||||
previous_version: self.previous_version_id,
|
previous_version: self.previous_version_id,
|
||||||
object: ObjectId::parse(&article.ap_id)?,
|
object: article.ap_id,
|
||||||
attributed_to: creator.ap_id,
|
attributed_to: creator.ap_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::backend::database::user::{DbPerson, DbPersonForm};
|
use crate::backend::database::user::DbPersonForm;
|
||||||
use crate::backend::database::MyDataHandle;
|
use crate::backend::database::MyDataHandle;
|
||||||
use crate::backend::error::Error;
|
use crate::backend::error::Error;
|
||||||
|
use crate::common::DbPerson;
|
||||||
use activitypub_federation::kinds::actor::PersonType;
|
use activitypub_federation::kinds::actor::PersonType;
|
||||||
use activitypub_federation::{
|
use activitypub_federation::{
|
||||||
config::Data,
|
config::Data,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::backend::database::instance::DbInstance;
|
use crate::backend::database::instance::DbInstance;
|
||||||
use crate::backend::database::user::DbPerson;
|
|
||||||
use crate::backend::database::MyDataHandle;
|
use crate::backend::database::MyDataHandle;
|
||||||
use crate::backend::error::Error;
|
use crate::backend::error::Error;
|
||||||
use crate::backend::error::MyResult;
|
use crate::backend::error::MyResult;
|
||||||
|
@ -17,6 +16,7 @@ use crate::backend::federation::objects::edits_collection::{ApubEditCollection,
|
||||||
use crate::backend::federation::objects::instance::ApubInstance;
|
use crate::backend::federation::objects::instance::ApubInstance;
|
||||||
use crate::backend::federation::objects::user::ApubUser;
|
use crate::backend::federation::objects::user::ApubUser;
|
||||||
use crate::common::DbArticle;
|
use crate::common::DbArticle;
|
||||||
|
use crate::common::DbPerson;
|
||||||
use activitypub_federation::axum::inbox::{receive_activity, ActivityData};
|
use activitypub_federation::axum::inbox::{receive_activity, ActivityData};
|
||||||
use activitypub_federation::axum::json::FederationJson;
|
use activitypub_federation::axum::json::FederationJson;
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
#[cfg(feature = "ssr")]
|
use chrono::{DateTime, Utc};
|
||||||
use crate::backend::database::schema::{article, edit};
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use diesel::{Identifiable, Queryable, Selectable};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
use {
|
||||||
|
crate::backend::database::schema::{article, edit, local_user, person},
|
||||||
|
activitypub_federation::fetch::object_id::ObjectId,
|
||||||
|
diesel::{Identifiable, Queryable, Selectable},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct GetArticleData {
|
pub struct GetArticleData {
|
||||||
|
@ -26,6 +29,9 @@ pub struct DbArticle {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub ap_id: ObjectId<DbArticle>,
|
||||||
|
#[cfg(not(feature = "ssr"))]
|
||||||
pub ap_id: String,
|
pub ap_id: String,
|
||||||
pub instance_id: i32,
|
pub instance_id: i32,
|
||||||
pub local: bool,
|
pub local: bool,
|
||||||
|
@ -42,6 +48,9 @@ pub struct DbEdit {
|
||||||
pub creator_id: i32,
|
pub creator_id: i32,
|
||||||
/// UUID built from sha224 hash of diff
|
/// UUID built from sha224 hash of diff
|
||||||
pub hash: EditVersion,
|
pub hash: EditVersion,
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub ap_id: ObjectId<DbEdit>,
|
||||||
|
#[cfg(not(feature = "ssr"))]
|
||||||
pub ap_id: String,
|
pub ap_id: String,
|
||||||
pub diff: String,
|
pub diff: String,
|
||||||
pub article_id: i32,
|
pub article_id: i32,
|
||||||
|
@ -55,19 +64,53 @@ pub struct DbEdit {
|
||||||
#[cfg_attr(feature = "ssr", derive(diesel_derive_newtype::DieselNewType))]
|
#[cfg_attr(feature = "ssr", derive(diesel_derive_newtype::DieselNewType))]
|
||||||
pub struct EditVersion(pub(crate) Uuid);
|
pub struct EditVersion(pub(crate) Uuid);
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct RegisterUserData {
|
pub struct RegisterUserData {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
|
||||||
pub struct LoginResponse {
|
|
||||||
pub jwt: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct LoginUserData {
|
pub struct LoginUserData {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "ssr", derive(Queryable))]
|
||||||
|
#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||||
|
pub struct LocalUserView {
|
||||||
|
pub person: DbPerson,
|
||||||
|
pub local_user: DbLocalUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A user with account registered on local instance.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Identifiable))]
|
||||||
|
#[cfg_attr(feature = "ssr", diesel(table_name = local_user, check_for_backend(diesel::pg::Pg)))]
|
||||||
|
pub struct DbLocalUser {
|
||||||
|
pub id: i32,
|
||||||
|
pub password_encrypted: String,
|
||||||
|
pub person_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Federation related data from a local or remote user.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Identifiable))]
|
||||||
|
#[cfg_attr(feature = "ssr", diesel(table_name = person, check_for_backend(diesel::pg::Pg)))]
|
||||||
|
pub struct DbPerson {
|
||||||
|
pub id: i32,
|
||||||
|
pub username: String,
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub ap_id: ObjectId<DbPerson>,
|
||||||
|
#[cfg(not(feature = "ssr"))]
|
||||||
|
pub ap_id: String,
|
||||||
|
pub inbox_url: String,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub public_key: String,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub private_key: Option<String>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub last_refreshed_at: DateTime<Utc>,
|
||||||
|
pub local: bool,
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::common::GetArticleData;
|
use crate::common::GetArticleData;
|
||||||
use crate::common::{ArticleView, LoginResponse, LoginUserData, RegisterUserData};
|
use crate::common::LocalUserView;
|
||||||
|
use crate::common::{ArticleView, LoginUserData, RegisterUserData};
|
||||||
use crate::frontend::error::MyResult;
|
use crate::frontend::error::MyResult;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
@ -39,20 +40,29 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn register(hostname: &str, register_form: RegisterUserData) -> MyResult<LoginResponse> {
|
pub async fn register(hostname: &str, register_form: RegisterUserData) -> MyResult<LocalUserView> {
|
||||||
let req = CLIENT
|
let req = CLIENT
|
||||||
.post(format!("http://{}/api/v1/user/register", hostname))
|
.post(format!("http://{}/api/v1/account/register", hostname))
|
||||||
.form(®ister_form);
|
.form(®ister_form);
|
||||||
handle_json_res(req).await
|
handle_json_res::<LocalUserView>(req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn login(hostname: &str, username: &str, password: &str) -> MyResult<LoginResponse> {
|
pub async fn login(hostname: &str, login_form: LoginUserData) -> MyResult<LocalUserView> {
|
||||||
let login_form = LoginUserData {
|
|
||||||
username: username.to_string(),
|
|
||||||
password: password.to_string(),
|
|
||||||
};
|
|
||||||
let req = CLIENT
|
let req = CLIENT
|
||||||
.post(format!("http://{}/api/v1/user/login", hostname))
|
.post(format!("http://{}/api/v1/account/login", hostname))
|
||||||
.form(&login_form);
|
.form(&login_form);
|
||||||
handle_json_res(req).await
|
handle_json_res::<LocalUserView>(req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn my_profile(hostname: &str) -> MyResult<LocalUserView> {
|
||||||
|
let req = CLIENT.get(format!("http://{}/api/v1/account/my_profile", hostname));
|
||||||
|
handle_json_res::<LocalUserView>(req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn logout(hostname: &str) -> MyResult<()> {
|
||||||
|
CLIENT
|
||||||
|
.get(format!("http://{}/api/v1/account/logout", hostname))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,62 @@
|
||||||
|
use crate::common::LocalUserView;
|
||||||
|
use crate::frontend::api::my_profile;
|
||||||
use crate::frontend::components::nav::Nav;
|
use crate::frontend::components::nav::Nav;
|
||||||
use crate::frontend::pages::article::Article;
|
use crate::frontend::pages::article::Article;
|
||||||
use crate::frontend::pages::login::Login;
|
use crate::frontend::pages::login::Login;
|
||||||
use crate::frontend::pages::register::Register;
|
use crate::frontend::pages::register::Register;
|
||||||
use crate::frontend::pages::Page;
|
use crate::frontend::pages::Page;
|
||||||
use leptos::{component, provide_context, use_context, view, IntoView};
|
use leptos::{
|
||||||
|
component, create_local_resource, create_rw_signal, expect_context, provide_context,
|
||||||
|
use_context, view, IntoView, RwSignal, SignalGetUntracked, SignalUpdate,
|
||||||
|
};
|
||||||
use leptos_meta::provide_meta_context;
|
use leptos_meta::provide_meta_context;
|
||||||
use leptos_meta::*;
|
use leptos_meta::*;
|
||||||
use leptos_router::Route;
|
use leptos_router::Route;
|
||||||
use leptos_router::Router;
|
use leptos_router::Router;
|
||||||
use leptos_router::Routes;
|
use leptos_router::Routes;
|
||||||
|
|
||||||
// TODO: change to GlobalState and also store auth token here
|
// https://book.leptos.dev/15_global_state.html
|
||||||
// https://book.leptos.dev/15_global_state.html
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct BackendHostname(String);
|
pub struct GlobalState {
|
||||||
|
backend_hostname: String,
|
||||||
|
pub(crate) my_profile: Option<LocalUserView>,
|
||||||
|
}
|
||||||
|
|
||||||
impl BackendHostname {
|
impl GlobalState {
|
||||||
pub fn read() -> String {
|
pub fn read_hostname() -> String {
|
||||||
use_context::<BackendHostname>()
|
use_context::<RwSignal<GlobalState>>()
|
||||||
.expect("backend hostname is provided")
|
.expect("backend hostname is provided")
|
||||||
.0
|
.get_untracked()
|
||||||
|
.backend_hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_my_profile(&self) {
|
||||||
|
let backend_hostname_ = self.backend_hostname.clone();
|
||||||
|
create_local_resource(
|
||||||
|
move || backend_hostname_.clone(),
|
||||||
|
|backend_hostname| async move {
|
||||||
|
if let Ok(my_profile) = my_profile(&backend_hostname).await {
|
||||||
|
expect_context::<RwSignal<GlobalState>>()
|
||||||
|
.update(|state| state.my_profile = Some(my_profile.clone()))
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
|
let backend_hostname = "localhost:8080".to_string();
|
||||||
|
|
||||||
provide_meta_context();
|
provide_meta_context();
|
||||||
let backend_hostname = BackendHostname("localhost:8080".to_string());
|
let backend_hostname = GlobalState {
|
||||||
provide_context(backend_hostname);
|
backend_hostname,
|
||||||
|
my_profile: None,
|
||||||
|
};
|
||||||
|
// Load user profile in case we are already logged in
|
||||||
|
backend_hostname.update_my_profile();
|
||||||
|
provide_context(create_rw_signal(backend_hostname));
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<>
|
<>
|
||||||
<Stylesheet id="simple" href="/assets/simple.css"/>
|
<Stylesheet id="simple" href="/assets/simple.css"/>
|
||||||
|
@ -42,7 +71,6 @@ pub fn App() -> impl IntoView {
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,55 @@
|
||||||
use leptos::{component, view, IntoView};
|
use crate::frontend::api::logout;
|
||||||
|
use crate::frontend::app::GlobalState;
|
||||||
|
use leptos::*;
|
||||||
|
use leptos::{component, use_context, view, IntoView, RwSignal, SignalWith};
|
||||||
use leptos_router::*;
|
use leptos_router::*;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Nav() -> impl IntoView {
|
pub fn Nav() -> impl IntoView {
|
||||||
|
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
|
||||||
// TODO: use `<Show when` based on auth token for login/register/logout
|
// TODO: use `<Show when` based on auth token for login/register/logout
|
||||||
view! {
|
view! {
|
||||||
<nav class="inner">
|
<nav class="inner">
|
||||||
<li>
|
<li>
|
||||||
<A href="/">"Main Page"</A>
|
<A href="/">"Main Page"</A>
|
||||||
</li>
|
</li>
|
||||||
|
<Show
|
||||||
|
when=move || global_state.with(|state| state.my_profile.is_none())
|
||||||
|
fallback=move || {
|
||||||
|
view! {
|
||||||
|
<p>"Logged in as: "
|
||||||
|
{
|
||||||
|
move || global_state.with(|state| state.my_profile.clone().unwrap().person.username)
|
||||||
|
}
|
||||||
|
<button on:click=move |_| {
|
||||||
|
// TODO: not executed
|
||||||
|
dbg!(1);
|
||||||
|
do_logout()
|
||||||
|
}>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
<li>
|
<li>
|
||||||
<A href="/login">"Login"</A>
|
<A href="/login">"Login"</A>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<A href="/register">"Register"</A>
|
<A href="/register">"Register"</A>
|
||||||
</li>
|
</li>
|
||||||
|
</Show>
|
||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn do_logout() {
|
||||||
|
dbg!("do logout");
|
||||||
|
create_action(move |()| async move {
|
||||||
|
dbg!("run logout action");
|
||||||
|
logout(&GlobalState::read_hostname()).await.unwrap();
|
||||||
|
expect_context::<RwSignal<GlobalState>>()
|
||||||
|
.get()
|
||||||
|
.update_my_profile();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,51 +1,58 @@
|
||||||
|
use crate::common::LoginUserData;
|
||||||
use crate::frontend::api::login;
|
use crate::frontend::api::login;
|
||||||
use leptos::ev::SubmitEvent;
|
use crate::frontend::app::GlobalState;
|
||||||
|
use crate::frontend::components::credentials::*;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use log::info;
|
use leptos_router::Redirect;
|
||||||
|
|
||||||
// TODO: this seems to be working, but need to implement registration also
|
|
||||||
// TODO: use leptos_form if possible
|
|
||||||
// https://github.com/leptos-form/leptos_form/issues/18
|
|
||||||
fn do_login(ev: SubmitEvent, username: String, password: String) {
|
|
||||||
ev.prevent_default();
|
|
||||||
spawn_local(async move {
|
|
||||||
let res = login("localhost:8080", &username, &password).await;
|
|
||||||
info!("{}", res.unwrap().jwt);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Login() -> impl IntoView {
|
pub fn Login() -> impl IntoView {
|
||||||
let name = RwSignal::new(String::new());
|
let (login_response, set_login_response) = create_signal(None::<()>);
|
||||||
let password = RwSignal::new(String::new());
|
let (login_error, set_login_error) = create_signal(None::<String>);
|
||||||
|
let (wait_for_response, set_wait_for_response) = create_signal(false);
|
||||||
|
|
||||||
|
let login_action = create_action(move |(email, password): &(String, String)| {
|
||||||
|
let username = email.to_string();
|
||||||
|
let password = password.to_string();
|
||||||
|
let credentials = LoginUserData { username, password };
|
||||||
|
async move {
|
||||||
|
set_wait_for_response.update(|w| *w = true);
|
||||||
|
let result = login(&GlobalState::read_hostname(), credentials).await;
|
||||||
|
set_wait_for_response.update(|w| *w = false);
|
||||||
|
match result {
|
||||||
|
Ok(res) => {
|
||||||
|
expect_context::<RwSignal<GlobalState>>()
|
||||||
|
.update(|state| state.my_profile = Some(res));
|
||||||
|
set_login_response.update(|v| *v = Some(()));
|
||||||
|
set_login_error.update(|e| *e = None);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let msg = err.0.to_string();
|
||||||
|
log::warn!("Unable to login: {msg}");
|
||||||
|
set_login_error.update(|e| *e = Some(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let disabled = Signal::derive(move || wait_for_response.get());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<form on:submit=move |ev| do_login(ev, name.get(), password.get())>
|
<Show
|
||||||
<div>
|
when=move || login_response.get().is_some()
|
||||||
<label for="username">Username: </label>
|
fallback=move || {
|
||||||
<input
|
view! {
|
||||||
id="username"
|
<CredentialsForm
|
||||||
type="text"
|
title="Please enter the desired credentials"
|
||||||
on:input=move |ev| name.set(event_target_value(&ev))
|
action_label="Login"
|
||||||
label="Username"
|
action=login_action
|
||||||
/>
|
error=login_error.into()
|
||||||
</div>
|
disabled
|
||||||
|
/>
|
||||||
<div>
|
}
|
||||||
<label for="password">Password: </label>
|
}
|
||||||
<input
|
>
|
||||||
id="password"
|
<Redirect path="/"/>
|
||||||
type="password"
|
</Show>
|
||||||
on:input=move |ev| password.set(event_target_value(&ev))
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button type="submit">
|
|
||||||
"Login"
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
use crate::common::{LoginResponse, RegisterUserData};
|
use crate::common::RegisterUserData;
|
||||||
use crate::frontend::api::register;
|
use crate::frontend::api::register;
|
||||||
use crate::frontend::app::BackendHostname;
|
use crate::frontend::app::GlobalState;
|
||||||
use crate::frontend::components::credentials::*;
|
use crate::frontend::components::credentials::*;
|
||||||
use crate::frontend::pages::Page;
|
|
||||||
use leptos::{logging::log, *};
|
use leptos::{logging::log, *};
|
||||||
use leptos_router::*;
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Register() -> impl IntoView {
|
pub fn Register() -> impl IntoView {
|
||||||
let (register_response, set_register_response) = create_signal(None::<LoginResponse>);
|
let (register_response, set_register_response) = create_signal(None::<()>);
|
||||||
let (register_error, set_register_error) = create_signal(None::<String>);
|
let (register_error, set_register_error) = create_signal(None::<String>);
|
||||||
let (wait_for_response, set_wait_for_response) = create_signal(false);
|
let (wait_for_response, set_wait_for_response) = create_signal(false);
|
||||||
|
|
||||||
|
@ -19,11 +17,13 @@ pub fn Register() -> impl IntoView {
|
||||||
log!("Try to register new account for {}", credentials.username);
|
log!("Try to register new account for {}", credentials.username);
|
||||||
async move {
|
async move {
|
||||||
set_wait_for_response.update(|w| *w = true);
|
set_wait_for_response.update(|w| *w = true);
|
||||||
let result = register(&BackendHostname::read(), credentials).await;
|
let result = register(&GlobalState::read_hostname(), credentials).await;
|
||||||
set_wait_for_response.update(|w| *w = false);
|
set_wait_for_response.update(|w| *w = false);
|
||||||
match result {
|
match result {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
set_register_response.update(|v| *v = Some(res));
|
expect_context::<RwSignal<GlobalState>>()
|
||||||
|
.update(|state| state.my_profile = Some(res));
|
||||||
|
set_register_response.update(|v| *v = Some(()));
|
||||||
set_register_error.update(|e| *e = None);
|
set_register_error.update(|e| *e = None);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
@ -53,7 +53,6 @@ pub fn Register() -> impl IntoView {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p>"You have successfully registered."</p>
|
<p>"You have successfully registered."</p>
|
||||||
<p>"You can now " <A href=Page::Login.path()>"login"</A> " with your new account."</p>
|
|
||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,5 +23,4 @@ fn main() {
|
||||||
mount_to_body(|| {
|
mount_to_body(|| {
|
||||||
view! { <App/> }
|
view! { <App/> }
|
||||||
});
|
});
|
||||||
log::info!("test 2");
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use ibis::backend::api::article::{CreateArticleData, EditArticleData, ForkArticleData};
|
use ibis::backend::api::article::{CreateArticleData, EditArticleData, ForkArticleData};
|
||||||
use ibis::backend::api::instance::FollowInstance;
|
use ibis::backend::api::instance::FollowInstance;
|
||||||
use ibis::backend::api::user::RegisterUserData;
|
use ibis::backend::api::user::AUTH_COOKIE;
|
||||||
use ibis::backend::api::user::{LoginResponse, LoginUserData};
|
|
||||||
use ibis::backend::api::ResolveObject;
|
use ibis::backend::api::ResolveObject;
|
||||||
use ibis::backend::database::conflict::ApiConflict;
|
use ibis::backend::database::conflict::ApiConflict;
|
||||||
use ibis::backend::database::instance::DbInstance;
|
use ibis::backend::database::instance::DbInstance;
|
||||||
use ibis::backend::error::MyResult;
|
use ibis::backend::error::MyResult;
|
||||||
use ibis::backend::start;
|
use ibis::backend::start;
|
||||||
use ibis::common::ArticleView;
|
use ibis::common::ArticleView;
|
||||||
use ibis::frontend::api;
|
use ibis::common::LoginUserData;
|
||||||
use ibis::frontend::api::get_query;
|
use ibis::common::RegisterUserData;
|
||||||
use ibis_lib::frontend::api;
|
use ibis::frontend::api::{get_article, get_query, handle_json_res, register};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use reqwest::{Client, StatusCode};
|
use reqwest::{Client, ClientBuilder, StatusCode};
|
||||||
use serde::de::Deserialize;
|
use serde::de::Deserialize;
|
||||||
use std::env::current_dir;
|
use std::env::current_dir;
|
||||||
use std::fs::create_dir_all;
|
use std::fs::create_dir_all;
|
||||||
|
@ -97,6 +96,7 @@ fn generate_db_path(name: &'static str, port: i32) -> String {
|
||||||
|
|
||||||
pub struct IbisInstance {
|
pub struct IbisInstance {
|
||||||
pub hostname: String,
|
pub hostname: String,
|
||||||
|
pub client: Client,
|
||||||
pub jwt: String,
|
pub jwt: String,
|
||||||
db_path: String,
|
db_path: String,
|
||||||
db_handle: JoinHandle<()>,
|
db_handle: JoinHandle<()>,
|
||||||
|
@ -123,11 +123,18 @@ impl IbisInstance {
|
||||||
});
|
});
|
||||||
// wait a moment for the backend to start
|
// wait a moment for the backend to start
|
||||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
let register_res = api::register(&hostname, username, "hunter2").await.unwrap();
|
let form = RegisterUserData {
|
||||||
assert!(!register_res.jwt.is_empty());
|
username: username.to_string(),
|
||||||
|
password: "hunter2".to_string(),
|
||||||
|
};
|
||||||
|
// TODO: use a separate http client for each backend instance, with cookie store for auth
|
||||||
|
// TODO: how to pass the client/hostname to api client methods?
|
||||||
|
// probably create a struct ApiClient(hostname, client) with all api methods in impl
|
||||||
|
let client = ClientBuilder::new().cookie_store(true).build();
|
||||||
|
let register_res = register(&hostname, form).await.unwrap();
|
||||||
Self {
|
Self {
|
||||||
jwt: register_res.jwt,
|
|
||||||
hostname,
|
hostname,
|
||||||
|
client,
|
||||||
db_path,
|
db_path,
|
||||||
db_handle: handle,
|
db_handle: handle,
|
||||||
}
|
}
|
||||||
|
@ -156,7 +163,7 @@ pub async fn create_article(instance: &IbisInstance, title: String) -> MyResult<
|
||||||
.post(format!("http://{}/api/v1/article", &instance.hostname))
|
.post(format!("http://{}/api/v1/article", &instance.hostname))
|
||||||
.form(&create_form)
|
.form(&create_form)
|
||||||
.bearer_auth(&instance.jwt);
|
.bearer_auth(&instance.jwt);
|
||||||
let article: ArticleView = api::handle_json_res(req).await?;
|
let article: ArticleView = handle_json_res(req).await?;
|
||||||
|
|
||||||
// create initial edit to ensure that conflicts are generated (there are no conflicts on empty file)
|
// create initial edit to ensure that conflicts are generated (there are no conflicts on empty file)
|
||||||
let edit_form = EditArticleData {
|
let edit_form = EditArticleData {
|
||||||
|
@ -176,7 +183,7 @@ pub async fn edit_article_with_conflict(
|
||||||
.patch(format!("http://{}/api/v1/article", instance.hostname))
|
.patch(format!("http://{}/api/v1/article", instance.hostname))
|
||||||
.form(edit_form)
|
.form(edit_form)
|
||||||
.bearer_auth(&instance.jwt);
|
.bearer_auth(&instance.jwt);
|
||||||
api::handle_json_res(req).await
|
handle_json_res(req).await?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_conflicts(instance: &IbisInstance) -> MyResult<Vec<ApiConflict>> {
|
pub async fn get_conflicts(instance: &IbisInstance) -> MyResult<Vec<ApiConflict>> {
|
||||||
|
@ -186,7 +193,7 @@ pub async fn get_conflicts(instance: &IbisInstance) -> MyResult<Vec<ApiConflict>
|
||||||
&instance.hostname
|
&instance.hostname
|
||||||
))
|
))
|
||||||
.bearer_auth(&instance.jwt);
|
.bearer_auth(&instance.jwt);
|
||||||
api::handle_json_res(req).await
|
handle_json_res(req).await?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn edit_article(
|
pub async fn edit_article(
|
||||||
|
@ -195,7 +202,8 @@ pub async fn edit_article(
|
||||||
) -> MyResult<ArticleView> {
|
) -> MyResult<ArticleView> {
|
||||||
let edit_res = edit_article_with_conflict(instance, edit_form).await?;
|
let edit_res = edit_article_with_conflict(instance, edit_form).await?;
|
||||||
assert!(edit_res.is_none());
|
assert!(edit_res.is_none());
|
||||||
api::get_article(&instance.hostname, edit_form.article_id).await
|
|
||||||
|
get_article(&instance.hostname, todo!("{}", edit_form.article_id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get<T>(hostname: &str, endpoint: &str) -> MyResult<T>
|
pub async fn get<T>(hostname: &str, endpoint: &str) -> MyResult<T>
|
||||||
|
@ -213,7 +221,7 @@ pub async fn fork_article(
|
||||||
.post(format!("http://{}/api/v1/article/fork", instance.hostname))
|
.post(format!("http://{}/api/v1/article/fork", instance.hostname))
|
||||||
.form(form)
|
.form(form)
|
||||||
.bearer_auth(&instance.jwt);
|
.bearer_auth(&instance.jwt);
|
||||||
api::handle_json_res(req).await
|
handle_json_res(req).await?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn follow_instance(instance: &IbisInstance, follow_instance: &str) -> MyResult<()> {
|
pub async fn follow_instance(instance: &IbisInstance, follow_instance: &str) -> MyResult<()> {
|
||||||
|
@ -222,7 +230,7 @@ pub async fn follow_instance(instance: &IbisInstance, follow_instance: &str) ->
|
||||||
id: Url::parse(&format!("http://{}", follow_instance))?,
|
id: Url::parse(&format!("http://{}", follow_instance))?,
|
||||||
};
|
};
|
||||||
let instance_resolved: DbInstance =
|
let instance_resolved: DbInstance =
|
||||||
api::get_query(&instance.hostname, "resolve_instance", Some(resolve_form)).await?;
|
get_query(&instance.hostname, "resolve_instance", Some(resolve_form)).await?;
|
||||||
|
|
||||||
// send follow
|
// send follow
|
||||||
let follow_form = FollowInstance {
|
let follow_form = FollowInstance {
|
||||||
|
|
|
@ -2,12 +2,12 @@ extern crate ibis;
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
|
use crate::common::fork_article;
|
||||||
|
use crate::common::get_conflicts;
|
||||||
use crate::common::{
|
use crate::common::{
|
||||||
create_article, edit_article, edit_article_with_conflict, follow_instance, get, TestData,
|
create_article, edit_article, edit_article_with_conflict, follow_instance, get, TestData,
|
||||||
CLIENT, TEST_ARTICLE_DEFAULT_TEXT,
|
CLIENT, TEST_ARTICLE_DEFAULT_TEXT,
|
||||||
};
|
};
|
||||||
use crate::common::{fork_article, login};
|
|
||||||
use crate::common::{get_conflicts, register};
|
|
||||||
use ibis::backend::api::article::{CreateArticleData, EditArticleData, ForkArticleData};
|
use ibis::backend::api::article::{CreateArticleData, EditArticleData, ForkArticleData};
|
||||||
use ibis::backend::api::{ResolveObject, SearchArticleData};
|
use ibis::backend::api::{ResolveObject, SearchArticleData};
|
||||||
use ibis::backend::database::instance::{DbInstance, InstanceView};
|
use ibis::backend::database::instance::{DbInstance, InstanceView};
|
||||||
|
@ -17,6 +17,7 @@ use ibis::common::DbArticle;
|
||||||
use ibis::frontend::api::get_article;
|
use ibis::frontend::api::get_article;
|
||||||
use ibis::frontend::api::get_query;
|
use ibis::frontend::api::get_query;
|
||||||
use ibis::frontend::api::handle_json_res;
|
use ibis::frontend::api::handle_json_res;
|
||||||
|
use ibis::frontend::api::login;
|
||||||
use pretty_assertions::{assert_eq, assert_ne};
|
use pretty_assertions::{assert_eq, assert_ne};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue