diff --git a/.dockerignore b/.dockerignore index 4200a43..007d945 100644 --- a/.dockerignore +++ b/.dockerignore @@ -28,3 +28,4 @@ README.md readme **/*.db* **/*.db3* +/mnemodata diff --git a/.gitignore b/.gitignore index 6a38e1e..b6ee0c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /target .DS_Store /database +/mnemodata *.db *.db-shm *.db-wal diff --git a/Dockerfile b/Dockerfile index 4db5d72..0a2f152 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,7 +53,7 @@ RUN adduser \ --uid "${UID}" \ appuser -RUN mkdir -p /app && chown appuser:appuser /app +RUN mkdir -p /app/data && chown -R appuser:appuser /app USER appuser WORKDIR /app ENV IN_DOCKER=true diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..1a67466 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,26 @@ +services: + mnemosyne: + container_name: mnemosyne + build: + context: . + target: final + ports: + - 39321:39321 + restart: unless-stopped + volumes: + # Mnemosyne would greatly enjoy not having an ephemeral database. + # If you're okay with storing it side by side with the compose file, + # a bind mount like this is one way to do it. Remember to mkdir! + - ./mnemodata:/app/data + # Another way is to use a docker volume. + # - mnemodata:/app/data + environment: + # DATABASE_URL is crucial for Mnemosyne to work; it will fail without it. + # Point it at where you'd like your database to be. + - DATABASE_URL=/app/data/db.db + # Mnemosyne uses port 39321 for HTTP by default; + # - PORT=39321 + +# Declaring a volume for the docker volume example. +# volumes: +# mnemodata: diff --git a/src/api/mod.rs b/src/api/mod.rs index d92bc0c..5bae6c2 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,18 +1,8 @@ use axum::{ Router, - response::{IntoResponse, Response}, routing::{delete, get, patch, post}, }; -use crate::{ - database::DatabaseError, - persons::PersonError, - quotes::QuoteError, - tags::TagError, - users::{UserError, auth::AuthError, sessions::SessionError}, - web::RedirectViaError, -}; - mod auth; mod persons; mod quotes; @@ -58,32 +48,3 @@ pub fn api_router() -> Router { .route("/api/quotes", post(quotes::create)) .route("/api/quotes/{id}", get(quotes::get_by_id)) } - -pub struct CompositeError(Response); -impl IntoResponse for CompositeError { - fn into_response(self) -> Response { - self.0 - } -} - -macro_rules! composite_from { - ($($t:ty),+ $(,)?) => { - $( - impl From<$t> for CompositeError { - fn from(e: $t) -> Self { - CompositeError(e.into_response()) - } - } - )+ - }; -} -composite_from!( - AuthError, - UserError, - SessionError, - TagError, - PersonError, - QuoteError, - DatabaseError, - RedirectViaError, -); diff --git a/src/api/persons.rs b/src/api/persons.rs index b6c6b00..343f6f9 100644 --- a/src/api/persons.rs +++ b/src/api/persons.rs @@ -8,8 +8,8 @@ use serde::Deserialize; use uuid::Uuid; use crate::{ - api::CompositeError, - database::{self, DatabaseError}, + database::{self}, + error::CompositeError, logs::{LogAction, LogEntry}, persons::{Name, Person}, users::{ @@ -54,7 +54,7 @@ pub async fn create( ) -> Result { let u = User::authenticate(&headers)?.required()?; let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; let p = Person::create(&tx, form.name, u.id)?; LogEntry::new( @@ -65,7 +65,7 @@ pub async fn create( pname: p.primary_name.as_str().to_string(), }, )?; - tx.commit().map_err(DatabaseError::from)?; + tx.commit()?; Ok((StatusCode::CREATED, Json(p)).into_response()) } pub async fn add_name( @@ -75,7 +75,7 @@ pub async fn add_name( ) -> Result { let u = User::authenticate(&headers)?.required()?; let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; let p = Person::get_by_id(&tx, id)?; let n = p.add_name(&tx, form.name, u.id)?; @@ -89,7 +89,7 @@ pub async fn add_name( nn: n.name.clone(), }, )?; - tx.commit().map_err(DatabaseError::from)?; + tx.commit()?; Ok((StatusCode::CREATED, Json(n)).into_response()) } @@ -104,7 +104,7 @@ pub async fn n_setprimary( ) -> Result { let u = User::authenticate(&headers)?.required()?; let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; if !u.has_permission(&tx, Permission::ChangePersonPrimaryName)? { return Ok((StatusCode::FORBIDDEN, CANT_SET_PRIMARYNAME).into_response()); @@ -124,7 +124,7 @@ pub async fn n_setprimary( nn: n.name.clone(), }, )?; - tx.commit().map_err(DatabaseError::from)?; + tx.commit()?; Ok(Json(n).into_response()) } diff --git a/src/api/quotes.rs b/src/api/quotes.rs index 997a761..bde6b72 100644 --- a/src/api/quotes.rs +++ b/src/api/quotes.rs @@ -9,8 +9,8 @@ use serde::Deserialize; use uuid::Uuid; use crate::{ - api::CompositeError, - database::{self, DatabaseError}, + database::{self}, + error::CompositeError, logs::{LogAction, LogEntry}, persons::Name, quotes::Quote, @@ -50,7 +50,7 @@ pub async fn create( ) -> Result { let u = User::authenticate(&headers)?.required()?; let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; let lines = form .lines @@ -69,6 +69,6 @@ pub async fn create( )?; LogEntry::new(&tx, u, LogAction::CreateQuote { id: q.id })?; - tx.commit().map_err(DatabaseError::from)?; + tx.commit()?; Ok((StatusCode::CREATED, Json(q)).into_response()) } diff --git a/src/api/sessions.rs b/src/api/sessions.rs index 1c2a4db..942e3f9 100644 --- a/src/api/sessions.rs +++ b/src/api/sessions.rs @@ -7,8 +7,8 @@ use axum::{ use uuid::Uuid; use crate::{ - api::CompositeError, - database::{self, DatabaseError}, + database::{self}, + error::CompositeError, logs::{LogAction, LogEntry}, users::{ User, @@ -43,7 +43,7 @@ pub async fn revoke_by_id( ) -> Result { let u = User::authenticate(&headers)?.required()?; let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; let mut s = Session::get_by_id(&tx, id)?; match s.user_id == u.id @@ -53,7 +53,7 @@ pub async fn revoke_by_id( true => { s.revoke(&tx, Some(&u))?; LogEntry::new(&tx, u, LogAction::ManuallyRevokeSession { id })?; - tx.commit().map_err(DatabaseError::from)?; + tx.commit()?; Ok(Json(s).into_response()) } false => match u.has_permission(&tx, Permission::ListOthersSessions)? { diff --git a/src/api/tags.rs b/src/api/tags.rs index 21fd116..8051e53 100644 --- a/src/api/tags.rs +++ b/src/api/tags.rs @@ -8,8 +8,8 @@ use serde::Deserialize; use uuid::Uuid; use crate::{ - api::CompositeError, - database::{self, DatabaseError}, + database::{self}, + error::CompositeError, logs::{LogAction, LogEntry}, tags::{Tag, TagName}, users::{ @@ -58,7 +58,7 @@ pub async fn create( ) -> Result { let u = User::authenticate(&headers)?.required()?; let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; if !u.has_permission(&tx, Permission::CreateTags)? { return Ok((StatusCode::FORBIDDEN, CANT_MAKE_TAGS).into_response()); @@ -73,7 +73,7 @@ pub async fn create( name: t.name.as_str().to_string(), }, )?; - tx.commit().map_err(DatabaseError::from)?; + tx.commit()?; Ok(Json(t).into_response()) } @@ -84,7 +84,7 @@ pub async fn rename( ) -> Result { let u = User::authenticate(&headers)?.required()?; let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; if !u.has_permission(&tx, Permission::RenameTags)? { return Ok((StatusCode::FORBIDDEN, CANT_RENAME_TAGS).into_response()); @@ -101,7 +101,7 @@ pub async fn rename( nn: tag.name.as_str().to_string(), }, )?; - tx.commit().map_err(DatabaseError::from)?; + tx.commit()?; Ok(Json(tag).into_response()) } @@ -109,7 +109,7 @@ pub async fn rename( pub async fn delete(Path(id): Path, headers: HeaderMap) -> Result { let u = User::authenticate(&headers)?.required()?; let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; if !u.has_permission(&tx, Permission::DeleteTags)? { return Ok((StatusCode::FORBIDDEN, CANT_DEL_TAGS).into_response()); @@ -118,7 +118,7 @@ pub async fn delete(Path(id): Path, headers: HeaderMap) -> Result Result { let u = User::authenticate(&headers)?.required()?; let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; if !u.has_permission(&tx, Permission::ManuallyCreateUsers)? { return Ok((StatusCode::FORBIDDEN, CANT_MANUALLY_MAKE_USERS).into_response()); @@ -78,7 +78,7 @@ pub async fn create( handle: nu.handle.as_str().to_string(), }, )?; - tx.commit().map_err(DatabaseError::from)?; + tx.commit()?; Ok(Json(nu).into_response()) } @@ -89,7 +89,7 @@ pub async fn change_handle( ) -> Result { let u = User::authenticate(&headers)?.required()?; let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; let mut target = if u.id == id { u.clone() @@ -111,7 +111,7 @@ pub async fn change_handle( new: target.handle.as_str().to_string(), }, )?; - tx.commit().map_err(DatabaseError::from)?; + tx.commit()?; Ok(HANDLE_CHANGED_SUCCESS.into_response()) } @@ -127,7 +127,7 @@ pub async fn change_password( ) -> Result { let u = User::authenticate(&headers)?.required()?; let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; let mut target = if u.id == id { u.clone() @@ -144,7 +144,7 @@ pub async fn change_password( u, LogAction::ManuallyChangeUsersPassword { id: target.id }, )?; - tx.commit().map_err(DatabaseError::from)?; + tx.commit()?; Ok(PASSW_CHANGED_SUCCESS.into_response()) } diff --git a/src/database/migrations/2026-03-07--01.sql b/src/database/migrations/2026-04-04--01.sql similarity index 98% rename from src/database/migrations/2026-03-07--01.sql rename to src/database/migrations/2026-04-04--01.sql index 66e3949..9633405 100644 --- a/src/database/migrations/2026-03-07--01.sql +++ b/src/database/migrations/2026-04-04--01.sql @@ -2,7 +2,7 @@ CREATE TABLE users ( id BLOB NOT NULL UNIQUE PRIMARY KEY, -- UUIDv7 as bytes handle TEXT NOT NULL UNIQUE COLLATE NOCASE, password TEXT, -- hashed, nullable in case of OAuth2-only login - prof_pic TEXT -- link probably + prof_pic TEXT -- serialized ProfilePic ); CREATE TABLE sessions ( id BLOB NOT NULL UNIQUE PRIMARY KEY, -- UUIDv7 as bytes diff --git a/src/database/mod.rs b/src/database/mod.rs index a09175e..e9f43ff 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -8,7 +8,7 @@ macro_rules! migration { ($name, include_str!(concat!("./migrations/", $name, ".sql"))) }; } -const MIGRATIONS: &[(&str, &str)] = &[migration!("2026-03-07--01")]; +const MIGRATIONS: &[(&str, &str)] = &[migration!("2026-04-04--01")]; pub static DB_URL: LazyLock = LazyLock::new(|| env::var("DATABASE_URL").expect("DATABASE_URL is not set")); diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..506ee87 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,45 @@ +use axum::response::{IntoResponse, Response}; + +use crate::{ + database::DatabaseError, + persons::PersonError, + quotes::QuoteError, + tags::TagError, + users::{UserError, auth::AuthError, sessions::SessionError}, + web::RedirectViaError, +}; + +pub struct CompositeError(Response); +impl IntoResponse for CompositeError { + fn into_response(self) -> Response { + self.0 + } +} + +macro_rules! composite_from { + ($($t:ty),+ $(,)?) => { + $( + impl From<$t> for CompositeError { + fn from(e: $t) -> Self { + CompositeError(e.into_response()) + } + } + )+ + }; +} +composite_from!( + AuthError, + UserError, + SessionError, + TagError, + PersonError, + QuoteError, + DatabaseError, + RedirectViaError, +); + +impl From for CompositeError { + fn from(e: rusqlite::Error) -> Self { + CompositeError(DatabaseError::from(e).into_response()) + } +} diff --git a/src/logs.rs b/src/logs.rs index d15e66d..df44bfc 100644 --- a/src/logs.rs +++ b/src/logs.rs @@ -1,5 +1,3 @@ -use std::fmt::format; - use rusqlite::Connection; use serde::{Deserialize, Serialize}; use strum::IntoStaticStr; diff --git a/src/main.rs b/src/main.rs index 58a56af..3fe8d0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use tokio::net::TcpListener; mod api; mod config; mod database; +mod error; mod logs; mod persons; mod quotes; diff --git a/src/web/mod.rs b/src/web/mod.rs index 1a68d4d..123320e 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -1,16 +1,22 @@ use axum::{ Router, + http::header, response::{IntoResponse, Redirect, Response}, + routing::get, }; -use tower_http::services::ServeFile; mod components; mod icons; mod pages; +pub const STYLES_CSS: &str = include_str!("./styles.css"); + pub fn web_router() -> Router { Router::new() - .route_service("/styles.css", ServeFile::new("src/web/styles.css")) + .route( + "/styles.css", + get(|| async { ([(header::CONTENT_TYPE, "text/css")], STYLES_CSS) }), + ) .merge(pages::pages()) } diff --git a/src/web/pages/dashboard.rs b/src/web/pages/dashboard.rs index 3727ba7..095b0fa 100644 --- a/src/web/pages/dashboard.rs +++ b/src/web/pages/dashboard.rs @@ -4,8 +4,8 @@ use maud::{Markup, PreEscaped, html}; use uuid::Uuid; use crate::{ - api::CompositeError, - database::{self, DatabaseError}, + database::{self}, + error::CompositeError, persons::{Name, Person}, quotes::{Quote, QuoteLine}, tags::Tag, @@ -25,7 +25,7 @@ const LINKS: &[(&str, &str, &str)] = &[ pub async fn page(req: Request) -> Result { let u = User::authenticate(req.headers()).ok().flatten(); let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; Ok(base( "Dashboard | Mnemosyne", diff --git a/src/web/pages/logs.rs b/src/web/pages/logs.rs index e31eaa3..e9e51d6 100644 --- a/src/web/pages/logs.rs +++ b/src/web/pages/logs.rs @@ -5,8 +5,8 @@ use axum::{ use maud::{PreEscaped, html}; use crate::{ - api::CompositeError, - database::{self, DatabaseError}, + database::{self}, + error::CompositeError, logs::LogEntry, users::{User, auth::UserAuthenticate, permissions::Permission}, web::{RedirectViaError, components::nav::nav, icons, pages::base}, @@ -16,7 +16,7 @@ pub async fn page(req: Request) -> Result { let u = User::authenticate(req.headers())? .ok_or(RedirectViaError(Redirect::to("/login?re=/logs")))?; let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; let logs = LogEntry::get_all(&tx)?; Ok(base( diff --git a/src/web/pages/persons.rs b/src/web/pages/persons.rs index 12fa0b4..332816e 100644 --- a/src/web/pages/persons.rs +++ b/src/web/pages/persons.rs @@ -8,8 +8,8 @@ use maud::{PreEscaped, html}; use serde::Deserialize; use crate::{ - api::CompositeError, - database::{self, DatabaseError}, + database::{self}, + error::CompositeError, logs::{LogAction, LogEntry}, persons::Person, users::{ @@ -100,7 +100,7 @@ pub async fn create( ) -> Result { let u = User::authenticate(&headers)?.required()?; let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; let p = Person::create(&tx, form.primary_name, u.id)?; LogEntry::new( @@ -111,6 +111,6 @@ pub async fn create( pname: p.primary_name, }, )?; - tx.commit().map_err(DatabaseError::from)?; + tx.commit()?; Ok(Redirect::to("/persons").into_response()) } diff --git a/src/web/pages/tags.rs b/src/web/pages/tags.rs index 8222f79..a7d886f 100644 --- a/src/web/pages/tags.rs +++ b/src/web/pages/tags.rs @@ -8,8 +8,8 @@ use maud::{PreEscaped, html}; use serde::Deserialize; use crate::{ - api::CompositeError, - database::{self, DatabaseError}, + database::{self}, + error::CompositeError, logs::{LogAction, LogEntry}, tags::{Tag, TagName}, users::{ @@ -99,7 +99,7 @@ pub async fn create( ) -> Result { let u = User::authenticate(&headers)?.required()?; let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; let t = Tag::create(&tx, form.tagname)?; LogEntry::new( @@ -110,6 +110,6 @@ pub async fn create( name: t.name.to_string(), }, )?; - tx.commit().map_err(DatabaseError::from)?; + tx.commit()?; Ok(Redirect::to("/tags").into_response()) } diff --git a/src/web/pages/users/create.rs b/src/web/pages/users/create.rs index 1d80301..595103c 100644 --- a/src/web/pages/users/create.rs +++ b/src/web/pages/users/create.rs @@ -8,8 +8,8 @@ use maud::{PreEscaped, html}; use serde::Deserialize; use crate::{ - api::CompositeError, - database::{self, DatabaseError}, + database::{self}, + error::CompositeError, logs::{LogAction, LogEntry}, users::{ User, @@ -73,8 +73,8 @@ pub async fn create_user( Form(form): Form, ) -> Result { let u = User::authenticate(&headers)?.required()?; - let mut conn = database::conn().map_err(DatabaseError::from)?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let mut conn = database::conn()?; + let tx = conn.transaction()?; if !u.has_permission(&tx, Permission::ManuallyCreateUsers)? { return Ok((StatusCode::FORBIDDEN).into_response()); @@ -89,6 +89,6 @@ pub async fn create_user( handle: nu.handle.as_str().to_string(), }, )?; - tx.commit().map_err(DatabaseError::from)?; + tx.commit()?; Ok(Redirect::to("/users").into_response()) } diff --git a/src/web/pages/users/profile.rs b/src/web/pages/users/profile.rs index 33534cb..4bbebe0 100644 --- a/src/web/pages/users/profile.rs +++ b/src/web/pages/users/profile.rs @@ -7,8 +7,8 @@ use maud::{PreEscaped, html}; use uuid::Uuid; use crate::{ - api::CompositeError, - database::{self, DatabaseError}, + database::{self}, + error::CompositeError, persons::Name, quotes::{Quote, QuoteLine}, users::{User, UserError, auth::UserAuthenticate}, @@ -24,8 +24,8 @@ pub async fn page(Path(id): Path, req: Request) -> Result u, None => return Ok(Redirect::to("/users").into_response()), }; - let mut conn = database::conn().map_err(DatabaseError::from)?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let mut conn = database::conn()?; + let tx = conn.transaction()?; let user = match User::get_by_id(&tx, id) { Ok(u) => u, diff --git a/src/web/pages/usersettings.rs b/src/web/pages/usersettings.rs index 3eb96d4..31a6668 100644 --- a/src/web/pages/usersettings.rs +++ b/src/web/pages/usersettings.rs @@ -8,8 +8,8 @@ use maud::{PreEscaped, html}; use serde::Deserialize; use crate::{ - api::CompositeError, - database::{self, DatabaseError}, + database::{self}, + error::CompositeError, logs::{LogAction, LogEntry}, users::{ User, @@ -83,7 +83,7 @@ pub async fn change_handle( ) -> Result { let mut u = User::authenticate(&headers)?.required()?; let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; let oldhandle = u.handle.as_str().to_string(); u.set_handle(&tx, form.handle)?; @@ -96,7 +96,7 @@ pub async fn change_handle( new: u.handle.as_str().to_string(), }, )?; - tx.commit().map_err(DatabaseError::from)?; + tx.commit()?; Ok(Redirect::to("/user-settings").into_response()) } @@ -110,8 +110,8 @@ pub async fn change_password( ) -> Result { let mut u = User::authenticate(&headers)?.required()?; let mut conn = database::conn()?; - let tx = conn.transaction().map_err(DatabaseError::from)?; + let tx = conn.transaction()?; u.set_password(&tx, Some(&form.password))?; - tx.commit().map_err(DatabaseError::from)?; + tx.commit()?; Ok(Redirect::to("/user-settings").into_response()) }