mirror of
https://github.com/perstarkse/minne.git
synced 2026-01-18 15:56:55 +01:00
feat: admin status, first user is admin, site settings
This commit is contained in:
@@ -27,6 +27,7 @@ use zettle_db::{
|
||||
},
|
||||
html::{
|
||||
account::{delete_account, set_api_key, show_account_page},
|
||||
admin_panel::show_admin_panel,
|
||||
documentation::index::show_documentation_index,
|
||||
gdpr::{accept_gdpr, deny_gdpr},
|
||||
index::index_handler,
|
||||
@@ -41,7 +42,10 @@ use zettle_db::{
|
||||
},
|
||||
AppState,
|
||||
},
|
||||
storage::{db::SurrealDbClient, types::user::User},
|
||||
storage::{
|
||||
db::SurrealDbClient,
|
||||
types::{analytics::Analytics, system_settings::SystemSettings, user::User},
|
||||
},
|
||||
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?;
|
||||
setup_auth(&app_state.surreal_db_client).await?;
|
||||
// app_state.surreal_db_client.drop_table::<FileInfo>().await?;
|
||||
// app_state.surreal_db_client.drop_table::<User>().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?;
|
||||
Analytics::ensure_initialized(&app_state.surreal_db_client).await?;
|
||||
SystemSettings::ensure_initialized(&app_state.surreal_db_client).await?;
|
||||
|
||||
// Create Axum router
|
||||
let app = Router::new()
|
||||
@@ -181,6 +168,7 @@ fn html_routes(
|
||||
.route("/queue", get(show_queue_tasks))
|
||||
.route("/queue/:delivery_tag", delete(delete_task))
|
||||
.route("/account", get(show_account_page))
|
||||
.route("/admin", get(show_admin_panel))
|
||||
.route("/set-api-key", post(set_api_key))
|
||||
.route("/delete-account", delete(delete_account))
|
||||
.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};
|
||||
|
||||
pub mod account;
|
||||
pub mod admin_panel;
|
||||
pub mod documentation;
|
||||
pub mod gdpr;
|
||||
pub mod index;
|
||||
|
||||
@@ -7,6 +7,7 @@ use axum::{
|
||||
use axum_htmx::{HxBoosted, HxRedirect};
|
||||
use axum_session_auth::AuthSession;
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
use chrono::RoundingError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{engine::any::Any, Surreal};
|
||||
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 {
|
||||
Ok(user) => user,
|
||||
Err(e) => {
|
||||
info!("{:?}", e);
|
||||
return Ok(Html("<p>User already exists</p>").into_response());
|
||||
tracing::error!("{:?}", e);
|
||||
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 serde::{Deserialize, Serialize};
|
||||
pub mod analytics;
|
||||
pub mod file_info;
|
||||
pub mod job;
|
||||
pub mod knowledge_entity;
|
||||
pub mod knowledge_relationship;
|
||||
pub mod system_settings;
|
||||
pub mod text_chunk;
|
||||
pub mod text_content;
|
||||
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
|
||||
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 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", {
|
||||
email: String,
|
||||
password: String,
|
||||
anonymous: bool,
|
||||
api_key: Option<String>
|
||||
api_key: Option<String>,
|
||||
admin: bool
|
||||
});
|
||||
|
||||
#[async_trait]
|
||||
@@ -42,31 +45,35 @@ impl User {
|
||||
password: String,
|
||||
db: &SurrealDbClient,
|
||||
) -> Result<Self, AppError> {
|
||||
// Check if user exists
|
||||
if (Self::find_by_email(&email, db).await?).is_some() {
|
||||
return Err(AppError::Auth("User already exists".into()));
|
||||
// verify that the application allows new creations
|
||||
let systemsettings = SystemSettings::get_current(db).await?;
|
||||
if !systemsettings.registrations_enabled {
|
||||
return Err(AppError::Auth("Registration is not allowed".into()));
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
|
||||
let user: Option<User> = db
|
||||
.client
|
||||
.query(
|
||||
"CREATE type::thing('user', $id) SET
|
||||
email = $email,
|
||||
"LET $count = (SELECT count() FROM type::table($table))[0].count;
|
||||
CREATE type::thing('user', $id) SET
|
||||
email = $email,
|
||||
password = crypto::argon2::generate($password),
|
||||
admin = $count < 1, // Changed from == 0 to < 1
|
||||
anonymous = false,
|
||||
created_at = $created_at,
|
||||
updated_at = $updated_at",
|
||||
)
|
||||
.bind(("table", "user"))
|
||||
.bind(("id", id))
|
||||
.bind(("email", email))
|
||||
.bind(("password", password))
|
||||
.bind(("created_at", now))
|
||||
.bind(("updated_at", now))
|
||||
.await?
|
||||
.take(0)?;
|
||||
.take(1)?;
|
||||
|
||||
user.ok_or(AppError::Auth("User failed to create".into()))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
</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>
|
||||
<div class="form-control">
|
||||
<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>
|
||||
<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="/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>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{% extends "body_base.html" %}
|
||||
|
||||
{% extends 'body_base.html' %}
|
||||
{% 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">
|
||||
<h1 class="text-2xl font-bold text-error">
|
||||
{{ status_code }}
|
||||
@@ -10,5 +9,5 @@
|
||||
<p class="text-base-content/60">{{ description }}</p>
|
||||
<a href="/" class="btn btn-primary mt-8">Go Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user