merge upstream
All checks were successful
mnemo-build-and-publish / gractwo-mnemo-build (push) Successful in 1m38s
All checks were successful
mnemo-build-and-publish / gractwo-mnemo-build (push) Successful in 1m38s
This commit is contained in:
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"languages": {
|
"languages": {
|
||||||
"Rust": {
|
"Rust": {
|
||||||
"language_servers": ["rust-analyzer", "tailwindcss-language-server"],
|
"language_servers": [
|
||||||
|
"rust-analyzer",
|
||||||
|
//
|
||||||
|
"tailwindcss-language-server",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"lsp": {
|
"lsp": {
|
||||||
|
|||||||
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -1490,6 +1490,7 @@ dependencies = [
|
|||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"http",
|
||||||
"log",
|
"log",
|
||||||
"maud",
|
"maud",
|
||||||
"rand 0.10.0",
|
"rand 0.10.0",
|
||||||
@@ -1504,6 +1505,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2939,6 +2941,7 @@ dependencies = [
|
|||||||
"idna",
|
"idna",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ chrono = { version = "0.4.43", features = ["serde"] }
|
|||||||
chrono-tz = "0.10.4"
|
chrono-tz = "0.10.4"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
env_logger = "0.11.9"
|
env_logger = "0.11.9"
|
||||||
|
http = "1.4.0"
|
||||||
log = "0.4.29"
|
log = "0.4.29"
|
||||||
maud = { version = "0.27.0", features = ["axum"] }
|
maud = { version = "0.27.0", features = ["axum"] }
|
||||||
rand = "0.10.0"
|
rand = "0.10.0"
|
||||||
@@ -27,4 +28,5 @@ thiserror = "2.0.18"
|
|||||||
tokio = { version = "1.49.0", features = ["full"] }
|
tokio = { version = "1.49.0", features = ["full"] }
|
||||||
tower = { version = "0.5.3", features = ["full"] }
|
tower = { version = "0.5.3", features = ["full"] }
|
||||||
tower-http = { version = "0.6.8", features = ["full"] }
|
tower-http = { version = "0.6.8", features = ["full"] }
|
||||||
|
url = { version = "2.5.8", features = ["serde"] }
|
||||||
uuid = { version = "1.21.0", features = ["serde", "v7"] }
|
uuid = { version = "1.21.0", features = ["serde", "v7"] }
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
volumes:
|
volumes:
|
||||||
- pg_volume:/var/lib/postgresql/data:rw
|
- pg_volume:/var/lib/postgresql
|
||||||
stop_grace_period: 120s
|
stop_grace_period: 120s
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: mnemo
|
POSTGRES_USER: mnemo
|
||||||
|
|||||||
@@ -8,10 +8,31 @@ use std::{
|
|||||||
use env_logger::fmt::Formatter;
|
use env_logger::fmt::Formatter;
|
||||||
use log::{LevelFilter, Record};
|
use log::{LevelFilter, Record};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
/// Mnemosyne, the mother of the nine muses
|
/// Mnemosyne, the mother of the nine muses
|
||||||
pub const DEFAULT_PORT: u16 = 0x9999; // 39321
|
pub const DEFAULT_PORT: u16 = 0x9999; // 39321
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct MnemoConf {
|
||||||
|
pub instance_name: String,
|
||||||
|
pub discord_webhook: Option<Url>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MnemoConf {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
MnemoConf::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Default for MnemoConf {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
instance_name: String::from("Mnemosyne"),
|
||||||
|
discord_webhook: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const REFERENCE_SPLASHES: &[&str] = &[
|
pub const REFERENCE_SPLASHES: &[&str] = &[
|
||||||
"quote engine",
|
"quote engine",
|
||||||
"powered by rust",
|
"powered by rust",
|
||||||
|
|||||||
3
src/database/migrations/0002_conf.sql
Normal file
3
src/database/migrations/0002_conf.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
CREATE TABLE mnemoconf (
|
||||||
|
config JSONB NOT NULL
|
||||||
|
);
|
||||||
21
src/logs.rs
21
src/logs.rs
@@ -1,6 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{PgConnection, Row};
|
use sqlx::{PgConnection, Row};
|
||||||
use strum::{EnumDiscriminants, EnumIter, IntoStaticStr, VariantNames};
|
use strum::{EnumDiscriminants, EnumIter, IntoStaticStr, VariantNames};
|
||||||
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{database::DatabaseError, users::User, web::icons};
|
use crate::{database::DatabaseError, users::User, web::icons};
|
||||||
@@ -154,11 +155,20 @@ pub enum LogAction {
|
|||||||
ManuallyRevokeSession {
|
ManuallyRevokeSession {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
},
|
},
|
||||||
|
ChangeInstanceName {
|
||||||
|
old: String,
|
||||||
|
new: String,
|
||||||
|
},
|
||||||
|
ChangeDiscordWebhookUrl {
|
||||||
|
old: Option<Url>,
|
||||||
|
new: Option<Url>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
impl LogAction {
|
impl LogAction {
|
||||||
pub fn get_target_id(&self) -> Option<Uuid> {
|
pub fn get_target_id(&self) -> Option<Uuid> {
|
||||||
match self {
|
match self {
|
||||||
Self::Initialize | Self::RegenInfradmin => None,
|
Self::Initialize | Self::RegenInfradmin => None,
|
||||||
|
|
||||||
Self::CreateUser { id, .. }
|
Self::CreateUser { id, .. }
|
||||||
| Self::CreateTag { id, .. }
|
| Self::CreateTag { id, .. }
|
||||||
| Self::CreatePerson { id, .. }
|
| Self::CreatePerson { id, .. }
|
||||||
@@ -168,9 +178,14 @@ impl LogAction {
|
|||||||
| Self::RenameTag { id, .. }
|
| Self::RenameTag { id, .. }
|
||||||
| Self::DeleteTag { id, .. }
|
| Self::DeleteTag { id, .. }
|
||||||
| Self::ManuallyChangeUsersPassword { id } => Some(*id),
|
| Self::ManuallyChangeUsersPassword { id } => Some(*id),
|
||||||
|
|
||||||
Self::AddPersonName { pid, .. }
|
Self::AddPersonName { pid, .. }
|
||||||
| Self::DeletePersonName { pid, .. }
|
| Self::DeletePersonName { pid, .. }
|
||||||
| Self::SetPersonPrimaryName { pid, .. } => Some(*pid),
|
| Self::SetPersonPrimaryName { pid, .. } => Some(*pid),
|
||||||
|
|
||||||
|
Self::ChangeInstanceName { .. } | Self::ChangeDiscordWebhookUrl { .. } => {
|
||||||
|
Some(Uuid::nil())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn get_humanreadable_payload(&self) -> String {
|
pub fn get_humanreadable_payload(&self) -> String {
|
||||||
@@ -213,6 +228,10 @@ impl LogAction {
|
|||||||
LogAction::ManuallyRevokeSession { id } => {
|
LogAction::ManuallyRevokeSession { id } => {
|
||||||
format!("Revoked session of ID {id}")
|
format!("Revoked session of ID {id}")
|
||||||
}
|
}
|
||||||
|
LogAction::ChangeInstanceName { old, new } => {
|
||||||
|
format!("Changed instance name from \"{old}\" to \"{new}\"")
|
||||||
|
}
|
||||||
|
LogAction::ChangeDiscordWebhookUrl { .. } => "Changed Discord webhook URL".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,6 +254,8 @@ impl LogActionDiscriminant {
|
|||||||
LAD::SetPersonPrimaryName => "Person Primary Name Set",
|
LAD::SetPersonPrimaryName => "Person Primary Name Set",
|
||||||
LAD::CreateQuote => "Quote Creation",
|
LAD::CreateQuote => "Quote Creation",
|
||||||
LAD::ManuallyRevokeSession => "Manual Session Revocation",
|
LAD::ManuallyRevokeSession => "Manual Session Revocation",
|
||||||
|
LAD::ChangeInstanceName => "Instance Name Change",
|
||||||
|
LAD::ChangeDiscordWebhookUrl => "Discord Webhook URL Change",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn icon(&self) -> &'static str {
|
pub fn icon(&self) -> &'static str {
|
||||||
|
|||||||
10
src/main.rs
10
src/main.rs
@@ -1,8 +1,10 @@
|
|||||||
use std::error::Error;
|
use std::{error::Error, sync::Arc};
|
||||||
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use tokio::net::TcpListener;
|
use tokio::{net::TcpListener, sync::RwLock};
|
||||||
|
|
||||||
|
use crate::config::MnemoConf;
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
mod config;
|
mod config;
|
||||||
@@ -21,6 +23,7 @@ const ISE_MSG: &str = "Internal server error";
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MnemoState {
|
pub struct MnemoState {
|
||||||
pool: PgPool,
|
pool: PgPool,
|
||||||
|
conf: Arc<RwLock<MnemoConf>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -31,6 +34,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let pool = config::init_pool().await?;
|
let pool = config::init_pool().await?;
|
||||||
sqlx::migrate!("src/database/migrations").run(&pool).await?;
|
sqlx::migrate!("src/database/migrations").run(&pool).await?;
|
||||||
log::info!("Migrations applied successfully.");
|
log::info!("Migrations applied successfully.");
|
||||||
|
let conf = Arc::new(RwLock::new(MnemoConf::new()));
|
||||||
users::auth::init_password_dummies();
|
users::auth::init_password_dummies();
|
||||||
users::setup::initialise_reserved_users_if_needed(&pool).await?;
|
users::setup::initialise_reserved_users_if_needed(&pool).await?;
|
||||||
|
|
||||||
@@ -38,7 +42,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let r = Router::new()
|
let r = Router::new()
|
||||||
.merge(api::api_router())
|
.merge(api::api_router())
|
||||||
.merge(web::web_router())
|
.merge(web::web_router())
|
||||||
.with_state(MnemoState { pool });
|
.with_state(MnemoState { pool, conf });
|
||||||
let l = TcpListener::bind(format!("0.0.0.0:{port}")).await?;
|
let l = TcpListener::bind(format!("0.0.0.0:{port}")).await?;
|
||||||
log::info!("Listener bound to {}", l.local_addr()?);
|
log::info!("Listener bound to {}", l.local_addr()?);
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub enum Permission {
|
|||||||
ChangePersonPrimaryName,
|
ChangePersonPrimaryName,
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
BrowseServerLogs,
|
BrowseServerLogs,
|
||||||
|
ConfigureInstance,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
|
|||||||
@@ -46,15 +46,20 @@ pub fn nav(user: Option<&User>, uri: &str) -> Markup {
|
|||||||
span class="hidden sm:block"{(u.handle)}
|
span class="hidden sm:block"{(u.handle)}
|
||||||
div class="scale-[.75]" {(PreEscaped(icons::USER))}
|
div class="scale-[.75]" {(PreEscaped(icons::USER))}
|
||||||
}
|
}
|
||||||
div class="absolute right-0 top-full pt-1 w-40 opacity-0 invisible group-focus-within:opacity-100 group-focus-within:visible transition-all duration-100 z-50" {
|
div class="absolute right-0 top-full pt-1 w-44 opacity-0 invisible group-focus-within:opacity-100 group-focus-within:visible transition-all duration-100 z-50" {
|
||||||
div class="rounded bg-neutral-900 border border-neutral-200/25 shadow-lg flex flex-col overflow-hidden" {
|
div class="rounded bg-neutral-900 border border-neutral-200/25 shadow-lg flex flex-col overflow-hidden" {
|
||||||
a href=(format!("/users/{}", u.id)) class="px-4 py-2 flex items-center gap-2 hover:bg-neutral-200/10 font-lexend text-sm text-neutral-200 transition-colors" {
|
a href=(format!("/users/{}", u.id)) class="px-4 py-2 flex items-center gap-2 hover:bg-neutral-200/10 font-lexend text-sm text-neutral-200 transition-colors" {
|
||||||
div class="scale-[.7]" {(PreEscaped(icons::USER))}
|
div class="scale-[.7]" {(PreEscaped(icons::USER))}
|
||||||
p {"Profile"}
|
p {"Profile"}
|
||||||
}
|
}
|
||||||
a href="/user-settings" class="px-4 py-2 flex items-center gap-2 hover:bg-neutral-200/10 font-lexend text-sm text-neutral-200 transition-colors" {
|
a href="/user-settings" class="px-4 py-2 flex items-center gap-2 hover:bg-neutral-200/10 font-lexend text-sm text-neutral-200 transition-colors" {
|
||||||
|
div class="scale-[.7]" {(PreEscaped(icons::SETTINGS))}
|
||||||
|
p {"User Settings"}
|
||||||
|
}
|
||||||
|
div class="h-px w-full bg-neutral-200/15" {}
|
||||||
|
a href="/instance-config" class="px-4 py-2 flex items-center gap-2 hover:bg-neutral-200/10 font-lexend text-sm text-neutral-200 transition-colors" {
|
||||||
div class="scale-[.7]" {(PreEscaped(icons::SERVER))}
|
div class="scale-[.7]" {(PreEscaped(icons::SERVER))}
|
||||||
p {"Settings"}
|
p {"Instance Config"}
|
||||||
}
|
}
|
||||||
div class="h-px w-full bg-neutral-200/15" {}
|
div class="h-px w-full bg-neutral-200/15" {}
|
||||||
form action="/api/auth/logout-form" method="post" {
|
form action="/api/auth/logout-form" method="post" {
|
||||||
|
|||||||
1
src/web/icons/code.svg
Normal file
1
src/web/icons/code.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code-icon lucide-code"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>
|
||||||
|
After Width: | Height: | Size: 282 B |
1
src/web/icons/message-square-code.svg
Normal file
1
src/web/icons/message-square-code.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-square-code-icon lucide-message-square-code"><path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"/><path d="m10 8-3 3 3 3"/><path d="m14 14 3-3-3-3"/></svg>
|
||||||
|
After Width: | Height: | Size: 440 B |
@@ -5,6 +5,7 @@ pub const CALENDAR_ARROW_DOWN: &str = include_str!("calendar-arrow-down.svg");
|
|||||||
pub const CIRCLE_MINUS: &str = include_str!("circle-minus.svg");
|
pub const CIRCLE_MINUS: &str = include_str!("circle-minus.svg");
|
||||||
pub const CLIPBOARD_CLOCK: &str = include_str!("clipboard-clock.svg");
|
pub const CLIPBOARD_CLOCK: &str = include_str!("clipboard-clock.svg");
|
||||||
pub const CLOCK: &str = include_str!("clock.svg");
|
pub const CLOCK: &str = include_str!("clock.svg");
|
||||||
|
pub const CODE: &str = include_str!("code.svg");
|
||||||
pub const CONTACT: &str = include_str!("contact.svg");
|
pub const CONTACT: &str = include_str!("contact.svg");
|
||||||
pub const EYE: &str = include_str!("eye.svg");
|
pub const EYE: &str = include_str!("eye.svg");
|
||||||
pub const FILE_IMAGE: &str = include_str!("file-image.svg");
|
pub const FILE_IMAGE: &str = include_str!("file-image.svg");
|
||||||
@@ -14,14 +15,17 @@ pub const LAYOUT_DASHBOARD: &str = include_str!("layout-dashboard.svg");
|
|||||||
pub const LINE_DOT_RIGHT_HORIZONTAL: &str = include_str!("line-dot-right-horizontal.svg");
|
pub const LINE_DOT_RIGHT_HORIZONTAL: &str = include_str!("line-dot-right-horizontal.svg");
|
||||||
pub const LOG_OUT: &str = include_str!("log-out.svg");
|
pub const LOG_OUT: &str = include_str!("log-out.svg");
|
||||||
pub const MAP_PIN: &str = include_str!("map-pin.svg");
|
pub const MAP_PIN: &str = include_str!("map-pin.svg");
|
||||||
|
pub const MESSAGE_SQUARE_CODE: &str = include_str!("message-square-code.svg");
|
||||||
pub const PEN: &str = include_str!("pen.svg");
|
pub const PEN: &str = include_str!("pen.svg");
|
||||||
pub const PLUS: &str = include_str!("plus.svg");
|
pub const PLUS: &str = include_str!("plus.svg");
|
||||||
pub const QUOTE: &str = include_str!("quote.svg");
|
pub const QUOTE: &str = include_str!("quote.svg");
|
||||||
pub const REFRESH_CW: &str = include_str!("refresh-cw.svg");
|
pub const REFRESH_CW: &str = include_str!("refresh-cw.svg");
|
||||||
pub const SCROLL_TEXT: &str = include_str!("scroll-text.svg");
|
pub const SCROLL_TEXT: &str = include_str!("scroll-text.svg");
|
||||||
pub const SERVER: &str = include_str!("server.svg");
|
pub const SERVER: &str = include_str!("server.svg");
|
||||||
|
pub const SETTINGS: &str = include_str!("settings.svg");
|
||||||
pub const SHIELD_USER: &str = include_str!("shield-user.svg");
|
pub const SHIELD_USER: &str = include_str!("shield-user.svg");
|
||||||
pub const TAG: &str = include_str!("tag.svg");
|
pub const TAG: &str = include_str!("tag.svg");
|
||||||
|
pub const TYPE: &str = include_str!("type.svg");
|
||||||
pub const USER: &str = include_str!("user.svg");
|
pub const USER: &str = include_str!("user.svg");
|
||||||
pub const USER_KEY: &str = include_str!("user-key.svg");
|
pub const USER_KEY: &str = include_str!("user-key.svg");
|
||||||
pub const USER_PLUS: &str = include_str!("user-plus.svg");
|
pub const USER_PLUS: &str = include_str!("user-plus.svg");
|
||||||
|
|||||||
1
src/web/icons/settings.svg
Normal file
1
src/web/icons/settings.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-settings-icon lucide-settings"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/></svg>
|
||||||
|
After Width: | Height: | Size: 610 B |
1
src/web/icons/type.svg
Normal file
1
src/web/icons/type.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-type-icon lucide-type"><path d="M12 4v16"/><path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2"/><path d="M9 20h6"/></svg>
|
||||||
|
After Width: | Height: | Size: 322 B |
209
src/web/pages/conf.rs
Normal file
209
src/web/pages/conf.rs
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
use axum::{
|
||||||
|
Form,
|
||||||
|
extract::{Request, State},
|
||||||
|
http::HeaderMap,
|
||||||
|
response::{IntoResponse, Redirect, Response},
|
||||||
|
};
|
||||||
|
use http::StatusCode;
|
||||||
|
use maud::{Markup, PreEscaped, html};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
MnemoState,
|
||||||
|
error::CompositeError,
|
||||||
|
logs::{LogAction, LogEntry},
|
||||||
|
users::{
|
||||||
|
User,
|
||||||
|
auth::{UserAuthRequired, UserAuthenticate},
|
||||||
|
permissions::Permission,
|
||||||
|
},
|
||||||
|
web::{components::nav::nav, icons, pages::base},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn page(
|
||||||
|
State(state): State<MnemoState>,
|
||||||
|
req: Request,
|
||||||
|
) -> Result<Response, CompositeError> {
|
||||||
|
let mut conn = state.pool.acquire().await?;
|
||||||
|
let u = match User::authenticate(&mut *conn, req.headers()).await? {
|
||||||
|
Some(u) => Some(u),
|
||||||
|
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (current_name, current_webhook) = {
|
||||||
|
let conf = state.conf.read().await;
|
||||||
|
let current_name = conf.instance_name.clone();
|
||||||
|
let current_webhook = conf
|
||||||
|
.discord_webhook
|
||||||
|
.as_ref()
|
||||||
|
.map(|u| u.to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
(current_name, current_webhook)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(base(
|
||||||
|
"Instance Config | Mnemosyne",
|
||||||
|
html!(
|
||||||
|
(nav(u.as_ref(), req.uri().path()))
|
||||||
|
|
||||||
|
div class="max-w-4xl mx-auto px-2" {
|
||||||
|
div class="mx-auto max-w-4xl my-4 mb-8" {
|
||||||
|
p class="flex items-center gap-2" {
|
||||||
|
span class="text-neutral-500" {(PreEscaped(icons::SERVER))}
|
||||||
|
span class="text-2xl font-semibold font-lora" {"Mnemosyne Instance Settings"}
|
||||||
|
}
|
||||||
|
p class="text-neutral-500 text-sm font-light mt-1" {
|
||||||
|
"Manage global configuration for your Mnemosyne instance."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(setting_block(
|
||||||
|
"Instance Name",
|
||||||
|
"The name of this instance. This is displayed on the dashboard, in page titles, and used in webhook payloads to identify this server.",
|
||||||
|
icons::PEN,
|
||||||
|
"/instance-config/name",
|
||||||
|
"instance_name",
|
||||||
|
"text",
|
||||||
|
"e.g. Mnemosyne",
|
||||||
|
¤t_name,
|
||||||
|
))
|
||||||
|
|
||||||
|
hr class="mt-6 mb-4 border-neutral-600";
|
||||||
|
|
||||||
|
(setting_block(
|
||||||
|
"Discord Webhook URL",
|
||||||
|
"Mnemosyne will attempt to send a message to this webhook for all newly created quotes, regardless of whether they are public or not. Leave empty to disable.",
|
||||||
|
icons::MESSAGE_SQUARE_CODE,
|
||||||
|
"/instance-config/dsc-webhook",
|
||||||
|
"webhook_url",
|
||||||
|
"url",
|
||||||
|
"https://discord.com/api/webhooks/...",
|
||||||
|
¤t_webhook,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setting_block(
|
||||||
|
title: &str,
|
||||||
|
description: &str,
|
||||||
|
icon: &str,
|
||||||
|
form_action: &str,
|
||||||
|
input_name: &str,
|
||||||
|
input_type: &str,
|
||||||
|
input_placeholder: &str,
|
||||||
|
current_value: &str,
|
||||||
|
) -> Markup {
|
||||||
|
html! {
|
||||||
|
div class="mb-6" {
|
||||||
|
p class="flex items-center gap-1" {
|
||||||
|
span class="text-neutral-500 scale-[.8]" {(PreEscaped(icon))}
|
||||||
|
span class="text-lg font-semibold font-lora" {(title)}
|
||||||
|
}
|
||||||
|
p class="text-neutral-500 text-sm font-light mb-3" {
|
||||||
|
(description)
|
||||||
|
}
|
||||||
|
form action=(form_action) method="post" class="flex gap-2" {
|
||||||
|
input id=(input_name) name=(input_name) type=(input_type) placeholder=(input_placeholder) autocomplete="off" value=(current_value)
|
||||||
|
class="w-full max-w-md px-2 py-1 border border-neutral-200/25 bg-neutral-950/50 rounded outline-none focus:border-neutral-200/50";
|
||||||
|
button type="submit" class="px-4 py-1 border border-neutral-200/25 bg-neutral-200/5 rounded cursor-pointer hover:border-neutral-200/40" {
|
||||||
|
"Save"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct InstanceNameForm {
|
||||||
|
instance_name: String,
|
||||||
|
}
|
||||||
|
pub async fn change_name(
|
||||||
|
State(state): State<MnemoState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Form(form): Form<InstanceNameForm>,
|
||||||
|
) -> Result<Response, CompositeError> {
|
||||||
|
let mut tx = state.pool.begin().await?;
|
||||||
|
let u = User::authenticate(&mut tx, &headers).await?.required()?;
|
||||||
|
if !u
|
||||||
|
.has_permission(&mut tx, Permission::ConfigureInstance)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
return Ok((
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"You do not have permission to change this.",
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.instance_name.trim().is_empty() {
|
||||||
|
return Ok((StatusCode::BAD_REQUEST, "Instance name cannot be empty.").into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
LogEntry::new(
|
||||||
|
&mut tx,
|
||||||
|
u,
|
||||||
|
LogAction::ChangeInstanceName {
|
||||||
|
old: state.conf.read().await.instance_name.clone(),
|
||||||
|
new: form.instance_name.trim().to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
state.conf.write().await.instance_name = form.instance_name.trim().to_string();
|
||||||
|
Ok(Redirect::to("/instance-config").into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct WebhookForm {
|
||||||
|
webhook_url: String,
|
||||||
|
}
|
||||||
|
pub async fn change_webhook(
|
||||||
|
State(state): State<MnemoState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Form(form): Form<WebhookForm>,
|
||||||
|
) -> Result<Response, CompositeError> {
|
||||||
|
let mut tx = state.pool.begin().await?;
|
||||||
|
let u = User::authenticate(&mut *tx, &headers).await?.required()?;
|
||||||
|
if !u
|
||||||
|
.has_permission(&mut tx, Permission::ConfigureInstance)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
return Ok((
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"You do not have permission to change this.",
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_webhook = if form.webhook_url.trim().is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
match Url::parse(form.webhook_url.trim()) {
|
||||||
|
Ok(url) => Some(url),
|
||||||
|
Err(_) => {
|
||||||
|
return Ok(
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, "Invalid URL format.").into_response()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
LogEntry::new(
|
||||||
|
&mut tx,
|
||||||
|
u,
|
||||||
|
LogAction::ChangeDiscordWebhookUrl {
|
||||||
|
old: state.conf.read().await.discord_webhook.clone(),
|
||||||
|
new: new_webhook.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
state.conf.write().await.discord_webhook = new_webhook;
|
||||||
|
Ok(Redirect::to("/instance-config").into_response())
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use maud::{DOCTYPE, Markup, html};
|
|||||||
|
|
||||||
use crate::MnemoState;
|
use crate::MnemoState;
|
||||||
|
|
||||||
|
pub mod conf;
|
||||||
pub mod dashboard;
|
pub mod dashboard;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
@@ -22,9 +23,15 @@ pub fn pages() -> Router<MnemoState> {
|
|||||||
.route("/", get(index::page))
|
.route("/", get(index::page))
|
||||||
.route("/login", get(login::page))
|
.route("/login", get(login::page))
|
||||||
.route("/dashboard", get(dashboard::page))
|
.route("/dashboard", get(dashboard::page))
|
||||||
|
//
|
||||||
|
.route("/instance-config", get(conf::page))
|
||||||
|
.route("/instance-config/name", post(conf::change_name))
|
||||||
|
.route("/instance-config/dsc-webhook", post(conf::change_webhook))
|
||||||
|
//
|
||||||
.route("/user-settings", get(usersettings::page))
|
.route("/user-settings", get(usersettings::page))
|
||||||
.route("/user-settings/handle", post(usersettings::change_handle))
|
.route("/user-settings/handle", post(usersettings::change_handle))
|
||||||
.route("/user-settings/passwd", post(usersettings::change_password))
|
.route("/user-settings/passwd", post(usersettings::change_password))
|
||||||
|
//
|
||||||
.route("/users", get(users::page))
|
.route("/users", get(users::page))
|
||||||
.route("/users/{id}", get(users::profile::page))
|
.route("/users/{id}", get(users::profile::page))
|
||||||
.route("/users/create", get(users::create::page))
|
.route("/users/create", get(users::create::page))
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use axum::{
|
|||||||
use axum_extra::extract::Form;
|
use axum_extra::extract::Form;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use maud::{Markup, PreEscaped, html};
|
use maud::{Markup, PreEscaped, html};
|
||||||
use reqwest::Url;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -179,11 +178,8 @@ pub async fn form(
|
|||||||
LogEntry::new(&mut *tx, u, LogAction::CreateQuote { id: q.id }).await?;
|
LogEntry::new(&mut *tx, u, LogAction::CreateQuote { id: q.id }).await?;
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
if let Ok(webhook_url) = std::env::var("DISCORD_WEBHOOK_URL") {
|
if let Some(ref url) = state.conf.read().await.discord_webhook {
|
||||||
match Url::parse(&webhook_url) {
|
q.post_msg_webhook(url.clone());
|
||||||
Ok(u) => q.post_msg_webhook(u),
|
|
||||||
Err(e) => log::error!("Tried to post webhook, failed to parse url: {e}"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Redirect::to("/dashboard").into_response())
|
Ok(Redirect::to("/dashboard").into_response())
|
||||||
|
|||||||
@@ -37,13 +37,11 @@ pub async fn page(
|
|||||||
div class="max-w-4xl mx-auto px-2" {
|
div class="max-w-4xl mx-auto px-2" {
|
||||||
div class="mx-auto max-w-4xl my-4" {
|
div class="mx-auto max-w-4xl my-4" {
|
||||||
p class="flex items-center gap-2" {
|
p class="flex items-center gap-2" {
|
||||||
span class="text-neutral-500" {(PreEscaped(icons::SERVER))}
|
span class="text-neutral-500" {(PreEscaped(icons::SETTINGS))}
|
||||||
span class="text-2xl font-semibold font-lora" {"Your User Settings"}
|
span class="text-2xl font-semibold font-lora" {"Your User Settings"}
|
||||||
}
|
}
|
||||||
p class="text-neutral-500 text-sm font-light" {
|
p class="text-neutral-500 text-sm font-light" {
|
||||||
// "Hi, " (u.handle) "!" " " "This is your user settings page." br;
|
"Hi, " (u.handle) "!" " " "This is your user settings page."
|
||||||
"Looking for Mnemosyne settings?" " "
|
|
||||||
a class="text-blue-500 hover:text-blue-400 hover:underline" href="/mnemosyne-settings" {"Here."}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user