Merge remote-tracking branch 'refs/remotes/origin/rear_auth' into rear_auth
This commit is contained in:
Generated
+25
-12
@@ -1839,9 +1839,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minijinja"
|
name = "minijinja"
|
||||||
version = "1.0.11"
|
version = "2.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "431d72874542d43aba1ca605870eacab134fdeb0c8fe27666ecf4b2662239df2"
|
checksum = "933ee10775d58fca8238a84fe165dfe4bde8b07d7574f24d76ffea91170f3ac6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memo-map",
|
"memo-map",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
@@ -1851,9 +1851,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minijinja-autoreload"
|
name = "minijinja-autoreload"
|
||||||
version = "1.0.8"
|
version = "2.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dbe548b8e2b0590e25f0baf95f76c1d7ea73ca264f1f90fe1bf6e00cc6612d19"
|
checksum = "bfe3362301b5f450f0f07175cc8cacdd9edb352c7d0375af72646dfb5769fc2a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"minijinja",
|
"minijinja",
|
||||||
"notify",
|
"notify",
|
||||||
@@ -1872,6 +1872,8 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum 0.7.4",
|
"axum 0.7.4",
|
||||||
|
"axum-login",
|
||||||
|
"axum-messages",
|
||||||
"barrel",
|
"barrel",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
@@ -1894,6 +1896,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
|
"tower-sessions",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2002,6 +2005,12 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.45"
|
version = "0.1.45"
|
||||||
@@ -2385,9 +2394,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pulldown-cmark"
|
name = "pulldown-cmark"
|
||||||
version = "0.10.0"
|
version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7"
|
checksum = "8746739f11d39ce5ad5c2520a9b75285310dbfe78c541ccf832d38615765aec0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.4.0",
|
"bitflags 2.4.0",
|
||||||
"getopts",
|
"getopts",
|
||||||
@@ -2398,9 +2407,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pulldown-cmark-escape"
|
name = "pulldown-cmark-escape"
|
||||||
version = "0.10.0"
|
version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d5d8f9aa0e3cbcfaf8bf00300004ee3b72f74770f9cbac93f6928771f613276b"
|
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
@@ -2461,6 +2470,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"slug",
|
"slug",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
@@ -2475,6 +2485,7 @@ dependencies = [
|
|||||||
"axum-login",
|
"axum-login",
|
||||||
"axum-messages",
|
"axum-messages",
|
||||||
"blake3",
|
"blake3",
|
||||||
|
"chrono",
|
||||||
"entity",
|
"entity",
|
||||||
"log",
|
"log",
|
||||||
"password-auth",
|
"password-auth",
|
||||||
@@ -3455,12 +3466,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.31"
|
version = "0.3.36"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e"
|
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"itoa",
|
"itoa",
|
||||||
|
"num-conv",
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde",
|
"serde",
|
||||||
"time-core",
|
"time-core",
|
||||||
@@ -3475,10 +3487,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.16"
|
version = "0.2.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f"
|
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -28,16 +28,18 @@ sea-orm = { version = "0.12.10", features = [
|
|||||||
sqlformat = { version = "0.2.2", optional = true }
|
sqlformat = { version = "0.2.2", optional = true }
|
||||||
anyhow = "1.0.75"
|
anyhow = "1.0.75"
|
||||||
axum = "0.7"
|
axum = "0.7"
|
||||||
|
axum-login = "0.15.3"
|
||||||
|
axum-messages = "0.6.1"
|
||||||
barrel = { version = "0.7.0", optional = true, features = ["pg"] }
|
barrel = { version = "0.7.0", optional = true, features = ["pg"] }
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
mime_guess = "2.0.4"
|
mime_guess = "2.0.4"
|
||||||
minijinja = { version = "1.0.11", features = [
|
minijinja = { version = "2.0.3", features = [
|
||||||
"loader",
|
"loader",
|
||||||
"builtins",
|
"builtins",
|
||||||
"urlencode",
|
"urlencode",
|
||||||
"deserialization",
|
"deserialization",
|
||||||
] }
|
] }
|
||||||
minijinja-autoreload = "1.0.8"
|
minijinja-autoreload = "2.0.3"
|
||||||
once_cell = "1.18.0"
|
once_cell = "1.18.0"
|
||||||
rust-embed = { version = "8.0.0", features = [
|
rust-embed = { version = "8.0.0", features = [
|
||||||
"axum",
|
"axum",
|
||||||
@@ -56,3 +58,4 @@ tracing = "0.1.40"
|
|||||||
tower-http = { version = "0.5.1", features = ["trace"] }
|
tower-http = { version = "0.5.1", features = ["trace"] }
|
||||||
chrono = "0.4.38"
|
chrono = "0.4.38"
|
||||||
thiserror = "1.0.61"
|
thiserror = "1.0.61"
|
||||||
|
tower-sessions = { version = "0.12.2" }
|
||||||
|
|||||||
@@ -66,3 +66,7 @@ These errors should encompass:
|
|||||||
|
|
||||||
Repository functions also need to be redesigned to be Results.
|
Repository functions also need to be redesigned to be Results.
|
||||||
|
|
||||||
|
Finally, RepositoryResponse might require us to add abilities to
|
||||||
|
- render your own html ?
|
||||||
|
- at least influence HX Response Headers for htmx support
|
||||||
|
|
||||||
+4
-5
@@ -3,18 +3,17 @@ name = "entity"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
default-run = "entity"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "entity"
|
name = "entity"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
tokio = { version = "1.32.0", features = ["full"] }
|
tokio = { version = "1.32.0", features = ["full"] }
|
||||||
|
|
||||||
[dependencies.sea-orm]
|
[dependencies.sea-orm]
|
||||||
version = "0.12.10" # sea-orm version
|
version = "0.12.10" # sea-orm version
|
||||||
features = [
|
features = ["runtime-tokio-native-tls", "sqlx-postgres"]
|
||||||
"runtime-tokio-native-tls",
|
|
||||||
"sqlx-postgres",
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
use sea_orm::{
|
||||||
|
ConnectionTrait, Database, DatabaseConnection, DbConn, EntityTrait, Schema, Statement,
|
||||||
|
};
|
||||||
|
|
||||||
|
use entity::{group, group_permission, permission, user, user_group, user_permission};
|
||||||
|
|
||||||
|
async fn drop_table<E>(db: &DatabaseConnection, entity: E)
|
||||||
|
where
|
||||||
|
E: EntityTrait,
|
||||||
|
{
|
||||||
|
let table_name = entity.table_name();
|
||||||
|
let sql = format!("DROP TABLE IF EXISTS {} CASCADE;", table_name.to_string());
|
||||||
|
|
||||||
|
println!("{}", sql);
|
||||||
|
|
||||||
|
let stmt = Statement::from_string(db.get_database_backend(), sql);
|
||||||
|
|
||||||
|
match db.execute(stmt).await {
|
||||||
|
Ok(_) => println!("Dropped table {}", table_name),
|
||||||
|
Err(e) => println!("Error dropping table {}: {}", table_name, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn drop_tables(db: &DbConn) {
|
||||||
|
drop_table(db, user::Entity).await;
|
||||||
|
drop_table(db, permission::Entity).await;
|
||||||
|
drop_table(db, group::Entity).await;
|
||||||
|
drop_table(db, user_permission::Entity).await;
|
||||||
|
drop_table(db, group_permission::Entity).await;
|
||||||
|
drop_table(db, user_group::Entity).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// Running Entities manually creates the tables from the entities in their latest incarnation.
|
||||||
|
println!("Connecting to database...");
|
||||||
|
let db: DatabaseConnection =
|
||||||
|
Database::connect("postgresql://miniweb:miniweb@localhost:54321/miniweb")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
println!("Dropping tables for entities...");
|
||||||
|
drop_tables(&db).await;
|
||||||
|
}
|
||||||
+5
-1
@@ -21,7 +21,7 @@ where
|
|||||||
let stmt = backend.build(&*table_create_statement);
|
let stmt = backend.build(&*table_create_statement);
|
||||||
|
|
||||||
match db.execute(stmt).await {
|
match db.execute(stmt).await {
|
||||||
Ok(_) => println!("Migrated {}", entity.table_name()),
|
Ok(_) => println!("Created {}", entity.table_name()),
|
||||||
Err(e) => println!("Error: {}", e),
|
Err(e) => println!("Error: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,6 +29,10 @@ where
|
|||||||
pub async fn create_tables(db: &DbConn) {
|
pub async fn create_tables(db: &DbConn) {
|
||||||
create_table(db, user::Entity).await;
|
create_table(db, user::Entity).await;
|
||||||
create_table(db, permission::Entity).await;
|
create_table(db, permission::Entity).await;
|
||||||
|
create_table(db, group::Entity).await;
|
||||||
|
create_table(db, user_permission::Entity).await;
|
||||||
|
create_table(db, group_permission::Entity).await;
|
||||||
|
create_table(db, user_group::Entity).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|||||||
+12
-2
@@ -1,4 +1,5 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
use sea_orm::ActiveValue::{NotSet, Set};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::group::Entity as Group;
|
use super::group::Entity as Group;
|
||||||
@@ -12,7 +13,7 @@ pub struct Model {
|
|||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
#[serde(skip_deserializing)]
|
#[serde(skip_deserializing)]
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
#[sea_orm(index = "user_usernames")]
|
#[sea_orm(index = "user_usernames", unique)]
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
|
||||||
@@ -32,7 +33,16 @@ pub struct Model {
|
|||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {}
|
pub enum Relation {}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
is_active: Set(true),
|
||||||
|
is_staff: Set(false),
|
||||||
|
is_superuser: Set(false),
|
||||||
|
..<Self as ActiveModelTrait>::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Related<Group> for Entity {
|
impl Related<Group> for Entity {
|
||||||
// The final relation is User -> UserGroup -> Group
|
// The final relation is User -> UserGroup -> Group
|
||||||
|
|||||||
+4
-3
@@ -11,14 +11,14 @@ path = "src/lib.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.75"
|
anyhow = "1.0.75"
|
||||||
axum = "0.7"
|
axum = "0.7"
|
||||||
minijinja = { version = "1.0.11", features = [
|
minijinja = { version = "2.0.3", features = [
|
||||||
"loader",
|
"loader",
|
||||||
"builtins",
|
"builtins",
|
||||||
"urlencode",
|
"urlencode",
|
||||||
"deserialization",
|
"deserialization",
|
||||||
] }
|
] }
|
||||||
minijinja-autoreload = "1.0.8"
|
minijinja-autoreload = "2.0.3"
|
||||||
pulldown-cmark = "0.10"
|
pulldown-cmark = "0.11"
|
||||||
serde = { version = "1.0.188", features = ["derive"] }
|
serde = { version = "1.0.188", features = ["derive"] }
|
||||||
tokio = { version = "1.32.0", features = ["full"] }
|
tokio = { version = "1.32.0", features = ["full"] }
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
@@ -26,3 +26,4 @@ serde_json = "1.0.108"
|
|||||||
slug = "0.1.5"
|
slug = "0.1.5"
|
||||||
async-trait = "0.1.77"
|
async-trait = "0.1.77"
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
|
thiserror = "1.0.61"
|
||||||
|
|||||||
@@ -1,482 +0,0 @@
|
|||||||
pub use config::AdminModelConfig;
|
|
||||||
pub use dto::AdminApp;
|
|
||||||
pub use dto::AdminModel;
|
|
||||||
pub use repository::{
|
|
||||||
AdminRepository, DynAdminRepository, RepoInfo, RepositoryContext, RepositoryInfo,
|
|
||||||
RepositoryItem, RepositoryList, Widget,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod auth {
|
|
||||||
|
|
||||||
struct AdminUser {}
|
|
||||||
|
|
||||||
struct AdminRole {}
|
|
||||||
|
|
||||||
struct AdminGroup {}
|
|
||||||
|
|
||||||
struct AdminActionLog {}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod config {
|
|
||||||
// user uses this configuration object to register another model.
|
|
||||||
pub struct AdminModelConfig {
|
|
||||||
pub name: String,
|
|
||||||
pub app_key: String,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod dto {
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub struct AdminModel {
|
|
||||||
pub key: String,
|
|
||||||
pub name: String,
|
|
||||||
|
|
||||||
pub admin_url: String,
|
|
||||||
pub view_only: bool,
|
|
||||||
|
|
||||||
pub add_url: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub struct AdminApp {
|
|
||||||
pub key: String,
|
|
||||||
pub name: String,
|
|
||||||
|
|
||||||
pub app_url: String,
|
|
||||||
pub models: Vec<AdminModel>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod repository {
|
|
||||||
use super::dto::AdminModel;
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use serde::{Serialize, Serializer};
|
|
||||||
use serde_json::Value;
|
|
||||||
use std::any::Any;
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use std::vec::IntoIter;
|
|
||||||
|
|
||||||
pub type RepositoryContext = AdminModel;
|
|
||||||
|
|
||||||
impl RepositoryContext {
|
|
||||||
pub fn get_default_detail_url(&self, key: &str) -> Option<String> {
|
|
||||||
Some(format!("{}/detail/{}", self.admin_url, key))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_default_change_url(&self, key: &str) -> Option<String> {
|
|
||||||
Some(format!("{}/change/{}", self.admin_url, key))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_item(&self, key: &str, fields: Value) -> RepositoryItem {
|
|
||||||
RepositoryItem {
|
|
||||||
detail_url: self.get_default_detail_url(key),
|
|
||||||
change_url: self.get_default_change_url(key),
|
|
||||||
fields: fields,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This is a static configuration object.
|
|
||||||
/// It might be changed in the future to have a dynamic counterpart.
|
|
||||||
///
|
|
||||||
/// ## Example:
|
|
||||||
/// Creating a simple required and readonly text input field with a label:
|
|
||||||
///
|
|
||||||
/// ```ignore
|
|
||||||
/// let my_field = Field::widget("/admin/widgets/input_text.jinja")
|
|
||||||
/// .labelled("Username")
|
|
||||||
/// .required()
|
|
||||||
/// .readonly();
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Serialize, Clone, Copy)]
|
|
||||||
pub struct Widget {
|
|
||||||
pub widget: &'static str,
|
|
||||||
pub label: Option<&'static str>,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub field_type: &'static str,
|
|
||||||
pub required: bool,
|
|
||||||
pub readonly: bool,
|
|
||||||
pub options: &'static [(&'static str, &'static str)],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget {
|
|
||||||
pub fn widget(widget: &'static str) -> Self {
|
|
||||||
Widget {
|
|
||||||
widget: widget,
|
|
||||||
label: None,
|
|
||||||
field_type: "text",
|
|
||||||
required: false,
|
|
||||||
readonly: false,
|
|
||||||
options: &[],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default() -> Self {
|
|
||||||
Self::widget("/admin/widgets/input_text.jinja")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn textarea() -> Self {
|
|
||||||
Self::widget("/admin/widgets/input_textarea.jinja")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn required(mut self) -> Self {
|
|
||||||
self.required = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readonly(mut self) -> Self {
|
|
||||||
self.readonly = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn labeled(mut self, label: &'static str) -> Self {
|
|
||||||
self.label = Some(label);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_password(mut self) -> Self {
|
|
||||||
self.field_type = "password";
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_hidden(mut self) -> Self {
|
|
||||||
self.field_type = "hidden";
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn options(mut self, options: &'static [(&'static str, &'static str)]) -> Self {
|
|
||||||
self.options = options;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct Field {
|
|
||||||
widget: String,
|
|
||||||
label: Option<String>,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
field_type: String,
|
|
||||||
readonly: bool,
|
|
||||||
required: bool,
|
|
||||||
options: Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Widget> for Field {
|
|
||||||
fn from(value: Widget) -> Self {
|
|
||||||
Field {
|
|
||||||
widget: value.widget.to_string(),
|
|
||||||
label: value.label.map(|s| s.to_string()),
|
|
||||||
field_type: value.field_type.to_string(),
|
|
||||||
readonly: value.readonly,
|
|
||||||
required: value.required,
|
|
||||||
options: value
|
|
||||||
.options
|
|
||||||
.iter()
|
|
||||||
.map(|(key, val)| (key.to_string(), serde_json::json!(val)))
|
|
||||||
.collect::<serde_json::Map<String, Value>>()
|
|
||||||
.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct RepositoryItem {
|
|
||||||
pub fields: Value,
|
|
||||||
pub detail_url: Option<String>,
|
|
||||||
pub change_url: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum RepositoryList {
|
|
||||||
Empty,
|
|
||||||
List {
|
|
||||||
values: Vec<RepositoryItem>,
|
|
||||||
},
|
|
||||||
Page {
|
|
||||||
values: Vec<RepositoryItem>,
|
|
||||||
offset: usize,
|
|
||||||
total: usize,
|
|
||||||
},
|
|
||||||
Stream {
|
|
||||||
values: Vec<RepositoryItem>,
|
|
||||||
next_index: Option<String>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoIterator for RepositoryList {
|
|
||||||
type Item = RepositoryItem;
|
|
||||||
type IntoIter = IntoIter<Self::Item>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
match self {
|
|
||||||
RepositoryList::Empty => vec![].into_iter(),
|
|
||||||
RepositoryList::List { values } => values.into_iter(),
|
|
||||||
RepositoryList::Page { values, .. } => values.into_iter(),
|
|
||||||
RepositoryList::Stream { values, .. } => values.into_iter(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for RepositoryList {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
RepositoryList::Empty => serializer.serialize_unit(),
|
|
||||||
RepositoryList::List { values }
|
|
||||||
| RepositoryList::Page { values, .. }
|
|
||||||
| RepositoryList::Stream { values, .. } => values.serialize(serializer),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Static initializer for RepositoryInfo.
|
|
||||||
pub struct RepoInfo {
|
|
||||||
pub name: &'static str,
|
|
||||||
pub lookup_key: &'static str,
|
|
||||||
pub display_list: &'static [&'static str],
|
|
||||||
pub fields: &'static [&'static str],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RepoInfo {
|
|
||||||
pub fn build(self) -> RepositoryInfo {
|
|
||||||
self.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct RepositoryInfo {
|
|
||||||
name: String,
|
|
||||||
lookup_key: String,
|
|
||||||
display_list: Vec<String>,
|
|
||||||
fields: Vec<(String, Field)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RepositoryInfo {
|
|
||||||
pub fn new(name: &str, lookup_key: &str) -> Self {
|
|
||||||
RepositoryInfo {
|
|
||||||
name: name.to_owned(),
|
|
||||||
lookup_key: lookup_key.to_owned(),
|
|
||||||
display_list: vec![],
|
|
||||||
fields: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// self mutating builder pattern
|
|
||||||
pub fn display_list(mut self, display_list: &[&str]) -> Self {
|
|
||||||
self.display_list = display_list.iter().map(|&e| e.to_string()).collect();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_widget<T: Into<Field>>(mut self, name: &str, item: T) -> Self {
|
|
||||||
let field = item.into(); // Convert the input into a Field
|
|
||||||
|
|
||||||
// Find the index of the existing entry with the same name, if it exists
|
|
||||||
let pos = self
|
|
||||||
.fields
|
|
||||||
.iter()
|
|
||||||
.position(|(existing_name, _)| existing_name == name);
|
|
||||||
|
|
||||||
match pos {
|
|
||||||
Some(index) => {
|
|
||||||
self.fields[index].1 = field;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
self.fields.push((name.to_owned(), field));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RepoInfo> for RepositoryInfo {
|
|
||||||
fn from(repo_info: RepoInfo) -> Self {
|
|
||||||
RepositoryInfo {
|
|
||||||
name: repo_info.name.to_string(),
|
|
||||||
lookup_key: repo_info.lookup_key.to_string(),
|
|
||||||
display_list: repo_info
|
|
||||||
.display_list
|
|
||||||
.iter()
|
|
||||||
.map(|&s| s.to_string())
|
|
||||||
.collect(),
|
|
||||||
fields: repo_info
|
|
||||||
.fields
|
|
||||||
.iter()
|
|
||||||
.map(|x| (x.to_string(), Field::from(Widget::default())))
|
|
||||||
.collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait PrimaryKeyType: Any + Debug + Send + Sync {
|
|
||||||
fn as_any(&self) -> &dyn Any;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrimaryKeyType for i64 {
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrimaryKeyType for String {
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait AdminRepository: Send + Sync {
|
|
||||||
type Key: PrimaryKeyType;
|
|
||||||
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Self::Key>;
|
|
||||||
|
|
||||||
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
|
|
||||||
async fn list(&self, context: &RepositoryContext) -> RepositoryList;
|
|
||||||
async fn get(&self, context: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem>;
|
|
||||||
async fn create(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
data: Value,
|
|
||||||
) -> Option<RepositoryItem>;
|
|
||||||
async fn update(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &Self::Key,
|
|
||||||
data: Value,
|
|
||||||
) -> Option<RepositoryItem>;
|
|
||||||
async fn replace(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &Self::Key,
|
|
||||||
data: Value,
|
|
||||||
) -> Option<RepositoryItem>;
|
|
||||||
async fn delete(&mut self, context: &RepositoryContext, id: &Self::Key) -> Option<Value>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait DynAdminRepository: Send + Sync {
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Box<dyn PrimaryKeyType>>;
|
|
||||||
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
|
|
||||||
async fn list(&self, context: &RepositoryContext) -> RepositoryList;
|
|
||||||
async fn get(
|
|
||||||
&self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &dyn PrimaryKeyType,
|
|
||||||
) -> Option<RepositoryItem>;
|
|
||||||
async fn create(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
data: Value,
|
|
||||||
) -> Option<RepositoryItem>;
|
|
||||||
async fn update(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &dyn PrimaryKeyType,
|
|
||||||
data: Value,
|
|
||||||
) -> Option<RepositoryItem>;
|
|
||||||
async fn replace(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &dyn PrimaryKeyType,
|
|
||||||
data: Value,
|
|
||||||
) -> Option<RepositoryItem>;
|
|
||||||
async fn delete(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &dyn PrimaryKeyType,
|
|
||||||
) -> Option<Value>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AdminRepositoryWrapper<T: AdminRepository> {
|
|
||||||
inner: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: AdminRepository> AdminRepositoryWrapper<T> {
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn key_from_string(&self, s: String) -> Option<<T as AdminRepository>::Key> {
|
|
||||||
self.inner.key_from_string(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<T: AdminRepository> DynAdminRepository for AdminRepositoryWrapper<T> {
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Box<dyn PrimaryKeyType>> {
|
|
||||||
if let Some(key) = self.inner.key_from_string(s) {
|
|
||||||
Some(Box::new(key))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo {
|
|
||||||
self.inner.info(context).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list(&self, context: &RepositoryContext) -> RepositoryList {
|
|
||||||
self.inner.list(context).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get(
|
|
||||||
&self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &dyn PrimaryKeyType,
|
|
||||||
) -> Option<RepositoryItem> {
|
|
||||||
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
|
||||||
self.inner.get(context, key).await
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
data: Value,
|
|
||||||
) -> Option<RepositoryItem> {
|
|
||||||
self.inner.create(context, data).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &dyn PrimaryKeyType,
|
|
||||||
data: Value,
|
|
||||||
) -> Option<RepositoryItem> {
|
|
||||||
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
|
||||||
self.inner.update(context, key, data).await
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn replace(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &dyn PrimaryKeyType,
|
|
||||||
data: Value,
|
|
||||||
) -> Option<RepositoryItem> {
|
|
||||||
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
|
||||||
self.inner.replace(context, key, data).await
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &dyn PrimaryKeyType,
|
|
||||||
) -> Option<Value> {
|
|
||||||
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
|
||||||
self.inner.delete(context, key).await
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
use super::domain::repository::AdminRepositoryWrapper;
|
|
||||||
use super::domain::{AdminApp, AdminModel, AdminModelConfig, AdminRepository, DynAdminRepository};
|
|
||||||
use crate::service::templates::Templates;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
pub trait AdminState {
|
|
||||||
fn get_templates(&self) -> &Templates;
|
|
||||||
fn get_registry(&self) -> SharedAdminRegistry;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type SharedAdminRegistry = Arc<AdminRegistry>;
|
|
||||||
|
|
||||||
// main registry.
|
|
||||||
pub struct AdminRegistry {
|
|
||||||
base_path: String,
|
|
||||||
apps: HashMap<String, internal::AdminApp>,
|
|
||||||
models: HashMap<String, internal::AdminModel>,
|
|
||||||
repositories: HashMap<String, Arc<Mutex<dyn DynAdminRepository>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AdminRegistry {
|
|
||||||
pub fn new(base_path: &str) -> Self {
|
|
||||||
AdminRegistry {
|
|
||||||
base_path: base_path.to_owned(),
|
|
||||||
apps: HashMap::new(),
|
|
||||||
models: HashMap::new(),
|
|
||||||
repositories: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_apps(&self) -> Vec<AdminApp> {
|
|
||||||
self.apps
|
|
||||||
.iter()
|
|
||||||
.map(|(key, node)| self.get_app(key, node))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_app(&self, key: &str, node: &internal::AdminApp) -> AdminApp {
|
|
||||||
let my_models = self.get_models(key);
|
|
||||||
AdminApp {
|
|
||||||
name: key.to_owned(),
|
|
||||||
key: node.name.to_owned(),
|
|
||||||
app_url: format!("/{}/app/{}", self.base_path, key.to_owned()),
|
|
||||||
models: my_models,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn register_app(&mut self, name: &str) -> String {
|
|
||||||
let key = self.get_key(name);
|
|
||||||
self.apps.insert(
|
|
||||||
key.to_owned(),
|
|
||||||
internal::AdminApp {
|
|
||||||
key: key.to_owned(),
|
|
||||||
name: name.to_owned(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
key
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_key(&self, name: &str) -> String {
|
|
||||||
slug::slugify(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn model_from_internal(&self, internal_model: &internal::AdminModel) -> AdminModel {
|
|
||||||
let admin_url = format!(
|
|
||||||
"/{}/app/{}/model/{}",
|
|
||||||
self.base_path, internal_model.app_key, internal_model.model_key
|
|
||||||
);
|
|
||||||
AdminModel {
|
|
||||||
key: internal_model.model_key.clone(),
|
|
||||||
name: internal_model.name.clone(),
|
|
||||||
view_only: false,
|
|
||||||
add_url: Some(format!("{}/add", admin_url)),
|
|
||||||
admin_url: admin_url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_models(&self, app_key: &str) -> Vec<AdminModel> {
|
|
||||||
self.models
|
|
||||||
.iter()
|
|
||||||
.filter(|(key, _)| key.starts_with(&format!("{}.", app_key)))
|
|
||||||
.map(|(_, model)| self.model_from_internal(model))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_model(&self, app_key: &str, model_key: &str) -> Option<AdminModel> {
|
|
||||||
let full_model_key = format!("{}.{}", app_key, model_key);
|
|
||||||
let internal_model = self.models.get(&full_model_key)?;
|
|
||||||
|
|
||||||
// unfinished: we need to think about model_key vs. model_id vs. entry_id, as "name" is ambiguous.
|
|
||||||
Some(self.model_from_internal(internal_model))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_model_config(&mut self, model: AdminModelConfig) -> Result<String, String> {
|
|
||||||
let local_config = internal::AdminModel::from(model);
|
|
||||||
if local_config.model_key.is_empty() {
|
|
||||||
return Err("No model name".to_owned());
|
|
||||||
}
|
|
||||||
let local_config_name = format!("{}.{}", local_config.app_key, local_config.model_key);
|
|
||||||
if self.models.contains_key(&local_config_name) {
|
|
||||||
return Err(format!("Model {} already exists", local_config_name));
|
|
||||||
}
|
|
||||||
let full_model_key = local_config_name.clone();
|
|
||||||
self.models.insert(local_config_name, local_config);
|
|
||||||
Ok(full_model_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn register_model<R: AdminRepository + 'static>(
|
|
||||||
&mut self,
|
|
||||||
model: AdminModelConfig,
|
|
||||||
repository: R,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let model_key = self.register_model_config(model)?;
|
|
||||||
let repository = AdminRepositoryWrapper::new(repository);
|
|
||||||
self.repositories
|
|
||||||
.insert(model_key, Arc::new(Mutex::new(repository)));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_repository(
|
|
||||||
&self,
|
|
||||||
app_key: &str,
|
|
||||||
model_key: &str,
|
|
||||||
) -> Result<Arc<Mutex<dyn DynAdminRepository>>, String> {
|
|
||||||
let full_model_key = format!("{}.{}", app_key, model_key);
|
|
||||||
if let Some(repo) = self.repositories.get(&full_model_key) {
|
|
||||||
// Clone the Arc to return a reference to the repository
|
|
||||||
return Ok(Arc::clone(repo));
|
|
||||||
} else {
|
|
||||||
return Err("Couldn't find repository".to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod internal {
|
|
||||||
// how the registry saves data internally.
|
|
||||||
|
|
||||||
use super::super::domain::AdminModelConfig;
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct AdminApp {
|
|
||||||
pub key: String,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct AdminModel {
|
|
||||||
pub app_key: String,
|
|
||||||
pub model_key: String,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<AdminModelConfig> for AdminModel {
|
|
||||||
fn from(value: AdminModelConfig) -> Self {
|
|
||||||
AdminModel {
|
|
||||||
app_key: value.app_key,
|
|
||||||
model_key: slug::slugify(value.name.clone()),
|
|
||||||
name: value.name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(&str, &str)> for AdminModel {
|
|
||||||
fn from(value: (&str, &str)) -> Self {
|
|
||||||
AdminModel {
|
|
||||||
app_key: value.0.to_owned(),
|
|
||||||
model_key: slug::slugify(value.1.to_owned()),
|
|
||||||
name: value.1.to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,349 +0,0 @@
|
|||||||
use axum::extract::Path;
|
|
||||||
use axum::http::HeaderMap;
|
|
||||||
use axum::Form;
|
|
||||||
use axum::{extract::State, response::IntoResponse};
|
|
||||||
use log::info;
|
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
use crate::admin::domain::{AdminApp, AdminModel};
|
|
||||||
use crate::admin::state::AdminState;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::domain::{RepositoryInfo, RepositoryItem, RepositoryList};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct AdminRequest {
|
|
||||||
pub path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct AdminContext {
|
|
||||||
pub base: Option<String>,
|
|
||||||
pub language_code: Option<String>,
|
|
||||||
pub language_bidi: Option<bool>,
|
|
||||||
pub user: Option<String>, // Todo: user type
|
|
||||||
pub admin_url: String,
|
|
||||||
pub site_url: Option<String>,
|
|
||||||
pub docsroot: Option<String>,
|
|
||||||
pub messages: Vec<String>, // Todo: message type
|
|
||||||
pub title: Option<String>,
|
|
||||||
pub subtitle: Option<String>,
|
|
||||||
pub content: String,
|
|
||||||
|
|
||||||
pub request: AdminRequest,
|
|
||||||
pub available_apps: Vec<AdminApp>,
|
|
||||||
pub item_model: Option<AdminModel>,
|
|
||||||
pub item_info: Option<RepositoryInfo>,
|
|
||||||
pub item_list: RepositoryList,
|
|
||||||
pub item: Option<RepositoryItem>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AdminContext {
|
|
||||||
fn default() -> Self {
|
|
||||||
AdminContext {
|
|
||||||
base: None, // TODO: what is this used for?
|
|
||||||
language_code: Some("en-us".to_string()), // Default language code
|
|
||||||
language_bidi: Some(false), // Default language bidi
|
|
||||||
user: None, //UserType::default(), // Assuming UserType has a Default impl
|
|
||||||
admin_url: "/admin".to_owned(),
|
|
||||||
site_url: None,
|
|
||||||
docsroot: None,
|
|
||||||
messages: Vec::new(), // Empty vector for messages
|
|
||||||
title: None,
|
|
||||||
subtitle: None,
|
|
||||||
content: String::new(), // Empty string for content
|
|
||||||
available_apps: Vec::new(),
|
|
||||||
request: AdminRequest {
|
|
||||||
path: "".to_owned(),
|
|
||||||
},
|
|
||||||
item_model: None,
|
|
||||||
item_info: None,
|
|
||||||
item_list: RepositoryList::Empty,
|
|
||||||
item: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn base_template(headers: &HeaderMap) -> Option<String> {
|
|
||||||
let hx_request = headers.get("HX-Request").is_some();
|
|
||||||
if hx_request {
|
|
||||||
Some("admin/base_hx.jinja".to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn index<S: AdminState + Clone + Send + Sync + 'static>(
|
|
||||||
admin: State<S>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let templates = admin.get_templates();
|
|
||||||
let registry = admin.get_registry();
|
|
||||||
templates.render_html(
|
|
||||||
"admin/index.html",
|
|
||||||
AdminContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
available_apps: registry.get_apps(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index Action is POST to the index site. We can anchor some general business code here.
|
|
||||||
pub async fn index_action<S: AdminState + Clone + Send + Sync + 'static>(
|
|
||||||
admin: State<S>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
"There is your answer!".to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list_app<S: AdminState + Clone + Send + Sync + 'static>(
|
|
||||||
admin: State<S>,
|
|
||||||
Path(app_key): Path<String>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let templates = admin.get_templates();
|
|
||||||
templates.render_html("admin/app_list.jinja", ())
|
|
||||||
}
|
|
||||||
|
|
||||||
// List Items renders the entire list item page.
|
|
||||||
pub async fn list_item_collection<S: AdminState + Clone + Send + Sync + 'static>(
|
|
||||||
admin: State<S>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Path((app_key, model_key)): Path<(String, String)>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
info!("list_item_collection {} for model {}", app_key, model_key);
|
|
||||||
let templates = admin.get_templates();
|
|
||||||
let registry = admin.get_registry();
|
|
||||||
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
|
||||||
let repo = repo.lock().await;
|
|
||||||
let admin_model = registry
|
|
||||||
.get_model(&app_key, &model_key)
|
|
||||||
.expect("Admin Model not found?"); // we will need a proper error route; so something that implements IntoResponse and can be substituted in the unwraps and expects.
|
|
||||||
AdminContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
available_apps: registry.get_apps(),
|
|
||||||
item_info: Some(repo.info(&admin_model).await),
|
|
||||||
item_list: repo.list(&admin_model).await,
|
|
||||||
item_model: Some(admin_model),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
AdminContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
available_apps: registry.get_apps(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
templates.render_html("admin/items/item_list.jinja", context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Items Action is a POST to an item list. By default these are actions, that work on a list of items as input.
|
|
||||||
pub async fn item_collection_action<S: AdminState + Clone + Send + Sync + 'static>(
|
|
||||||
admin: State<S>,
|
|
||||||
Path((app_key, model_key)): Path<(String, String)>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
"There is your answer!".to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Item Details shows one single dataset.
|
|
||||||
pub async fn view_item_details<S: AdminState + Clone + Send + Sync + 'static>(
|
|
||||||
admin: State<S>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Path((app_key, model_key, id)): Path<(String, String, String)>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let templates = admin.get_templates();
|
|
||||||
let registry = admin.get_registry();
|
|
||||||
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
|
||||||
let repo = repo.lock().await;
|
|
||||||
let admin_model = registry
|
|
||||||
.get_model(&app_key, &model_key)
|
|
||||||
.expect("Admin Model not found?");
|
|
||||||
if let Some(key) = repo.key_from_string(id) {
|
|
||||||
AdminContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
available_apps: registry.get_apps(),
|
|
||||||
item_info: Some(repo.info(&admin_model).await),
|
|
||||||
item_list: repo.list(&admin_model).await,
|
|
||||||
item: repo.get(&admin_model, key.as_ref()).await,
|
|
||||||
item_model: Some(admin_model),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
AdminContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
available_apps: registry.get_apps(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
AdminContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
available_apps: registry.get_apps(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
templates.render_html("admin/items/item_detail.jinja", context)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn new_item<S: AdminState + Clone + Send + Sync + 'static>(
|
|
||||||
admin: State<S>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Path((app_key, model_key)): Path<(String, String)>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let templates = admin.get_templates();
|
|
||||||
let registry = admin.get_registry();
|
|
||||||
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
|
||||||
let repo = repo.lock().await;
|
|
||||||
let admin_model = registry
|
|
||||||
.get_model(&app_key, &model_key)
|
|
||||||
.expect("Admin Model not found?");
|
|
||||||
AdminContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
available_apps: registry.get_apps(),
|
|
||||||
item_info: Some(repo.info(&admin_model).await),
|
|
||||||
item_list: repo.list(&admin_model).await,
|
|
||||||
item_model: Some(admin_model),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
AdminContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
available_apps: registry.get_apps(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
templates.render_html("admin/items/item_create.jinja", context)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_item<S: AdminState + Clone + Send + Sync + 'static>(
|
|
||||||
admin: State<S>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Path((app_key, model_key)): Path<(String, String)>,
|
|
||||||
Form(form): Form<Value>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let templates = admin.get_templates();
|
|
||||||
let registry = admin.get_registry();
|
|
||||||
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
|
||||||
let mut repo = repo.lock().await;
|
|
||||||
let admin_model = registry
|
|
||||||
.get_model(&app_key, &model_key)
|
|
||||||
.expect("Admin Model not found?");
|
|
||||||
|
|
||||||
// create our item.
|
|
||||||
let result = repo.create(&admin_model, form).await;
|
|
||||||
|
|
||||||
// TODO: refactor run over these views, way too much repetition.
|
|
||||||
AdminContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
available_apps: registry.get_apps(),
|
|
||||||
item_info: Some(repo.info(&admin_model).await),
|
|
||||||
item_list: repo.list(&admin_model).await,
|
|
||||||
item_model: Some(admin_model),
|
|
||||||
item: result,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
AdminContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
available_apps: registry.get_apps(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
templates.render_html("admin/items/item_create.jinja", context)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change is the GET version.
|
|
||||||
pub async fn change_item<S: AdminState + Clone + Send + Sync + 'static>(
|
|
||||||
admin: State<S>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Path((app_key, model_key, id)): Path<(String, String, String)>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let templates = admin.get_templates();
|
|
||||||
let registry = admin.get_registry();
|
|
||||||
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
|
||||||
let repo = repo.lock().await;
|
|
||||||
let admin_model = registry
|
|
||||||
.get_model(&app_key, &model_key)
|
|
||||||
.expect("Admin Model not found?");
|
|
||||||
if let Some(key) = repo.key_from_string(id) {
|
|
||||||
AdminContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
available_apps: registry.get_apps(),
|
|
||||||
item_info: Some(repo.info(&admin_model).await),
|
|
||||||
item_list: repo.list(&admin_model).await,
|
|
||||||
item: repo.get(&admin_model, key.as_ref()).await,
|
|
||||||
item_model: Some(admin_model),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
AdminContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
available_apps: registry.get_apps(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
AdminContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
available_apps: registry.get_apps(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
templates.render_html("admin/items/item_change.jinja", context)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_item<S: AdminState + Clone + Send + Sync + 'static>(
|
|
||||||
admin: State<S>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Path((app_key, model_key, id)): Path<(String, String, String)>,
|
|
||||||
Form(form): Form<Value>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let templates = admin.get_templates();
|
|
||||||
let registry = admin.get_registry();
|
|
||||||
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
|
||||||
let mut repo = repo.lock().await;
|
|
||||||
let admin_model = registry
|
|
||||||
.get_model(&app_key, &model_key)
|
|
||||||
.expect("Admin Model not found?");
|
|
||||||
if let Some(key) = repo.key_from_string(id) {
|
|
||||||
let result = repo.update(&admin_model, key.as_ref(), form).await;
|
|
||||||
AdminContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
available_apps: registry.get_apps(),
|
|
||||||
item_info: Some(repo.info(&admin_model).await),
|
|
||||||
item_list: repo.list(&admin_model).await,
|
|
||||||
item: result,
|
|
||||||
item_model: Some(admin_model),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
AdminContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
available_apps: registry.get_apps(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
AdminContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
available_apps: registry.get_apps(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
templates.render_html("admin/items/item_change.jinja", context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Item Action allows running an action on one single dataset.
|
|
||||||
pub async fn item_action<S: AdminState + Clone + Send + Sync + 'static>(
|
|
||||||
admin: State<S>,
|
|
||||||
Path((app_key, model_key, model_id)): Path<(String, String, String)>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
"There is your answer!".to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn debug_view<S: AdminState + Clone + Send + Sync + 'static>(
|
|
||||||
admin: State<S>,
|
|
||||||
Path(data): Path<String>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
println!("debug: {}", data);
|
|
||||||
"Debug!".to_owned()
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
pub struct User {
|
|
||||||
pub id: i32,
|
|
||||||
pub username: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct NewUser<'a> {
|
|
||||||
pub username: &'a str,
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub(crate) struct TemplateFiles {
|
||||||
|
base_hx: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static TPL: TemplateFiles = TemplateFiles { base_hx: "" };
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::repository::{RepositoryInfo, RepositoryItem, RepositoryList};
|
||||||
|
|
||||||
|
// representation of a Model in the template.
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct DepotModel {
|
||||||
|
pub key: String,
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
pub model_url: String,
|
||||||
|
pub view_only: bool,
|
||||||
|
|
||||||
|
pub add_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// representation of a Section in the template.
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct DepotSection {
|
||||||
|
pub key: String,
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
pub section_url: String,
|
||||||
|
pub models: Vec<DepotModel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct DepotRequest {
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct DepotContext {
|
||||||
|
pub base: Option<String>,
|
||||||
|
pub language_code: Option<String>,
|
||||||
|
pub language_bidi: Option<bool>,
|
||||||
|
pub user: Option<String>, // Todo: user type
|
||||||
|
pub depot_url: String,
|
||||||
|
pub site_url: Option<String>,
|
||||||
|
pub docsroot: Option<String>,
|
||||||
|
pub messages: Vec<String>, // Todo: message type
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub subtitle: Option<String>,
|
||||||
|
pub content: String,
|
||||||
|
|
||||||
|
pub request: DepotRequest,
|
||||||
|
pub sections: Vec<DepotSection>,
|
||||||
|
pub item_model: Option<DepotModel>,
|
||||||
|
pub item_info: Option<RepositoryInfo>,
|
||||||
|
pub item_list: RepositoryList,
|
||||||
|
pub item: Option<RepositoryItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DepotContext {
|
||||||
|
fn default() -> Self {
|
||||||
|
DepotContext {
|
||||||
|
base: None, // TODO: what is this used for?
|
||||||
|
language_code: Some("en-us".to_string()), // Default language code
|
||||||
|
language_bidi: Some(false), // Default language bidi
|
||||||
|
user: None, //UserType::default(), // Assuming UserType has a Default impl
|
||||||
|
depot_url: "/depot".to_owned(),
|
||||||
|
site_url: None,
|
||||||
|
docsroot: None,
|
||||||
|
messages: Vec::new(), // Empty vector for messages
|
||||||
|
title: None,
|
||||||
|
subtitle: None,
|
||||||
|
content: String::new(), // Empty string for content
|
||||||
|
sections: Vec::new(),
|
||||||
|
request: DepotRequest {
|
||||||
|
path: "".to_owned(),
|
||||||
|
},
|
||||||
|
item_model: None,
|
||||||
|
item_info: None,
|
||||||
|
item_list: RepositoryList::Empty,
|
||||||
|
item: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
|
||||||
|
UPDATE:
|
||||||
|
- The Item is edited successfully: Success Message
|
||||||
|
- The Item could not be saved, it needs adjustment: Form Errors, Warning Message?
|
||||||
|
- The Item could not be saved, there was an Error of some sort: Error Message
|
||||||
|
|
||||||
|
Widget -builds-> Field
|
||||||
|
|
||||||
|
|
||||||
|
Validation: https://github.com/tokio-rs/axum/blob/main/examples/validator/src/main.rs
|
||||||
|
AvoRED PRoject seems similar: https://github.com/avored/avored-rust-cms
|
||||||
@@ -1,27 +1,34 @@
|
|||||||
use axum::{routing::get, Router};
|
use axum::{routing::get, Router};
|
||||||
|
|
||||||
pub mod domain;
|
mod constants;
|
||||||
|
mod context;
|
||||||
|
pub mod prelude;
|
||||||
|
mod registry;
|
||||||
|
pub mod repository;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod views;
|
pub mod views;
|
||||||
|
pub mod widgets;
|
||||||
|
|
||||||
pub fn routes<S: state::AdminState + Clone + Send + Sync + 'static>() -> Router<S> {
|
use state::DepotFn;
|
||||||
|
|
||||||
|
pub fn routes<S: DepotFn>() -> Router<S> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(views::index::<S>).post(views::index_action::<S>))
|
.route("/", get(views::index::<S>).post(views::index_action::<S>))
|
||||||
.route("/app/:app", get(views::list_app::<S>))
|
.route("/:section", get(views::list_section::<S>))
|
||||||
.route(
|
.route(
|
||||||
"/app/:app/model/:model",
|
"/:section/model/:model",
|
||||||
get(views::list_item_collection::<S>),
|
get(views::list_item_collection::<S>),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/app/:app/model/:model/add",
|
"/:section/model/:model/add",
|
||||||
get(views::new_item::<S>).post(views::create_item::<S>),
|
get(views::new_item::<S>).post(views::create_item::<S>),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/app/:app/model/:model/change/:id",
|
"/:section/model/:model/change/:id",
|
||||||
get(views::change_item::<S>).patch(views::update_item::<S>),
|
get(views::change_item::<S>).patch(views::update_item::<S>),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/app/:app/model/:model/detail/:id",
|
"/:section/model/:model/detail/:id",
|
||||||
get(views::view_item_details::<S>),
|
get(views::view_item_details::<S>),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub use super::context::{DepotContext, DepotModel};
|
||||||
|
pub use super::registry::DepotRegistry;
|
||||||
|
pub use super::repository::{
|
||||||
|
DepotModelConfig, DepotRepository, RepoInfo, RepositoryContext, RepositoryError,
|
||||||
|
RepositoryInfo, RepositoryItem, RepositoryList, RepositoryResponse, RepositoryResult,
|
||||||
|
};
|
||||||
|
pub use super::widgets::Widget;
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
context::{DepotModel, DepotSection},
|
||||||
|
repository::{DepotModelConfig, DepotRepository, DepotRepositoryWrapper, DynDepotRepository},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct DepotRegistry {
|
||||||
|
base_path: String,
|
||||||
|
sections: HashMap<String, internal::DepotSectionInfo>,
|
||||||
|
models: HashMap<String, internal::DepotModelInfo>,
|
||||||
|
repositories: HashMap<String, Arc<Mutex<dyn DynDepotRepository>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DepotRegistry {
|
||||||
|
pub fn new(base_path: &str) -> Self {
|
||||||
|
DepotRegistry {
|
||||||
|
base_path: base_path.to_owned(),
|
||||||
|
sections: HashMap::new(),
|
||||||
|
models: HashMap::new(),
|
||||||
|
repositories: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_sections(&self) -> Vec<DepotSection> {
|
||||||
|
self.sections
|
||||||
|
.iter()
|
||||||
|
.map(|(key, section_info)| self.get_section(key, section_info))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_section(&self, key: &str, section_info: &internal::DepotSectionInfo) -> DepotSection {
|
||||||
|
let my_models = self.get_models(key);
|
||||||
|
DepotSection {
|
||||||
|
key: key.to_owned(),
|
||||||
|
name: section_info.name.to_owned(),
|
||||||
|
section_url: format!("/{}/{}", self.base_path, key.to_owned()),
|
||||||
|
models: my_models,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_section(&mut self, name: &str) -> String {
|
||||||
|
let key = self.get_key(name);
|
||||||
|
self.sections.insert(
|
||||||
|
key.to_owned(),
|
||||||
|
internal::DepotSectionInfo {
|
||||||
|
key: key.to_owned(),
|
||||||
|
name: name.to_owned(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
key
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_key(&self, name: &str) -> String {
|
||||||
|
slug::slugify(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn model_from_model_info(&self, model_info: &internal::DepotModelInfo) -> DepotModel {
|
||||||
|
let model_url = format!(
|
||||||
|
"/{}/{}/model/{}",
|
||||||
|
self.base_path, model_info.section_key, model_info.model_key
|
||||||
|
);
|
||||||
|
DepotModel {
|
||||||
|
key: model_info.model_key.clone(),
|
||||||
|
name: model_info.name.clone(),
|
||||||
|
view_only: false,
|
||||||
|
add_url: Some(format!("{}/add", model_url)),
|
||||||
|
model_url: model_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_models(&self, section_key: &str) -> Vec<DepotModel> {
|
||||||
|
self.models
|
||||||
|
.iter()
|
||||||
|
.filter(|(key, _)| key.starts_with(&format!("{}.", section_key)))
|
||||||
|
.map(|(_, model)| self.model_from_model_info(model))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_model(&self, section_key: &str, model_key: &str) -> Option<DepotModel> {
|
||||||
|
let full_model_key = format!("{}.{}", section_key, model_key);
|
||||||
|
let internal_model = self.models.get(&full_model_key)?;
|
||||||
|
Some(self.model_from_model_info(internal_model))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_model_config(&mut self, model: DepotModelConfig) -> Result<String, String> {
|
||||||
|
let local_config = internal::DepotModelInfo::from(model);
|
||||||
|
if local_config.model_key.is_empty() {
|
||||||
|
return Err("No model name".to_owned());
|
||||||
|
}
|
||||||
|
let local_config_name = format!("{}.{}", local_config.section_key, local_config.model_key);
|
||||||
|
if self.models.contains_key(&local_config_name) {
|
||||||
|
return Err(format!("Model {} already exists", local_config_name));
|
||||||
|
}
|
||||||
|
let full_model_key = local_config_name.clone();
|
||||||
|
self.models.insert(local_config_name, local_config);
|
||||||
|
Ok(full_model_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_model<R: DepotRepository + 'static>(
|
||||||
|
&mut self,
|
||||||
|
model: DepotModelConfig,
|
||||||
|
repository: R,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let model_key = self.register_model_config(model)?;
|
||||||
|
let repository = DepotRepositoryWrapper::new(repository);
|
||||||
|
self.repositories
|
||||||
|
.insert(model_key, Arc::new(Mutex::new(repository)));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_repository(
|
||||||
|
&self,
|
||||||
|
section_key: &str,
|
||||||
|
model_key: &str,
|
||||||
|
) -> Result<Arc<Mutex<dyn DynDepotRepository>>, String> {
|
||||||
|
let full_model_key = format!("{}.{}", section_key, model_key);
|
||||||
|
if let Some(repo) = self.repositories.get(&full_model_key) {
|
||||||
|
// Clone the Arc to return a reference to the repository
|
||||||
|
return Ok(Arc::clone(repo));
|
||||||
|
} else {
|
||||||
|
return Err("Couldn't find repository".to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod internal {
|
||||||
|
// how the registry saves data internally.
|
||||||
|
use super::super::repository::DepotModelConfig;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(super) struct DepotSectionInfo {
|
||||||
|
pub key: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(super) struct DepotModelInfo {
|
||||||
|
pub section_key: String,
|
||||||
|
pub model_key: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DepotModelConfig> for DepotModelInfo {
|
||||||
|
fn from(value: DepotModelConfig) -> Self {
|
||||||
|
DepotModelInfo {
|
||||||
|
section_key: value.section_key,
|
||||||
|
model_key: slug::slugify(value.name.clone()),
|
||||||
|
name: value.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(&str, &str)> for DepotModelInfo {
|
||||||
|
fn from(value: (&str, &str)) -> Self {
|
||||||
|
DepotModelInfo {
|
||||||
|
section_key: value.0.to_owned(),
|
||||||
|
model_key: slug::slugify(value.1.to_owned()),
|
||||||
|
name: value.1.to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,391 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use serde::{Serialize, Serializer};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::any::Any;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::vec::IntoIter;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use super::context::DepotModel;
|
||||||
|
use super::widgets::Widget;
|
||||||
|
|
||||||
|
// user uses this configuration object to register another model.
|
||||||
|
pub struct DepotModelConfig {
|
||||||
|
pub name: String,
|
||||||
|
pub section_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(&str, &str)> for DepotModelConfig {
|
||||||
|
fn from(value: (&str, &str)) -> Self {
|
||||||
|
DepotModelConfig {
|
||||||
|
section_key: value.0.to_owned(),
|
||||||
|
name: value.1.to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// May become it's own structure.
|
||||||
|
pub type RepositoryContext = DepotModel;
|
||||||
|
|
||||||
|
impl RepositoryContext {
|
||||||
|
pub fn get_default_detail_url(&self, key: &str) -> Option<String> {
|
||||||
|
Some(format!("{}/detail/{}", self.model_url, key))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_default_change_url(&self, key: &str) -> Option<String> {
|
||||||
|
Some(format!("{}/change/{}", self.model_url, key))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_item(&self, key: &str, fields: Value) -> RepositoryItem {
|
||||||
|
RepositoryItem {
|
||||||
|
detail_url: self.get_default_detail_url(key),
|
||||||
|
change_url: self.get_default_change_url(key),
|
||||||
|
fields: fields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct Field {
|
||||||
|
widget: String,
|
||||||
|
label: Option<String>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
field_type: String,
|
||||||
|
readonly: bool,
|
||||||
|
required: bool,
|
||||||
|
options: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Widget> for Field {
|
||||||
|
fn from(value: Widget) -> Self {
|
||||||
|
Field {
|
||||||
|
widget: value.widget.to_string(),
|
||||||
|
label: value.label.map(|s| s.to_string()),
|
||||||
|
field_type: value.field_type.to_string(),
|
||||||
|
readonly: value.readonly,
|
||||||
|
required: value.required,
|
||||||
|
options: value
|
||||||
|
.options
|
||||||
|
.iter()
|
||||||
|
.map(|(key, val)| (key.to_string(), serde_json::json!(val)))
|
||||||
|
.collect::<serde_json::Map<String, Value>>()
|
||||||
|
.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct RepositoryItem {
|
||||||
|
pub fields: Value,
|
||||||
|
pub detail_url: Option<String>,
|
||||||
|
pub change_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum RepositoryResponse {
|
||||||
|
NoItem,
|
||||||
|
ItemOnly(RepositoryItem),
|
||||||
|
ItemAndHeaders(RepositoryItem, axum::http::HeaderMap),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Option<RepositoryItem>> for RepositoryResponse {
|
||||||
|
fn into(self) -> Option<RepositoryItem> {
|
||||||
|
match self {
|
||||||
|
RepositoryResponse::NoItem => None,
|
||||||
|
RepositoryResponse::ItemOnly(some) => Some(some),
|
||||||
|
RepositoryResponse::ItemAndHeaders(some, _) => Some(some),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum RepositoryError {
|
||||||
|
// used internally.
|
||||||
|
#[error("key not found in downcast?")]
|
||||||
|
WrapperDowncastError,
|
||||||
|
|
||||||
|
// to be used by repositories:
|
||||||
|
#[error("repository item not found")]
|
||||||
|
ItemNotFound,
|
||||||
|
#[error("database error: {0}")]
|
||||||
|
DatabaseError(#[source] Box<dyn std::error::Error + Send + Sync>),
|
||||||
|
#[error("external error: {0}")]
|
||||||
|
ExternalError(#[source] Box<dyn std::error::Error + Send + Sync>),
|
||||||
|
|
||||||
|
// to be removed or refactored:
|
||||||
|
#[error("an unknown occurred: {0}")]
|
||||||
|
UnknownError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type RepositoryResult = Result<RepositoryResponse, RepositoryError>;
|
||||||
|
|
||||||
|
pub enum RepositoryList {
|
||||||
|
Empty,
|
||||||
|
List {
|
||||||
|
values: Vec<RepositoryItem>,
|
||||||
|
},
|
||||||
|
Page {
|
||||||
|
values: Vec<RepositoryItem>,
|
||||||
|
offset: usize,
|
||||||
|
total: usize,
|
||||||
|
},
|
||||||
|
Stream {
|
||||||
|
values: Vec<RepositoryItem>,
|
||||||
|
next_index: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for RepositoryList {
|
||||||
|
type Item = RepositoryItem;
|
||||||
|
type IntoIter = IntoIter<Self::Item>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
match self {
|
||||||
|
RepositoryList::Empty => vec![].into_iter(),
|
||||||
|
RepositoryList::List { values } => values.into_iter(),
|
||||||
|
RepositoryList::Page { values, .. } => values.into_iter(),
|
||||||
|
RepositoryList::Stream { values, .. } => values.into_iter(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for RepositoryList {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
RepositoryList::Empty => serializer.serialize_unit(),
|
||||||
|
RepositoryList::List { values }
|
||||||
|
| RepositoryList::Page { values, .. }
|
||||||
|
| RepositoryList::Stream { values, .. } => values.serialize(serializer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Static initializer for RepositoryInfo.
|
||||||
|
pub struct RepoInfo {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub lookup_key: &'static str,
|
||||||
|
pub display_list: &'static [&'static str],
|
||||||
|
pub fields: &'static [&'static str],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RepoInfo {
|
||||||
|
pub fn build(self) -> RepositoryInfo {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct RepositoryInfo {
|
||||||
|
name: String,
|
||||||
|
lookup_key: String,
|
||||||
|
display_list: Vec<String>,
|
||||||
|
fields: Vec<(String, Field)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RepositoryInfo {
|
||||||
|
pub fn new(name: &str, lookup_key: &str) -> Self {
|
||||||
|
RepositoryInfo {
|
||||||
|
name: name.to_owned(),
|
||||||
|
lookup_key: lookup_key.to_owned(),
|
||||||
|
display_list: vec![],
|
||||||
|
fields: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// self mutating builder pattern
|
||||||
|
pub fn display_list(mut self, display_list: &[&str]) -> Self {
|
||||||
|
self.display_list = display_list.iter().map(|&e| e.to_string()).collect();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_widget<T: Into<Field>>(mut self, name: &str, item: T) -> Self {
|
||||||
|
let field = item.into(); // Convert the input into a Field
|
||||||
|
|
||||||
|
// Find the index of the existing entry with the same name, if it exists
|
||||||
|
let pos = self
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.position(|(existing_name, _)| existing_name == name);
|
||||||
|
|
||||||
|
match pos {
|
||||||
|
Some(index) => {
|
||||||
|
self.fields[index].1 = field;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.fields.push((name.to_owned(), field));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RepoInfo> for RepositoryInfo {
|
||||||
|
fn from(repo_info: RepoInfo) -> Self {
|
||||||
|
RepositoryInfo {
|
||||||
|
name: repo_info.name.to_string(),
|
||||||
|
lookup_key: repo_info.lookup_key.to_string(),
|
||||||
|
display_list: repo_info
|
||||||
|
.display_list
|
||||||
|
.iter()
|
||||||
|
.map(|&s| s.to_string())
|
||||||
|
.collect(),
|
||||||
|
fields: repo_info
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.map(|x| (x.to_string(), Field::from(Widget::default())))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PrimaryKeyType: Any + Debug + Send + Sync {
|
||||||
|
fn as_any(&self) -> &dyn Any;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimaryKeyType for i64 {
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimaryKeyType for String {
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait DepotRepository: Send + Sync {
|
||||||
|
type Key: PrimaryKeyType;
|
||||||
|
|
||||||
|
fn key_from_string(&self, s: String) -> Option<Self::Key>;
|
||||||
|
|
||||||
|
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
|
||||||
|
async fn list(&self, context: &RepositoryContext) -> RepositoryList;
|
||||||
|
async fn get(&self, context: &RepositoryContext, id: &Self::Key) -> RepositoryResult;
|
||||||
|
async fn create(&mut self, context: &RepositoryContext, data: Value) -> RepositoryResult;
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &Self::Key,
|
||||||
|
data: Value,
|
||||||
|
) -> RepositoryResult;
|
||||||
|
async fn replace(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &Self::Key,
|
||||||
|
data: Value,
|
||||||
|
) -> RepositoryResult;
|
||||||
|
async fn delete(&mut self, context: &RepositoryContext, id: &Self::Key) -> Option<Value>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub(crate) trait DynDepotRepository: Send + Sync {
|
||||||
|
fn key_from_string(&self, s: String) -> Option<Box<dyn PrimaryKeyType>>;
|
||||||
|
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
|
||||||
|
async fn list(&self, context: &RepositoryContext) -> RepositoryList;
|
||||||
|
async fn get(&self, context: &RepositoryContext, id: &dyn PrimaryKeyType) -> RepositoryResult;
|
||||||
|
async fn create(&mut self, context: &RepositoryContext, data: Value) -> RepositoryResult;
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
data: Value,
|
||||||
|
) -> RepositoryResult;
|
||||||
|
async fn replace(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
data: Value,
|
||||||
|
) -> RepositoryResult;
|
||||||
|
async fn delete(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
) -> Option<Value>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DepotRepositoryWrapper<T: DepotRepository> {
|
||||||
|
inner: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DepotRepository> DepotRepositoryWrapper<T> {
|
||||||
|
pub fn new(inner: T) -> Self {
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_from_string(&self, s: String) -> Option<<T as DepotRepository>::Key> {
|
||||||
|
self.inner.key_from_string(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: DepotRepository> DynDepotRepository for DepotRepositoryWrapper<T> {
|
||||||
|
fn key_from_string(&self, s: String) -> Option<Box<dyn PrimaryKeyType>> {
|
||||||
|
if let Some(key) = self.inner.key_from_string(s) {
|
||||||
|
Some(Box::new(key))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo {
|
||||||
|
self.inner.info(context).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list(&self, context: &RepositoryContext) -> RepositoryList {
|
||||||
|
self.inner.list(context).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, context: &RepositoryContext, id: &dyn PrimaryKeyType) -> RepositoryResult {
|
||||||
|
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
||||||
|
self.inner.get(context, key).await
|
||||||
|
} else {
|
||||||
|
Err(RepositoryError::WrapperDowncastError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&mut self, context: &RepositoryContext, data: Value) -> RepositoryResult {
|
||||||
|
self.inner.create(context, data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
data: Value,
|
||||||
|
) -> RepositoryResult {
|
||||||
|
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
||||||
|
self.inner.update(context, key, data).await
|
||||||
|
} else {
|
||||||
|
Err(RepositoryError::WrapperDowncastError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn replace(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
data: Value,
|
||||||
|
) -> RepositoryResult {
|
||||||
|
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
||||||
|
self.inner.replace(context, key, data).await
|
||||||
|
} else {
|
||||||
|
Err(RepositoryError::WrapperDowncastError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
) -> Option<Value> {
|
||||||
|
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
||||||
|
self.inner.delete(context, key).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use crate::service::templates::Templates;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use super::registry::DepotRegistry;
|
||||||
|
|
||||||
|
pub trait DepotState {
|
||||||
|
fn get_templates(&self) -> &Templates;
|
||||||
|
fn get_registry(&self) -> SharedDepotRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SharedDepotRegistry = Arc<DepotRegistry>;
|
||||||
|
|
||||||
|
pub trait DepotFn = DepotState + Clone + Send + Sync + 'static;
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
use axum::extract::Path;
|
||||||
|
use axum::http::HeaderMap;
|
||||||
|
use axum::Form;
|
||||||
|
use axum::{extract::State, response::IntoResponse};
|
||||||
|
use log::info;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use super::context::DepotContext;
|
||||||
|
use super::state::{DepotFn, DepotState};
|
||||||
|
|
||||||
|
pub fn base_template(headers: &HeaderMap) -> Option<String> {
|
||||||
|
let hx_request = headers.get("HX-Request").is_some();
|
||||||
|
if hx_request {
|
||||||
|
Some("depot/base_hx.jinja".to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn index<S: DepotFn>(depot: State<S>, headers: HeaderMap) -> impl IntoResponse {
|
||||||
|
let templates = depot.get_templates();
|
||||||
|
let registry = depot.get_registry();
|
||||||
|
templates.render_html(
|
||||||
|
"depot/index.html",
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index Action is POST to the index site. We can anchor some general business code here.
|
||||||
|
pub async fn index_action<S: DepotFn>(depot: State<S>) -> impl IntoResponse {
|
||||||
|
"There is your answer!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_section<S: DepotFn>(
|
||||||
|
depot: State<S>,
|
||||||
|
Path(depot_key): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = depot.get_templates();
|
||||||
|
templates.render_html("depot/depot.jinja", ())
|
||||||
|
}
|
||||||
|
|
||||||
|
// List Items renders the entire list item page.
|
||||||
|
pub async fn list_item_collection<S: DepotFn>(
|
||||||
|
depot: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((depot_key, model_key)): Path<(String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
info!("list_item_collection {} for model {}", depot_key, model_key);
|
||||||
|
let templates = depot.get_templates();
|
||||||
|
let registry = depot.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
||||||
|
let repo = repo.lock().await;
|
||||||
|
let depot_model = registry
|
||||||
|
.get_model(&depot_key, &model_key)
|
||||||
|
.expect("depot Model not found?"); // we will need a proper error route; so something that implements IntoResponse and can be substituted in the unwraps and expects.
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
item_info: Some(repo.info(&depot_model).await),
|
||||||
|
item_list: repo.list(&depot_model).await,
|
||||||
|
item_model: Some(depot_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("depot/items/item_list.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Items Action is a POST to an item list. By default these are actions, that work on a list of items as input.
|
||||||
|
pub async fn item_collection_action<S: DepotFn>(
|
||||||
|
depot: State<S>,
|
||||||
|
Path((depot_key, model_key)): Path<(String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
"There is your answer!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item Details shows one single dataset.
|
||||||
|
pub async fn view_item_details<S: DepotFn>(
|
||||||
|
depot: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((depot_key, model_key, id)): Path<(String, String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = depot.get_templates();
|
||||||
|
let registry = depot.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
||||||
|
let repo = repo.lock().await;
|
||||||
|
let depot_model = registry
|
||||||
|
.get_model(&depot_key, &model_key)
|
||||||
|
.expect("depot Model not found?");
|
||||||
|
if let Some(key) = repo.key_from_string(id) {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
item_info: Some(repo.info(&depot_model).await),
|
||||||
|
item_list: repo.list(&depot_model).await,
|
||||||
|
item: repo.get(&depot_model, key.as_ref()).await.unwrap().into(),
|
||||||
|
item_model: Some(depot_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("depot/items/item_detail.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_item<S: DepotFn>(
|
||||||
|
depot: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((depot_key, model_key)): Path<(String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = depot.get_templates();
|
||||||
|
let registry = depot.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
||||||
|
let repo = repo.lock().await;
|
||||||
|
let depot_model = registry
|
||||||
|
.get_model(&depot_key, &model_key)
|
||||||
|
.expect("depot Model not found?");
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
item_info: Some(repo.info(&depot_model).await),
|
||||||
|
item_list: repo.list(&depot_model).await,
|
||||||
|
item_model: Some(depot_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("depot/items/item_create.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_item<S: DepotFn>(
|
||||||
|
depot: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((depot_key, model_key)): Path<(String, String)>,
|
||||||
|
Form(form): Form<Value>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = depot.get_templates();
|
||||||
|
let registry = depot.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
||||||
|
let mut repo = repo.lock().await;
|
||||||
|
let depot_model = registry
|
||||||
|
.get_model(&depot_key, &model_key)
|
||||||
|
.expect("Depot Model not found?");
|
||||||
|
|
||||||
|
// create our item.
|
||||||
|
let result = repo.create(&depot_model, form).await;
|
||||||
|
|
||||||
|
// TODO: refactor run over these views, way too much repetition.
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
item_info: Some(repo.info(&depot_model).await),
|
||||||
|
item_list: repo.list(&depot_model).await,
|
||||||
|
item_model: Some(depot_model),
|
||||||
|
item: result.unwrap().into(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("depot/items/item_create.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change is the GET version.
|
||||||
|
pub async fn change_item<S: DepotFn>(
|
||||||
|
depot: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((depot_key, model_key, id)): Path<(String, String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = depot.get_templates();
|
||||||
|
let registry = depot.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
||||||
|
let repo = repo.lock().await;
|
||||||
|
let depot_model = registry
|
||||||
|
.get_model(&depot_key, &model_key)
|
||||||
|
.expect("Depot Model not found?");
|
||||||
|
if let Some(key) = repo.key_from_string(id) {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
item_info: Some(repo.info(&depot_model).await),
|
||||||
|
item_list: repo.list(&depot_model).await,
|
||||||
|
item: repo.get(&depot_model, key.as_ref()).await.unwrap().into(),
|
||||||
|
item_model: Some(depot_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Repository did not have key.
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Repository could not be loaded.
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("depot/items/item_change.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_item<S: DepotFn>(
|
||||||
|
depot: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((depot_key, model_key, id)): Path<(String, String, String)>,
|
||||||
|
Form(form): Form<Value>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = depot.get_templates();
|
||||||
|
let registry = depot.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
||||||
|
let mut repo = repo.lock().await;
|
||||||
|
let depot_model = registry
|
||||||
|
.get_model(&depot_key, &model_key)
|
||||||
|
.expect("depot Model not found?");
|
||||||
|
if let Some(key) = repo.key_from_string(id) {
|
||||||
|
let result = repo.update(&depot_model, key.as_ref(), form).await;
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
item_info: Some(repo.info(&depot_model).await),
|
||||||
|
item_list: repo.list(&depot_model).await,
|
||||||
|
item: result.unwrap().into(),
|
||||||
|
item_model: Some(depot_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let response = templates.render_html("depot/items/item_change.jinja", context);
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item Action allows running an action on one single dataset.
|
||||||
|
pub async fn item_action<S: DepotFn>(
|
||||||
|
depot: State<S>,
|
||||||
|
Path((depot_key, model_key, model_id)): Path<(String, String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
"There is your answer!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn debug_view<S: DepotFn>(
|
||||||
|
depot: State<S>,
|
||||||
|
Path(data): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
println!("debug: {}", data);
|
||||||
|
"Debug!".to_owned()
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
/// This is a static configuration object.
|
||||||
|
/// It might be changed in the future to have a dynamic counterpart.
|
||||||
|
///
|
||||||
|
/// ## Example:
|
||||||
|
/// Creating a simple required and readonly text input field with a label:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// let my_field = Field::widget("/admin/widgets/input_text.jinja")
|
||||||
|
/// .labelled("Username")
|
||||||
|
/// .required()
|
||||||
|
/// .readonly();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Serialize, Clone, Copy)]
|
||||||
|
pub struct Widget {
|
||||||
|
pub widget: &'static str,
|
||||||
|
pub label: Option<&'static str>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub field_type: &'static str,
|
||||||
|
pub required: bool,
|
||||||
|
pub readonly: bool,
|
||||||
|
pub options: &'static [(&'static str, &'static str)],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget {
|
||||||
|
pub fn widget(widget: &'static str) -> Self {
|
||||||
|
Widget {
|
||||||
|
widget: widget,
|
||||||
|
label: None,
|
||||||
|
field_type: "text",
|
||||||
|
required: false,
|
||||||
|
readonly: false,
|
||||||
|
options: &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Self::widget("/depot/widgets/input_text.jinja")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn textarea() -> Self {
|
||||||
|
Self::widget("/depot/widgets/input_textarea.jinja")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn checkbox() -> Self {
|
||||||
|
Self::widget("/depot/widgets/checkbox_toggle.jinja")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn required(mut self) -> Self {
|
||||||
|
self.required = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readonly(mut self) -> Self {
|
||||||
|
self.readonly = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn labeled(mut self, label: &'static str) -> Self {
|
||||||
|
self.label = Some(label);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_password(mut self) -> Self {
|
||||||
|
self.field_type = "password";
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_hidden(mut self) -> Self {
|
||||||
|
self.field_type = "hidden";
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn options(mut self, options: &'static [(&'static str, &'static str)]) -> Self {
|
||||||
|
self.options = options;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -1,2 +1,3 @@
|
|||||||
pub mod admin;
|
#![feature(trait_alias)]
|
||||||
|
pub mod depot;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
|||||||
@@ -3,7 +3,23 @@ use axum::response::Html;
|
|||||||
use minijinja::{path_loader, Environment, Value};
|
use minijinja::{path_loader, Environment, Value};
|
||||||
use minijinja_autoreload::AutoReloader;
|
use minijinja_autoreload::AutoReloader;
|
||||||
use pulldown_cmark::Event;
|
use pulldown_cmark::Event;
|
||||||
use std::sync::Arc;
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
|
pub fn composite_loader<P: AsRef<Path>>(
|
||||||
|
dirs: Vec<P>,
|
||||||
|
) -> impl Fn(&str) -> Result<Option<String>, minijinja::Error> + Send + Sync + 'static {
|
||||||
|
let loaders: Vec<_> = dirs.into_iter().map(|dir| path_loader(dir)).collect();
|
||||||
|
move |name| {
|
||||||
|
for loader in &loaders {
|
||||||
|
match loader(name) {
|
||||||
|
Ok(Some(template)) => return Ok(Some(template)),
|
||||||
|
Ok(None) => continue,
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Templates {
|
pub struct Templates {
|
||||||
@@ -15,7 +31,8 @@ impl Templates {
|
|||||||
let reloader = AutoReloader::new(move |notifier| {
|
let reloader = AutoReloader::new(move |notifier| {
|
||||||
let mut environment = Environment::new();
|
let mut environment = Environment::new();
|
||||||
let template_path = "templates";
|
let template_path = "templates";
|
||||||
environment.set_loader(path_loader(&template_path));
|
let loader = composite_loader(vec![template_path]);
|
||||||
|
environment.set_loader(loader);
|
||||||
environment.add_filter("none", none);
|
environment.add_filter("none", none);
|
||||||
environment.add_filter("markdown", markdown);
|
environment.add_filter("markdown", markdown);
|
||||||
environment.add_filter("yesno", filter_yesno);
|
environment.add_filter("yesno", filter_yesno);
|
||||||
|
|||||||
@@ -28,3 +28,4 @@ async-trait = "0.1.80"
|
|||||||
password-auth = "1.0.0"
|
password-auth = "1.0.0"
|
||||||
thiserror = "1.0.61"
|
thiserror = "1.0.61"
|
||||||
blake3 = "1.5.1"
|
blake3 = "1.5.1"
|
||||||
|
chrono = "0.4"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod user_admin_repository;
|
pub mod user_admin_repository;
|
||||||
|
pub mod utils;
|
||||||
pub mod views;
|
pub mod views;
|
||||||
|
|||||||
+33
-15
@@ -5,11 +5,16 @@ use axum_login::{AuthUser, AuthzBackend};
|
|||||||
use axum_login::{AuthnBackend, UserId};
|
use axum_login::{AuthnBackend, UserId};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use password_auth::{generate_hash, is_hash_obsolete, verify_password};
|
use password_auth::{generate_hash, is_hash_obsolete, verify_password};
|
||||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter};
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait,
|
||||||
|
QueryFilter,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
|
|
||||||
|
use crate::utils::get_current_timestamp;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct UserRepository {
|
pub struct UserRepository {
|
||||||
pub(crate) connection: DatabaseConnection,
|
pub(crate) connection: DatabaseConnection,
|
||||||
@@ -35,6 +40,17 @@ impl UserRepository {
|
|||||||
// As checking for obsoleteness errored out, we assume this a raw password.
|
// As checking for obsoleteness errored out, we assume this a raw password.
|
||||||
generate_hash(password)
|
generate_hash(password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_user(username: &str, password: &str) -> entity::user::ActiveModel {
|
||||||
|
entity::user::ActiveModel {
|
||||||
|
username: sea_orm::ActiveValue::Set(username.to_owned()),
|
||||||
|
password: sea_orm::ActiveValue::Set(UserRepository::encode_password(
|
||||||
|
password.to_owned(),
|
||||||
|
)),
|
||||||
|
date_joined: sea_orm::ActiveValue::Set(Some(get_current_timestamp())),
|
||||||
|
..<entity::user::ActiveModel as ActiveModelTrait>::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
@@ -146,21 +162,23 @@ impl AuthnBackend for UserRepository {
|
|||||||
.one(&self.connection)
|
.one(&self.connection)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let user = if let Some(user) = user_found {
|
if let Some(user) = user_found {
|
||||||
let rear_user: AuthenticatedUser = user.into();
|
let given_password = creds.password.clone();
|
||||||
Some(rear_user)
|
let user_password = user.password.clone();
|
||||||
} else {
|
let verified =
|
||||||
None
|
task::spawn_blocking(move || verify_password(&given_password, &user_password))
|
||||||
};
|
.await?;
|
||||||
|
|
||||||
// Verifying the password is blocking and potentially slow, so we'll do so via
|
if verified.is_ok() {
|
||||||
// `spawn_blocking`.
|
let mut db_user: entity::user::ActiveModel = user.into();
|
||||||
task::spawn_blocking(|| {
|
db_user.last_login = ActiveValue::Set(Some(get_current_timestamp()));
|
||||||
// We're using password-based authentication--this works by comparing our form
|
let user = db_user.update(&self.connection).await?;
|
||||||
// input with an argon2 password hash.
|
let rear_user: AuthenticatedUser = user.into();
|
||||||
Ok(user.filter(|user| verify_password(creds.password, &user.password).is_ok()))
|
return Ok(Some(rear_user));
|
||||||
})
|
}
|
||||||
.await?
|
}
|
||||||
|
|
||||||
|
Ok(None) // No user found or verification failed
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
|
async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::debug;
|
use log::{debug, warn};
|
||||||
use rear::admin::domain::*;
|
use rear::depot::prelude::*;
|
||||||
use rear::admin::state::AdminRegistry;
|
|
||||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, Set};
|
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, Set};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::models::UserRepository;
|
use crate::models::UserRepository;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AdminRepository for UserRepository {
|
impl DepotRepository for UserRepository {
|
||||||
type Key = i64;
|
type Key = i64;
|
||||||
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
||||||
@@ -26,6 +25,7 @@ impl AdminRepository for UserRepository {
|
|||||||
display_list: &["id", "username"],
|
display_list: &["id", "username"],
|
||||||
fields: &[
|
fields: &[
|
||||||
"username",
|
"username",
|
||||||
|
"password",
|
||||||
"first_name",
|
"first_name",
|
||||||
"last_name",
|
"last_name",
|
||||||
"email",
|
"email",
|
||||||
@@ -35,28 +35,35 @@ impl AdminRepository for UserRepository {
|
|||||||
],
|
],
|
||||||
// fields_readonly: &["last_login", "date_joined"]
|
// fields_readonly: &["last_login", "date_joined"]
|
||||||
}
|
}
|
||||||
.into()
|
.build()
|
||||||
|
.set_widget(
|
||||||
|
"password",
|
||||||
|
Widget::widget("/depot/widgets/password_change.jinja").as_password(),
|
||||||
|
)
|
||||||
|
.set_widget("is_staff", Widget::checkbox())
|
||||||
|
.set_widget("is_active", Widget::checkbox())
|
||||||
|
.set_widget("is_superuser", Widget::checkbox())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem> {
|
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult {
|
||||||
let id: i32 = *id as i32; // use try_into() instead.
|
let id: i32 = *id as i32; // use try_into() instead.
|
||||||
let get_user = entity::User::find_by_id(id).one(&self.connection).await;
|
let get_user = entity::User::find_by_id(id).one(&self.connection).await;
|
||||||
match get_user {
|
if let Ok(get_user) = get_user {
|
||||||
Ok(get_user) => {
|
if let Some(user) = get_user {
|
||||||
if let Some(user) = get_user {
|
let id = user.id.to_string();
|
||||||
let id = user.id.to_string();
|
match serde_json::to_value(&user) {
|
||||||
match serde_json::to_value(&user) {
|
Ok(item) => {
|
||||||
Ok(item) => {
|
return Ok(RepositoryResponse::ItemOnly(model.build_item(&*id, item)));
|
||||||
return Some(model.build_item(&*id, item));
|
}
|
||||||
}
|
Err(_) => {
|
||||||
Err(_) => return None,
|
return Err(RepositoryError::UnknownError(
|
||||||
|
"JSON Error creating value".to_owned(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => return None,
|
|
||||||
}
|
}
|
||||||
|
Ok(RepositoryResponse::NoItem)
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list(&self, model: &RepositoryContext) -> RepositoryList {
|
async fn list(&self, model: &RepositoryContext) -> RepositoryList {
|
||||||
@@ -80,17 +87,14 @@ impl AdminRepository for UserRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create(&mut self, model: &RepositoryContext, data: Value) -> Option<RepositoryItem> {
|
async fn create(&mut self, model: &RepositoryContext, data: Value) -> RepositoryResult {
|
||||||
if let Value::Object(data) = data {
|
if let Value::Object(data) = data {
|
||||||
let username = data.get("username").unwrap().as_str().unwrap();
|
let username = data.get("username").unwrap().as_str().unwrap();
|
||||||
|
let password = data.get("password").unwrap().as_str().unwrap();
|
||||||
|
|
||||||
let mut user = entity::user::ActiveModel {
|
let mut user = UserRepository::new_user(username, password);
|
||||||
username: Set(username.to_owned()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let keys = [
|
let keys = [
|
||||||
"password",
|
|
||||||
"first_name",
|
"first_name",
|
||||||
"last_name",
|
"last_name",
|
||||||
"email",
|
"email",
|
||||||
@@ -102,16 +106,11 @@ impl AdminRepository for UserRepository {
|
|||||||
for key in &keys {
|
for key in &keys {
|
||||||
if let Some(value) = data.get(*key) {
|
if let Some(value) = data.get(*key) {
|
||||||
match *key {
|
match *key {
|
||||||
"password" => {
|
|
||||||
user.password = Set(UserRepository::encode_password(
|
|
||||||
value.as_str().unwrap().to_owned(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
"first_name" => user.first_name = Set(value.as_str().map(|s| s.to_owned())),
|
"first_name" => user.first_name = Set(value.as_str().map(|s| s.to_owned())),
|
||||||
"last_name" => user.last_name = Set(value.as_str().map(|s| s.to_owned())),
|
"last_name" => user.last_name = Set(value.as_str().map(|s| s.to_owned())),
|
||||||
"email" => user.email = Set(value.as_str().map(|s| s.to_owned())),
|
"email" => user.email = Set(value.as_str().map(|s| s.to_owned())),
|
||||||
"is_staff" => user.is_staff = Set(value.as_bool().unwrap_or(false)),
|
"is_staff" => user.is_staff = Set(value.as_bool().unwrap_or(false)),
|
||||||
"is_active" => user.is_active = Set(value.as_bool().unwrap_or(false)),
|
"is_active" => user.is_active = Set(value.as_bool().unwrap_or(true)),
|
||||||
"is_superuser" => user.is_superuser = Set(value.as_bool().unwrap_or(false)),
|
"is_superuser" => user.is_superuser = Set(value.as_bool().unwrap_or(false)),
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
@@ -120,10 +119,11 @@ impl AdminRepository for UserRepository {
|
|||||||
|
|
||||||
if let Ok(user) = user.insert(&self.connection).await {
|
if let Ok(user) = user.insert(&self.connection).await {
|
||||||
let id = user.id.to_string();
|
let id = user.id.to_string();
|
||||||
return Some(model.build_item(&*id, serde_json::to_value(&user).unwrap()));
|
let item = model.build_item(&*id, serde_json::to_value(&user).unwrap());
|
||||||
|
return Ok(RepositoryResponse::ItemOnly(item));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
Ok(RepositoryResponse::NoItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update(
|
async fn update(
|
||||||
@@ -131,32 +131,57 @@ impl AdminRepository for UserRepository {
|
|||||||
model: &RepositoryContext,
|
model: &RepositoryContext,
|
||||||
id: &Self::Key,
|
id: &Self::Key,
|
||||||
data: Value,
|
data: Value,
|
||||||
) -> Option<RepositoryItem> {
|
) -> RepositoryResult {
|
||||||
let id: i32 = *id as i32;
|
let id: i32 = *id as i32;
|
||||||
let user: Option<entity::user::Model> = entity::User::find_by_id(id)
|
let user: Option<entity::user::Model> = entity::User::find_by_id(id)
|
||||||
.one(&self.connection)
|
.one(&self.connection)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.map_err(|e| RepositoryError::DatabaseError(Box::new(e)))?;
|
||||||
let mut user: entity::user::ActiveModel = user.unwrap().into();
|
let mut user: entity::user::ActiveModel = user.ok_or(RepositoryError::ItemNotFound)?.into();
|
||||||
|
|
||||||
// change values
|
// should we really allow username change?
|
||||||
if let Some(value) = data.get("username") {
|
if let Some(value) = data.get("username") {
|
||||||
if let Some(value) = value.as_str() {
|
if let Some(value) = value.as_str() {
|
||||||
user.username = Set(value.to_owned());
|
user.username = Set(value.to_owned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(value) = data.get("password") {
|
let keys = [
|
||||||
user.password = Set(value.as_str().unwrap().to_owned());
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"email",
|
||||||
|
"is_staff",
|
||||||
|
"is_active",
|
||||||
|
"is_superuser",
|
||||||
|
];
|
||||||
|
|
||||||
|
for key in &keys {
|
||||||
|
if let Some(value) = data.get(*key) {
|
||||||
|
match *key {
|
||||||
|
"first_name" => user.first_name = Set(value.as_str().map(|s| s.to_owned())),
|
||||||
|
"last_name" => user.last_name = Set(value.as_str().map(|s| s.to_owned())),
|
||||||
|
"email" => user.email = Set(value.as_str().map(|s| s.to_owned())),
|
||||||
|
"is_staff" => user.is_staff = Set(value.as_bool().unwrap_or(false)),
|
||||||
|
"is_active" => user.is_active = Set(value.as_bool().unwrap_or(true)),
|
||||||
|
"is_superuser" => user.is_superuser = Set(value.as_bool().unwrap_or(false)),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update
|
// update
|
||||||
if let Ok(user) = user.update(&self.connection).await {
|
match user.update(&self.connection).await {
|
||||||
let id = user.id.to_string();
|
Ok(user) => {
|
||||||
return Some(model.build_item(&*id, serde_json::to_value(&user).unwrap()));
|
let id = user.id.to_string();
|
||||||
|
return Ok(RepositoryResponse::ItemOnly(
|
||||||
|
model.build_item(&*id, serde_json::to_value(&user).unwrap()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Error updating user");
|
||||||
|
return Err(RepositoryError::DatabaseError(Box::new(err)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn replace(
|
async fn replace(
|
||||||
@@ -164,7 +189,7 @@ impl AdminRepository for UserRepository {
|
|||||||
model: &RepositoryContext,
|
model: &RepositoryContext,
|
||||||
id: &Self::Key,
|
id: &Self::Key,
|
||||||
data: Value,
|
data: Value,
|
||||||
) -> Option<RepositoryItem> {
|
) -> RepositoryResult {
|
||||||
self.update(model, id, data).await
|
self.update(model, id, data).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +201,7 @@ impl AdminRepository for UserRepository {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
if let Some(user) = user {
|
if let Some(user) = user {
|
||||||
let delete_result = user.delete(&self.connection).await.unwrap();
|
let delete_result = user.delete(&self.connection).await.unwrap();
|
||||||
|
// .ok_or(RepositoryError::DatabaseError(Box::new(err)))?;
|
||||||
debug!("deleted rows: {}", delete_result.rows_affected);
|
debug!("deleted rows: {}", delete_result.rows_affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,16 +209,16 @@ impl AdminRepository for UserRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(registry: &mut AdminRegistry, db: DatabaseConnection) {
|
pub fn register(registry: &mut DepotRegistry, db: DatabaseConnection) -> UserRepository {
|
||||||
let app_key = registry.register_app("Auth");
|
let section_key = registry.register_section("Auth");
|
||||||
let repo = UserRepository::new(db);
|
let repo = UserRepository::new(db);
|
||||||
let model_config = AdminModelConfig {
|
let model_config = DepotModelConfig {
|
||||||
app_key: app_key,
|
section_key: section_key,
|
||||||
name: "User".to_owned(),
|
name: "User".to_owned(),
|
||||||
};
|
};
|
||||||
let model_result = registry.register_model(model_config, repo);
|
let model_result = registry.register_model(model_config, repo.clone());
|
||||||
match model_result {
|
match model_result {
|
||||||
Err(err) => panic!("{}", err),
|
Err(err) => panic!("{}", err),
|
||||||
_ => (),
|
_ => repo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use sea_orm::prelude::DateTimeWithTimeZone;
|
||||||
|
|
||||||
|
pub fn get_current_timestamp() -> DateTimeWithTimeZone {
|
||||||
|
let utc_now = chrono::Utc::now();
|
||||||
|
// Apply the timezone offset to the UTC time: FixedOffset::east(offset_hours * 3600);
|
||||||
|
let local_time = utc_now;
|
||||||
|
local_time.into()
|
||||||
|
}
|
||||||
+16
-15
@@ -6,7 +6,6 @@ use axum::{
|
|||||||
Form, Router,
|
Form, Router,
|
||||||
};
|
};
|
||||||
use axum_messages::{Message, Messages};
|
use axum_messages::{Message, Messages};
|
||||||
use rear::service::templates::Templates;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::models::{AuthSession, Credentials};
|
use crate::models::{AuthSession, Credentials};
|
||||||
@@ -24,11 +23,12 @@ pub struct NextUrl {
|
|||||||
next: Option<String>,
|
next: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn router() -> Router<Templates> {
|
pub fn routes<S: rear::depot::state::DepotState + Clone + Send + Sync + 'static>() -> Router<S>
|
||||||
|
where {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/login", post(self::post::login))
|
.route("/login", post(self::post::login))
|
||||||
.route("/login", get(self::get::login))
|
.route("/login", get(self::get::login::<S>))
|
||||||
.route("/logout", get(self::get::logout))
|
.route("/logout", post(self::post::logout))
|
||||||
}
|
}
|
||||||
|
|
||||||
mod post {
|
mod post {
|
||||||
@@ -44,7 +44,7 @@ mod post {
|
|||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
messages.error("Invalid credentials");
|
messages.error("Invalid credentials");
|
||||||
|
|
||||||
let mut login_url = "/login".to_string();
|
let mut login_url = "/depot/login".to_string();
|
||||||
if let Some(next) = creds.next {
|
if let Some(next) = creds.next {
|
||||||
login_url = format!("{}?next={}", login_url, next);
|
login_url = format!("{}?next={}", login_url, next);
|
||||||
};
|
};
|
||||||
@@ -67,29 +67,30 @@ mod post {
|
|||||||
}
|
}
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse {
|
||||||
|
match auth_session.logout().await {
|
||||||
|
Ok(_) => Redirect::to("/depot/login").into_response(),
|
||||||
|
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod get {
|
mod get {
|
||||||
use super::*;
|
use super::*;
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
|
|
||||||
pub async fn login(
|
pub async fn login<S: rear::depot::state::DepotState + Clone + Send + Sync + 'static>(
|
||||||
messages: Messages,
|
messages: Messages,
|
||||||
templates: State<Templates>,
|
admin: State<S>,
|
||||||
Query(NextUrl { next }): Query<NextUrl>,
|
Query(NextUrl { next }): Query<NextUrl>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
|
let templates = admin.get_templates();
|
||||||
let context = LoginTemplate {
|
let context = LoginTemplate {
|
||||||
messages: messages.into_iter().collect(),
|
messages: messages.into_iter().collect(),
|
||||||
next,
|
next,
|
||||||
};
|
};
|
||||||
templates.render_html("login.html", context)
|
templates.render_html("depot/login.html", context)
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse {
|
|
||||||
match auth_session.logout().await {
|
|
||||||
Ok(_) => Redirect::to("/login").into_response(),
|
|
||||||
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::admin::domain::*;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use rear::depot::prelude::*;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
struct Repository {}
|
struct Repository {}
|
||||||
@@ -7,7 +7,7 @@ struct Repository {}
|
|||||||
impl Repository {}
|
impl Repository {}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AdminRepository for Repository {
|
impl DepotRepository for Repository {
|
||||||
type Key = i64;
|
type Key = i64;
|
||||||
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
||||||
@@ -34,17 +34,13 @@ impl AdminRepository for Repository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// POST on item collection.
|
// POST on item collection.
|
||||||
async fn create(
|
async fn create(&mut self, model: &RepositoryContext, mut data: Value) -> RepositoryResult {
|
||||||
&mut self,
|
Ok(RepositoryResponse::NoItem)
|
||||||
model: &RepositoryContext,
|
|
||||||
mut data: Value,
|
|
||||||
) -> Option<RepositoryItem> {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET single item.
|
// GET single item.
|
||||||
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem> {
|
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult {
|
||||||
None
|
Ok(RepositoryResponse::NoItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH single item.
|
// PATCH single item.
|
||||||
@@ -53,8 +49,8 @@ impl AdminRepository for Repository {
|
|||||||
model: &RepositoryContext,
|
model: &RepositoryContext,
|
||||||
id: &Self::Key,
|
id: &Self::Key,
|
||||||
data: Value,
|
data: Value,
|
||||||
) -> Option<RepositoryItem> {
|
) -> RepositoryResult {
|
||||||
None
|
Ok(RepositoryResponse::NoItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PUT single item.
|
// PUT single item.
|
||||||
@@ -63,8 +59,8 @@ impl AdminRepository for Repository {
|
|||||||
model: &RepositoryContext,
|
model: &RepositoryContext,
|
||||||
id: &Self::Key,
|
id: &Self::Key,
|
||||||
data: Value,
|
data: Value,
|
||||||
) -> Option<RepositoryItem> {
|
) -> RepositoryResult {
|
||||||
None
|
Ok(RepositoryResponse::NoItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE single item.
|
// DELETE single item.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::admin::domain::*;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use rear::admin::state::AdminRegistry;
|
use rear::depot::prelude::*;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ impl FileRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AdminRepository for FileRepository {
|
impl DepotRepository for FileRepository {
|
||||||
type Key = String;
|
type Key = String;
|
||||||
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
||||||
@@ -62,17 +61,13 @@ impl AdminRepository for FileRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// POST on item collection.
|
// POST on item collection.
|
||||||
async fn create(
|
async fn create(&mut self, model: &RepositoryContext, mut data: Value) -> RepositoryResult {
|
||||||
&mut self,
|
Ok(RepositoryResponse::NoItem)
|
||||||
model: &RepositoryContext,
|
|
||||||
mut data: Value,
|
|
||||||
) -> Option<RepositoryItem> {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET single item.
|
// GET single item.
|
||||||
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem> {
|
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult {
|
||||||
None
|
Ok(RepositoryResponse::NoItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH single item.
|
// PATCH single item.
|
||||||
@@ -81,8 +76,8 @@ impl AdminRepository for FileRepository {
|
|||||||
model: &RepositoryContext,
|
model: &RepositoryContext,
|
||||||
id: &Self::Key,
|
id: &Self::Key,
|
||||||
data: Value,
|
data: Value,
|
||||||
) -> Option<RepositoryItem> {
|
) -> RepositoryResult {
|
||||||
None
|
Ok(RepositoryResponse::NoItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PUT single item.
|
// PUT single item.
|
||||||
@@ -91,8 +86,8 @@ impl AdminRepository for FileRepository {
|
|||||||
model: &RepositoryContext,
|
model: &RepositoryContext,
|
||||||
id: &Self::Key,
|
id: &Self::Key,
|
||||||
data: Value,
|
data: Value,
|
||||||
) -> Option<RepositoryItem> {
|
) -> RepositoryResult {
|
||||||
None
|
Ok(RepositoryResponse::NoItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE single item.
|
// DELETE single item.
|
||||||
@@ -101,11 +96,11 @@ impl AdminRepository for FileRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(registry: &mut AdminRegistry, path: &str) {
|
pub fn register(registry: &mut DepotRegistry, path: &str) {
|
||||||
let app_key = registry.register_app("Files");
|
let section_key = registry.register_section("Files");
|
||||||
let repo = FileRepository::new(path);
|
let repo = FileRepository::new(path);
|
||||||
let model_config = AdminModelConfig {
|
let model_config = DepotModelConfig {
|
||||||
app_key: app_key,
|
section_key: section_key,
|
||||||
name: "Files".to_owned(),
|
name: "Files".to_owned(),
|
||||||
};
|
};
|
||||||
let model_result = registry.register_model(model_config, repo);
|
let model_result = registry.register_model(model_config, repo);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use crate::admin::domain::*;
|
use crate::depot::prelude::*;
|
||||||
use crate::admin::state::AdminRegistry;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::{debug, warn};
|
use log::debug;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
/// This is a showcase implementation with a static repository
|
/// This is a showcase implementation with a static repository
|
||||||
@@ -35,7 +34,7 @@ impl MyStaticRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AdminRepository for MyStaticRepository {
|
impl DepotRepository for MyStaticRepository {
|
||||||
type Key = i64;
|
type Key = i64;
|
||||||
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
||||||
@@ -58,11 +57,13 @@ impl AdminRepository for MyStaticRepository {
|
|||||||
//.set_widget("name", Widget::textarea().options(&[("disabled", "true")]))
|
//.set_widget("name", Widget::textarea().options(&[("disabled", "true")]))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem> {
|
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult {
|
||||||
let id = *id as usize;
|
let id = *id as usize;
|
||||||
let item = self.content.get(id - 1).cloned().unwrap();
|
let item = self.content.get(id - 1).cloned().unwrap();
|
||||||
let id = item.get("id").unwrap();
|
let id = item.get("id").unwrap();
|
||||||
Some(model.build_item(&*id.to_string(), item))
|
Ok(RepositoryResponse::ItemOnly(
|
||||||
|
model.build_item(&*id.to_string(), item),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list(&self, model: &RepositoryContext) -> RepositoryList {
|
async fn list(&self, model: &RepositoryContext) -> RepositoryList {
|
||||||
@@ -71,16 +72,21 @@ impl AdminRepository for MyStaticRepository {
|
|||||||
.content
|
.content
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|item| model.build_item(&*item.get("id").unwrap().to_string(), item))
|
.filter_map(|item| match item.get("id") {
|
||||||
|
Some(id_value) => {
|
||||||
|
let id_str = id_value.to_string();
|
||||||
|
Some(model.build_item(&id_str, item))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
eprintln!("Skipping item due to missing 'id'");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create(
|
async fn create(&mut self, model: &RepositoryContext, mut data: Value) -> RepositoryResult {
|
||||||
&mut self,
|
|
||||||
model: &RepositoryContext,
|
|
||||||
mut data: Value,
|
|
||||||
) -> Option<RepositoryItem> {
|
|
||||||
debug!("Asked to create: {}", data);
|
debug!("Asked to create: {}", data);
|
||||||
|
|
||||||
let new_id = self.next_id;
|
let new_id = self.next_id;
|
||||||
@@ -94,7 +100,9 @@ impl AdminRepository for MyStaticRepository {
|
|||||||
self.content.push(data.clone());
|
self.content.push(data.clone());
|
||||||
|
|
||||||
// Return the newly created item
|
// Return the newly created item
|
||||||
Some(model.build_item(&*new_id.to_string(), data))
|
Ok(RepositoryResponse::ItemOnly(
|
||||||
|
model.build_item(&*new_id.to_string(), data),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update(
|
async fn update(
|
||||||
@@ -102,7 +110,7 @@ impl AdminRepository for MyStaticRepository {
|
|||||||
model: &RepositoryContext,
|
model: &RepositoryContext,
|
||||||
id: &Self::Key,
|
id: &Self::Key,
|
||||||
data: Value,
|
data: Value,
|
||||||
) -> Option<RepositoryItem> {
|
) -> RepositoryResult {
|
||||||
debug!("I would now update: {}, {}", id, data);
|
debug!("I would now update: {}, {}", id, data);
|
||||||
|
|
||||||
// First, find the index of the item to update
|
// First, find the index of the item to update
|
||||||
@@ -118,13 +126,16 @@ impl AdminRepository for MyStaticRepository {
|
|||||||
if let Some(index) = item_index {
|
if let Some(index) = item_index {
|
||||||
let item = &mut self.content[index];
|
let item = &mut self.content[index];
|
||||||
*item = data.clone();
|
*item = data.clone();
|
||||||
|
item["id"] = (*id).into();
|
||||||
|
|
||||||
if let Some(item_id) = item.get("id") {
|
if let Some(item_id) = item.get("id") {
|
||||||
return Some(model.build_item(&*item_id.to_string(), data));
|
return Ok(RepositoryResponse::ItemOnly(
|
||||||
|
model.build_item(&*item_id.to_string(), data),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
Ok(RepositoryResponse::NoItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn replace(
|
async fn replace(
|
||||||
@@ -132,7 +143,7 @@ impl AdminRepository for MyStaticRepository {
|
|||||||
model: &RepositoryContext,
|
model: &RepositoryContext,
|
||||||
id: &Self::Key,
|
id: &Self::Key,
|
||||||
data: Value,
|
data: Value,
|
||||||
) -> Option<RepositoryItem> {
|
) -> RepositoryResult {
|
||||||
self.update(model, id, data).await
|
self.update(model, id, data).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,11 +167,11 @@ impl AdminRepository for MyStaticRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(registry: &mut AdminRegistry) {
|
pub fn register(registry: &mut DepotRegistry) {
|
||||||
let app_key = registry.register_app("Example App");
|
let section_key = registry.register_section("Example App");
|
||||||
let repo = MyStaticRepository::new();
|
let repo = MyStaticRepository::new();
|
||||||
let model_config = AdminModelConfig {
|
let model_config = DepotModelConfig {
|
||||||
app_key: app_key,
|
section_key: section_key,
|
||||||
name: "ExampleModel".to_owned(),
|
name: "ExampleModel".to_owned(),
|
||||||
};
|
};
|
||||||
let model_result = registry.register_model(model_config, repo);
|
let model_result = registry.register_model(model_config, repo);
|
||||||
|
|||||||
+12
-7
@@ -1,5 +1,5 @@
|
|||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use entity;
|
use rear_auth::models::UserRepository;
|
||||||
use sea_orm::{ActiveModelTrait, Database, DatabaseConnection, DbConn, DbErr, Set};
|
use sea_orm::{ActiveModelTrait, Database, DatabaseConnection, DbConn, DbErr, Set};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::io::stdin;
|
use std::io::stdin;
|
||||||
@@ -11,11 +11,12 @@ pub async fn establish_connection() -> DatabaseConnection {
|
|||||||
db.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
|
db.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_user(conn: &DbConn, username: &str) -> Result<entity::user::Model, DbErr> {
|
pub async fn create_user(
|
||||||
let user = entity::user::ActiveModel {
|
conn: &DbConn,
|
||||||
username: Set(username.to_owned()),
|
username: &str,
|
||||||
..Default::default()
|
password: &str,
|
||||||
};
|
) -> Result<entity::user::Model, DbErr> {
|
||||||
|
let user = UserRepository::new_user(username, password);
|
||||||
let user = user.insert(conn).await?;
|
let user = user.insert(conn).await?;
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
@@ -25,12 +26,16 @@ async fn main() {
|
|||||||
let connection = establish_connection().await;
|
let connection = establish_connection().await;
|
||||||
|
|
||||||
let mut username = String::new();
|
let mut username = String::new();
|
||||||
|
let mut password = String::new();
|
||||||
|
|
||||||
println!("Enter username for new user:");
|
println!("Enter username for new user:");
|
||||||
stdin().read_line(&mut username).unwrap();
|
stdin().read_line(&mut username).unwrap();
|
||||||
let username = username.trim_end(); // Remove the trailing newline
|
let username = username.trim_end(); // Remove the trailing newline
|
||||||
|
println!("Enter password for new user:");
|
||||||
|
stdin().read_line(&mut password).unwrap();
|
||||||
|
let password = password.trim_end(); // Remove the trailing newline
|
||||||
|
|
||||||
let user = create_user(&connection, username)
|
let user = create_user(&connection, username, password)
|
||||||
.await
|
.await
|
||||||
.expect("Error creating user");
|
.expect("Error creating user");
|
||||||
println!("\nSaved user {} with id {}", username, user.id);
|
println!("\nSaved user {} with id {}", username, user.id);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ async fn main() {
|
|||||||
|
|
||||||
println!("Displaying {} users", results.len());
|
println!("Displaying {} users", results.len());
|
||||||
for user in results {
|
for user in results {
|
||||||
println!("{}", user.username);
|
println!("{} - {}", user.username, user.password);
|
||||||
}
|
}
|
||||||
println!("-----------\n");
|
println!("-----------\n");
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-4
@@ -16,9 +16,14 @@ use axum::{
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::State, handler::HandlerWithoutStateExt, response::IntoResponse, routing::get, Router,
|
extract::State, handler::HandlerWithoutStateExt, response::IntoResponse, routing::get, Router,
|
||||||
};
|
};
|
||||||
|
use axum_login::login_required;
|
||||||
|
use axum_login::tower_sessions::MemoryStore;
|
||||||
|
use axum_login::tower_sessions::SessionManagerLayer;
|
||||||
|
use axum_login::AuthManagerLayerBuilder;
|
||||||
|
use axum_messages::MessagesManagerLayer;
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use log::info;
|
use log::info;
|
||||||
use rear::admin;
|
use rear::depot;
|
||||||
use rear::service::{handlers, templates};
|
use rear::service::{handlers, templates};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
@@ -48,11 +53,11 @@ async fn main() {
|
|||||||
|
|
||||||
// Prepare Application State Members
|
// Prepare Application State Members
|
||||||
let tmpl = templates::Templates::initialize().expect("Template Engine could not be loaded.");
|
let tmpl = templates::Templates::initialize().expect("Template Engine could not be loaded.");
|
||||||
let mut admin = admin::state::AdminRegistry::new("admin");
|
let mut admin = depot::prelude::DepotRegistry::new("admin");
|
||||||
|
|
||||||
// Register Admin Apps
|
// Register Admin Apps
|
||||||
static_repository::register(&mut admin);
|
static_repository::register(&mut admin);
|
||||||
//user_repository::register(&mut admin, db_connection);
|
let admin_user_repo = rear_auth::user_admin_repository::register(&mut admin, db_connection);
|
||||||
file_repository::register(&mut admin, "static/admin");
|
file_repository::register(&mut admin, "static/admin");
|
||||||
|
|
||||||
// Create global Application State.
|
// Create global Application State.
|
||||||
@@ -61,12 +66,28 @@ async fn main() {
|
|||||||
admin: Arc::new(admin),
|
admin: Arc::new(admin),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Session System
|
||||||
|
let session_store = MemoryStore::default();
|
||||||
|
let session_layer = SessionManagerLayer::new(session_store);
|
||||||
|
// Auth service.
|
||||||
|
let auth_layer = AuthManagerLayerBuilder::new(admin_user_repo, session_layer).build();
|
||||||
|
|
||||||
// Application Route
|
// Application Route
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(home))
|
.route("/", get(home))
|
||||||
.route("/hello", get(hello_world))
|
.route("/hello", get(hello_world))
|
||||||
//.merge(admin_router)
|
//.merge(admin_router)
|
||||||
.nest("/admin", admin::routes())
|
.nest(
|
||||||
|
"/admin",
|
||||||
|
depot::routes()
|
||||||
|
.route_layer(login_required!(
|
||||||
|
rear_auth::models::UserRepository,
|
||||||
|
login_url = "/admin/login"
|
||||||
|
))
|
||||||
|
.merge(rear_auth::views::routes())
|
||||||
|
.layer(MessagesManagerLayer)
|
||||||
|
.layer(auth_layer),
|
||||||
|
)
|
||||||
.nest("/howto", howto::routes())
|
.nest("/howto", howto::routes())
|
||||||
.route_service("/static/*file", embed::static_handler.into_service())
|
.route_service("/static/*file", embed::static_handler.into_service())
|
||||||
.fallback(handlers::not_found_handler)
|
.fallback(handlers::not_found_handler)
|
||||||
|
|||||||
+4
-4
@@ -1,12 +1,12 @@
|
|||||||
use axum::extract::FromRef;
|
use axum::extract::FromRef;
|
||||||
|
|
||||||
use rear::admin::state::{AdminState, SharedAdminRegistry};
|
use rear::depot::state::{DepotState, SharedDepotRegistry};
|
||||||
use rear::service::templates;
|
use rear::service::templates;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub templates: templates::Templates,
|
pub templates: templates::Templates,
|
||||||
pub admin: SharedAdminRegistry,
|
pub admin: SharedDepotRegistry,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromRef<AppState> for templates::Templates {
|
impl FromRef<AppState> for templates::Templates {
|
||||||
@@ -15,12 +15,12 @@ impl FromRef<AppState> for templates::Templates {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AdminState for AppState {
|
impl DepotState for AppState {
|
||||||
fn get_templates(&self) -> &templates::Templates {
|
fn get_templates(&self) -> &templates::Templates {
|
||||||
&self.templates
|
&self.templates
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_registry(&self) -> SharedAdminRegistry {
|
fn get_registry(&self) -> SharedDepotRegistry {
|
||||||
self.admin.clone()
|
self.admin.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
https://unpkg.com/hyperscript.org@0.9.11/dist/_hyperscript.min.js
|
https://unpkg.com/hyperscript.org@0.9.12/dist/_hyperscript.min.js
|
||||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -1,10 +0,0 @@
|
|||||||
{% extends "admin/base.jinja" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% include "admin/dashboard.jinja" %}
|
|
||||||
<div>
|
|
||||||
Some text
|
|
||||||
another text
|
|
||||||
Third text
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{% extends base|none("admin/base.jinja") %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="ui container">
|
|
||||||
<h1>Update {{item_model.name}} in {{item_info.name}}</h1>
|
|
||||||
{% set fields = item_info.fields %}
|
|
||||||
{% set form = { 'action': item.change_url } %}
|
|
||||||
{% include "/admin/items/item_change_form.jinja" %}
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<form action="{{form.action}}" method="{{form.method|default('POST')}}" class="ui form">
|
|
||||||
<!--noformat-->
|
|
||||||
{% from "/admin/items/items.jinja" import field_widget %}
|
|
||||||
{% for field_name, field_defs in fields %}
|
|
||||||
{% if item %}
|
|
||||||
{% set field_value = item.fields[field_name]|none("") %}
|
|
||||||
{% else %}
|
|
||||||
{% set field_value = "" %}
|
|
||||||
{% endif %}
|
|
||||||
<div class="field">
|
|
||||||
{{ field_widget(field_name, field_defs, field_value) }}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
<!--noformat-->
|
|
||||||
<button class="ui button" type="submit">Create</button>
|
|
||||||
</form>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{% extends base|none("admin/base.jinja") %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
{% if item %}
|
|
||||||
{{item.fields}}
|
|
||||||
{% else %}
|
|
||||||
No Item found.
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock content %}
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
{% include "fomantic.html" %}
|
{% include "fomantic.html" %}
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/admin/admin.css">
|
<link rel="stylesheet" type="text/css" href="/static/rear/depot.css">
|
||||||
|
|
||||||
{% block extrastyle %}{% endblock %}
|
{% block extrastyle %}{% endblock %}
|
||||||
{% block extrahead %}{% endblock %}
|
{% block extrahead %}{% endblock %}
|
||||||
@@ -42,14 +42,14 @@
|
|||||||
{% endblock usertools %}
|
{% endblock usertools %}
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<a class="ui blue image label">
|
<a class="ui blue image label">
|
||||||
<img src="/static/admin/teddy_bear.png">
|
<img src="/static/rear/teddy_bear.png">
|
||||||
Person
|
Person
|
||||||
<div class="detail">Admin</div>
|
<div class="detail">Admiral</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="icon item">
|
<div class="icon item">
|
||||||
<form id="logout-form" method="post" action="{{ url('admin:logout') }}">
|
<form id="logout-form" method="post" action="{{ url('depot:logout') }}">
|
||||||
<i class="power link icon" style="float: none;"></i>
|
<i class="power link icon" style="float: none;"></i>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
<div class="ui vertical sidebar left visible overlay" id="main_sidemenu">
|
<div class="ui vertical sidebar left visible overlay" id="main_sidemenu">
|
||||||
<div class="ui vertical inverted fluid menu">
|
<div class="ui vertical inverted fluid menu">
|
||||||
<div class="item"><a href="{{admin_url}}">
|
<div class="item"><a href="{{depot_url}}">
|
||||||
<i class="big d20 dice icon" style="float: none;"></i>
|
<i class="big d20 dice icon" style="float: none;"></i>
|
||||||
<b>Administration</b>
|
<b>Administration</b>
|
||||||
</a>
|
</a>
|
||||||
@@ -97,17 +97,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if available_apps %}
|
{% if sections %}
|
||||||
{% for app in available_apps %}
|
{% for section in sections %}
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<i class="cog link icon" href="{{ app.admin_url }}"></i>
|
<i class="cog link icon" href="{{ section.section_url }}"></i>
|
||||||
<div class="header">{{ app.name }} </div>
|
<div class="header">{{ section.name }} </div>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
{% for model in app.models %}
|
{% for model in section.models %}
|
||||||
<div
|
<div
|
||||||
class="item model-{{ model.key }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
|
class="item model-{{ model.key }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
|
||||||
{% if model.admin_url %}
|
{% if model.model_url %}
|
||||||
<a href="{{ model.admin_url }}" hx-get="{{ model.admin_url }}" hx-target="#main" hx-push-url="true" {% if
|
<a href="{{ model.model_url }}" hx-get="{{ model.model_url }}" hx-target="#main" hx-push-url="true" {% if
|
||||||
model.admin_url in request.path|urlencode %} aria-current="page" {% endif %}>{{ model.name }}</a>
|
model.admin_url in request.path|urlencode %} aria-current="page" {% endif %}>{{ model.name }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span>{{ model.name }}</span>
|
<span>{{ model.name }}</span>
|
||||||
@@ -139,6 +139,9 @@
|
|||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
{% block bodyjs %}
|
{% block bodyjs %}
|
||||||
<script src="/static/htmx/htmx.min.js"></script>
|
<script src="/static/htmx/htmx.min.js"></script>
|
||||||
|
<script src="/static/hyperscript/hyperscript.min.js"></script>
|
||||||
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$('#main_sidemenu').sidebar({
|
$('#main_sidemenu').sidebar({
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
{% if app_list %}
|
{% if sections %}
|
||||||
{% for app in app_list %}
|
{% for section in sections %}
|
||||||
<div
|
<div
|
||||||
class="app-{{ app.key }} module{% if app.app_url in request.path|urlencode %} current-app{% endif %} ui short scrolling container">
|
class="section-{{ section.key }} module{% if section.section_url in request.path|urlencode %} current-section{% endif %} ui short scrolling container">
|
||||||
<table class="ui very compact celled table head stuck unstackable">
|
<table class="ui very compact celled table head stuck unstackable">
|
||||||
<caption>
|
<caption>
|
||||||
<a href="{{ app.app_url }}" class="section" title="Models in the {{ name }} application">{{ app.name }}</a>
|
<a href="{{ section.section_url }}" class="section"
|
||||||
|
title="Models in the {{ name }} Section">{{ section.name }}</a>
|
||||||
</caption>
|
</caption>
|
||||||
{% for model in app.models %}
|
{% for model in section.models %}
|
||||||
<tr class="model-{{ model.key }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
|
<tr class="model-{{ model.key }}{% if model.model_url in request.path|urlencode %} current-model{% endif %}">
|
||||||
{% if model.admin_url %}
|
{% if model.model_url %}
|
||||||
<th scope="row"><a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %}
|
<th scope="row"><a href="{{ model.model_url }}" {% if model.model_url in request.path|urlencode %}
|
||||||
aria-current="page" {% endif %}>{{ model.name }}</a></th>
|
aria-current="page" {% endif %}>{{ model.name }}</a></th>
|
||||||
{% else %}
|
{% else %}
|
||||||
<th scope="row">{{ model.name }}</th>
|
<th scope="row">{{ model.name }}</th>
|
||||||
@@ -21,11 +22,11 @@
|
|||||||
<td></td>
|
<td></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if model.admin_url and show_changelinks %}
|
{% if model.model_url and show_changelinks %}
|
||||||
{% if model.view_only %}
|
{% if model.view_only %}
|
||||||
<td><a href="{{ model.admin_url }}" class="viewlink">{{ translate( 'View') }}</a></td>
|
<td><a href="{{ model.model_url }}" class="viewlink">{{ translate( 'View') }}</a></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td><a href="{{ model.admin_url }}" class="changelink">{{ translate( 'Change') }}</a></td>
|
<td><a href="{{ model.model_url }}" class="changelink">{{ translate( 'Change') }}</a></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif show_changelinks %}
|
{% elif show_changelinks %}
|
||||||
<td></td>
|
<td></td>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{% extends "depot/base.jinja" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include "depot/dashboard.jinja" %}
|
||||||
|
<div>
|
||||||
|
Some text
|
||||||
|
another text
|
||||||
|
Third text
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{% extends base|none("depot/base.jinja") %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="message-box-area" class="ui message" _="init wait 3s then transition opacity to 0 then remove me">
|
||||||
|
Hi there.
|
||||||
|
</div>
|
||||||
|
<div class="ui container">
|
||||||
|
<h1>Update {{item_model.name}} in {{item_info.name}}</h1>
|
||||||
|
{% set fields = item_info.fields %}
|
||||||
|
{% set form = { 'action': item.change_url, 'method': 'PATCH' } %}
|
||||||
|
{% include "/depot/items/item_change_form.jinja" %}
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<!--noformat-->
|
||||||
|
<form class="ui form"
|
||||||
|
hx-target="main"
|
||||||
|
{% if form.method|upper == 'POST' or not form.method %}
|
||||||
|
hx-post="{{ form.action }}"
|
||||||
|
{% elif form.method|upper == 'GET' %}
|
||||||
|
hx-get="{{ form.action }}"
|
||||||
|
{% elif form.method|upper == 'PATCH' %}
|
||||||
|
hx-patch="{{ form.action }}"
|
||||||
|
{% elif form.method|upper == 'PUT' %}
|
||||||
|
hx-put="{{ form.action }}"
|
||||||
|
{% else %}
|
||||||
|
unknown-form-method="{{ form.method }}"
|
||||||
|
{% endif %}>
|
||||||
|
|
||||||
|
{% from "/depot/items/items.jinja" import field_widget %}
|
||||||
|
{% for field_name, field_defs in fields %}
|
||||||
|
{% if item %}
|
||||||
|
{% set field_value = item.fields[field_name]|none("") %}
|
||||||
|
{% else %}
|
||||||
|
{% set field_value = "" %}
|
||||||
|
{% endif %}
|
||||||
|
<div class="field">
|
||||||
|
{{ field_widget(field_name, field_defs, field_value) }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<button class="ui button" type="submit">Create</button>
|
||||||
|
</form>
|
||||||
|
<!--noformat-->
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{% extends base|none("admin/base.jinja") %}
|
{% extends base|none("depot/base.jinja") %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
@@ -12,6 +12,6 @@
|
|||||||
|
|
||||||
{% set fields = item_info.fields %}
|
{% set fields = item_info.fields %}
|
||||||
{% set form = { 'action': item_model.add_url } %}
|
{% set form = { 'action': item_model.add_url } %}
|
||||||
{% include "/admin/items/item_change_form.jinja" %}
|
{% include "/depot/items/item_change_form.jinja" %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends base|none("depot/base.jinja") %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% if item %}
|
||||||
|
{{item.fields}}
|
||||||
|
{% else %}
|
||||||
|
No Item found.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{% extends base|none("admin/base.jinja") %}
|
{% extends base|none("depot/base.jinja") %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Login</title>
|
||||||
|
<style>
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<ul>
|
||||||
|
{% for message in messages %}
|
||||||
|
<li>
|
||||||
|
<span><strong>{{ message }}</strong></span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
<fieldset>
|
||||||
|
<legend>User login</legend>
|
||||||
|
<p>
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input name="username" id="username" value="admin" />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input name="password" id="password" type="password" value="admin" />
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<input type="submit" value="login" />
|
||||||
|
|
||||||
|
{% if next %}
|
||||||
|
<input type="hidden" name="next" value="{{next}}" />
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<div class="ui toggle checkbox">
|
||||||
|
<!--noformat-->
|
||||||
|
<input type="checkbox"
|
||||||
|
name="{{ field.name }}"
|
||||||
|
{% if field.value %}checked="checked" {% endif %}>
|
||||||
|
<label>{{field.label or field.name|capitalize}}</label>
|
||||||
|
<!--noformat-->
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<div class="ui action input inline field">
|
||||||
|
<label>{{field.label or field.name|capitalize}}</label>
|
||||||
|
<input type="text" value="{{field.value}}" readonly="readonly">
|
||||||
|
<button class="ui teal right labeled icon button" hx-disable>
|
||||||
|
<i class="key icon"></i>
|
||||||
|
Set Password...
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user