mirror of
https://github.com/perstarkse/minne.git
synced 2026-04-19 15:31:23 +02:00
feat: admin status, first user is admin, site settings
This commit is contained in:
@@ -27,6 +27,7 @@ use zettle_db::{
|
|||||||
},
|
},
|
||||||
html::{
|
html::{
|
||||||
account::{delete_account, set_api_key, show_account_page},
|
account::{delete_account, set_api_key, show_account_page},
|
||||||
|
admin_panel::show_admin_panel,
|
||||||
documentation::index::show_documentation_index,
|
documentation::index::show_documentation_index,
|
||||||
gdpr::{accept_gdpr, deny_gdpr},
|
gdpr::{accept_gdpr, deny_gdpr},
|
||||||
index::index_handler,
|
index::index_handler,
|
||||||
@@ -41,7 +42,10 @@ use zettle_db::{
|
|||||||
},
|
},
|
||||||
AppState,
|
AppState,
|
||||||
},
|
},
|
||||||
storage::{db::SurrealDbClient, types::user::User},
|
storage::{
|
||||||
|
db::SurrealDbClient,
|
||||||
|
types::{analytics::Analytics, system_settings::SystemSettings, user::User},
|
||||||
|
},
|
||||||
utils::{config::get_config, mailer::Mailer},
|
utils::{config::get_config, mailer::Mailer},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,25 +110,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
app_state.surreal_db_client.build_indexes().await?;
|
app_state.surreal_db_client.build_indexes().await?;
|
||||||
setup_auth(&app_state.surreal_db_client).await?;
|
setup_auth(&app_state.surreal_db_client).await?;
|
||||||
// app_state.surreal_db_client.drop_table::<FileInfo>().await?;
|
Analytics::ensure_initialized(&app_state.surreal_db_client).await?;
|
||||||
// app_state.surreal_db_client.drop_table::<User>().await?;
|
SystemSettings::ensure_initialized(&app_state.surreal_db_client).await?;
|
||||||
// app_state.surreal_db_client.drop_table::<Job>().await?;
|
|
||||||
// app_state
|
|
||||||
// .surreal_db_client
|
|
||||||
// .drop_table::<KnowledgeEntity>()
|
|
||||||
// .await?;
|
|
||||||
// app_state
|
|
||||||
// .surreal_db_client
|
|
||||||
// .drop_table::<KnowledgeRelationship>()
|
|
||||||
// .await?;
|
|
||||||
// app_state
|
|
||||||
// .surreal_db_client
|
|
||||||
// .drop_table::<TextContent>()
|
|
||||||
// .await?;
|
|
||||||
// app_state
|
|
||||||
// .surreal_db_client
|
|
||||||
// .drop_table::<TextChunk>()
|
|
||||||
// .await?;
|
|
||||||
|
|
||||||
// Create Axum router
|
// Create Axum router
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
@@ -181,6 +168,7 @@ fn html_routes(
|
|||||||
.route("/queue", get(show_queue_tasks))
|
.route("/queue", get(show_queue_tasks))
|
||||||
.route("/queue/:delivery_tag", delete(delete_task))
|
.route("/queue/:delivery_tag", delete(delete_task))
|
||||||
.route("/account", get(show_account_page))
|
.route("/account", get(show_account_page))
|
||||||
|
.route("/admin", get(show_admin_panel))
|
||||||
.route("/set-api-key", post(set_api_key))
|
.route("/set-api-key", post(set_api_key))
|
||||||
.route("/delete-account", delete(delete_account))
|
.route("/delete-account", delete(delete_account))
|
||||||
.route(
|
.route(
|
||||||
|
|||||||
52
src/server/routes/html/admin_panel.rs
Normal file
52
src/server/routes/html/admin_panel.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
http::{StatusCode, Uri},
|
||||||
|
response::{IntoResponse, Redirect},
|
||||||
|
};
|
||||||
|
use axum_htmx::HxRedirect;
|
||||||
|
use axum_session_auth::AuthSession;
|
||||||
|
use axum_session_surreal::SessionSurrealPool;
|
||||||
|
use surrealdb::{engine::any::Any, Surreal};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::{AppError, HtmlError},
|
||||||
|
page_data,
|
||||||
|
server::{routes::html::render_template, AppState},
|
||||||
|
storage::types::{analytics::Analytics, system_settings::SystemSettings, user::User},
|
||||||
|
};
|
||||||
|
|
||||||
|
page_data!(AdminPanelData, "auth/admin_panel.html", {
|
||||||
|
user: User,
|
||||||
|
settings: SystemSettings,
|
||||||
|
analytics: Analytics
|
||||||
|
});
|
||||||
|
|
||||||
|
pub async fn show_admin_panel(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
|
||||||
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
|
// Early return if the user is not authenticated
|
||||||
|
let user = match auth.current_user {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return Ok(Redirect::to("/").into_response()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let settings = SystemSettings::get_current(&state.surreal_db_client)
|
||||||
|
.await
|
||||||
|
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
|
||||||
|
let analytics = Analytics::get_current(&state.surreal_db_client)
|
||||||
|
.await
|
||||||
|
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
|
||||||
|
|
||||||
|
let output = render_template(
|
||||||
|
AdminPanelData::template_name(),
|
||||||
|
AdminPanelData {
|
||||||
|
user,
|
||||||
|
settings,
|
||||||
|
analytics,
|
||||||
|
},
|
||||||
|
state.templates.clone(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(output.into_response())
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use minijinja_autoreload::AutoReloader;
|
|||||||
use crate::error::{HtmlError, IntoHtmlError};
|
use crate::error::{HtmlError, IntoHtmlError};
|
||||||
|
|
||||||
pub mod account;
|
pub mod account;
|
||||||
|
pub mod admin_panel;
|
||||||
pub mod documentation;
|
pub mod documentation;
|
||||||
pub mod gdpr;
|
pub mod gdpr;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use axum::{
|
|||||||
use axum_htmx::{HxBoosted, HxRedirect};
|
use axum_htmx::{HxBoosted, HxRedirect};
|
||||||
use axum_session_auth::AuthSession;
|
use axum_session_auth::AuthSession;
|
||||||
use axum_session_surreal::SessionSurrealPool;
|
use axum_session_surreal::SessionSurrealPool;
|
||||||
|
use chrono::RoundingError;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use surrealdb::{engine::any::Any, Surreal};
|
use surrealdb::{engine::any::Any, Surreal};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@@ -59,8 +60,8 @@ pub async fn process_signup_and_show_verification(
|
|||||||
let user = match User::create_new(form.email, form.password, &state.surreal_db_client).await {
|
let user = match User::create_new(form.email, form.password, &state.surreal_db_client).await {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
info!("{:?}", e);
|
tracing::error!("{:?}", e);
|
||||||
return Ok(Html("<p>User already exists</p>").into_response());
|
return Ok(Html(format!("<p>{}</p>", e)).into_response());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
50
src/storage/types/analytics.rs
Normal file
50
src/storage/types/analytics.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
use crate::storage::types::file_info::deserialize_flexible_id;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{error::AppError, storage::db::SurrealDbClient};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Analytics {
|
||||||
|
#[serde(deserialize_with = "deserialize_flexible_id")]
|
||||||
|
pub id: String,
|
||||||
|
pub visitors: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Analytics {
|
||||||
|
pub async fn ensure_initialized(db: &SurrealDbClient) -> Result<Self, AppError> {
|
||||||
|
let analytics = db.select(("analytics", "current")).await?;
|
||||||
|
|
||||||
|
if analytics.is_none() {
|
||||||
|
let created: Option<Analytics> = db
|
||||||
|
.create(("analytics", "current"))
|
||||||
|
.content(Analytics {
|
||||||
|
id: "current".to_string(),
|
||||||
|
visitors: 0,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return created.ok_or(AppError::Validation("Failed to initialize settings".into()));
|
||||||
|
};
|
||||||
|
|
||||||
|
analytics.ok_or(AppError::Validation("Failed to initialize settings".into()))
|
||||||
|
}
|
||||||
|
pub async fn get_current(db: &SurrealDbClient) -> Result<Self, AppError> {
|
||||||
|
let analytics: Option<Self> = db
|
||||||
|
.client
|
||||||
|
.query("SELECT * FROM type::thing('analytics', 'current')")
|
||||||
|
.await?
|
||||||
|
.take(0)?;
|
||||||
|
|
||||||
|
analytics.ok_or(AppError::NotFound("Analytics not found".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn increment_visitors(db: &SurrealDbClient) -> Result<Self, AppError> {
|
||||||
|
let updated: Option<Self> = db
|
||||||
|
.client
|
||||||
|
.query("UPDATE type::thing('analytics', 'current') SET visitors += 1 RETURN AFTER")
|
||||||
|
.await?
|
||||||
|
.take(0)?;
|
||||||
|
|
||||||
|
updated.ok_or(AppError::Validation("Failed to update analytics".into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
use axum::async_trait;
|
use axum::async_trait;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
pub mod analytics;
|
||||||
pub mod file_info;
|
pub mod file_info;
|
||||||
pub mod job;
|
pub mod job;
|
||||||
pub mod knowledge_entity;
|
pub mod knowledge_entity;
|
||||||
pub mod knowledge_relationship;
|
pub mod knowledge_relationship;
|
||||||
|
pub mod system_settings;
|
||||||
pub mod text_chunk;
|
pub mod text_chunk;
|
||||||
pub mod text_content;
|
pub mod text_content;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
@@ -58,7 +60,7 @@ macro_rules! stored_object {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_flexible_id<'de, D>(deserializer: D) -> Result<String, D::Error>
|
pub fn deserialize_flexible_id<'de, D>(deserializer: D) -> Result<String, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
|
|||||||
56
src/storage/types/system_settings.rs
Normal file
56
src/storage/types/system_settings.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use crate::storage::types::file_info::deserialize_flexible_id;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{error::AppError, storage::db::SurrealDbClient};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SystemSettings {
|
||||||
|
#[serde(deserialize_with = "deserialize_flexible_id")]
|
||||||
|
pub id: String,
|
||||||
|
pub registrations_enabled: bool,
|
||||||
|
pub require_email_verification: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemSettings {
|
||||||
|
pub async fn ensure_initialized(db: &SurrealDbClient) -> Result<Self, AppError> {
|
||||||
|
let settings = db.select(("system_settings", "current")).await?;
|
||||||
|
|
||||||
|
if settings.is_none() {
|
||||||
|
let created: Option<SystemSettings> = db
|
||||||
|
.create(("system_settings", "current"))
|
||||||
|
.content(SystemSettings {
|
||||||
|
id: "current".to_string(),
|
||||||
|
registrations_enabled: true,
|
||||||
|
require_email_verification: false,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return created.ok_or(AppError::Validation("Failed to initialize settings".into()));
|
||||||
|
};
|
||||||
|
|
||||||
|
settings.ok_or(AppError::Validation("Failed to initialize settings".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_current(db: &SurrealDbClient) -> Result<Self, AppError> {
|
||||||
|
let settings: Option<Self> = db
|
||||||
|
.client
|
||||||
|
.query("SELECT * FROM type::thing('system_settings', 'current')")
|
||||||
|
.await?
|
||||||
|
.take(0)?;
|
||||||
|
|
||||||
|
settings.ok_or(AppError::NotFound("System settings not found".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update(db: &SurrealDbClient, changes: Self) -> Result<Self, AppError> {
|
||||||
|
let updated: Option<Self> = db
|
||||||
|
.client
|
||||||
|
.query("UPDATE type::thing('system_settings', 'current') MERGE $changes RETURN AFTER")
|
||||||
|
.bind(("changes", changes))
|
||||||
|
.await?
|
||||||
|
.take(0)?;
|
||||||
|
|
||||||
|
updated.ok_or(AppError::Validation(
|
||||||
|
"Something went wrong updating the settings".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,13 +7,16 @@ use axum_session_auth::Authentication;
|
|||||||
use surrealdb::{engine::any::Any, Surreal};
|
use surrealdb::{engine::any::Any, Surreal};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::{knowledge_entity::KnowledgeEntity, text_content::TextContent};
|
use super::{
|
||||||
|
knowledge_entity::KnowledgeEntity, system_settings::SystemSettings, text_content::TextContent,
|
||||||
|
};
|
||||||
|
|
||||||
stored_object!(User, "user", {
|
stored_object!(User, "user", {
|
||||||
email: String,
|
email: String,
|
||||||
password: String,
|
password: String,
|
||||||
anonymous: bool,
|
anonymous: bool,
|
||||||
api_key: Option<String>
|
api_key: Option<String>,
|
||||||
|
admin: bool
|
||||||
});
|
});
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -42,31 +45,35 @@ impl User {
|
|||||||
password: String,
|
password: String,
|
||||||
db: &SurrealDbClient,
|
db: &SurrealDbClient,
|
||||||
) -> Result<Self, AppError> {
|
) -> Result<Self, AppError> {
|
||||||
// Check if user exists
|
// verify that the application allows new creations
|
||||||
if (Self::find_by_email(&email, db).await?).is_some() {
|
let systemsettings = SystemSettings::get_current(db).await?;
|
||||||
return Err(AppError::Auth("User already exists".into()));
|
if !systemsettings.registrations_enabled {
|
||||||
|
return Err(AppError::Auth("Registration is not allowed".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|
||||||
let id = Uuid::new_v4().to_string();
|
let id = Uuid::new_v4().to_string();
|
||||||
|
|
||||||
let user: Option<User> = db
|
let user: Option<User> = db
|
||||||
.client
|
.client
|
||||||
.query(
|
.query(
|
||||||
"CREATE type::thing('user', $id) SET
|
"LET $count = (SELECT count() FROM type::table($table))[0].count;
|
||||||
email = $email,
|
CREATE type::thing('user', $id) SET
|
||||||
|
email = $email,
|
||||||
password = crypto::argon2::generate($password),
|
password = crypto::argon2::generate($password),
|
||||||
|
admin = $count < 1, // Changed from == 0 to < 1
|
||||||
anonymous = false,
|
anonymous = false,
|
||||||
created_at = $created_at,
|
created_at = $created_at,
|
||||||
updated_at = $updated_at",
|
updated_at = $updated_at",
|
||||||
)
|
)
|
||||||
|
.bind(("table", "user"))
|
||||||
.bind(("id", id))
|
.bind(("id", id))
|
||||||
.bind(("email", email))
|
.bind(("email", email))
|
||||||
.bind(("password", password))
|
.bind(("password", password))
|
||||||
.bind(("created_at", now))
|
.bind(("created_at", now))
|
||||||
.bind(("updated_at", now))
|
.bind(("updated_at", now))
|
||||||
.await?
|
.await?
|
||||||
.take(0)?;
|
.take(1)?;
|
||||||
|
|
||||||
user.ok_or(AppError::Auth("User failed to create".into()))
|
user.ok_or(AppError::Auth("User failed to create".into()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<main class="grow flex flex-col prose container mx-auto mt-2 sm:mt-4">
|
<main class="grow flex flex-col prose container mx-auto mt-2 p-5 sm:mt-4">
|
||||||
<h1 class="text-center">Account Settings</h1>
|
<h1 class="text-center">Account Settings</h1>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
|
|||||||
13
templates/auth/admin_panel.html
Normal file
13
templates/auth/admin_panel.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{% extends 'body_base.html' %}
|
||||||
|
{% block main %}
|
||||||
|
<main class="container justify-center flex-grow flex mx-auto mt-4 p-5">
|
||||||
|
Hello
|
||||||
|
{% if user.admin %}
|
||||||
|
admin
|
||||||
|
{% else %}
|
||||||
|
user
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{{settings}}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<summary>Account</summary>
|
<summary>Account</summary>
|
||||||
<ul class="bg-base-100 rounded-t-none p-2">
|
<ul class="bg-base-100 rounded-t-none p-2">
|
||||||
<li><a hx-boost="true" class="" href="/account">Account</a></li>
|
<li><a hx-boost="true" class="" href="/account">Account</a></li>
|
||||||
<li><a hx-boost="true" class="" href="/admin">Admin Panel</a></li>
|
<li><a hx-boost="true" class="" href="/admin">Admin</a></li>
|
||||||
<li><a hx-boost="true" href="/signout">Sign out</a></li>
|
<li><a hx-boost="true" href="/signout">Sign out</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{% extends "body_base.html" %}
|
{% extends 'body_base.html' %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<div class="container mx-auto flex items-center justify-center">
|
<main class="container justify-center flex-grow flex mx-auto mt-4 p-5">
|
||||||
<div class="flex flex-col space-y-4 text-center">
|
<div class="flex flex-col space-y-4 text-center">
|
||||||
<h1 class="text-2xl font-bold text-error">
|
<h1 class="text-2xl font-bold text-error">
|
||||||
{{ status_code }}
|
{{ status_code }}
|
||||||
@@ -10,5 +9,5 @@
|
|||||||
<p class="text-base-content/60">{{ description }}</p>
|
<p class="text-base-content/60">{{ description }}</p>
|
||||||
<a href="/" class="btn btn-primary mt-8">Go Home</a>
|
<a href="/" class="btn btn-primary mt-8">Go Home</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user