Compare commits
6 Commits
a0c90f57d5
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 676f0ca01c | |||
| 92b6d6f1dd | |||
| 754dd8b053 | |||
| 6fa26b3ddb | |||
| a1c9832515 | |||
| 5c3057e998 |
@@ -12,3 +12,4 @@ frontend/node_modules
|
|||||||
|
|
||||||
backend/target
|
backend/target
|
||||||
backend/.gitignore
|
backend/.gitignore
|
||||||
|
backend/data
|
||||||
|
|||||||
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
target
|
target
|
||||||
serve
|
serve
|
||||||
.env
|
.env
|
||||||
|
data
|
||||||
|
|||||||
2
backend/Cargo.lock
generated
2
backend/Cargo.lock
generated
@@ -1235,6 +1235,7 @@ version = "0.30.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"cc",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
@@ -2720,6 +2721,7 @@ version = "1.17.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"getrandom 0.3.3",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"serde",
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ path = "src/bin/backend/main.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.98"
|
||||||
argon2 = "0.5.3"
|
argon2 = { version = "0.5.3", features = ["std"] }
|
||||||
axum = { version = "0.8.4", features = ["multipart", "query"] }
|
axum = { version = "0.8.4", features = ["multipart", "query"] }
|
||||||
axum-extra = { version = "0.10.1", features = ["cookie", "multipart"] }
|
axum-extra = { version = "0.10.1", features = ["cookie", "multipart"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
@@ -29,13 +29,7 @@ regex = "1.11.1"
|
|||||||
rustix = { version = "1.0.8", features = ["fs"] }
|
rustix = { version = "1.0.8", features = ["fs"] }
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
sqlx = { version = "0.8.6", features = [
|
sqlx = { version = "0.8.6", features = ["chrono", "runtime-tokio", "sqlite", "time", "uuid"] }
|
||||||
"chrono",
|
|
||||||
"postgres",
|
|
||||||
"runtime-tokio",
|
|
||||||
"time",
|
|
||||||
"uuid",
|
|
||||||
] }
|
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
tokio = { version = "1.46.1", features = ["full"] }
|
tokio = { version = "1.46.1", features = ["full"] }
|
||||||
tokio-stream = "0.1.17"
|
tokio-stream = "0.1.17"
|
||||||
@@ -45,5 +39,5 @@ tower-http = { version = "0.6.6", features = ["cors", "fs", "trace"] }
|
|||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.19"
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
uuid = { version = "1.17.0", features = ["serde"] }
|
uuid = { version = "1.17.0", features = ["serde", "v4"] }
|
||||||
zip = "4.5.0"
|
zip = "4.5.0"
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
CREATE TABLE warrens (
|
|
||||||
id UUID PRIMARY KEY DEFAULT GEN_RANDOM_UUID(),
|
|
||||||
path VARCHAR NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_warrens_path ON warrens(path);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE warrens ADD COLUMN name VARCHAR NOT NULL;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
CREATE TABLE users (
|
|
||||||
id UUID PRIMARY KEY DEFAULT GEN_RANDOM_UUID(),
|
|
||||||
name VARCHAR NOT NULL,
|
|
||||||
email VARCHAR NOT NULL,
|
|
||||||
hash VARCHAR NOT NULL,
|
|
||||||
admin BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE (email);
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
CREATE TABLE auth_sessions (
|
|
||||||
session_id VARCHAR NOT NULL PRIMARY KEY,
|
|
||||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
expires_at TIMESTAMP NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
CREATE TABLE user_warrens (
|
|
||||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
warren_id UUID NOT NULL REFERENCES warrens(id) ON DELETE CASCADE,
|
|
||||||
can_create_children BOOLEAN NOT NULL,
|
|
||||||
can_list_files BOOLEAN NOT NULL,
|
|
||||||
can_read_files BOOLEAN NOT NULL,
|
|
||||||
can_modify_files BOOLEAN NOT NULL,
|
|
||||||
can_delete_files BOOLEAN NOT NULL,
|
|
||||||
can_delete_warren BOOLEAN NOT NULL,
|
|
||||||
PRIMARY KEY(user_id, warren_id)
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE user_warrens DROP COLUMN can_create_children, DROP COLUMN can_delete_warren;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE users ALTER COLUMN hash DROP NOT NULL;
|
|
||||||
ALTER TABLE users ADD COLUMN oidc_sub VARCHAR UNIQUE;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
CREATE TABLE shares (
|
|
||||||
id UUID PRIMARY KEY DEFAULT GEN_RANDOM_UUID(),
|
|
||||||
creator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
warren_id UUID NOT NULL REFERENCES warrens(id) ON DELETE CASCADE,
|
|
||||||
path VARCHAR NOT NULL,
|
|
||||||
password_hash VARCHAR NOT NULL,
|
|
||||||
expires_at TIMESTAMP,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
ALTER TABLE
|
|
||||||
user_warrens
|
|
||||||
ADD COLUMN
|
|
||||||
can_list_shares BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
ADD COLUMN
|
|
||||||
can_create_shares BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
ADD COLUMN
|
|
||||||
can_modify_shares BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
ADD COLUMN
|
|
||||||
can_delete_shares BOOLEAN NOT NULL DEFAULT false;
|
|
||||||
|
|
||||||
ALTER TABLE
|
|
||||||
user_warrens
|
|
||||||
ALTER COLUMN
|
|
||||||
can_list_shares DROP DEFAULT,
|
|
||||||
ALTER COLUMN
|
|
||||||
can_create_shares DROP DEFAULT,
|
|
||||||
ALTER COLUMN
|
|
||||||
can_modify_shares DROP DEFAULT,
|
|
||||||
ALTER COLUMN
|
|
||||||
can_delete_shares DROP DEFAULT;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE shares ALTER COLUMN password_hash DROP NOT NULL;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
CREATE INDEX idx_shares_path ON shares(path);
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
INSERT INTO users (
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
hash,
|
|
||||||
admin
|
|
||||||
) VALUES (
|
|
||||||
'admin',
|
|
||||||
'admin@example.com',
|
|
||||||
'$argon2id$v=19$m=19456,t=2,p=1$H1WsElL4921/WD5oPkY7JQ$aHudNG8z0ns3pRULfuDpuEkxPUbGxq9AHC4QGyt5odc',
|
|
||||||
true
|
|
||||||
);
|
|
||||||
55
backend/migrations/20250906174941_init.sql
Normal file
55
backend/migrations/20250906174941_init.sql
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
CREATE TABLE users (
|
||||||
|
id BLOB NOT NULL PRIMARY KEY,
|
||||||
|
oidc_sub TEXT UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
admin BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
hash TEXT,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE warrens (
|
||||||
|
id BLOB NOT NULL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
path TEXT NOT NULL UNIQUE,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_warrens (
|
||||||
|
user_id BLOB NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
warren_id BLOB NOT NULL REFERENCES warrens(id) ON DELETE CASCADE,
|
||||||
|
can_list_files BOOLEAN NOT NULL,
|
||||||
|
can_read_files BOOLEAN NOT NULL,
|
||||||
|
can_modify_files BOOLEAN NOT NULL,
|
||||||
|
can_delete_files BOOLEAN NOT NULL,
|
||||||
|
can_list_shares BOOLEAN NOT NULL,
|
||||||
|
can_create_shares BOOLEAN NOT NULL,
|
||||||
|
can_modify_shares BOOLEAN NOT NULL,
|
||||||
|
can_delete_shares BOOLEAN NOT NULL,
|
||||||
|
PRIMARY KEY(user_id, warren_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE shares (
|
||||||
|
id BLOB NOT NULL PRIMARY KEY,
|
||||||
|
creator_id BLOB NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
warren_id BLOB NOT NULL REFERENCES warrens(id) ON DELETE CASCADE,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
password_hash TEXT,
|
||||||
|
expires_at DATETIME,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_shares_path ON shares(path);
|
||||||
|
|
||||||
|
CREATE TABLE auth_sessions (
|
||||||
|
session_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
user_id BLOB NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
expires_at DATETIME NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE application_options (
|
||||||
|
key TEXT NOT NULL PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
@@ -7,7 +7,7 @@ use warren::{
|
|||||||
metrics_debug_logger::MetricsDebugLogger,
|
metrics_debug_logger::MetricsDebugLogger,
|
||||||
notifier_debug_logger::NotifierDebugLogger,
|
notifier_debug_logger::NotifierDebugLogger,
|
||||||
oidc::{Oidc, OidcConfig},
|
oidc::{Oidc, OidcConfig},
|
||||||
postgres::{Postgres, PostgresConfig},
|
sqlite::{Sqlite, SqliteConfig},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,16 +25,15 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let metrics = MetricsDebugLogger::new();
|
let metrics = MetricsDebugLogger::new();
|
||||||
let notifier = NotifierDebugLogger::new();
|
let notifier = NotifierDebugLogger::new();
|
||||||
|
|
||||||
let postgres_config =
|
let sqlite_config = SqliteConfig::new(config.database_url.clone());
|
||||||
PostgresConfig::new(config.database_url.clone(), config.database_name.clone());
|
let sqlite = Sqlite::new(sqlite_config).await?;
|
||||||
let postgres = Postgres::new(postgres_config).await?;
|
|
||||||
|
|
||||||
let fs_config = FileSystemConfig::from_env(config.serve_dir.clone())?;
|
let fs_config = FileSystemConfig::from_env(config.serve_dir.clone())?;
|
||||||
let fs = FileSystem::new(fs_config)?;
|
let fs = FileSystem::new(fs_config)?;
|
||||||
let fs_service = domain::warren::service::file_system::Service::new(fs, metrics, notifier);
|
let fs_service = domain::warren::service::file_system::Service::new(fs, metrics, notifier);
|
||||||
|
|
||||||
let warren_service = domain::warren::service::warren::Service::new(
|
let warren_service = domain::warren::service::warren::Service::new(
|
||||||
postgres.clone(),
|
sqlite.clone(),
|
||||||
metrics,
|
metrics,
|
||||||
notifier,
|
notifier,
|
||||||
fs_service.clone(),
|
fs_service.clone(),
|
||||||
@@ -47,13 +46,18 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let option_service =
|
||||||
|
domain::warren::service::option::Service::new(sqlite.clone(), metrics, notifier);
|
||||||
|
|
||||||
let auth_service = domain::warren::service::auth::Service::new(
|
let auth_service = domain::warren::service::auth::Service::new(
|
||||||
postgres,
|
sqlite,
|
||||||
metrics,
|
metrics,
|
||||||
notifier,
|
notifier,
|
||||||
config.auth,
|
config.auth,
|
||||||
oidc_service,
|
oidc_service,
|
||||||
);
|
option_service,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let server_config = HttpServerConfig::new(
|
let server_config = HttpServerConfig::new(
|
||||||
&config.server_address,
|
&config.server_address,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use tracing::level_filters::LevelFilter;
|
|||||||
use crate::domain::warren::service::auth::AuthConfig;
|
use crate::domain::warren::service::auth::AuthConfig;
|
||||||
|
|
||||||
const DATABASE_URL_KEY: &str = "DATABASE_URL";
|
const DATABASE_URL_KEY: &str = "DATABASE_URL";
|
||||||
const DATABASE_NAME_KEY: &str = "DATABASE_NAME";
|
|
||||||
|
|
||||||
const SERVER_ADDRESS_KEY: &str = "SERVER_ADDRESS";
|
const SERVER_ADDRESS_KEY: &str = "SERVER_ADDRESS";
|
||||||
const SERVER_PORT_KEY: &str = "SERVER_PORT";
|
const SERVER_PORT_KEY: &str = "SERVER_PORT";
|
||||||
@@ -28,7 +27,6 @@ pub struct Config {
|
|||||||
pub static_frontend_dir: Option<String>,
|
pub static_frontend_dir: Option<String>,
|
||||||
|
|
||||||
pub database_url: String,
|
pub database_url: String,
|
||||||
pub database_name: String,
|
|
||||||
|
|
||||||
pub log_level: LevelFilter,
|
pub log_level: LevelFilter,
|
||||||
|
|
||||||
@@ -45,7 +43,6 @@ impl Config {
|
|||||||
let static_frontend_dir = Self::load_env(STATIC_FRONTEND_DIRECTORY).ok();
|
let static_frontend_dir = Self::load_env(STATIC_FRONTEND_DIRECTORY).ok();
|
||||||
|
|
||||||
let database_url = Self::load_env(DATABASE_URL_KEY)?;
|
let database_url = Self::load_env(DATABASE_URL_KEY)?;
|
||||||
let database_name = Self::load_env(DATABASE_NAME_KEY)?;
|
|
||||||
|
|
||||||
let log_level =
|
let log_level =
|
||||||
LevelFilter::from_str(&Self::load_env(LOG_LEVEL_KEY).unwrap_or("INFO".to_string()))
|
LevelFilter::from_str(&Self::load_env(LOG_LEVEL_KEY).unwrap_or("INFO".to_string()))
|
||||||
@@ -62,7 +59,6 @@ impl Config {
|
|||||||
static_frontend_dir,
|
static_frontend_dir,
|
||||||
|
|
||||||
database_url,
|
database_url,
|
||||||
database_name,
|
|
||||||
|
|
||||||
log_level,
|
log_level,
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod auth_session;
|
pub mod auth_session;
|
||||||
pub mod file;
|
pub mod file;
|
||||||
|
pub mod option;
|
||||||
pub mod share;
|
pub mod share;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod user_warren;
|
pub mod user_warren;
|
||||||
|
|||||||
74
backend/src/lib/domain/warren/models/option/mod.rs
Normal file
74
backend/src/lib/domain/warren/models/option/mod.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
mod requests;
|
||||||
|
use derive_more::Display;
|
||||||
|
pub use requests::*;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display)]
|
||||||
|
pub struct OptionKey(String);
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum OptionKeyError {
|
||||||
|
#[error("An OptionKey must not be empty")]
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OptionKey {
|
||||||
|
pub fn new(raw: &str) -> Result<Self, OptionKeyError> {
|
||||||
|
let raw = raw.trim();
|
||||||
|
|
||||||
|
if raw.is_empty() {
|
||||||
|
return Err(OptionKeyError::Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(raw.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct OptionValue<T>(T)
|
||||||
|
where
|
||||||
|
T: OptionType;
|
||||||
|
|
||||||
|
impl<T> OptionValue<T>
|
||||||
|
where
|
||||||
|
T: OptionType,
|
||||||
|
{
|
||||||
|
pub fn new(value: T) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inner(&self) -> &T {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_inner(self) -> T {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait OptionType: std::fmt::Debug + Clone + Send + Sync {
|
||||||
|
type Error: std::fmt::Debug;
|
||||||
|
|
||||||
|
fn parse(raw: &str) -> Result<Self, Self::Error>;
|
||||||
|
fn to_string(&self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OptionType for bool {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn parse(raw: &str) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match raw.to_lowercase().as_str() {
|
||||||
|
"true" => true,
|
||||||
|
"false" => false,
|
||||||
|
_ => anyhow::bail!("Expected 'true' or 'false': {raw}"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
if *self { "true" } else { "false" }.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::option::OptionKey;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DeleteOptionRequest {
|
||||||
|
key: OptionKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeleteOptionRequest {
|
||||||
|
pub fn new(key: OptionKey) -> Self {
|
||||||
|
Self { key }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&self) -> &OptionKey {
|
||||||
|
&self.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DeleteOptionRequest> for OptionKey {
|
||||||
|
fn from(value: DeleteOptionRequest) -> Self {
|
||||||
|
value.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct DeleteOptionResponse {
|
||||||
|
key: OptionKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeleteOptionResponse {
|
||||||
|
pub fn new(key: OptionKey) -> Self {
|
||||||
|
Self { key }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&self) -> &OptionKey {
|
||||||
|
&self.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DeleteOptionError {
|
||||||
|
#[error("Could not find option with key: {0}")]
|
||||||
|
NotFound(OptionKey),
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
57
backend/src/lib/domain/warren/models/option/requests/get.rs
Normal file
57
backend/src/lib/domain/warren/models/option/requests/get.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::option::{OptionKey, OptionType};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct GetOptionRequest {
|
||||||
|
key: OptionKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetOptionRequest {
|
||||||
|
pub fn new(key: OptionKey) -> Self {
|
||||||
|
Self { key }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&self) -> &OptionKey {
|
||||||
|
&self.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GetOptionRequest> for OptionKey {
|
||||||
|
fn from(value: GetOptionRequest) -> Self {
|
||||||
|
value.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GetOptionResponse<T: OptionType> {
|
||||||
|
key: OptionKey,
|
||||||
|
value: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> GetOptionResponse<T>
|
||||||
|
where
|
||||||
|
T: OptionType,
|
||||||
|
{
|
||||||
|
pub fn new(key: OptionKey, value: T) -> Self {
|
||||||
|
Self { key, value }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&self) -> &OptionKey {
|
||||||
|
&self.key
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(&self) -> &T {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GetOptionError {
|
||||||
|
#[error("Could not find option with key: {0}")]
|
||||||
|
NotFound(OptionKey),
|
||||||
|
#[error("Could not parse the option value with the specified type")]
|
||||||
|
Parse,
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
mod delete;
|
||||||
|
mod get;
|
||||||
|
mod set;
|
||||||
|
pub use delete::*;
|
||||||
|
pub use get::*;
|
||||||
|
pub use set::*;
|
||||||
83
backend/src/lib/domain/warren/models/option/requests/set.rs
Normal file
83
backend/src/lib/domain/warren/models/option/requests/set.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::option::{OptionKey, OptionType, OptionValue};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SetOptionRequest<T>
|
||||||
|
where
|
||||||
|
T: OptionType,
|
||||||
|
{
|
||||||
|
key: OptionKey,
|
||||||
|
value: OptionValue<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SetOptionRequest<T>
|
||||||
|
where
|
||||||
|
T: OptionType,
|
||||||
|
{
|
||||||
|
pub fn new(key: OptionKey, value: T) -> Self {
|
||||||
|
Self {
|
||||||
|
key,
|
||||||
|
value: OptionValue::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&self) -> &OptionKey {
|
||||||
|
&self.key
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(&self) -> &OptionValue<T> {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unpack(self) -> (OptionKey, OptionValue<T>) {
|
||||||
|
(self.key, self.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<SetOptionRequest<T>> for OptionKey
|
||||||
|
where
|
||||||
|
T: OptionType,
|
||||||
|
{
|
||||||
|
fn from(value: SetOptionRequest<T>) -> Self {
|
||||||
|
value.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<SetOptionRequest<T>> for OptionValue<T>
|
||||||
|
where
|
||||||
|
T: OptionType,
|
||||||
|
{
|
||||||
|
fn from(value: SetOptionRequest<T>) -> Self {
|
||||||
|
value.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SetOptionResponse<T: OptionType> {
|
||||||
|
key: OptionKey,
|
||||||
|
value: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SetOptionResponse<T>
|
||||||
|
where
|
||||||
|
T: OptionType,
|
||||||
|
{
|
||||||
|
pub fn new(key: OptionKey, value: T) -> Self {
|
||||||
|
Self { key, value }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&self) -> &OptionKey {
|
||||||
|
&self.key
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(&self) -> &T {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum SetOptionError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ pub struct RegisterUserRequest {
|
|||||||
name: UserName,
|
name: UserName,
|
||||||
email: UserEmail,
|
email: UserEmail,
|
||||||
password: UserPassword,
|
password: UserPassword,
|
||||||
|
bypass_registration_flag: bool,
|
||||||
|
admin: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegisterUserRequest {
|
impl RegisterUserRequest {
|
||||||
@@ -17,6 +19,23 @@ impl RegisterUserRequest {
|
|||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
|
bypass_registration_flag: false,
|
||||||
|
admin: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_bypass_flag(
|
||||||
|
name: UserName,
|
||||||
|
email: UserEmail,
|
||||||
|
password: UserPassword,
|
||||||
|
admin: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
bypass_registration_flag: true,
|
||||||
|
admin,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,11 +50,19 @@ impl RegisterUserRequest {
|
|||||||
pub fn password(&self) -> &UserPassword {
|
pub fn password(&self) -> &UserPassword {
|
||||||
&self.password
|
&self.password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn admin(&self) -> bool {
|
||||||
|
self.admin
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bypass_registration_flag(&self) -> bool {
|
||||||
|
self.bypass_registration_flag
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<RegisterUserRequest> for CreateUserRequest {
|
impl From<RegisterUserRequest> for CreateUserRequest {
|
||||||
fn from(value: RegisterUserRequest) -> Self {
|
fn from(value: RegisterUserRequest) -> Self {
|
||||||
Self::new(value.name, value.email, value.password, false)
|
Self::new(value.name, value.email, value.password, value.admin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,3 +185,14 @@ pub trait AuthMetrics: Clone + Send + Sync + 'static {
|
|||||||
fn record_auth_share_deletion_success(&self) -> impl Future<Output = ()> + Send;
|
fn record_auth_share_deletion_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
fn record_auth_share_deletion_failure(&self) -> impl Future<Output = ()> + Send;
|
fn record_auth_share_deletion_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait OptionMetrics: Clone + Send + Sync + 'static {
|
||||||
|
fn record_option_get_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_option_get_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_option_set_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_option_set_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_option_delete_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_option_delete_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ use super::models::{
|
|||||||
RmRequest, SaveError, SaveRequest, SaveResponse, StatError, StatRequest, StatResponse,
|
RmRequest, SaveError, SaveRequest, SaveResponse, StatError, StatRequest, StatResponse,
|
||||||
TouchError, TouchRequest,
|
TouchError, TouchRequest,
|
||||||
},
|
},
|
||||||
|
option::{
|
||||||
|
DeleteOptionError, DeleteOptionRequest, DeleteOptionResponse, GetOptionError,
|
||||||
|
GetOptionRequest, GetOptionResponse, OptionType, SetOptionError, SetOptionRequest,
|
||||||
|
SetOptionResponse,
|
||||||
|
},
|
||||||
share::{
|
share::{
|
||||||
CreateShareBaseRequest, CreateShareError, CreateShareRequest, CreateShareResponse,
|
CreateShareBaseRequest, CreateShareError, CreateShareRequest, CreateShareResponse,
|
||||||
DeleteShareError, DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest,
|
DeleteShareError, DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest,
|
||||||
@@ -337,3 +342,18 @@ pub trait AuthService: Clone + Send + Sync + 'static {
|
|||||||
warren_service: &WS,
|
warren_service: &WS,
|
||||||
) -> impl Future<Output = Result<DeleteShareResponse, AuthError<DeleteShareError>>> + Send;
|
) -> impl Future<Output = Result<DeleteShareResponse, AuthError<DeleteShareError>>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait OptionService: Clone + Send + Sync + 'static {
|
||||||
|
fn get_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
request: GetOptionRequest,
|
||||||
|
) -> impl Future<Output = Result<GetOptionResponse<T>, GetOptionError>> + Send;
|
||||||
|
fn set_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
request: SetOptionRequest<T>,
|
||||||
|
) -> impl Future<Output = Result<SetOptionResponse<T>, SetOptionError>> + Send;
|
||||||
|
fn delete_option(
|
||||||
|
&self,
|
||||||
|
request: DeleteOptionRequest,
|
||||||
|
) -> impl Future<Output = Result<DeleteOptionResponse, DeleteOptionError>> + Send;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use uuid::Uuid;
|
|||||||
use crate::domain::warren::models::{
|
use crate::domain::warren::models::{
|
||||||
auth_session::requests::FetchAuthSessionResponse,
|
auth_session::requests::FetchAuthSessionResponse,
|
||||||
file::{AbsoluteFilePath, AbsoluteFilePathList, LsResponse},
|
file::{AbsoluteFilePath, AbsoluteFilePathList, LsResponse},
|
||||||
|
option::{DeleteOptionResponse, GetOptionResponse, OptionType, SetOptionResponse},
|
||||||
share::{
|
share::{
|
||||||
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
|
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
|
||||||
ShareCatResponse, ShareLsResponse, VerifySharePasswordResponse,
|
ShareCatResponse, ShareLsResponse, VerifySharePasswordResponse,
|
||||||
@@ -219,3 +220,15 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
|||||||
response: &DeleteShareResponse,
|
response: &DeleteShareResponse,
|
||||||
) -> impl Future<Output = ()> + Send;
|
) -> impl Future<Output = ()> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait OptionNotifier: Clone + Send + Sync + 'static {
|
||||||
|
fn got_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
response: &GetOptionResponse<T>,
|
||||||
|
) -> impl Future<Output = ()> + Send;
|
||||||
|
fn set_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
response: &SetOptionResponse<T>,
|
||||||
|
) -> impl Future<Output = ()> + Send;
|
||||||
|
fn deleted_option(&self, response: &DeleteOptionResponse) -> impl Future<Output = ()> + Send;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ use crate::domain::warren::models::{
|
|||||||
RmRequest, SaveError, SaveRequest, SaveResponse, StatError, StatRequest, StatResponse,
|
RmRequest, SaveError, SaveRequest, SaveResponse, StatError, StatRequest, StatResponse,
|
||||||
TouchError, TouchRequest,
|
TouchError, TouchRequest,
|
||||||
},
|
},
|
||||||
|
option::{
|
||||||
|
DeleteOptionError, DeleteOptionRequest, DeleteOptionResponse, GetOptionError,
|
||||||
|
GetOptionRequest, GetOptionResponse, OptionType, SetOptionError, SetOptionRequest,
|
||||||
|
SetOptionResponse,
|
||||||
|
},
|
||||||
share::{
|
share::{
|
||||||
CreateShareError, CreateShareRequest, CreateShareResponse, DeleteShareError,
|
CreateShareError, CreateShareRequest, CreateShareResponse, DeleteShareError,
|
||||||
DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest, ListSharesError,
|
DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest, ListSharesError,
|
||||||
@@ -188,3 +193,18 @@ pub trait AuthRepository: Clone + Send + Sync + 'static {
|
|||||||
request: FetchUserWarrenRequest,
|
request: FetchUserWarrenRequest,
|
||||||
) -> impl Future<Output = Result<UserWarren, FetchUserWarrenError>> + Send;
|
) -> impl Future<Output = Result<UserWarren, FetchUserWarrenError>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait OptionRepository: Clone + Send + Sync + 'static {
|
||||||
|
fn get_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
request: GetOptionRequest,
|
||||||
|
) -> impl Future<Output = Result<GetOptionResponse<T>, GetOptionError>> + Send;
|
||||||
|
fn set_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
request: SetOptionRequest<T>,
|
||||||
|
) -> impl Future<Output = Result<SetOptionResponse<T>, SetOptionError>> + Send;
|
||||||
|
fn delete_option(
|
||||||
|
&self,
|
||||||
|
request: DeleteOptionRequest,
|
||||||
|
) -> impl Future<Output = Result<DeleteOptionResponse, DeleteOptionError>> + Send;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
file::FileStream,
|
file::FileStream,
|
||||||
|
option::{GetOptionError, GetOptionRequest, OptionKey, SetOptionRequest},
|
||||||
share::{
|
share::{
|
||||||
CreateShareBaseRequest, CreateShareError, CreateShareResponse,
|
CreateShareBaseRequest, CreateShareError, CreateShareResponse,
|
||||||
DeleteShareError, DeleteShareRequest, DeleteShareResponse, ListSharesError,
|
DeleteShareError, DeleteShareRequest, DeleteShareResponse, ListSharesError,
|
||||||
@@ -24,7 +25,7 @@ use crate::{
|
|||||||
ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse, ListUsersError,
|
ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse, ListUsersError,
|
||||||
ListUsersRequest, LoginUserError, LoginUserOidcError, LoginUserOidcRequest,
|
ListUsersRequest, LoginUserError, LoginUserOidcError, LoginUserOidcRequest,
|
||||||
LoginUserOidcResponse, LoginUserRequest, LoginUserResponse, RegisterUserError,
|
LoginUserOidcResponse, LoginUserRequest, LoginUserResponse, RegisterUserError,
|
||||||
RegisterUserRequest, User,
|
RegisterUserRequest, User, UserEmail, UserName, UserPassword,
|
||||||
},
|
},
|
||||||
user_warren::{
|
user_warren::{
|
||||||
UserWarren,
|
UserWarren,
|
||||||
@@ -46,7 +47,10 @@ use crate::{
|
|||||||
WarrenTouchResponse,
|
WarrenTouchResponse,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService},
|
ports::{
|
||||||
|
AuthMetrics, AuthNotifier, AuthRepository, AuthService, OptionService,
|
||||||
|
WarrenService,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -97,54 +101,100 @@ impl AuthConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Service<R, M, N, OIDC>
|
pub struct Service<R, M, N, OIDC, O>
|
||||||
where
|
where
|
||||||
R: AuthRepository,
|
R: AuthRepository,
|
||||||
M: AuthMetrics,
|
M: AuthMetrics,
|
||||||
N: AuthNotifier,
|
N: AuthNotifier,
|
||||||
OIDC: OidcService,
|
OIDC: OidcService,
|
||||||
|
O: OptionService,
|
||||||
{
|
{
|
||||||
repository: R,
|
repository: R,
|
||||||
metrics: M,
|
metrics: M,
|
||||||
notifier: N,
|
notifier: N,
|
||||||
oidc: Option<OIDC>,
|
oidc: Option<OIDC>,
|
||||||
|
option_service: O,
|
||||||
config: AuthConfig,
|
config: AuthConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R, M, N, OIDC> Service<R, M, N, OIDC>
|
impl<R, M, N, OIDC, O> Service<R, M, N, OIDC, O>
|
||||||
where
|
where
|
||||||
R: AuthRepository,
|
R: AuthRepository,
|
||||||
M: AuthMetrics,
|
M: AuthMetrics,
|
||||||
N: AuthNotifier,
|
N: AuthNotifier,
|
||||||
OIDC: OidcService,
|
OIDC: OidcService,
|
||||||
|
O: OptionService,
|
||||||
{
|
{
|
||||||
pub fn new(
|
pub async fn new(
|
||||||
repository: R,
|
repository: R,
|
||||||
metrics: M,
|
metrics: M,
|
||||||
notifier: N,
|
notifier: N,
|
||||||
config: AuthConfig,
|
config: AuthConfig,
|
||||||
oidc: Option<OIDC>,
|
oidc: Option<OIDC>,
|
||||||
) -> Self {
|
option_service: O,
|
||||||
Self {
|
) -> anyhow::Result<Self> {
|
||||||
|
let service = Self {
|
||||||
repository,
|
repository,
|
||||||
metrics,
|
metrics,
|
||||||
notifier,
|
notifier,
|
||||||
config,
|
config,
|
||||||
oidc,
|
oidc,
|
||||||
}
|
option_service,
|
||||||
|
};
|
||||||
|
|
||||||
|
service.init().await?;
|
||||||
|
|
||||||
|
Ok(service)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn oidc(&self) -> Option<&OIDC> {
|
pub fn oidc(&self) -> Option<&OIDC> {
|
||||||
self.oidc.as_ref()
|
self.oidc.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn init(&self) -> anyhow::Result<()> {
|
||||||
|
self.create_admin_user_if_init().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_admin_user_if_init(&self) -> anyhow::Result<()> {
|
||||||
|
const CREATED_ADMIN_USER_KEY: &str = "CREATED_ADMIN_USER";
|
||||||
|
|
||||||
|
let key = OptionKey::new(CREATED_ADMIN_USER_KEY)?;
|
||||||
|
let request = GetOptionRequest::new(key.clone());
|
||||||
|
match self.option_service.get_option::<bool>(request).await {
|
||||||
|
// If the option is already set and true we don't have to do anything anymore
|
||||||
|
Ok(opt) if *opt.value() => return Ok(()),
|
||||||
|
Err(e) => match e {
|
||||||
|
// The option is not yet set so we proceed with the admin user creation
|
||||||
|
GetOptionError::NotFound(_) => (),
|
||||||
|
_ => return Err(e.into()),
|
||||||
|
},
|
||||||
|
// The option was set but it was false so we proceed with the admin user creation
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = UserName::new("admin")?;
|
||||||
|
let email = UserEmail::new("admin@example.com")?;
|
||||||
|
let password = UserPassword::new("admin1234567")?;
|
||||||
|
let request = RegisterUserRequest::new_bypass_flag(name, email, password, true);
|
||||||
|
|
||||||
|
self.register_user(request).await?;
|
||||||
|
self.option_service
|
||||||
|
.set_option(SetOptionRequest::new(key, true))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R, M, N, OIDC> AuthService for Service<R, M, N, OIDC>
|
impl<R, M, N, OIDC, O> AuthService for Service<R, M, N, OIDC, O>
|
||||||
where
|
where
|
||||||
R: AuthRepository,
|
R: AuthRepository,
|
||||||
M: AuthMetrics,
|
M: AuthMetrics,
|
||||||
N: AuthNotifier,
|
N: AuthNotifier,
|
||||||
OIDC: OidcService,
|
OIDC: OidcService,
|
||||||
|
O: OptionService,
|
||||||
{
|
{
|
||||||
async fn create_warren<WS: WarrenService>(
|
async fn create_warren<WS: WarrenService>(
|
||||||
&self,
|
&self,
|
||||||
@@ -253,7 +303,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn register_user(&self, request: RegisterUserRequest) -> Result<User, RegisterUserError> {
|
async fn register_user(&self, request: RegisterUserRequest) -> Result<User, RegisterUserError> {
|
||||||
if !self.config.allow_registration {
|
if !self.config.allow_registration && !request.bypass_registration_flag() {
|
||||||
self.metrics.record_user_registration_failure().await;
|
self.metrics.record_user_registration_failure().await;
|
||||||
return Err(RegisterUserError::Disabled);
|
return Err(RegisterUserError::Disabled);
|
||||||
}
|
}
|
||||||
@@ -972,12 +1022,13 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R, M, N, OIDC> Service<R, M, N, OIDC>
|
impl<R, M, N, OIDC, O> Service<R, M, N, OIDC, O>
|
||||||
where
|
where
|
||||||
R: AuthRepository,
|
R: AuthRepository,
|
||||||
M: AuthMetrics,
|
M: AuthMetrics,
|
||||||
N: AuthNotifier,
|
N: AuthNotifier,
|
||||||
OIDC: OidcService,
|
OIDC: OidcService,
|
||||||
|
O: OptionService,
|
||||||
{
|
{
|
||||||
/// A helper to get a [UserWarren], [User] and the underlying request from an [AuthRequest]
|
/// A helper to get a [UserWarren], [User] and the underlying request from an [AuthRequest]
|
||||||
async fn get_session_data_and_user_warren<T, E>(
|
async fn get_session_data_and_user_warren<T, E>(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod file_system;
|
pub mod file_system;
|
||||||
|
pub mod option;
|
||||||
pub mod warren;
|
pub mod warren;
|
||||||
|
|||||||
90
backend/src/lib/domain/warren/service/option.rs
Normal file
90
backend/src/lib/domain/warren/service/option.rs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
use crate::domain::warren::{
|
||||||
|
models::option::{
|
||||||
|
DeleteOptionError, DeleteOptionRequest, DeleteOptionResponse, GetOptionError,
|
||||||
|
GetOptionRequest, GetOptionResponse, OptionType, SetOptionError, SetOptionRequest,
|
||||||
|
SetOptionResponse,
|
||||||
|
},
|
||||||
|
ports::{OptionMetrics, OptionNotifier, OptionRepository, OptionService},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Service<R, M, N>
|
||||||
|
where
|
||||||
|
R: OptionRepository,
|
||||||
|
M: OptionMetrics,
|
||||||
|
N: OptionNotifier,
|
||||||
|
{
|
||||||
|
repository: R,
|
||||||
|
metrics: M,
|
||||||
|
notifier: N,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R, M, N> Service<R, M, N>
|
||||||
|
where
|
||||||
|
R: OptionRepository,
|
||||||
|
M: OptionMetrics,
|
||||||
|
N: OptionNotifier,
|
||||||
|
{
|
||||||
|
pub fn new(repository: R, metrics: M, notifier: N) -> Self {
|
||||||
|
Self {
|
||||||
|
repository,
|
||||||
|
metrics,
|
||||||
|
notifier,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R, M, N> OptionService for Service<R, M, N>
|
||||||
|
where
|
||||||
|
R: OptionRepository,
|
||||||
|
M: OptionMetrics,
|
||||||
|
N: OptionNotifier,
|
||||||
|
{
|
||||||
|
async fn get_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
request: GetOptionRequest,
|
||||||
|
) -> Result<GetOptionResponse<T>, GetOptionError> {
|
||||||
|
let result = self.repository.get_option(request).await;
|
||||||
|
|
||||||
|
if let Ok(response) = result.as_ref() {
|
||||||
|
self.metrics.record_option_get_success().await;
|
||||||
|
self.notifier.got_option(response).await;
|
||||||
|
} else {
|
||||||
|
self.metrics.record_option_get_failure().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
request: SetOptionRequest<T>,
|
||||||
|
) -> Result<SetOptionResponse<T>, SetOptionError> {
|
||||||
|
let result = self.repository.set_option(request).await;
|
||||||
|
|
||||||
|
if let Ok(response) = result.as_ref() {
|
||||||
|
self.metrics.record_option_set_success().await;
|
||||||
|
self.notifier.set_option(response).await;
|
||||||
|
} else {
|
||||||
|
self.metrics.record_option_set_failure().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_option(
|
||||||
|
&self,
|
||||||
|
request: DeleteOptionRequest,
|
||||||
|
) -> Result<DeleteOptionResponse, DeleteOptionError> {
|
||||||
|
let result = self.repository.delete_option(request).await;
|
||||||
|
|
||||||
|
if let Ok(response) = result.as_ref() {
|
||||||
|
self.metrics.record_option_delete_success().await;
|
||||||
|
self.notifier.deleted_option(response).await;
|
||||||
|
} else {
|
||||||
|
self.metrics.record_option_delete_failure().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::domain::{
|
use crate::domain::{
|
||||||
oidc::ports::OidcMetrics,
|
oidc::ports::OidcMetrics,
|
||||||
warren::ports::{AuthMetrics, FileSystemMetrics, WarrenMetrics},
|
warren::ports::{AuthMetrics, FileSystemMetrics, OptionMetrics, WarrenMetrics},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@@ -452,3 +452,26 @@ impl OidcMetrics for MetricsDebugLogger {
|
|||||||
tracing::debug!("[Metrics] OIDC get user info failed");
|
tracing::debug!("[Metrics] OIDC get user info failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OptionMetrics for MetricsDebugLogger {
|
||||||
|
async fn record_option_get_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Get option succeeded");
|
||||||
|
}
|
||||||
|
async fn record_option_get_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Get option failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_option_set_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Set option succeeded");
|
||||||
|
}
|
||||||
|
async fn record_option_set_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Set option failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_option_delete_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Delete option succeeded");
|
||||||
|
}
|
||||||
|
async fn record_option_delete_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Delete option failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ pub mod file_system;
|
|||||||
pub mod metrics_debug_logger;
|
pub mod metrics_debug_logger;
|
||||||
pub mod notifier_debug_logger;
|
pub mod notifier_debug_logger;
|
||||||
pub mod oidc;
|
pub mod oidc;
|
||||||
pub mod postgres;
|
pub mod sqlite;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::domain::{
|
|||||||
models::{
|
models::{
|
||||||
auth_session::requests::FetchAuthSessionResponse,
|
auth_session::requests::FetchAuthSessionResponse,
|
||||||
file::{AbsoluteFilePath, AbsoluteFilePathList, LsResponse},
|
file::{AbsoluteFilePath, AbsoluteFilePathList, LsResponse},
|
||||||
|
option::{DeleteOptionResponse, GetOptionResponse, OptionType, SetOptionResponse},
|
||||||
share::{
|
share::{
|
||||||
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
|
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
|
||||||
ShareCatResponse, ShareLsResponse, VerifySharePasswordResponse,
|
ShareCatResponse, ShareLsResponse, VerifySharePasswordResponse,
|
||||||
@@ -22,7 +23,7 @@ use crate::domain::{
|
|||||||
WarrenRmResponse, WarrenSaveResponse, WarrenTouchResponse,
|
WarrenRmResponse, WarrenSaveResponse, WarrenTouchResponse,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ports::{AuthNotifier, FileSystemNotifier, WarrenNotifier},
|
ports::{AuthNotifier, FileSystemNotifier, OptionNotifier, WarrenNotifier},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -515,3 +516,25 @@ impl OidcNotifier for NotifierDebugLogger {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OptionNotifier for NotifierDebugLogger {
|
||||||
|
async fn got_option<T: OptionType>(&self, response: &GetOptionResponse<T>) {
|
||||||
|
tracing::debug!(
|
||||||
|
"[Notifier] Got option {}: {}",
|
||||||
|
response.key().to_string(),
|
||||||
|
response.value().to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_option<T: OptionType>(&self, response: &SetOptionResponse<T>) {
|
||||||
|
tracing::debug!(
|
||||||
|
"[Notifier] Set option {} to {}",
|
||||||
|
response.key().to_string(),
|
||||||
|
response.value().to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn deleted_option(&self, response: &DeleteOptionResponse) {
|
||||||
|
tracing::debug!("[Notifier] Deleted option {}", response.key().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
use std::{str::FromStr as _, time::Duration};
|
|
||||||
|
|
||||||
use anyhow::Context as _;
|
|
||||||
use sqlx::{
|
|
||||||
ConnectOptions as _, Connection as _, PgConnection, PgPool,
|
|
||||||
postgres::{PgConnectOptions, PgPoolOptions},
|
|
||||||
};
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
pub mod auth;
|
|
||||||
pub mod share;
|
|
||||||
pub mod warrens;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct PostgresConfig {
|
|
||||||
database_url: String,
|
|
||||||
database_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PostgresConfig {
|
|
||||||
pub fn new(database_url: String, database_name: String) -> Self {
|
|
||||||
Self {
|
|
||||||
database_url,
|
|
||||||
database_name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Postgres {
|
|
||||||
pool: PgPool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Postgres {
|
|
||||||
pub async fn new(config: PostgresConfig) -> anyhow::Result<Self> {
|
|
||||||
let opts = PgConnectOptions::from_str(&config.database_url)?.disable_statement_logging();
|
|
||||||
|
|
||||||
let mut connection = PgConnection::connect_with(&opts)
|
|
||||||
.await
|
|
||||||
.context("Failed to connect to the PostgreSQL database")?;
|
|
||||||
|
|
||||||
match sqlx::query("SELECT datname FROM pg_database WHERE datname = $1")
|
|
||||||
.bind(&config.database_name)
|
|
||||||
.fetch_one(&mut connection)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(sqlx::Error::RowNotFound) => {
|
|
||||||
sqlx::query(&format!("CREATE DATABASE {}", config.database_name))
|
|
||||||
.execute(&mut connection)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Err(e) => return Err(e.into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
connection.close().await?;
|
|
||||||
|
|
||||||
let pool = PgPoolOptions::new()
|
|
||||||
.connect_with(opts.database(&config.database_name))
|
|
||||||
.await?;
|
|
||||||
sqlx::migrate!("./migrations").run(&pool).await?;
|
|
||||||
|
|
||||||
// 3600 seconds = 1 hour
|
|
||||||
Self::start_cleanup_tasks(pool.clone(), Duration::from_secs(3600));
|
|
||||||
|
|
||||||
Ok(Self { pool })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn start_cleanup_tasks(pool: PgPool, interval: Duration) -> JoinHandle<()> {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
{
|
|
||||||
let Ok(mut connection) = pool.acquire().await else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(count) = Self::delete_expired_auth_sessions(&mut connection).await {
|
|
||||||
tracing::debug!("Removed {count} expired auth session(s)");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(count) = Self::delete_expired_shares(&mut connection).await {
|
|
||||||
tracing::debug!("Deleted {count} expired share(s)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::time::sleep(interval).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::debug!("Session cleanup task stopped");
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn is_not_found_error(err: &sqlx::Error) -> bool {
|
|
||||||
matches!(err, sqlx::Error::RowNotFound)
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ use argon2::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use sqlx::{Acquire as _, PgConnection};
|
use sqlx::{Acquire as _, SqliteConnection};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::domain::warren::{
|
use crate::domain::warren::{
|
||||||
@@ -40,15 +40,15 @@ use crate::domain::warren::{
|
|||||||
ports::{AuthRepository, WarrenService},
|
ports::{AuthRepository, WarrenService},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Postgres, is_not_found_error};
|
use super::{Sqlite, is_not_found_error};
|
||||||
|
|
||||||
impl AuthRepository for Postgres {
|
impl AuthRepository for Sqlite {
|
||||||
async fn create_user(&self, request: CreateUserRequest) -> Result<User, CreateUserError> {
|
async fn create_user(&self, request: CreateUserRequest) -> Result<User, CreateUserError> {
|
||||||
let mut connection = self
|
let mut connection = self
|
||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user = self
|
let user = self
|
||||||
.create_user(
|
.create_user(
|
||||||
@@ -72,7 +72,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user = self
|
let user = self
|
||||||
.create_or_update_user(
|
.create_or_update_user(
|
||||||
@@ -93,7 +93,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user = self
|
let user = self
|
||||||
.edit_user(
|
.edit_user(
|
||||||
@@ -115,7 +115,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
self.delete_user_from_database(&mut connection, request.user_id())
|
self.delete_user_from_database(&mut connection, request.user_id())
|
||||||
.await
|
.await
|
||||||
@@ -136,7 +136,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user = self
|
let user = self
|
||||||
.get_user_from_email(&mut connection, request.email())
|
.get_user_from_email(&mut connection, request.email())
|
||||||
@@ -166,7 +166,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let session = self
|
let session = self
|
||||||
.create_session(&mut connection, request.user(), request.expiration())
|
.create_session(&mut connection, request.user(), request.expiration())
|
||||||
@@ -184,7 +184,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let session = self
|
let session = self
|
||||||
.get_auth_session(&mut connection, request.session_id())
|
.get_auth_session(&mut connection, request.session_id())
|
||||||
@@ -212,7 +212,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user_warren = self
|
let user_warren = self
|
||||||
.add_user_to_warren(&mut connection, request.user_warren())
|
.add_user_to_warren(&mut connection, request.user_warren())
|
||||||
@@ -230,7 +230,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user_warren = self
|
let user_warren = self
|
||||||
.update_user_warren(&mut connection, request.user_warren())
|
.update_user_warren(&mut connection, request.user_warren())
|
||||||
@@ -248,7 +248,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user_warren = self
|
let user_warren = self
|
||||||
.remove_user_from_warren(&mut connection, request.user_id(), request.warren_id())
|
.remove_user_from_warren(&mut connection, request.user_id(), request.warren_id())
|
||||||
@@ -272,7 +272,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user_warrens = self
|
let user_warrens = self
|
||||||
.get_user_warrens(&mut connection, request.user_id())
|
.get_user_warrens(&mut connection, request.user_id())
|
||||||
@@ -290,7 +290,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let user_warrens = self
|
let user_warrens = self
|
||||||
.get_all_user_warrens(&mut connection)
|
.get_all_user_warrens(&mut connection)
|
||||||
@@ -308,7 +308,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
self.get_user_warren(&mut connection, request.user_id(), request.warren_id())
|
self.get_user_warren(&mut connection, request.user_id(), request.warren_id())
|
||||||
.await
|
.await
|
||||||
@@ -326,7 +326,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let users = self
|
let users = self
|
||||||
.fetch_users(&mut connection)
|
.fetch_users(&mut connection)
|
||||||
@@ -345,7 +345,7 @@ impl AuthRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let users = self
|
let users = self
|
||||||
.fetch_users(&mut connection)
|
.fetch_users(&mut connection)
|
||||||
@@ -368,9 +368,9 @@ impl AuthRepository for Postgres {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Postgres {
|
impl Sqlite {
|
||||||
pub(super) async fn delete_expired_auth_sessions(
|
pub(super) async fn delete_expired_auth_sessions(
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
) -> Result<u64, sqlx::Error> {
|
) -> Result<u64, sqlx::Error> {
|
||||||
let delete_count = sqlx::query(
|
let delete_count = sqlx::query(
|
||||||
"
|
"
|
||||||
@@ -389,7 +389,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn create_user(
|
async fn create_user(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
name: &UserName,
|
name: &UserName,
|
||||||
email: &UserEmail,
|
email: &UserEmail,
|
||||||
password: &UserPassword,
|
password: &UserPassword,
|
||||||
@@ -402,6 +402,7 @@ impl Postgres {
|
|||||||
|
|
||||||
let user: User = sqlx::query_as(
|
let user: User = sqlx::query_as(
|
||||||
"INSERT INTO users (
|
"INSERT INTO users (
|
||||||
|
id,
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
hash,
|
hash,
|
||||||
@@ -411,12 +412,14 @@ impl Postgres {
|
|||||||
$1,
|
$1,
|
||||||
$2,
|
$2,
|
||||||
$3,
|
$3,
|
||||||
$4
|
$4,
|
||||||
|
$5
|
||||||
)
|
)
|
||||||
RETURNING
|
RETURNING
|
||||||
*
|
*
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
|
.bind(Uuid::new_v4())
|
||||||
.bind(name)
|
.bind(name)
|
||||||
.bind(email)
|
.bind(email)
|
||||||
.bind(password_hash)
|
.bind(password_hash)
|
||||||
@@ -431,7 +434,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn create_or_update_user(
|
async fn create_or_update_user(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
sub: &String,
|
sub: &String,
|
||||||
name: &UserName,
|
name: &UserName,
|
||||||
email: &UserEmail,
|
email: &UserEmail,
|
||||||
@@ -546,7 +549,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn edit_user(
|
async fn edit_user(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
id: &Uuid,
|
id: &Uuid,
|
||||||
name: &UserName,
|
name: &UserName,
|
||||||
email: &UserEmail,
|
email: &UserEmail,
|
||||||
@@ -592,7 +595,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn delete_user_sessions(
|
async fn delete_user_sessions(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
user_id: &Uuid,
|
user_id: &Uuid,
|
||||||
) -> Result<u64, sqlx::Error> {
|
) -> Result<u64, sqlx::Error> {
|
||||||
let rows_affected = sqlx::query(
|
let rows_affected = sqlx::query(
|
||||||
@@ -613,7 +616,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn delete_user_from_database(
|
async fn delete_user_from_database(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
user_id: &Uuid,
|
user_id: &Uuid,
|
||||||
) -> Result<User, sqlx::Error> {
|
) -> Result<User, sqlx::Error> {
|
||||||
let user: User = sqlx::query_as(
|
let user: User = sqlx::query_as(
|
||||||
@@ -635,7 +638,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn get_user_from_id(
|
async fn get_user_from_id(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
id: &Uuid,
|
id: &Uuid,
|
||||||
) -> Result<User, sqlx::Error> {
|
) -> Result<User, sqlx::Error> {
|
||||||
let user: User = sqlx::query_as(
|
let user: User = sqlx::query_as(
|
||||||
@@ -657,7 +660,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn get_user_from_email(
|
async fn get_user_from_email(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
email: &UserEmail,
|
email: &UserEmail,
|
||||||
) -> Result<User, sqlx::Error> {
|
) -> Result<User, sqlx::Error> {
|
||||||
let user: User = sqlx::query_as(
|
let user: User = sqlx::query_as(
|
||||||
@@ -698,7 +701,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn create_session(
|
async fn create_session(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
user: &User,
|
user: &User,
|
||||||
expiration: &SessionExpirationTime,
|
expiration: &SessionExpirationTime,
|
||||||
) -> anyhow::Result<AuthSession> {
|
) -> anyhow::Result<AuthSession> {
|
||||||
@@ -721,7 +724,7 @@ impl Postgres {
|
|||||||
) VALUES (
|
) VALUES (
|
||||||
$1,
|
$1,
|
||||||
$2,
|
$2,
|
||||||
TO_TIMESTAMP($3::double precision / 1000)
|
datetime($3, 'unixepoch')
|
||||||
)
|
)
|
||||||
RETURNING
|
RETURNING
|
||||||
*
|
*
|
||||||
@@ -729,7 +732,7 @@ impl Postgres {
|
|||||||
)
|
)
|
||||||
.bind(session_id)
|
.bind(session_id)
|
||||||
.bind(user.id())
|
.bind(user.id())
|
||||||
.bind(expiration_time)
|
.bind(expiration_time / 1000)
|
||||||
.fetch_one(&mut *tx)
|
.fetch_one(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -740,7 +743,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn get_auth_session(
|
async fn get_auth_session(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
session_id: &AuthSessionId,
|
session_id: &AuthSessionId,
|
||||||
) -> Result<AuthSession, sqlx::Error> {
|
) -> Result<AuthSession, sqlx::Error> {
|
||||||
let session: AuthSession = sqlx::query_as(
|
let session: AuthSession = sqlx::query_as(
|
||||||
@@ -762,7 +765,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn get_user_warrens(
|
async fn get_user_warrens(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
user_id: &Uuid,
|
user_id: &Uuid,
|
||||||
) -> Result<Vec<UserWarren>, sqlx::Error> {
|
) -> Result<Vec<UserWarren>, sqlx::Error> {
|
||||||
let user_warrens: Vec<UserWarren> = sqlx::query_as(
|
let user_warrens: Vec<UserWarren> = sqlx::query_as(
|
||||||
@@ -784,7 +787,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn get_all_user_warrens(
|
async fn get_all_user_warrens(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
) -> Result<Vec<UserWarren>, sqlx::Error> {
|
) -> Result<Vec<UserWarren>, sqlx::Error> {
|
||||||
let user_warrens: Vec<UserWarren> = sqlx::query_as(
|
let user_warrens: Vec<UserWarren> = sqlx::query_as(
|
||||||
"
|
"
|
||||||
@@ -802,7 +805,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn get_user_warren(
|
async fn get_user_warren(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
user_id: &Uuid,
|
user_id: &Uuid,
|
||||||
warren_id: &Uuid,
|
warren_id: &Uuid,
|
||||||
) -> Result<UserWarren, sqlx::Error> {
|
) -> Result<UserWarren, sqlx::Error> {
|
||||||
@@ -825,7 +828,10 @@ impl Postgres {
|
|||||||
Ok(ids)
|
Ok(ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_users(&self, connection: &mut PgConnection) -> Result<Vec<User>, sqlx::Error> {
|
async fn fetch_users(
|
||||||
|
&self,
|
||||||
|
connection: &mut SqliteConnection,
|
||||||
|
) -> Result<Vec<User>, sqlx::Error> {
|
||||||
let users: Vec<User> = sqlx::query_as(
|
let users: Vec<User> = sqlx::query_as(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -844,7 +850,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn add_user_to_warren(
|
async fn add_user_to_warren(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
user_warren: &UserWarren,
|
user_warren: &UserWarren,
|
||||||
) -> Result<UserWarren, sqlx::Error> {
|
) -> Result<UserWarren, sqlx::Error> {
|
||||||
let user_warren: UserWarren = sqlx::query_as(
|
let user_warren: UserWarren = sqlx::query_as(
|
||||||
@@ -855,14 +861,22 @@ impl Postgres {
|
|||||||
can_list_files,
|
can_list_files,
|
||||||
can_read_files,
|
can_read_files,
|
||||||
can_modify_files,
|
can_modify_files,
|
||||||
can_delete_files
|
can_delete_files,
|
||||||
|
can_list_shares,
|
||||||
|
can_create_shares,
|
||||||
|
can_modify_shares,
|
||||||
|
can_delete_shares
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1,
|
$1,
|
||||||
$2,
|
$2,
|
||||||
$3,
|
$3,
|
||||||
$4,
|
$4,
|
||||||
$5,
|
$5,
|
||||||
$6
|
$6,
|
||||||
|
$7,
|
||||||
|
$8,
|
||||||
|
$9,
|
||||||
|
$10
|
||||||
)
|
)
|
||||||
RETURNING
|
RETURNING
|
||||||
*
|
*
|
||||||
@@ -874,6 +888,10 @@ impl Postgres {
|
|||||||
.bind(user_warren.can_read_files())
|
.bind(user_warren.can_read_files())
|
||||||
.bind(user_warren.can_modify_files())
|
.bind(user_warren.can_modify_files())
|
||||||
.bind(user_warren.can_delete_files())
|
.bind(user_warren.can_delete_files())
|
||||||
|
.bind(user_warren.can_list_shares())
|
||||||
|
.bind(user_warren.can_create_shares())
|
||||||
|
.bind(user_warren.can_modify_shares())
|
||||||
|
.bind(user_warren.can_delete_shares())
|
||||||
.fetch_one(connection)
|
.fetch_one(connection)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -882,7 +900,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn update_user_warren(
|
async fn update_user_warren(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
user_warren: &UserWarren,
|
user_warren: &UserWarren,
|
||||||
) -> Result<UserWarren, sqlx::Error> {
|
) -> Result<UserWarren, sqlx::Error> {
|
||||||
let user_warren: UserWarren = sqlx::query_as(
|
let user_warren: UserWarren = sqlx::query_as(
|
||||||
@@ -923,7 +941,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn remove_user_from_warren(
|
async fn remove_user_from_warren(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
user_id: &Uuid,
|
user_id: &Uuid,
|
||||||
warren_id: &Uuid,
|
warren_id: &Uuid,
|
||||||
) -> Result<UserWarren, sqlx::Error> {
|
) -> Result<UserWarren, sqlx::Error> {
|
||||||
72
backend/src/lib/outbound/sqlite/mod.rs
Normal file
72
backend/src/lib/outbound/sqlite/mod.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
use std::{str::FromStr as _, time::Duration};
|
||||||
|
|
||||||
|
use sqlx::{
|
||||||
|
ConnectOptions as _, SqlitePool,
|
||||||
|
sqlite::{SqliteConnectOptions, SqlitePoolOptions},
|
||||||
|
};
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
pub mod auth;
|
||||||
|
pub mod options;
|
||||||
|
pub mod share;
|
||||||
|
pub mod warrens;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SqliteConfig {
|
||||||
|
database_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SqliteConfig {
|
||||||
|
pub fn new(database_url: String) -> Self {
|
||||||
|
Self { database_url }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Sqlite {
|
||||||
|
pool: SqlitePool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sqlite {
|
||||||
|
pub async fn new(config: SqliteConfig) -> anyhow::Result<Self> {
|
||||||
|
let opts = SqliteConnectOptions::from_str(&config.database_url)?
|
||||||
|
.create_if_missing(true)
|
||||||
|
.read_only(false)
|
||||||
|
.disable_statement_logging();
|
||||||
|
|
||||||
|
let pool = SqlitePoolOptions::new().connect_with(opts).await?;
|
||||||
|
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||||
|
|
||||||
|
// 3600 seconds = 1 hour
|
||||||
|
Self::start_cleanup_tasks(pool.clone(), Duration::from_secs(3600));
|
||||||
|
|
||||||
|
Ok(Self { pool })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn start_cleanup_tasks(pool: SqlitePool, interval: Duration) -> JoinHandle<()> {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
{
|
||||||
|
let Ok(mut connection) = pool.acquire().await else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(count) = Self::delete_expired_auth_sessions(&mut connection).await {
|
||||||
|
tracing::debug!("Removed {count} expired auth session(s)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(count) = Self::delete_expired_shares(&mut connection).await {
|
||||||
|
tracing::debug!("Deleted {count} expired share(s)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(interval).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!("Session cleanup task stopped");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn is_not_found_error(err: &sqlx::Error) -> bool {
|
||||||
|
matches!(err, sqlx::Error::RowNotFound)
|
||||||
|
}
|
||||||
134
backend/src/lib/outbound/sqlite/options.rs
Normal file
134
backend/src/lib/outbound/sqlite/options.rs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use sqlx::FromRow;
|
||||||
|
|
||||||
|
use crate::domain::warren::{
|
||||||
|
models::option::{
|
||||||
|
DeleteOptionError, DeleteOptionRequest, DeleteOptionResponse, GetOptionError,
|
||||||
|
GetOptionRequest, GetOptionResponse, OptionKey, OptionType, SetOptionError,
|
||||||
|
SetOptionRequest, SetOptionResponse,
|
||||||
|
},
|
||||||
|
ports::OptionRepository,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{Sqlite, is_not_found_error};
|
||||||
|
|
||||||
|
#[derive(Debug, FromRow)]
|
||||||
|
struct OptionRow {
|
||||||
|
key: String,
|
||||||
|
value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OptionRepository for Sqlite {
|
||||||
|
async fn get_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
request: GetOptionRequest,
|
||||||
|
) -> Result<GetOptionResponse<T>, GetOptionError> {
|
||||||
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
|
let key: OptionKey = request.into();
|
||||||
|
|
||||||
|
let row: OptionRow = sqlx::query_as(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
FROM
|
||||||
|
application_options
|
||||||
|
WHERE
|
||||||
|
key = $1",
|
||||||
|
)
|
||||||
|
.bind(key.as_str())
|
||||||
|
.fetch_one(&mut *connection)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
if is_not_found_error(&e) {
|
||||||
|
GetOptionError::NotFound(key)
|
||||||
|
} else {
|
||||||
|
GetOptionError::Unknown(e.into())
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let parsed = T::parse(&row.value).map_err(|_| GetOptionError::Parse)?;
|
||||||
|
|
||||||
|
Ok(GetOptionResponse::new(
|
||||||
|
OptionKey::new(&row.key).unwrap(),
|
||||||
|
parsed,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_option<T: OptionType>(
|
||||||
|
&self,
|
||||||
|
request: SetOptionRequest<T>,
|
||||||
|
) -> Result<SetOptionResponse<T>, SetOptionError> {
|
||||||
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
|
let (key, value) = request.unpack();
|
||||||
|
|
||||||
|
sqlx::query_as::<_, OptionRow>(
|
||||||
|
"
|
||||||
|
INSERT INTO application_options (
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2
|
||||||
|
)
|
||||||
|
RETURNING
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(key.as_str())
|
||||||
|
.bind(value.inner().to_string())
|
||||||
|
.fetch_one(&mut *connection)
|
||||||
|
.await
|
||||||
|
.map_err(|e| SetOptionError::Unknown(e.into()))?;
|
||||||
|
|
||||||
|
Ok(SetOptionResponse::new(key, value.get_inner()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_option(
|
||||||
|
&self,
|
||||||
|
request: DeleteOptionRequest,
|
||||||
|
) -> Result<DeleteOptionResponse, DeleteOptionError> {
|
||||||
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
|
let key: OptionKey = request.into();
|
||||||
|
|
||||||
|
sqlx::query_as::<_, OptionRow>(
|
||||||
|
"
|
||||||
|
DELETE FROM
|
||||||
|
application_options
|
||||||
|
WHERE
|
||||||
|
key = $1
|
||||||
|
RETURNING
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(key.as_str())
|
||||||
|
.fetch_one(&mut *connection)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
if is_not_found_error(&e) {
|
||||||
|
DeleteOptionError::NotFound(key.clone())
|
||||||
|
} else {
|
||||||
|
DeleteOptionError::Unknown(e.into())
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(DeleteOptionResponse::new(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use argon2::{
|
use argon2::{
|
||||||
Argon2, PasswordHash, PasswordVerifier as _,
|
Argon2, PasswordHash, PasswordVerifier as _,
|
||||||
password_hash::{PasswordHasher as _, SaltString, rand_core::OsRng},
|
password_hash::{PasswordHasher as _, SaltString},
|
||||||
};
|
};
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use sqlx::{Acquire as _, PgConnection};
|
use sqlx::{Acquire as _, SqliteConnection};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ use crate::domain::warren::models::{
|
|||||||
warren::HasWarrenId as _,
|
warren::HasWarrenId as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Postgres, is_not_found_error};
|
use super::{Sqlite, is_not_found_error};
|
||||||
|
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
struct ShareRow {
|
struct ShareRow {
|
||||||
@@ -62,7 +62,7 @@ impl TryFrom<ShareRow> for Share {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn get_share(
|
pub(super) async fn get_share(
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
request: GetShareRequest,
|
request: GetShareRequest,
|
||||||
) -> anyhow::Result<Share> {
|
) -> anyhow::Result<Share> {
|
||||||
let share_row: ShareRow = sqlx::query_as(
|
let share_row: ShareRow = sqlx::query_as(
|
||||||
@@ -90,7 +90,7 @@ pub(super) async fn get_share(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn list_shares(
|
pub(super) async fn list_shares(
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
request: ListSharesRequest,
|
request: ListSharesRequest,
|
||||||
) -> anyhow::Result<Vec<Share>> {
|
) -> anyhow::Result<Vec<Share>> {
|
||||||
let share_rows: Vec<ShareRow> = sqlx::query_as(
|
let share_rows: Vec<ShareRow> = sqlx::query_as(
|
||||||
@@ -107,7 +107,8 @@ pub(super) async fn list_shares(
|
|||||||
shares
|
shares
|
||||||
WHERE
|
WHERE
|
||||||
warren_id = $1 AND
|
warren_id = $1 AND
|
||||||
path = $2
|
path = $2 AND
|
||||||
|
(expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
|
||||||
ORDER BY
|
ORDER BY
|
||||||
created_at DESC
|
created_at DESC
|
||||||
",
|
",
|
||||||
@@ -126,13 +127,13 @@ pub(super) async fn list_shares(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn create_share(
|
pub(super) async fn create_share(
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
request: CreateShareRequest,
|
request: CreateShareRequest,
|
||||||
) -> anyhow::Result<Share> {
|
) -> anyhow::Result<Share> {
|
||||||
let mut tx = connection.begin().await?;
|
let mut tx = connection.begin().await?;
|
||||||
|
|
||||||
let password_hash = if let Some(password) = request.base().password() {
|
let password_hash = if let Some(password) = request.base().password() {
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
let salt = SaltString::generate(&mut argon2::password_hash::rand_core::OsRng);
|
||||||
let argon2 = Argon2::default();
|
let argon2 = Argon2::default();
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
@@ -154,6 +155,7 @@ pub(super) async fn create_share(
|
|||||||
let share: ShareRow = sqlx::query_as(
|
let share: ShareRow = sqlx::query_as(
|
||||||
"
|
"
|
||||||
INSERT INTO shares (
|
INSERT INTO shares (
|
||||||
|
id,
|
||||||
creator_id,
|
creator_id,
|
||||||
warren_id,
|
warren_id,
|
||||||
path,
|
path,
|
||||||
@@ -164,17 +166,19 @@ pub(super) async fn create_share(
|
|||||||
$2,
|
$2,
|
||||||
$3,
|
$3,
|
||||||
$4,
|
$4,
|
||||||
TO_TIMESTAMP($5::double precision / 1000)
|
$5,
|
||||||
|
datetime($6, 'unixepoch')
|
||||||
)
|
)
|
||||||
RETURNING
|
RETURNING
|
||||||
*
|
*
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
|
.bind(Uuid::new_v4())
|
||||||
.bind(request.creator_id())
|
.bind(request.creator_id())
|
||||||
.bind(request.warren_id())
|
.bind(request.warren_id())
|
||||||
.bind(request.base().path())
|
.bind(request.base().path())
|
||||||
.bind(password_hash)
|
.bind(password_hash)
|
||||||
.bind(expires_at)
|
.bind(expires_at.map(|v| v / 1000))
|
||||||
.fetch_one(&mut *tx)
|
.fetch_one(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -184,7 +188,7 @@ pub(super) async fn create_share(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn delete_share(
|
pub(super) async fn delete_share(
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
request: DeleteShareRequest,
|
request: DeleteShareRequest,
|
||||||
) -> anyhow::Result<Share> {
|
) -> anyhow::Result<Share> {
|
||||||
let mut tx = connection.begin().await?;
|
let mut tx = connection.begin().await?;
|
||||||
@@ -209,7 +213,7 @@ pub(super) async fn delete_share(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn verify_password(
|
pub(super) async fn verify_password(
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
request: VerifySharePasswordRequest,
|
request: VerifySharePasswordRequest,
|
||||||
) -> Result<Share, VerifySharePasswordError> {
|
) -> Result<Share, VerifySharePasswordError> {
|
||||||
let share_row: ShareRow = sqlx::query_as(
|
let share_row: ShareRow = sqlx::query_as(
|
||||||
@@ -264,9 +268,9 @@ pub(super) async fn verify_password(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Postgres {
|
impl Sqlite {
|
||||||
pub(super) async fn delete_expired_shares(
|
pub(super) async fn delete_expired_shares(
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
) -> Result<u64, sqlx::Error> {
|
) -> Result<u64, sqlx::Error> {
|
||||||
let delete_count = sqlx::query(
|
let delete_count = sqlx::query(
|
||||||
"
|
"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use anyhow::{Context as _, anyhow};
|
use anyhow::{Context as _, anyhow};
|
||||||
use sqlx::{Acquire as _, PgConnection};
|
use sqlx::{Acquire as _, SqliteConnection};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::domain::warren::{
|
use crate::domain::warren::{
|
||||||
@@ -21,9 +21,9 @@ use crate::domain::warren::{
|
|||||||
ports::WarrenRepository,
|
ports::WarrenRepository,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Postgres, is_not_found_error};
|
use super::{Sqlite, is_not_found_error};
|
||||||
|
|
||||||
impl WarrenRepository for Postgres {
|
impl WarrenRepository for Sqlite {
|
||||||
async fn create_warren(
|
async fn create_warren(
|
||||||
&self,
|
&self,
|
||||||
request: CreateWarrenRequest,
|
request: CreateWarrenRequest,
|
||||||
@@ -32,7 +32,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let warren = self
|
let warren = self
|
||||||
.create_warren(&mut connection, request.name(), request.path())
|
.create_warren(&mut connection, request.name(), request.path())
|
||||||
@@ -47,7 +47,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let warren = self
|
let warren = self
|
||||||
.edit_warren(
|
.edit_warren(
|
||||||
@@ -70,7 +70,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let warren = self
|
let warren = self
|
||||||
.delete_warren(&mut connection, request.id())
|
.delete_warren(&mut connection, request.id())
|
||||||
@@ -88,7 +88,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let warrens = self
|
let warrens = self
|
||||||
.fetch_warrens(&mut connection, request.ids())
|
.fetch_warrens(&mut connection, request.ids())
|
||||||
@@ -106,7 +106,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let warrens = self
|
let warrens = self
|
||||||
.fetch_all_warrens(&mut connection)
|
.fetch_all_warrens(&mut connection)
|
||||||
@@ -121,7 +121,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let warren = self
|
let warren = self
|
||||||
.get_warren(&mut connection, request.id())
|
.get_warren(&mut connection, request.id())
|
||||||
@@ -144,7 +144,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
super::share::get_share(&mut connection, request)
|
super::share::get_share(&mut connection, request)
|
||||||
.await
|
.await
|
||||||
@@ -159,7 +159,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
super::share::create_share(&mut connection, request)
|
super::share::create_share(&mut connection, request)
|
||||||
.await
|
.await
|
||||||
@@ -177,7 +177,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
let path = request.path().clone();
|
let path = request.path().clone();
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
super::share::delete_share(&mut connection, request)
|
super::share::delete_share(&mut connection, request)
|
||||||
.await
|
.await
|
||||||
@@ -211,7 +211,7 @@ impl WarrenRepository for Postgres {
|
|||||||
.pool
|
.pool
|
||||||
.acquire()
|
.acquire()
|
||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a Sqlite connection")?;
|
||||||
|
|
||||||
super::share::verify_password(&mut connection, request)
|
super::share::verify_password(&mut connection, request)
|
||||||
.await
|
.await
|
||||||
@@ -220,10 +220,10 @@ impl WarrenRepository for Postgres {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Postgres {
|
impl Sqlite {
|
||||||
async fn create_warren(
|
async fn create_warren(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
name: &WarrenName,
|
name: &WarrenName,
|
||||||
path: &AbsoluteFilePath,
|
path: &AbsoluteFilePath,
|
||||||
) -> Result<Warren, sqlx::Error> {
|
) -> Result<Warren, sqlx::Error> {
|
||||||
@@ -232,16 +232,19 @@ impl Postgres {
|
|||||||
let warren: Warren = sqlx::query_as(
|
let warren: Warren = sqlx::query_as(
|
||||||
"
|
"
|
||||||
INSERT INTO warrens (
|
INSERT INTO warrens (
|
||||||
|
id,
|
||||||
name,
|
name,
|
||||||
path
|
path
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1,
|
$1,
|
||||||
$2
|
$2,
|
||||||
|
$3
|
||||||
)
|
)
|
||||||
RETURNING
|
RETURNING
|
||||||
*
|
*
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
|
.bind(Uuid::new_v4())
|
||||||
.bind(name)
|
.bind(name)
|
||||||
.bind(path)
|
.bind(path)
|
||||||
.fetch_one(&mut *tx)
|
.fetch_one(&mut *tx)
|
||||||
@@ -254,7 +257,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn edit_warren(
|
async fn edit_warren(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
id: &Uuid,
|
id: &Uuid,
|
||||||
name: &WarrenName,
|
name: &WarrenName,
|
||||||
path: &AbsoluteFilePath,
|
path: &AbsoluteFilePath,
|
||||||
@@ -287,7 +290,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn delete_warren(
|
async fn delete_warren(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
id: &Uuid,
|
id: &Uuid,
|
||||||
) -> Result<Warren, sqlx::Error> {
|
) -> Result<Warren, sqlx::Error> {
|
||||||
let mut tx = connection.begin().await?;
|
let mut tx = connection.begin().await?;
|
||||||
@@ -313,7 +316,7 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn get_warren(
|
async fn get_warren(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
id: &Uuid,
|
id: &Uuid,
|
||||||
) -> Result<Warren, sqlx::Error> {
|
) -> Result<Warren, sqlx::Error> {
|
||||||
let warren: Warren = sqlx::query_as(
|
let warren: Warren = sqlx::query_as(
|
||||||
@@ -335,20 +338,28 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn fetch_warrens(
|
async fn fetch_warrens(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
ids: &[Uuid],
|
ids: &[Uuid],
|
||||||
) -> Result<Vec<Warren>, sqlx::Error> {
|
) -> Result<Vec<Warren>, sqlx::Error> {
|
||||||
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Postgres, Warren>(
|
let mut ids_as_string = ids.into_iter().fold(String::new(), |mut acc, id| {
|
||||||
|
let encoded = hex::encode(id.as_bytes());
|
||||||
|
acc.push_str("x'");
|
||||||
|
acc.push_str(encoded.as_str());
|
||||||
|
acc.push_str("',");
|
||||||
|
acc
|
||||||
|
});
|
||||||
|
ids_as_string.pop();
|
||||||
|
|
||||||
|
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Sqlite, Warren>(&format!(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
*
|
*
|
||||||
FROM
|
FROM
|
||||||
warrens
|
warrens
|
||||||
WHERE
|
WHERE
|
||||||
id = ANY($1)
|
id IN ({ids_as_string})
|
||||||
",
|
",
|
||||||
)
|
))
|
||||||
.bind(ids)
|
|
||||||
.fetch_all(&mut *connection)
|
.fetch_all(&mut *connection)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -357,9 +368,9 @@ impl Postgres {
|
|||||||
|
|
||||||
async fn fetch_all_warrens(
|
async fn fetch_all_warrens(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut SqliteConnection,
|
||||||
) -> Result<Vec<Warren>, sqlx::Error> {
|
) -> Result<Vec<Warren>, sqlx::Error> {
|
||||||
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Postgres, Warren>(
|
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Sqlite, Warren>(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
*
|
*
|
||||||
19
compose.yaml
19
compose.yaml
@@ -1,7 +1,5 @@
|
|||||||
services:
|
services:
|
||||||
warren:
|
warren:
|
||||||
depends_on:
|
|
||||||
- 'postgres'
|
|
||||||
image: 'warren:latest'
|
image: 'warren:latest'
|
||||||
container_name: 'warren'
|
container_name: 'warren'
|
||||||
build: '.'
|
build: '.'
|
||||||
@@ -13,26 +11,15 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- 'SERVER_ADDRESS=0.0.0.0'
|
- 'SERVER_ADDRESS=0.0.0.0'
|
||||||
- 'SERVER_PORT=8080'
|
- 'SERVER_PORT=8080'
|
||||||
- 'DATABASE_URL=postgres://postgres:pg@warren-postgres:5432'
|
- 'DATABASE_URL=sqlite:///var/lib/warren/data/warren.db'
|
||||||
- 'DATABASE_NAME=warren'
|
|
||||||
- 'SERVE_DIRECTORY=/serve'
|
- 'SERVE_DIRECTORY=/serve'
|
||||||
- 'CORS_ALLOW_ORIGIN=http://localhost:8081'
|
- 'CORS_ALLOW_ORIGIN=http://localhost:8081'
|
||||||
- 'LOG_LEVEL=debug'
|
- 'LOG_LEVEL=debug'
|
||||||
- 'MAX_FILE_FETCH_BYTES=10737418240'
|
- 'MAX_FILE_FETCH_BYTES=10737418240'
|
||||||
|
- 'ZIP_READ_BUFFER_BYTES=4096'
|
||||||
volumes:
|
volumes:
|
||||||
- './backend/serve:/serve:rw'
|
- './backend/serve:/serve:rw'
|
||||||
postgres:
|
- './backend/data:/var/lib/warren/data:rw'
|
||||||
image: 'postgres:17'
|
|
||||||
container_name: 'warren-db'
|
|
||||||
hostname: 'warren-postgres'
|
|
||||||
networks:
|
|
||||||
- 'warren-net'
|
|
||||||
volumes:
|
|
||||||
- './postgres-data:/var/lib/postgresql/data'
|
|
||||||
environment:
|
|
||||||
- 'POSTGRES_PASSWORD=pg'
|
|
||||||
ports:
|
|
||||||
- '5432:5432/tcp'
|
|
||||||
networks:
|
networks:
|
||||||
warren-net:
|
warren-net:
|
||||||
name: 'warren-net'
|
name: 'warren-net'
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ function onClearCopy() {
|
|||||||
<ContextMenuContent>
|
<ContextMenuContent>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
:class="[warrenStore.current == null && 'hidden']"
|
:class="[warrenStore.current == null && 'hidden']"
|
||||||
|
@pointerdown.stop
|
||||||
@select="openRenameDialog"
|
@select="openRenameDialog"
|
||||||
>
|
>
|
||||||
<Icon name="lucide:pencil" />
|
<Icon name="lucide:pencil" />
|
||||||
@@ -136,22 +137,24 @@ function onClearCopy() {
|
|||||||
copyStore.file.path !== warrenStore.current.path ||
|
copyStore.file.path !== warrenStore.current.path ||
|
||||||
copyStore.file.name !== entry.name
|
copyStore.file.name !== entry.name
|
||||||
"
|
"
|
||||||
|
@pointerdown.stop
|
||||||
@select="onCopy"
|
@select="onCopy"
|
||||||
>
|
>
|
||||||
<Icon name="lucide:copy" />
|
<Icon name="lucide:copy" />
|
||||||
Copy
|
Copy
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem v-else @select="onClearCopy">
|
<ContextMenuItem v-else @pointerdown.stop @select="onClearCopy">
|
||||||
<Icon name="lucide:copy-x" />
|
<Icon name="lucide:copy-x" />
|
||||||
Clear clipboard
|
Clear clipboard
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</template>
|
</template>
|
||||||
<ContextMenuItem @select="onDownload">
|
<ContextMenuItem @pointerdown.stop @select="onDownload">
|
||||||
<Icon name="lucide:download" />
|
<Icon name="lucide:download" />
|
||||||
Download
|
Download
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
:class="[warrenStore.current == null && 'hidden']"
|
:class="[warrenStore.current == null && 'hidden']"
|
||||||
|
@pointerdown.stop
|
||||||
@select="onShare"
|
@select="onShare"
|
||||||
>
|
>
|
||||||
<Icon name="lucide:share" />
|
<Icon name="lucide:share" />
|
||||||
@@ -164,6 +167,7 @@ function onClearCopy() {
|
|||||||
|
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
:class="[warrenStore.current == null && 'hidden']"
|
:class="[warrenStore.current == null && 'hidden']"
|
||||||
|
@pointerdown.stop
|
||||||
@select="() => onDelete(false)"
|
@select="() => onDelete(false)"
|
||||||
>
|
>
|
||||||
<Icon name="lucide:trash-2" />
|
<Icon name="lucide:trash-2" />
|
||||||
@@ -171,6 +175,7 @@ function onClearCopy() {
|
|||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
:class="[warrenStore.current == null && 'hidden']"
|
:class="[warrenStore.current == null && 'hidden']"
|
||||||
|
@pointerdown.stop
|
||||||
@select="() => onDelete(true)"
|
@select="() => onDelete(true)"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const width = computed(() => Math.abs(rect.a.x - rect.b.x));
|
|||||||
const height = computed(() => Math.abs(rect.a.y - rect.b.y));
|
const height = computed(() => Math.abs(rect.a.y - rect.b.y));
|
||||||
|
|
||||||
function onDocumentPointerDown(e: MouseEvent) {
|
function onDocumentPointerDown(e: MouseEvent) {
|
||||||
if (e.button !== 0) {
|
if (e.button !== 0 || matchMedia('(pointer:coarse)').matches) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ function onDocumentPointerDown(e: MouseEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onDocumentPointerMove(e: MouseEvent) {
|
function onDocumentPointerMove(e: MouseEvent) {
|
||||||
if (!rect.enabled) {
|
if (!rect.enabled || matchMedia('(pointer:coarse)').matches) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,11 @@ function onDocumentPointerMove(e: MouseEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onDocumentPointerUp(e: MouseEvent) {
|
function onDocumentPointerUp(e: MouseEvent) {
|
||||||
if (e.button !== 0 || !rect.enabled) {
|
if (
|
||||||
|
!rect.enabled ||
|
||||||
|
e.button !== 0 ||
|
||||||
|
matchMedia('(pointer:coarse)').matches
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ const form = useForm({
|
|||||||
canReadFiles: false,
|
canReadFiles: false,
|
||||||
canModifyFiles: false,
|
canModifyFiles: false,
|
||||||
canDeleteFiles: false,
|
canDeleteFiles: false,
|
||||||
|
|
||||||
|
canListShares: false,
|
||||||
|
canCreateShares: false,
|
||||||
|
canModifyShares: false,
|
||||||
|
canDeleteShares: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -231,6 +236,70 @@ const onSubmit = form.handleSubmit(async (values) => {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
v-slot="{ value, handleChange }"
|
||||||
|
name="canListShares"
|
||||||
|
>
|
||||||
|
<FormItem class="flex flex-row justify-between">
|
||||||
|
<FormLabel class="grow">List shares</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
:model-value="value"
|
||||||
|
@update:model-value="handleChange"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
v-slot="{ value, handleChange }"
|
||||||
|
name="canCreateShares"
|
||||||
|
>
|
||||||
|
<FormItem class="flex flex-row justify-between">
|
||||||
|
<FormLabel class="grow">Create shares</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
:model-value="value"
|
||||||
|
@update:model-value="handleChange"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
v-slot="{ value, handleChange }"
|
||||||
|
name="canModifyShares"
|
||||||
|
>
|
||||||
|
<FormItem class="flex flex-row justify-between">
|
||||||
|
<FormLabel class="grow">Modify shares</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
:model-value="value"
|
||||||
|
@update:model-value="handleChange"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
v-slot="{ value, handleChange }"
|
||||||
|
name="canDeleteShares"
|
||||||
|
>
|
||||||
|
<FormItem class="flex flex-row justify-between">
|
||||||
|
<FormLabel class="grow">Delete shares</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
:model-value="value"
|
||||||
|
@update:model-value="handleChange"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const textarea = ref<HTMLTextAreaElement>(
|
|||||||
null as unknown as HTMLTextAreaElement
|
null as unknown as HTMLTextAreaElement
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentLine = ref<number>(0);
|
const currentLine = ref<number>(1);
|
||||||
const totalLines = computed(
|
const totalLines = computed(
|
||||||
() => editor.data?.editedContent.split('\n').length ?? 0
|
() => editor.data?.editedContent.split('\n').length ?? 0
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ export async function renameWarrenEntry(
|
|||||||
headers: getApiHeaders(),
|
headers: getApiHeaders(),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
warrenId,
|
warrenId,
|
||||||
path,
|
paths: [path],
|
||||||
targetPath,
|
targetPath,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ export const userWarrenSchema = object({
|
|||||||
canReadFiles: boolean().required(),
|
canReadFiles: boolean().required(),
|
||||||
canModifyFiles: boolean().required(),
|
canModifyFiles: boolean().required(),
|
||||||
canDeleteFiles: boolean().required(),
|
canDeleteFiles: boolean().required(),
|
||||||
|
|
||||||
|
canListShares: boolean().required(),
|
||||||
|
canCreateShares: boolean().required(),
|
||||||
|
canModifyShares: boolean().required(),
|
||||||
|
canDeleteShares: boolean().required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createWarrenSchema = object({
|
export const createWarrenSchema = object({
|
||||||
|
|||||||
Reference in New Issue
Block a user