diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml
index 7d87ccf..4aa2e18 100644
--- a/.github/workflows/check.yaml
+++ b/.github/workflows/check.yaml
@@ -40,6 +40,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
+ args: --workspace --all-targets
lints:
name: Lints
diff --git a/Cargo.lock b/Cargo.lock
index 93e9174..857869b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -628,8 +628,10 @@ dependencies = [
"async-trait",
"axum",
"http",
+ "photos_network_plugin",
"serde",
"serde_json",
+ "serde_with",
"time 0.3.27",
"tracing",
"uuid",
@@ -881,7 +883,9 @@ name = "database"
version = "0.6.0"
dependencies = [
"async-trait",
+ "common",
"sqlx",
+ "testdir",
"tokio",
"tracing",
"uuid",
@@ -2825,9 +2829,9 @@ dependencies = [
[[package]]
name = "serde_with"
-version = "3.2.0"
+version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1402f54f9a3b9e2efe71c1cea24e648acce55887983553eeb858cf3115acfd49"
+checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237"
dependencies = [
"base64 0.21.2",
"chrono",
@@ -2842,9 +2846,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
-version = "3.2.0"
+version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9197f1ad0e3c173a0222d3c4404fb04c3afe87e962bcb327af73e8301fa203c7"
+checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c"
dependencies = [
"darling 0.20.3",
"proc-macro2",
diff --git a/Cargo.toml b/Cargo.toml
index 7aa5944..2926f05 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -79,6 +79,7 @@ reqwest = { version = "0.11", default-features = false, features = ["blocking",
serde = "1.0.183"
serde_json = { version = "1.0.104", features = ["raw_value"] }
+serde_with = "3.3.0"
serde_urlencoded = "0.7.1"
smallvec = "1.8.0"
sqlx = "0.7.1"
diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml
index 8041b77..e6d4ef9 100644
--- a/crates/common/Cargo.toml
+++ b/crates/common/Cargo.toml
@@ -19,8 +19,11 @@ doctest = false
async-trait.workspace = true
axum.workspace = true
http.workspace = true
+photos_network_plugin = { path = "../plugin_interface" }
+
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
+serde_with.workspace = true
time.workspace = true
tracing.workspace = true
uuid = { workspace = true, features = ["serde"] }
diff --git a/crates/common/src/model/auth/login.rs b/crates/common/src/auth/login.rs
similarity index 70%
rename from crates/common/src/model/auth/login.rs
rename to crates/common/src/auth/login.rs
index 3e85e20..979da2f 100644
--- a/crates/common/src/model/auth/login.rs
+++ b/crates/common/src/auth/login.rs
@@ -1,43 +1,44 @@
/* Photos.network · A privacy first photo storage and sharing service for fediverse.
* Copyright (C) 2020 Photos network developers
- *
+ *
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
- *
+ *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
- *
+ *
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
- use crate::sensitive::Sensitive;
-
//! Login request
//!
//! Provides an abstraction over a vlue for sensitive data like passwords.
//! It is not printing its value to logs or tracing
-//!
+//!
+use serde::{Deserialize, Serialize};
+
+use crate::model::sensitive::Sensitive;
+
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct Login {
- pub username_or_email: Sensitive,
- pub password: Sensitive,
- pub totp_2fa_token: Option,
+ pub username_or_email: Sensitive,
+ pub password: Sensitive,
+ pub totp_2fa_token: Option,
}
-//! Login response
-//!
-//! * `jwt` - None if email verification is enabled.
-//! * `verify_email_sent` - Indicates if an email verification is needed.
-//!
-#[skip_serializing_none]
+// Login response
+//
+// * `jwt` - None if email verification is enabled.
+// * `verify_email_sent` - Indicates if an email verification is needed.
+//
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct LoginResponse {
- pub jwt: Option>,
- pub registration_created: bool,
- pub verify_email_sent: bool,
+ pub jwt: Option>,
+ pub registration_created: bool,
+ pub verify_email_sent: bool,
}
diff --git a/crates/common/src/auth/mod.rs b/crates/common/src/auth/mod.rs
new file mode 100644
index 0000000..063e88f
--- /dev/null
+++ b/crates/common/src/auth/mod.rs
@@ -0,0 +1,2 @@
+pub mod login;
+pub mod user;
diff --git a/crates/common/src/model/auth/user.rs b/crates/common/src/auth/user.rs
similarity index 80%
rename from crates/common/src/model/auth/user.rs
rename to crates/common/src/auth/user.rs
index b397749..82215ec 100644
--- a/crates/common/src/model/auth/user.rs
+++ b/crates/common/src/auth/user.rs
@@ -21,15 +21,15 @@ use uuid::Uuid;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct User {
- pub uuid: Uuid,
- email: String,
- password: Option,
- lastname: Option,
- firstname: Option,
- is_locked: bool,
- created_at: OffsetDateTime,
- updated_at: Option,
- last_login: Option,
+ pub uuid: String, //Uuid,
+ pub email: String,
+ pub password: Option,
+ pub lastname: Option,
+ pub firstname: Option,
+ pub is_locked: bool,
+ pub created_at: OffsetDateTime,
+ pub updated_at: Option,
+ pub last_login: Option,
}
impl fmt::Display for User {
@@ -45,7 +45,7 @@ impl fmt::Display for User {
impl User {
pub(crate) fn new(email: String) -> User {
User {
- uuid: Uuid::new_v4(),
+ uuid: Uuid::new_v4().hyphenated().to_string(),
email,
password: Option::None,
lastname: Option::None,
diff --git a/crates/common/src/database/mod.rs b/crates/common/src/database/mod.rs
new file mode 100644
index 0000000..f09198b
--- /dev/null
+++ b/crates/common/src/database/mod.rs
@@ -0,0 +1,12 @@
+use async_trait::async_trait;
+use std::error::Error;
+
+use crate::auth::user::User;
+
+#[async_trait]
+pub trait Database {
+ async fn setup(&mut self) -> Result<(), Box>;
+ async fn create_user(&self, user: &User) -> Result<(), Box>;
+ async fn update_email(&self, email: &str, user_id: &str) -> Result<(), Box>;
+ async fn get_users(&self) -> Result, Box>;
+}
diff --git a/crates/common/src/http/extractors/optuser.rs b/crates/common/src/http/extractors/optuser.rs
index 8aaf8ba..64a75c1 100644
--- a/crates/common/src/http/extractors/optuser.rs
+++ b/crates/common/src/http/extractors/optuser.rs
@@ -18,7 +18,7 @@
//! This extractor checks if an `Authorization` is header and contains a valid JWT token.
//! Otherwise it will respond `Some(None)` to indicate an unauthorized user or a visiter without an account at all.
//!
-use crate::model::auth::user::User;
+use crate::auth::user::User;
use async_trait::async_trait;
use axum::extract::FromRequestParts;
use http::request::Parts;
diff --git a/crates/common/src/http/extractors/user.rs b/crates/common/src/http/extractors/user.rs
index 762253e..2803b6f 100644
--- a/crates/common/src/http/extractors/user.rs
+++ b/crates/common/src/http/extractors/user.rs
@@ -23,7 +23,7 @@ use axum::extract::FromRequestParts;
use axum::http::StatusCode;
use http::request::Parts;
-use crate::model::auth::user::User;
+use crate::auth::user::User;
#[async_trait]
impl FromRequestParts for User
diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs
index df8467f..74b59d3 100644
--- a/crates/common/src/lib.rs
+++ b/crates/common/src/lib.rs
@@ -17,9 +17,41 @@
//! This crate offers shared data models for [Photos.network](https://photos.network) core application.
//!
+
+use std::collections::HashMap;
+
+use axum::Router;
+use config::configuration::Configuration;
+use database::Database;
+use photos_network_plugin::{PluginFactoryRef, PluginId};
+
+pub mod auth;
pub mod config;
+pub mod database;
pub mod http;
pub mod model {
- pub mod auth;
pub mod sensitive;
}
+
+/// Aggregates the applications configuration, its loaded plugins and the router for all REST APIs
+#[derive(Clone)]
+pub struct ApplicationState {
+ pub config: Configuration,
+ pub plugins: HashMap,
+ pub router: Option,
+ pub database: D,
+}
+
+impl ApplicationState
+where
+ D: Database,
+{
+ pub fn new(config: Configuration, database: D) -> Self {
+ Self {
+ config,
+ plugins: HashMap::new(),
+ router: None,
+ database,
+ }
+ }
+}
diff --git a/crates/common/src/model/auth/mod.rs b/crates/common/src/model/auth/mod.rs
deleted file mode 100644
index 22d12a3..0000000
--- a/crates/common/src/model/auth/mod.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub mod user;
diff --git a/crates/database/Cargo.toml b/crates/database/Cargo.toml
index 4b60827..80ae5d9 100644
--- a/crates/database/Cargo.toml
+++ b/crates/database/Cargo.toml
@@ -16,8 +16,12 @@ path = "src/lib.rs"
doctest = false
[dependencies]
+common.workspace = true
async-trait.workspace = true
tracing.workspace = true
uuid.workspace = true
tokio.workspace = true
sqlx = { workspace = true, features = ["runtime-tokio", "tls-native-tls", "postgres", "mysql", "sqlite", "any", "macros", "migrate", "time" ] }
+
+[dev-dependencies]
+testdir.workspace = true
diff --git a/crates/database/migrations/0001_initial.sql b/crates/database/migrations/0001_initial.sql
index 23b93c3..21177f5 100644
--- a/crates/database/migrations/0001_initial.sql
+++ b/crates/database/migrations/0001_initial.sql
@@ -1,7 +1,7 @@
CREATE TABLE IF NOT EXISTS users (
--auto generated
uuid VARCHAR NOT NULL,
- email VARCHAR,
+ email VARCHAR UNIQUE,
password VARCHAR,
lastname VARCHAR,
firstname VARCHAR,
diff --git a/crates/database/src/lib.rs b/crates/database/src/lib.rs
index f689ffe..fc3f629 100644
--- a/crates/database/src/lib.rs
+++ b/crates/database/src/lib.rs
@@ -1,112 +1,3 @@
-/* Photos.network · A privacy first photo storage and sharing service for fediverse.
- * Copyright (C) 2020 Photos network developers
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-
-//! This crate offers a database abstraction for [Photos.network](https://photos.network) core application.
-//!
-use async_trait::async_trait;
-use sqlx::PgPool;
-use sqlx::Row;
-use std::error::Error;
-use tracing::{error, info};
-use uuid::Uuid;
-
-pub struct User {
- pub uuid: String,
- pub email: String,
- pub password: String,
- pub lastname: String,
- pub firstname: String,
-}
-
-#[async_trait]
-pub trait Database {
- async fn setup(&mut self) -> Result<(), Box>;
- async fn create_user(&self, user: &User) -> Result<(), Box>;
- async fn update_user(&self, user: &User) -> Result<(), Box>;
- async fn get_users(&self) -> Result, Box>;
-}
-
-pub struct PostgresDatabase {
- pub pool: PgPool,
-}
-
-impl PostgresDatabase {
- pub async fn new(db_url: &str) -> Self {
- let pool = sqlx::postgres::PgPool::connect(db_url).await.unwrap();
-
- PostgresDatabase { pool }
- }
-}
-
-#[async_trait]
-impl Database for PostgresDatabase {
- async fn setup(&mut self) -> Result<(), Box> {
- // run migrations from `migrations` directory
- sqlx::migrate!("./migrations").run(&self.pool).await?;
-
- Ok(())
- }
-
- async fn create_user(&self, user: &User) -> Result<(), Box> {
- let query = "INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)";
- let id = Uuid::new_v4().hyphenated().to_string();
- info!("create new user with id `{}`.", id);
- sqlx::query(query)
- .bind(id)
- .bind(&user.email)
- .bind(&user.password)
- .bind(&user.lastname)
- .bind(&user.firstname)
- .execute(&self.pool)
- .await?;
-
- Ok(())
- }
-
- async fn update_user(&self, user: &User) -> Result<(), Box> {
- let query = "UPDATE users SET email = %1 WHERE uuid = $2";
-
- sqlx::query(query)
- .bind(&user.email)
- .bind(&user.uuid)
- .execute(&self.pool)
- .await?;
-
- Ok(())
- }
-
- async fn get_users(&self) -> Result, Box> {
- let query = "SELECT uuid, email, password, lastname, firstname FROM users";
-
- let res = sqlx::query(query);
-
- let rows = res.fetch_all(&self.pool).await?;
-
- let users = rows
- .iter()
- .map(|row| User {
- uuid: row.get("uuid"),
- email: row.get("email"),
- password: row.get("password"),
- lastname: row.get("lastname"),
- firstname: row.get("firstname"),
- })
- .collect();
-
- Ok(users)
- }
-}
+//pub mod postgres;
+pub mod postgres;
+pub mod sqlite;
diff --git a/crates/database/src/postgres.rs b/crates/database/src/postgres.rs
new file mode 100644
index 0000000..43f0055
--- /dev/null
+++ b/crates/database/src/postgres.rs
@@ -0,0 +1,104 @@
+/* Photos.network · A privacy first photo storage and sharing service for fediverse.
+ * Copyright (C) 2020 Photos network developers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+//! This crate offers a database abstraction for [Photos.network](https://photos.network) core application.
+//!
+use async_trait::async_trait;
+use common::auth::user::User;
+use common::database::Database;
+use sqlx::types::time::OffsetDateTime;
+use sqlx::PgPool;
+use sqlx::Row;
+use std::error::Error;
+use tracing::info;
+use uuid::Uuid;
+
+#[derive(Clone)]
+pub struct PostgresDatabase {
+ pub pool: PgPool,
+}
+
+impl PostgresDatabase {
+ pub async fn new(db_url: &str) -> Self {
+ let pool = sqlx::postgres::PgPool::connect(db_url).await.unwrap();
+
+ PostgresDatabase { pool }
+ }
+}
+
+#[async_trait]
+impl Database for PostgresDatabase {
+ async fn setup(&mut self) -> Result<(), Box> {
+ // run migrations from `migrations` directory
+ sqlx::migrate!("./migrations").run(&self.pool).await?;
+
+ Ok(())
+ }
+
+ async fn create_user(&self, user: &User) -> Result<(), Box> {
+ let query = "INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)";
+ let id = Uuid::new_v4().hyphenated().to_string();
+ info!("create new user with id `{}`.", id);
+ sqlx::query(query)
+ .bind(id)
+ .bind(&user.email)
+ .bind(&user.password)
+ .bind(&user.lastname)
+ .bind(&user.firstname)
+ .execute(&self.pool)
+ .await?;
+
+ Ok(())
+ }
+
+ async fn update_email(&self, email: &str, user_id: &str) -> Result<(), Box> {
+ let query = "UPDATE users SET email = $1 WHERE uuid = $2";
+
+ sqlx::query(query)
+ .bind(email)
+ .bind(user_id)
+ .execute(&self.pool)
+ .await?;
+
+ Ok(())
+ }
+
+ async fn get_users(&self) -> Result, Box> {
+ let query = "SELECT uuid, email, password, lastname, firstname FROM users";
+
+ let res = sqlx::query(query);
+
+ let rows = res.fetch_all(&self.pool).await?;
+
+ let users = rows
+ .iter()
+ .map(|row| User {
+ uuid: row.get("uuid"),
+ email: row.get("email"),
+ password: row.get("password"),
+ lastname: row.get("lastname"),
+ firstname: row.get("firstname"),
+ is_locked: false,
+ created_at: OffsetDateTime::now_utc(),
+ updated_at: None,
+ last_login: None,
+ })
+ .collect();
+
+ Ok(users)
+ }
+}
diff --git a/crates/database/src/sqlite.rs b/crates/database/src/sqlite.rs
new file mode 100644
index 0000000..fbce878
--- /dev/null
+++ b/crates/database/src/sqlite.rs
@@ -0,0 +1,266 @@
+use async_trait::async_trait;
+use common::auth::user::User;
+use common::database::Database;
+use sqlx::types::time::OffsetDateTime;
+use sqlx::Row;
+use sqlx::SqlitePool;
+use std::error::Error;
+use tracing::info;
+use uuid::Uuid;
+
+#[derive(Clone)]
+pub struct SqliteDatabase {
+ pub pool: SqlitePool,
+}
+
+impl SqliteDatabase {
+ pub async fn new(db_url: &str) -> Self {
+ let pool = SqlitePool::connect(db_url).await.unwrap();
+
+ SqliteDatabase { pool }
+ }
+}
+
+#[async_trait]
+impl Database for SqliteDatabase {
+ async fn setup(&mut self) -> Result<(), Box> {
+ sqlx::migrate!("./migrations").run(&self.pool).await?;
+
+ Ok(())
+ }
+
+ async fn create_user(&self, user: &User) -> Result<(), Box> {
+ let query = "INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)";
+ let id = Uuid::new_v4().hyphenated().to_string();
+ info!("create new user with id `{}`.", id);
+ sqlx::query(query)
+ .bind(id)
+ .bind(&user.email)
+ .bind(&user.password)
+ .bind(&user.lastname)
+ .bind(&user.firstname)
+ .execute(&self.pool)
+ .await?;
+
+ Ok(())
+ }
+
+ async fn update_email(&self, email: &str, user_id: &str) -> Result<(), Box> {
+ let query = "UPDATE users SET email = $1 WHERE uuid = $2";
+
+ sqlx::query(query)
+ .bind(email)
+ .bind(user_id)
+ .execute(&self.pool)
+ .await?;
+
+ Ok(())
+ }
+
+ async fn get_users(&self) -> Result, Box> {
+ let query = "SELECT uuid, email, password, lastname, firstname FROM users";
+
+ let res = sqlx::query(query);
+
+ let rows = res.fetch_all(&self.pool).await?;
+
+ let users = rows
+ .iter()
+ .map(|row| User {
+ uuid: row.get("uuid"),
+ email: row.get("email"),
+ password: row.get("password"),
+ lastname: row.get("lastname"),
+ firstname: row.get("firstname"),
+ is_locked: false,
+ created_at: OffsetDateTime::now_utc(),
+ updated_at: None,
+ last_login: None,
+ })
+ .collect();
+
+ Ok(users)
+ }
+}
+
+#[allow(unused_imports)]
+mod tests {
+ use super::*;
+
+ #[sqlx::test]
+ async fn create_user_should_succeed(pool: SqlitePool) -> sqlx::Result<()> {
+ // given
+ let db = SqliteDatabase::new(
+ "target/sqlx/test-dbs/database/sqlite/tests/create_user_should_succeed.sqlite",
+ )
+ .await;
+
+ // when
+ for i in 0..3 {
+ let user = User {
+ uuid: uuid::Uuid::new_v4().hyphenated().to_string(),
+ email: format!("test_{}@photos.network", i),
+ password: Some("unsecure".into()),
+ lastname: Some("Stuermer".into()),
+ firstname: Some("Benjamin".into()),
+ is_locked: false,
+ created_at: OffsetDateTime::now_utc(),
+ updated_at: None,
+ last_login: None,
+ };
+
+ // when
+ let _ = db.create_user(&user).await;
+ }
+
+ // then
+ let count = sqlx::query("SELECT COUNT(*) AS 'count!' FROM users")
+ .fetch_one(&pool)
+ .await?;
+ assert_eq!(count.get::("count!"), 3);
+
+ Ok(())
+ }
+
+ #[sqlx::test]
+ async fn create_already_existing_user_should_fail(pool: SqlitePool) -> sqlx::Result<()> {
+ // given
+ let db = SqliteDatabase::new(
+ "target/sqlx/test-dbs/database/sqlite/tests/create_already_existing_user_should_fail.sqlite",
+ )
+ .await;
+
+ // when
+ let uuid = uuid::Uuid::new_v4().hyphenated().to_string();
+ let user = User {
+ uuid,
+ email: "info@photos.network".into(),
+ password: Some("unsecure".into()),
+ lastname: Some("Stuermer".into()),
+ firstname: Some("Benjamin".into()),
+ is_locked: false,
+ created_at: OffsetDateTime::now_utc(),
+ updated_at: None,
+ last_login: None,
+ };
+
+ // then
+ let result1 = db.create_user(&user.clone()).await;
+ assert!(result1.is_ok());
+
+ let result2 = db.create_user(&user.clone()).await;
+ assert!(result2.is_err());
+
+ let count = sqlx::query("SELECT COUNT(*) AS 'count!' FROM users")
+ .fetch_one(&pool)
+ .await?;
+ assert_eq!(count.get::("count!"), 1);
+
+ Ok(())
+ }
+
+ #[sqlx::test]
+ async fn update_email_should_succeed(pool: SqlitePool) -> sqlx::Result<()> {
+ // given
+ sqlx::query("INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)")
+ .bind("570DC079-664A-4496-BAA3-668C445A447")
+ .bind("info@photos.network")
+ .bind("unsecure")
+ .bind("Stuermer")
+ .bind("Benjamin")
+ .execute(&pool).await?;
+ let db = SqliteDatabase::new(
+ "target/sqlx/test-dbs/database/sqlite/tests/update_email_should_succeed.sqlite",
+ )
+ .await;
+
+ // when
+ let result = db
+ .update_email(
+ "security@photos.network".into(),
+ "570DC079-664A-4496-BAA3-668C445A447".into(),
+ )
+ .await;
+
+ // then
+ assert!(result.is_ok());
+ let count = sqlx::query("SELECT email FROM users LIMIT 1")
+ .fetch_one(&pool)
+ .await?;
+ assert_eq!(count.get::("email"), "security@photos.network");
+
+ Ok(())
+ }
+
+ #[sqlx::test]
+ async fn update_email_to_existing_should_fail(pool: SqlitePool) -> sqlx::Result<()> {
+ // given
+ sqlx::query("INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)")
+ .bind("570DC079-664A-4496-BAA3-668C445A447")
+ .bind("info@photos.network")
+ .bind("unsecure")
+ .bind("Stuermer")
+ .bind("Benjamin")
+ .execute(&pool).await?;
+
+ sqlx::query("INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)")
+ .bind("0D341AD3-D38F-455F-8411-E25186665FC5")
+ .bind("security@photos.network")
+ .bind("unsecure")
+ .bind("Stuermer")
+ .bind("Benjamin")
+ .execute(&pool).await?;
+
+ let db = SqliteDatabase::new(
+ "target/sqlx/test-dbs/database/sqlite/tests/update_email_to_existing_should_fail.sqlite",
+ )
+ .await;
+
+ // when
+ let result = db
+ .update_email(
+ "security@photos.network".into(),
+ "570DC079-664A-4496-BAA3-668C445A447".into(),
+ )
+ .await;
+
+ // then
+ assert!(result.is_err());
+
+ let rows = sqlx::query("SELECT email FROM users")
+ .fetch_all(&pool)
+ .await?;
+ assert_eq!(rows[0].get::("email"), "info@photos.network");
+ assert_eq!(rows[1].get::("email"), "security@photos.network");
+
+ Ok(())
+ }
+
+ #[sqlx::test]
+ async fn get_users_should_succeed(pool: SqlitePool) -> sqlx::Result<()> {
+ // given
+ sqlx::query("INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)")
+ .bind("570DC079-664A-4496-BAA3-668C445A447")
+ .bind("info@photos.network")
+ .bind("unsecure")
+ .bind("Stuermer")
+ .bind("Benjamin")
+ .execute(&pool).await?;
+ let db = SqliteDatabase::new(
+ "target/sqlx/test-dbs/database/sqlite/tests/get_users_should_succeed.sqlite",
+ )
+ .await;
+
+ // when
+ let users = db.get_users().await.unwrap();
+
+ // then
+ assert_eq!(users.clone().len(), 1);
+ assert_eq!(
+ users.get(0).unwrap().uuid,
+ "570DC079-664A-4496-BAA3-668C445A447"
+ );
+
+ Ok(())
+ }
+}
diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml
index 660ffe5..458fbf7 100644
--- a/crates/media/Cargo.toml
+++ b/crates/media/Cargo.toml
@@ -16,7 +16,7 @@ path = "src/lib.rs"
doctest = false
[dependencies]
-common = { path = "../common" }
+common.workspace = true
database = { path = "../database" }
tracing.workspace = true
diff --git a/crates/media/src/api/router.rs b/crates/media/src/api/router.rs
index cb10267..584ac8e 100644
--- a/crates/media/src/api/router.rs
+++ b/crates/media/src/api/router.rs
@@ -15,17 +15,6 @@
* along with this program. If not, see .
*/
-use std::sync::Arc;
-
-use axum::routing::{delete, get, patch, post};
-use axum::Router;
-use common::config::configuration::Configuration;
-use database::Database;
-use tracing::error;
-
-use crate::data::error;
-use crate::repository::{MediaRepository, MediaRepositoryState};
-
use super::routes::delete_media_id::delete_media_id;
use super::routes::get_albums::get_albums;
use super::routes::get_albums_id::get_albums_id;
@@ -38,15 +27,22 @@ use super::routes::patch_media_id::patch_media_id;
use super::routes::post_albums::post_albums;
use super::routes::post_media::post_media;
use super::routes::post_media_id::post_media_id;
+use crate::repository::{MediaRepository, MediaRepositoryState};
+use axum::routing::{delete, get, patch, post};
+use axum::Router;
+use common::ApplicationState;
+use database::sqlite::SqliteDatabase;
+use std::sync::Arc;
pub struct MediaApi {}
impl MediaApi {
- pub async fn routes(database: &dyn Database) -> Router
+ pub async fn routes(state: ApplicationState) -> Router
where
S: Send + Sync + Clone,
{
- let media_repository: MediaRepository = MediaRepository::new(database).await;
+ let media_repository: MediaRepository =
+ MediaRepository::new(state.database.clone()).await;
let repository_state: MediaRepositoryState = Arc::new(media_repository);
Router::new()
@@ -95,18 +91,28 @@ impl MediaApi {
#[cfg(test)]
mod tests {
+ use std::collections::HashMap;
+
use super::*;
use axum::{
body::Body,
http::{self, Request, StatusCode},
};
+ use common::config::configuration::Configuration;
use serde_json::json;
+ use sqlx::SqlitePool;
use tower::ServiceExt;
- #[tokio::test]
- async fn get_media_with_query_success() {
+ #[sqlx::test]
+ async fn get_media_with_query_success(pool: SqlitePool) {
// given
- let app = Router::new().nest("/", MediaApi::routes(Configuration::empty()).await);
+ let state: ApplicationState = ApplicationState {
+ config: Configuration::empty(),
+ plugins: HashMap::new(),
+ router: None,
+ database: SqliteDatabase { pool },
+ };
+ let app = Router::new().nest("/", MediaApi::routes(state).await);
// when
let response = app
@@ -130,10 +136,16 @@ mod tests {
assert_eq!(body, "list media items. limit=100000, offset=1");
}
- #[tokio::test]
- async fn get_media_without_query_success() {
+ #[sqlx::test]
+ async fn get_media_without_query_success(pool: SqlitePool) {
// given
- let app = Router::new().nest("/", MediaApi::routes(Configuration::empty()).await);
+ let state: ApplicationState = ApplicationState {
+ config: Configuration::empty(),
+ plugins: HashMap::new(),
+ router: None,
+ database: SqliteDatabase { pool },
+ };
+ let app = Router::new().nest("/", MediaApi::routes(state).await);
// when
let response = app
@@ -157,12 +169,18 @@ mod tests {
assert_eq!(body, "list media items. limit=1000, offset=0");
}
- // TODO: re-enable test
- // #[tokio::test]
+ // TODO: test is failing due to missing multi-part body
+ //#[sqlx::test]
#[allow(dead_code)]
- async fn post_media_success() {
+ async fn post_media_success(pool: SqlitePool) {
// given
- let app = Router::new().nest("/", MediaApi::routes(Configuration::empty()).await);
+ let state: ApplicationState = ApplicationState {
+ config: Configuration::empty(),
+ plugins: HashMap::new(),
+ router: None,
+ database: SqliteDatabase { pool },
+ };
+ let app = Router::new().nest("/", MediaApi::routes(state).await);
// when
let response = app
@@ -170,8 +188,15 @@ mod tests {
Request::builder()
.uri("/media")
.method("POST")
+ .header("Authorization", "FakeAuth")
.header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
- // add multipart file to body
+ .header(
+ "Content-Disposition",
+ "attachment; filename=\"DSC_1234.NEF\"",
+ )
+ //.body(Body::from(bytes))
+ //.body(Body::empty())
+ // TODO: add multipart file to body
.body(Body::from(
serde_json::to_vec(&json!([1, 2, 3, 4])).unwrap(),
))
diff --git a/crates/media/src/api/routes/get_media.rs b/crates/media/src/api/routes/get_media.rs
index 1145317..e18f42e 100644
--- a/crates/media/src/api/routes/get_media.rs
+++ b/crates/media/src/api/routes/get_media.rs
@@ -2,10 +2,11 @@
//!
use axum::extract::State;
use axum::{extract::Query, http::StatusCode, Json};
-use common::model::auth::user::User;
+use common::auth::user::User;
use serde::{Deserialize, Serialize};
use std::result::Result;
use tracing::error;
+use uuid::Uuid;
use crate::data::error::DataAccessError;
use crate::data::media_item::MediaItem;
@@ -23,7 +24,7 @@ pub(crate) async fn get_media(
Query(query): Query,
) -> Result, StatusCode> {
let items: Result, DataAccessError> =
- repo.get_media_items_for_user(user.uuid.into());
+ repo.get_media_items_for_user(Uuid::parse_str(user.uuid.as_str()).unwrap());
match items {
Ok(i) => {
error!("Found {} items for user.", i.len());
@@ -48,19 +49,30 @@ pub(crate) async fn get_media(
#[cfg(test)]
mod tests {
+ use std::collections::HashMap;
+
use axum::Router;
- use common::config::configuration::Configuration;
+ use common::{config::configuration::Configuration, ApplicationState};
+ use database::sqlite::SqliteDatabase;
use hyper::{Body, Request};
+ use sqlx::SqlitePool;
use tower::ServiceExt;
use crate::api::router::MediaApi;
use super::*;
- #[tokio::test]
- async fn get_media_unauthorized_should_not_fail() {
+ #[sqlx::test]
+ async fn get_media_unauthorized_should_not_fail(pool: SqlitePool) {
// given
- let app = Router::new().nest("/", MediaApi::routes(Configuration::empty()).await);
+ let state: ApplicationState = ApplicationState {
+ config: Configuration::empty(),
+ plugins: HashMap::new(),
+ router: None,
+ database: SqliteDatabase { pool },
+ };
+
+ let app = Router::new().nest("/", MediaApi::routes(state).await);
// when
let response = app
diff --git a/crates/media/src/api/routes/post_media.rs b/crates/media/src/api/routes/post_media.rs
index 9155265..e7788e4 100644
--- a/crates/media/src/api/routes/post_media.rs
+++ b/crates/media/src/api/routes/post_media.rs
@@ -1,10 +1,13 @@
//! Creates a new media item to aggregate related files for current user
//!
-use axum::http::StatusCode;
-use common::model::auth::user::User;
+use axum::{extract::Multipart, http::StatusCode};
+use common::auth::user::User;
use tracing::{debug, error};
-pub(crate) async fn post_media(user: User) -> std::result::Result {
+pub(crate) async fn post_media(
+ user: User,
+ mut _multipart: Multipart,
+) -> std::result::Result {
error!("POST /media user={}", user);
let id = uuid::Uuid::new_v4();
diff --git a/crates/media/src/api/routes/post_media_id.rs b/crates/media/src/api/routes/post_media_id.rs
index 82079e0..f9b34bc 100644
--- a/crates/media/src/api/routes/post_media_id.rs
+++ b/crates/media/src/api/routes/post_media_id.rs
@@ -3,7 +3,7 @@
use axum::extract::{Multipart, Path};
use axum::http::StatusCode;
-use common::model::auth::user::User;
+use common::auth::user::User;
use tracing::{debug, error};
pub(crate) async fn post_media_id(
diff --git a/crates/media/src/repository.rs b/crates/media/src/repository.rs
index 1cf6bd9..e831542 100644
--- a/crates/media/src/repository.rs
+++ b/crates/media/src/repository.rs
@@ -15,24 +15,18 @@
* along with this program. If not, see .
*/
+use axum::async_trait;
use std::sync::Arc;
use std::time::Instant;
-
-use axum::async_trait;
-use common::config::database_config::DatabaseConfig;
-use common::config::database_config::DatabaseDriver;
-use database::Database;
-
-use rand::{distributions::Alphanumeric, Rng};
-use tracing::error;
use tracing::info;
use uuid::Uuid;
use crate::data::error::DataAccessError;
use crate::data::media_item::MediaItem;
-pub struct MediaRepository {
- pub(crate) database: Database,
+#[allow(dead_code)]
+pub struct MediaRepository {
+ pub(crate) database: D,
}
pub type MediaRepositoryState = Arc;
@@ -45,20 +39,21 @@ pub trait MediaRepositoryTrait {
fn get_media_items_for_user(&self, user_id: Uuid) -> Result, DataAccessError>;
}
-impl MediaRepository<'a> {
- pub async fn new(database: Database) -> self {
- self { database }
+impl MediaRepository {
+ pub async fn new(database: D) -> Self {
+ Self { database }
}
}
#[async_trait]
-impl MediaRepositoryTrait for MediaRepository {
+impl MediaRepositoryTrait for MediaRepository {
fn get_media_items_for_user(&self, user_id: Uuid) -> Result, DataAccessError> {
info!("get items for user {}", user_id);
// TODO: read from database
+ // TODO: read from filesystem
Ok(vec![MediaItem {
- uuid: "".into(),
+ uuid: "",
name: "",
date_added: Instant::now(),
date_taken: None,
@@ -68,23 +63,35 @@ impl MediaRepositoryTrait for MediaRepository {
references: None,
}])
}
-
- // async fn get_media_item() {
- // TODO: read from filesystem
- // }
}
+#[allow(unused_imports)]
mod tests {
+ use database::sqlite::SqliteDatabase;
+ use sqlx::SqlitePool;
+
use super::*;
- #[tokio::test]
- async fn test_new() {
+ #[sqlx::test(migrations = "../database/migrations")]
+ async fn test_new(pool: SqlitePool) -> sqlx::Result<()> {
// given
- let repository = MediaRepository::new(
- common::config::database_config::DatabaseDriver::SQLite,
- "file::memory:?cache=shared".into(),
- )
- .await;
+ sqlx::query("INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)")
+ .bind("570DC079-664A-4496-BAA3-668C445A447")
+ .bind("info@photos.network")
+ .bind("unsecure")
+ .bind("Stuermer")
+ .bind("Benjamin")
+ .execute(&pool).await?;
+ sqlx::query("INSERT INTO media (uuid, name, owner) VALUES ($1, $2, $3)")
+ .bind("6A92460C-53FB-4B42-AC1B-E6760A34E169")
+ .bind("DSC_1234")
+ .bind("570DC079-664A-4496-BAA3-668C445A447")
+ .execute(&pool)
+ .await?;
+
+ let db = SqliteDatabase::new("target/sqlx/test-dbs/media/repository/tests/test_new.sqlite")
+ .await;
+ let repository = MediaRepository::new(db).await;
// when
let result = repository.get_media_items_for_user(Uuid::new_v4());
@@ -92,5 +99,7 @@ mod tests {
// then
assert_eq!(result.is_ok(), true);
assert_eq!(result.ok().unwrap().len(), 1);
+
+ Ok(())
}
}
diff --git a/run_tests b/run_tests
new file mode 100755
index 0000000..3b6f2e5
--- /dev/null
+++ b/run_tests
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+cargo test --workspace --all-targets
+cargo fmt --all -- --check
+cargo clippy -- -D warnings
diff --git a/src/lib.rs b/src/lib.rs
index 9ab8a3a..4cd718e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -27,8 +27,7 @@
//! See also the following crates
//! * [Authentication](../oauth_authentication/index.html)
-use std::collections::HashMap;
-use std::fs;
+use std::fs::{self, OpenOptions};
use std::net::SocketAddr;
use abi_stable::external_types::crossbeam_channel;
@@ -38,8 +37,10 @@ use anyhow::Result;
use axum::extract::DefaultBodyLimit;
use axum::routing::{get, head};
use axum::{Json, Router};
-use common::model::auth::user::User;
-use database::{Database, PostgresDatabase};
+use common::auth::user::User;
+use common::database::Database;
+use common::ApplicationState;
+use database::sqlite::SqliteDatabase;
use media::api::router::MediaApi;
use oauth_authentication::AuthenticationManager;
use oauth_authorization_server::client::Client;
@@ -47,14 +48,12 @@ use oauth_authorization_server::config::ConfigRealm;
use oauth_authorization_server::config::ServerConfig;
use oauth_authorization_server::state::ServerState;
use oauth_authorization_server::AuthorizationServerManager;
-use photos_network_plugin::{PluginFactoryRef, PluginId};
use serde::{Deserialize, Serialize};
+use sqlx::types::time::OffsetDateTime;
use std::path::Path;
-use tokio::join;
use tower_http::cors::CorsLayer;
use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer;
-use tracing::callsite::DefaultCallsite;
use tracing::{debug, error, info};
use tracing_subscriber::{fmt, layer::SubscriberExt};
@@ -99,8 +98,43 @@ pub async fn start_server() -> Result<()> {
let configuration = Configuration::new(CONFIG_PATH).expect("Could not parse configuration!");
debug!("Configuration: {}", configuration);
+ // init database
+ //let db = PostgresDatabase::new("postgres://postgres:unsecure@localhost:5432/postgres").await;
+
+ let _file = OpenOptions::new()
+ .write(true)
+ .create_new(true)
+ .open("data/core.sqlite3");
+ let db = SqliteDatabase::new("data/core.sqlite3").await;
+ let _ = db.clone().setup().await;
+ let users = db.clone().get_users().await;
+ if users.unwrap().is_empty() {
+ info!("No user found, create a default admin user. Please check `data/credentials.txt` for details.");
+ let default_user = "photo@photos.network";
+ let default_pass = "unsecure";
+ let path = Path::new(DATA_PATH).join("credentials.txt");
+ let _ = fs::write(path, format!("{}\n{}", default_user, default_pass));
+ // let mut output = File::create(path)?;
+ // let line = "hello";
+ // write!(output, "{}\n{}", default_user, default_pass);
+
+ let user = User {
+ uuid: "".to_string(),
+ email: default_user.to_string(),
+ password: Some(default_pass.to_string()),
+ lastname: Some("Admin".to_string()),
+ firstname: Some("".to_string()),
+ is_locked: false,
+ created_at: OffsetDateTime::now_utc(),
+ updated_at: None,
+ last_login: None,
+ };
+ let _ = db.clone().create_user(&user).await;
+ }
+
// init application state
- let mut app_state = ApplicationState::new(configuration.clone());
+ //let mut app_state = ApplicationState::::new(configuration.clone(), db);
+ let mut app_state = ApplicationState::::new(configuration.clone(), db);
let cfg = ServerConfig {
listen_addr: configuration.internal_url.to_owned(),
@@ -119,27 +153,6 @@ pub async fn start_server() -> Result<()> {
};
let server = ServerState::new(cfg)?;
- let db = PostgresDatabase::new("postgres://postgres:unsecure@localhost:5432/postgres").await;
- let users = db.get_users().await;
- if users.unwrap().len() < 1 {
- info!("No user found, create a default admin user. Please check `data/credentials.txt` for details.");
- let default_user = "photo@photos.network";
- let default_pass = "unsecure";
- let path = Path::new(DATA_PATH).join("credentials.txt");
- let mut output = File::create(path)?;
- let line = "hello";
- write!(output, "{}\n{}", default_user, default_pass);
-
- let user = database::User {
- uuid: "".to_string(),
- email: default_user.to_string(),
- password: default_pass.to_string(),
- lastname: "Admin".to_string(),
- firstname: "".to_string(),
- };
- db.create_user(&user).await;
- }
-
// TODO: check if `data/credentials.txt` still exists and stop immediately!
let mut router = Router::new()
// favicon
@@ -150,7 +163,7 @@ pub async fn start_server() -> Result<()> {
.route("/", head(status))
// Media items
- .nest("/", MediaApi::routes(db.clone()).await)
+ .nest("/", MediaApi::routes(app_state.clone()).await)
// OAuth 2.0 Authentication
.nest("/", AuthenticationManager::routes())
@@ -239,23 +252,6 @@ pub async fn start_server() -> Result<()> {
Ok(())
}
-/// Aggregates the applications configuration, its loaded plugins and the router for all REST APIs
-pub struct ApplicationState {
- pub config: Configuration,
- pub plugins: HashMap,
- pub router: Option,
-}
-
-impl ApplicationState {
- pub fn new(config: Configuration) -> Self {
- Self {
- config,
- plugins: HashMap::new(),
- router: None,
- }
- }
-}
-
async fn status() -> Json {
// TODO: get app state
diff --git a/src/plugin/plugin_manager.rs b/src/plugin/plugin_manager.rs
index ac20cad..4bb7cfe 100644
--- a/src/plugin/plugin_manager.rs
+++ b/src/plugin/plugin_manager.rs
@@ -20,24 +20,24 @@ use std::path::PathBuf;
use abi_stable::library::{lib_header_from_path, LibrarySuffix, RawLibrary};
use anyhow::Result;
-use common::config::configuration::Configuration;
+use common::{config::configuration::Configuration, ApplicationState};
-use crate::ApplicationState;
use core_extensions::SelfOps;
+use database::sqlite::SqliteDatabase;
use photos_network_plugin::{PluginFactoryRef, PluginId};
use tracing::{debug, error, info};
pub struct PluginManager<'a> {
config: Configuration,
path: String,
- state: &'a mut ApplicationState,
+ state: &'a mut ApplicationState,
}
impl<'a> PluginManager<'a> {
pub fn new(
config: Configuration,
path: String,
- state: &'a mut ApplicationState,
+ state: &'a mut ApplicationState,
) -> Result {
Ok(Self {
config,