Merge branch 'master' into gractwo
All checks were successful
mnemo-build-and-publish / gractwo-mnemo-build (push) Successful in 1m13s
All checks were successful
mnemo-build-and-publish / gractwo-mnemo-build (push) Successful in 1m13s
This commit is contained in:
@@ -20,9 +20,6 @@ pub struct MnemoConf {
|
||||
}
|
||||
|
||||
impl MnemoConf {
|
||||
pub fn new() -> Self {
|
||||
MnemoConf::default()
|
||||
}
|
||||
pub async fn load(conn: &mut sqlx::PgConnection) -> Result<Self, sqlx::Error> {
|
||||
let row: Option<serde_json::Value> =
|
||||
sqlx::query_scalar("SELECT config FROM mnemoconf LIMIT 1")
|
||||
|
||||
11
src/logs.rs
11
src/logs.rs
@@ -4,7 +4,7 @@ use strum::{EnumDiscriminants, EnumIter, IntoStaticStr, VariantNames};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{database::DatabaseError, users::User, web::icons};
|
||||
use crate::{database::DatabaseError, quotes::Quote, users::User, web::icons};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LogEntry {
|
||||
@@ -152,6 +152,9 @@ pub enum LogAction {
|
||||
CreateQuote {
|
||||
id: Uuid,
|
||||
},
|
||||
DeleteQuote {
|
||||
quote: Quote,
|
||||
},
|
||||
ManuallyRevokeSession {
|
||||
id: Uuid,
|
||||
},
|
||||
@@ -179,6 +182,8 @@ impl LogAction {
|
||||
| Self::DeleteTag { id, .. }
|
||||
| Self::ManuallyChangeUsersPassword { id } => Some(*id),
|
||||
|
||||
Self::DeleteQuote { quote } => Some(quote.id),
|
||||
|
||||
Self::AddPersonName { pid, .. }
|
||||
| Self::DeletePersonName { pid, .. }
|
||||
| Self::SetPersonPrimaryName { pid, .. } => Some(*pid),
|
||||
@@ -225,6 +230,9 @@ impl LogAction {
|
||||
LogAction::CreateQuote { id } => {
|
||||
format!("Created quote of ID {id}")
|
||||
}
|
||||
LogAction::DeleteQuote { quote } => {
|
||||
format!("Deleted quote of ID {}", quote.id)
|
||||
}
|
||||
LogAction::ManuallyRevokeSession { id } => {
|
||||
format!("Revoked session of ID {id}")
|
||||
}
|
||||
@@ -253,6 +261,7 @@ impl LogActionDiscriminant {
|
||||
LAD::DeletePersonName => "Person Name Deletion",
|
||||
LAD::SetPersonPrimaryName => "Person Primary Name Set",
|
||||
LAD::CreateQuote => "Quote Creation",
|
||||
LAD::DeleteQuote => "Quote Deletion",
|
||||
LAD::ManuallyRevokeSession => "Manual Session Revocation",
|
||||
LAD::ChangeInstanceName => "Instance Name Change",
|
||||
LAD::ChangeDiscordWebhookUrl => "Discord Webhook URL Change",
|
||||
|
||||
@@ -2,7 +2,7 @@ use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{PgConnection, Row};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -14,7 +14,7 @@ pub struct Person {
|
||||
pub primary_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Name {
|
||||
pub id: Uuid,
|
||||
pub is_primary: bool,
|
||||
|
||||
@@ -3,7 +3,7 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{PgConnection, Row};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{database::DatabaseError, persons::Name};
|
||||
|
||||
mod webhook;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Quote {
|
||||
pub id: Uuid,
|
||||
pub lines: Vec<QuoteLine>,
|
||||
@@ -22,7 +22,7 @@ pub struct Quote {
|
||||
pub public: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct QuoteLine {
|
||||
pub id: Uuid,
|
||||
pub attribution: Vec<Name>,
|
||||
@@ -290,6 +290,40 @@ impl Quote {
|
||||
public,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn delete(self, conn: &mut PgConnection) -> Result<(), QuoteError> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM line_authors
|
||||
WHERE line_id IN (SELECT id FROM lines WHERE quote_id = $1)
|
||||
"#,
|
||||
)
|
||||
.bind(self.id)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
sqlx::query("DELETE FROM lines WHERE quote_id = $1")
|
||||
.bind(self.id)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
sqlx::query("DELETE FROM quote_tags WHERE quote_id = $1")
|
||||
.bind(self.id)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
sqlx::query("DELETE FROM user_quote_likes WHERE quote_id = $1")
|
||||
.bind(self.id)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
sqlx::query("DELETE FROM quotes WHERE id = $1")
|
||||
.bind(self.id)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for QuoteError {
|
||||
|
||||
@@ -16,6 +16,8 @@ pub enum Permission {
|
||||
CreateTags,
|
||||
RenameTags,
|
||||
DeleteTags,
|
||||
#[allow(unused)]
|
||||
DeleteQuotes,
|
||||
ChangePersonPrimaryName,
|
||||
#[allow(unused)]
|
||||
BrowseServerLogs,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(unused)]
|
||||
// Below icons sourced from https://lucide.dev
|
||||
pub const ARROW_RIGHT: &str = include_str!("arrow-right.svg");
|
||||
pub const CALENDAR_1: &str = include_str!("calendar-1.svg");
|
||||
@@ -25,6 +26,7 @@ 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 TAG: &str = include_str!("tag.svg");
|
||||
pub const TRASH: &str = include_str!("trash.svg");
|
||||
pub const TYPE: &str = include_str!("type.svg");
|
||||
pub const USER: &str = include_str!("user.svg");
|
||||
pub const USER_KEY: &str = include_str!("user-key.svg");
|
||||
|
||||
1
src/web/icons/trash.svg
Normal file
1
src/web/icons/trash.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-trash-icon lucide-trash"><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
||||
|
After Width: | Height: | Size: 355 B |
@@ -50,6 +50,8 @@ pub fn pages() -> Router<MnemoState> {
|
||||
.route("/logs", get(logs::page))
|
||||
//
|
||||
.route("/quotes", get(quotes::page))
|
||||
.route("/quotes/{id}", get(quotes::id::page))
|
||||
.route("/quotes/{id}/delete", post(quotes::id::delete))
|
||||
.route("/quotes/add", get(quotes::add::page))
|
||||
.route("/quotes/add-form", post(quotes::add::form))
|
||||
//
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use axum::extract::{Request, State};
|
||||
use maud::{Markup, html};
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use http::StatusCode;
|
||||
use maud::html;
|
||||
|
||||
use crate::{
|
||||
MnemoState,
|
||||
@@ -8,13 +12,16 @@ use crate::{
|
||||
web::{components::nav::nav, pages::base},
|
||||
};
|
||||
|
||||
pub async fn page(State(state): State<MnemoState>, req: Request) -> Result<Markup, CompositeError> {
|
||||
pub async fn page(
|
||||
State(state): State<MnemoState>,
|
||||
req: Request,
|
||||
) -> Result<Response, CompositeError> {
|
||||
let mut conn = state.pool.acquire().await?;
|
||||
let u = User::authenticate(&mut *conn, req.headers())
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
Ok(base(
|
||||
Ok((StatusCode::NOT_FOUND, base(
|
||||
"Not Found | Mnemosyne",
|
||||
html!(
|
||||
(nav(&mut conn, u.as_ref(), req.uri().path()).await)
|
||||
@@ -27,5 +34,5 @@ pub async fn page(State(state): State<MnemoState>, req: Request) -> Result<Marku
|
||||
}
|
||||
}
|
||||
),
|
||||
))
|
||||
)).into_response())
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub mod add;
|
||||
pub mod id;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PageQuery {
|
||||
@@ -85,7 +86,7 @@ pub async fn page(
|
||||
}
|
||||
div class="flex flex-col gap-4 mb-8" {
|
||||
@for q in "es {
|
||||
(quote(q))
|
||||
a href=(format!("/quotes/{}", q.id)) {(quote(q))}
|
||||
}
|
||||
|
||||
div class="flex justify-between items-center mt-4 text-neutral-400" {
|
||||
|
||||
86
src/web/pages/quotes/id.rs
Normal file
86
src/web/pages/quotes/id.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use axum::{
|
||||
extract::{Path, Request, State},
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use maud::{PreEscaped, html};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
MnemoState,
|
||||
error::CompositeError,
|
||||
logs::{LogAction, LogEntry},
|
||||
quotes::Quote,
|
||||
users::{
|
||||
User,
|
||||
auth::{UserAuthRequired, UserAuthenticate},
|
||||
},
|
||||
web::{
|
||||
components::{nav::nav, quote::quote},
|
||||
icons,
|
||||
pages::base,
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn page(
|
||||
State(state): State<MnemoState>,
|
||||
Path(id): Path<Uuid>,
|
||||
req: Request,
|
||||
) -> Result<Response, CompositeError> {
|
||||
let mut conn = state.pool.acquire().await?;
|
||||
let u = match User::authenticate(&mut *conn, req.headers()).await? {
|
||||
Some(u) => u,
|
||||
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
|
||||
};
|
||||
let q = Quote::get_by_id(&mut conn, id).await;
|
||||
|
||||
Ok(base(
|
||||
"Add Quote | Mnemosyne",
|
||||
html!(
|
||||
(nav(&mut conn, Some(&u), req.uri().path()).await)
|
||||
|
||||
div class="max-w-4xl mx-auto px-2" {
|
||||
div class="my-4 flex justify-between" {
|
||||
p class="flex items-center gap-2 text-neutral-500" {
|
||||
(PreEscaped(icons::SCROLL_TEXT))
|
||||
span class="font-lora" {"Quote of ID " (id)}
|
||||
}
|
||||
}
|
||||
@if let Ok(q) = q {
|
||||
(quote(&q))
|
||||
div class="flex flex-row w-full flex-wrap justify-end gap-2 mt-2" {
|
||||
a href="#" disabled class="opacity-[.5] cursor-not-allowed px-2 py-1 border rounded flex flex-row gap-1 bg-neutral-200/5 border-neutral-200/25 hover:bg-neutral-200/15 hover:border-neutral-200/45" {
|
||||
span class="scale-[.75]" {(PreEscaped(icons::PEN))}
|
||||
"Edit"
|
||||
}
|
||||
form method="post" action=(format!("/quotes/{id}/delete")) {
|
||||
button type="submit" class="px-2 py-1 border rounded flex flex-row gap-1 bg-pink-400/10 border-pink-400/25 hover:bg-pink-400/20 hover:border-pink-400/45" {
|
||||
span class="scale-[.75]" {(PreEscaped(icons::TRASH))}
|
||||
"Delete"
|
||||
}
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
"Failed to fetch quote. Are you sure it exists?"
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn delete(
|
||||
State(state): State<MnemoState>,
|
||||
Path(id): Path<Uuid>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Response, CompositeError> {
|
||||
let mut tx = state.pool.begin().await?;
|
||||
let u = User::authenticate(&mut *tx, &headers).await?.required()?;
|
||||
|
||||
let q = Quote::get_by_id(&mut *tx, id).await?;
|
||||
LogEntry::new(&mut *tx, u, LogAction::DeleteQuote { quote: q.clone() }).await?;
|
||||
q.delete(&mut tx).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Redirect::to("/quotes").into_response())
|
||||
}
|
||||
Reference in New Issue
Block a user