postgres via sqlx - workable?

This commit is contained in:
2026-04-20 01:17:30 +02:00
parent acfd8a6d72
commit 879c5ee3d3
42 changed files with 2536 additions and 1184 deletions

View File

@@ -5,13 +5,15 @@ use axum::{
routing::get,
};
use crate::MnemoState;
mod components;
mod icons;
mod pages;
pub const CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/styles.css"));
pub fn web_router() -> Router {
pub fn web_router() -> Router<MnemoState> {
Router::new()
.route(
"/styles.css",

View File

@@ -1,9 +1,9 @@
use axum::extract::Request;
use axum::extract::{Request, State};
use chrono::{DateTime, Utc};
use maud::{Markup, PreEscaped, html};
use crate::{
database::{self},
MnemoState,
error::CompositeError,
persons::Person,
quotes::Quote,
@@ -21,19 +21,27 @@ const LINKS: &[(&str, &str, &str)] = &[
("Add Person", "/persons", icons::CONTACT),
];
pub async fn page(req: Request) -> Result<Markup, CompositeError> {
let u = User::authenticate(req.headers()).ok().flatten();
let conn = database::conn()?;
pub async fn page(State(state): State<MnemoState>, req: Request) -> Result<Markup, CompositeError> {
let mut conn = state.pool.acquire().await?;
let u = User::authenticate(&mut *conn, req.headers())
.await
.ok()
.flatten();
let newest_quote = match u {
Some(_) => Quote::get_newest(&conn)?,
None => Quote::get_newest_public(&conn)?,
Some(_) => Quote::get_newest(&mut *conn).await?,
None => Quote::get_newest_public(&mut *conn).await?,
};
let random_quote = match u {
Some(_) => Quote::get_random(&conn)?,
Some(_) => Quote::get_random(&mut *conn).await?,
None => None,
};
let quote_count = Quote::total_count(&mut *conn).await;
let person_count = Person::total_count(&mut *conn).await;
let tag_count = Tag::total_count(&mut *conn).await;
let user_count = User::total_count(&mut *conn).await;
Ok(base(
"Dashboard | Mnemosyne",
html!(
@@ -79,25 +87,25 @@ pub async fn page(req: Request) -> Result<Markup, CompositeError> {
}
div class="mx-auto max-w-4xl px-2 mt-4 flex flex-row gap-2" {
(chip(html!({
@match Quote::total_count(&conn) {
@match quote_count {
Ok(count) => {(count) " QUOTES TOTAL"},
Err(_) => span class="text-red-400" {"QUOTE COUNT ERR"},
}
})))
(chip(html!({
@match Person::total_count(&conn) {
@match person_count {
Ok(count) => {(count) " PERSONS TOTAL"},
Err(_) => span class="text-red-400" {"PERSON COUNT ERR"},
}
})))
(chip(html!({
@match Tag::total_count(&conn) {
@match tag_count {
Ok(count) => {(count) " TAGS TOTAL"},
Err(_) => span class="text-red-400" {"TAG COUNT ERR"}
}
})))
(chip(html!({
@match User::total_count(&conn) {
@match user_count {
Ok(count) => {(count) " USERS TOTAL"},
Err(_) => span class="text-red-400" {"USER COUNT ERR"}
}

View File

@@ -1,5 +1,5 @@
use axum::{
extract::{Query, Request},
extract::{Query, Request, State},
response::{IntoResponse, Redirect, Response},
};
use maud::{PreEscaped, html};
@@ -8,10 +8,8 @@ use serde::Deserialize;
use crate::{
config::REFERENCE_SPLASHES,
users::{
User,
auth::{AuthError, UserAuthenticate},
},
error::CompositeError,
users::{User, auth::UserAuthenticate},
web::{components::marquee::marquee, icons, pages::base},
};
@@ -20,8 +18,13 @@ pub struct LoginMsg {
msg: Option<String>,
}
pub async fn page(Query(q): Query<LoginMsg>, req: Request) -> Result<Response, AuthError> {
let u = User::authenticate(req.headers())?;
pub async fn page(
State(state): State<crate::MnemoState>,
Query(q): Query<LoginMsg>,
req: Request,
) -> Result<Response, CompositeError> {
let mut conn = state.pool.acquire().await?;
let u = User::authenticate(&mut *conn, req.headers()).await?;
if u.is_some() {
return Ok(Redirect::to("/dashboard").into_response());
}

View File

@@ -1,12 +1,12 @@
use axum::{
extract::{Query, Request},
extract::{Query, Request, State},
response::{IntoResponse, Redirect, Response},
};
use maud::{PreEscaped, html};
use serde::Deserialize;
use crate::{
database::{self},
MnemoState,
error::CompositeError,
logs::LogEntry,
users::{User, auth::UserAuthenticate},
@@ -19,22 +19,22 @@ pub struct PageQuery {
}
pub async fn page(
State(state): State<MnemoState>,
Query(query): Query<PageQuery>,
req: Request,
) -> Result<Response, CompositeError> {
let u = match User::authenticate(req.headers())? {
let mut tx = state.pool.begin().await?;
let u = match User::authenticate(&mut *tx, req.headers()).await? {
Some(u) => u,
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
};
let mut conn = database::conn()?;
let tx = conn.transaction()?;
let page = query.page.unwrap_or(1).max(1);
let per_page = 20;
let offset = (page - 1) * per_page;
let logs = LogEntry::get_chronological_offset(&tx, offset, per_page)?;
let total_logs = LogEntry::total_count(&tx)?;
let logs = LogEntry::get_chronological_offset(&mut *tx, offset, per_page).await?;
let total_logs = LogEntry::total_count(&mut *tx).await?;
let total_pages = (total_logs as f64 / per_page as f64).ceil() as i64;
Ok(base(
@@ -42,7 +42,7 @@ pub async fn page(
html!(
(nav(Some(&u), req.uri().path()))
@if true {//let Ok(true) = u.has_permission(&tx, Permission::BrowseServerLogs) {
@if true {//let Ok(true) = u.has_permission(&mut *tx, Permission::BrowseServerLogs) {
div class="max-w-4xl mx-auto px-2" {
div class="my-4" {
p class="flex items-center gap-2" {

View File

@@ -4,6 +4,8 @@ use axum::{
};
use maud::{DOCTYPE, Markup, html};
use crate::MnemoState;
pub mod dashboard;
pub mod index;
pub mod login;
@@ -15,7 +17,7 @@ pub mod tags;
pub mod users;
pub mod usersettings;
pub fn pages() -> Router {
pub fn pages() -> Router<MnemoState> {
Router::new()
.route("/", get(index::page))
.route("/login", get(login::page))

View File

@@ -1,4 +1,4 @@
use axum::extract::Request;
use axum::extract::{Request, State};
use maud::{Markup, html};
use crate::{
@@ -7,8 +7,15 @@ use crate::{
web::{components::nav::nav, pages::base},
};
pub async fn page(req: Request) -> Result<Markup, CompositeError> {
let u = User::authenticate(req.headers()).ok().flatten();
pub async fn page(
State(state): State<crate::MnemoState>,
req: Request,
) -> Result<Markup, CompositeError> {
let mut conn = state.pool.acquire().await?;
let u = User::authenticate(&mut *conn, req.headers())
.await
.ok()
.flatten();
Ok(base(
"Not Found | Mnemosyne",
html!(

View File

@@ -1,6 +1,7 @@
use axum::{
Form,
extract::Request,
extract::State,
http::HeaderMap,
response::{IntoResponse, Redirect, Response},
};
@@ -8,26 +9,37 @@ use maud::{PreEscaped, html};
use serde::Deserialize;
use crate::{
database::{self},
error::CompositeError,
logs::{LogAction, LogEntry},
persons::Person,
users::{
User,
auth::{AuthError, UserAuthRequired, UserAuthenticate},
auth::{UserAuthRequired, UserAuthenticate},
},
web::{components::nav::nav, icons, pages::base},
};
pub mod profile;
pub async fn page(req: Request) -> Result<Response, AuthError> {
let u = match User::authenticate(req.headers())? {
pub async fn page(
State(state): State<crate::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) => u,
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
};
let mut conn = database::conn()?;
let tx = conn.transaction()?;
let total_count = Person::total_count(&mut *conn).await;
let persons_res = Person::get_all(&mut *conn).await;
let mut person_counts = vec![];
if let Ok(ref persons) = persons_res {
for p in persons {
person_counts.push(p.get_in_quote_count(&mut *conn).await);
}
}
Ok(base(
"Persons | Mnemosyne",
@@ -40,30 +52,28 @@ pub async fn page(req: Request) -> Result<Response, AuthError> {
span class="text-2xl font-semibold font-lora" {"Persons"}
}
p class="text-neutral-500 text-sm font-light" {
@if let Ok(c) = Person::total_count(&tx) {
@if let Ok(c) = total_count {
(c) " persons in total."
} @else {
"Could not get total person count."
}
}
}
@if let Ok(persons) = Person::get_all(&tx) {
@if let Ok(persons) = persons_res {
div class="max-w-4xl mx-auto px-2 mt-4 flex flex-wrap gap-2" {
@for person in &persons {
@for (idx, person) in persons.iter().enumerate() {
a href={"/persons/"(person.id)} class="rounded px-4 py-2 bg-neutral-200/5 hover:bg-neutral-200/10 border border-neutral-200/25 hover:border-neutral-200/25 flex items-center" {
span class="text-neutral-400 mr-1 scale-125" {"~"}
span class="text-sm" {(person.primary_name)}
div class="w-px h-2/3 my-auto mx-2 bg-neutral-200/15" {}
div class="text-xs flex items-center" {
(
if let Ok(i) = person.get_in_quote_count(&tx) {
if let Ok(i) = person_counts[idx] {
i.to_string()
} else {
"?".to_string()
}
) span class="*:size-3 ml-1 text-neutral-400" {(PreEscaped(icons::SCROLL_TEXT))}
// div class="ml-2" {}
// "4" span class="*:size-3 ml-1 text-neutral-400" {(PreEscaped(icons::FILE_IMAGE))}
}
}
}
@@ -96,22 +106,23 @@ pub struct PersonNameForm {
primary_name: String,
}
pub async fn create(
State(state): State<crate::MnemoState>,
headers: HeaderMap,
Form(form): Form<PersonNameForm>,
) -> Result<Response, CompositeError> {
let u = User::authenticate(&headers)?.required()?;
let mut conn = database::conn()?;
let tx = conn.transaction()?;
let mut tx = state.pool.begin().await?;
let u = User::authenticate(&mut *tx, &headers).await?.required()?;
let p = Person::create(&tx, form.primary_name, u.id)?;
let p = Person::create(&mut *tx, form.primary_name, u.id).await?;
LogEntry::new(
&tx,
&mut *tx,
u,
LogAction::CreatePerson {
id: p.id,
pname: p.primary_name,
},
)?;
tx.commit()?;
)
.await?;
tx.commit().await?;
Ok(Redirect::to("/persons").into_response())
}

View File

@@ -1,6 +1,6 @@
use axum::{
Form,
extract::{Path, Request},
extract::{Path, Request, State},
http::HeaderMap,
response::{IntoResponse, Redirect, Response},
};
@@ -9,29 +9,44 @@ use serde::Deserialize;
use uuid::Uuid;
use crate::{
database,
error::CompositeError,
logs::{LogAction, LogEntry},
persons::{Name, Person},
users::{
User,
auth::{AuthError, UserAuthRequired, UserAuthenticate},
auth::{UserAuthRequired, UserAuthenticate},
},
web::{components::nav::nav, pages::base},
};
pub async fn page(Path(id): Path<Uuid>, req: Request) -> Result<Response, AuthError> {
let u = match User::authenticate(req.headers())? {
pub async fn page(
State(state): State<crate::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 conn = database::conn()?;
let p = Person::get_by_id(&conn, id);
let p = Person::get_by_id(&mut *conn, id).await;
let title = match &p {
Ok(p) => format!("~{} | Mnemosyne", p.primary_name),
Err(_) => "Error! | Mnemosyne".into(),
};
let mut names_with_attribution = Vec::new();
let mut names_ok = false;
if let Ok(ref person) = p {
if let Ok(names) = person.get_all_names(&mut *conn).await {
names_ok = true;
for name in names {
let attr = name.times_attributed(&mut *conn).await.unwrap_or(0);
names_with_attribution.push((name, attr));
}
}
}
Ok(base(
&title,
html!(
@@ -46,14 +61,14 @@ pub async fn page(Path(id): Path<Uuid>, req: Request) -> Result<Response, AuthEr
div {
h2 class="text-lg font-semibold font-lora mb-2 text-neutral-300" {"Names"}
div class="flex flex-wrap gap-2 mb-4" {
@if let Ok(names) = p.get_all_names(&conn) {
@for name in &names {
@if names_ok {
@for (name, attr) in names_with_attribution {
div class="rounded px-3 py-1 bg-neutral-200/5 border border-neutral-200/10 text-sm flex items-center gap-2" {
(name.name)
@if name.is_primary {
span class="text-xs text-neutral-500" {"(primary)"}
}
@if let Ok(0) = name.times_attributed(&conn) && !name.is_primary {
@if attr == 0 && !name.is_primary {
form action=(format!("/names/{}/delete", name.id)) method="post" class="flex items-center ml-1" {
button type="submit" class="text-neutral-500 hover:text-red-400 flex items-center justify-center cursor-pointer" title="Delete" {
""
@@ -91,19 +106,19 @@ pub struct AddNameForm {
}
pub async fn add_name(
State(state): State<crate::MnemoState>,
Path(id): Path<Uuid>,
headers: HeaderMap,
Form(form): Form<AddNameForm>,
) -> Result<Response, CompositeError> {
let u = User::authenticate(&headers)?.required()?;
let mut conn = database::conn()?;
let tx = conn.transaction()?;
let mut tx = state.pool.begin().await?;
let u = User::authenticate(&mut *tx, &headers).await?.required()?;
let p = Person::get_by_id(&tx, id)?;
let n = p.add_name(&tx, form.name, u.id)?;
let p = Person::get_by_id(&mut *tx, id).await?;
let n = p.add_name(&mut *tx, form.name, u.id).await?;
LogEntry::new(
&tx,
&mut *tx,
u,
LogAction::AddPersonName {
pid: p.id,
@@ -111,28 +126,29 @@ pub async fn add_name(
pn: p.primary_name,
nn: n.name,
},
)?;
tx.commit()?;
)
.await?;
tx.commit().await?;
Ok(Redirect::to(&format!("/persons/{}", p.id)).into_response())
}
pub async fn delete_name(
State(state): State<crate::MnemoState>,
Path(id): Path<Uuid>,
headers: HeaderMap,
) -> Result<Response, CompositeError> {
let u = User::authenticate(&headers)?.required()?;
let mut conn = database::conn()?;
let tx = conn.transaction()?;
let mut tx = state.pool.begin().await?;
let u = User::authenticate(&mut *tx, &headers).await?.required()?;
let n = Name::get_by_id(&tx, id)?;
let p = Person::get_by_id(&tx, n.person_id)?;
let n = Name::get_by_id(&mut *tx, id).await?;
let p = Person::get_by_id(&mut *tx, n.person_id).await?;
let nn = n.name.clone();
n.delete(&tx)?;
n.delete(&mut *tx).await?;
LogEntry::new(
&tx,
&mut *tx,
u,
LogAction::DeletePersonName {
pid: p.id,
@@ -140,8 +156,9 @@ pub async fn delete_name(
pn: p.primary_name,
n: nn,
},
)?;
tx.commit()?;
)
.await?;
tx.commit().await?;
Ok(Redirect::to(&format!("/persons/{}", p.id)).into_response())
}

View File

@@ -6,7 +6,6 @@ use maud::{PreEscaped, html};
use serde::Deserialize;
use crate::{
database,
error::CompositeError,
quotes::Quote,
users::{User, auth::UserAuthenticate},
@@ -25,21 +24,22 @@ pub struct PageQuery {
}
pub async fn page(
axum::extract::State(state): axum::extract::State<crate::MnemoState>,
Query(query): Query<PageQuery>,
req: Request,
) -> Result<Response, CompositeError> {
let u = match User::authenticate(req.headers())? {
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 conn = database::conn()?;
let page = query.page.unwrap_or(1).max(1);
let per_page = 10;
let offset = (page - 1) * per_page;
let quotes = Quote::get_chronological_offset(&conn, offset, per_page)?;
let total_quotes = Quote::total_count(&conn)?;
let quotes = Quote::get_chronological_offset(&mut *conn, offset, per_page).await?;
let total_quotes = Quote::total_count(&mut *conn).await?;
let total_pages = (total_quotes as f64 / per_page as f64).ceil() as i64;
Ok(base(

View File

@@ -4,14 +4,12 @@ use axum::{
response::{IntoResponse, Redirect, Response},
};
use axum_extra::extract::Form;
use chrono::{TimeZone, Utc};
use chrono_tz::Europe::Warsaw;
use chrono::NaiveDateTime;
use maud::{Markup, PreEscaped, html};
use serde::Deserialize;
use uuid::Uuid;
use crate::{
database,
error::CompositeError,
logs::{LogAction, LogEntry},
persons::Name,
@@ -26,13 +24,16 @@ use crate::{
const LINE_ADD_RM_SCRIPT: &str = include_str!("line-add-rm.js");
const PREFILL_TIME_SCRIPT: &str = include_str!("prefill-time.js");
pub async fn page(req: Request) -> Result<Response, CompositeError> {
let u = match User::authenticate(req.headers())? {
pub async fn page(
axum::extract::State(state): axum::extract::State<crate::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) => u,
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
};
let conn = database::conn()?;
let names = Name::get_all(&conn)?;
let names = Name::get_all(&mut *conn).await?;
Ok(base(
"Add Quote | Mnemosyne",
@@ -137,30 +138,27 @@ pub struct IncomingQuote {
authors: Vec<Uuid>,
location: String,
time: String,
tz_offset: Option<i32>,
context: String,
}
pub async fn form(
axum::extract::State(state): axum::extract::State<crate::MnemoState>,
headers: HeaderMap,
Form(form): Form<IncomingQuote>,
) -> Result<Response, CompositeError> {
let u = User::authenticate(&headers)?.required()?;
let mut conn = database::conn()?;
let tx = conn.transaction()?;
let mut tx = state.pool.begin().await?;
let u = User::authenticate(&mut *tx, &headers).await?.required()?;
let authors = form
.authors
.into_iter()
.map(|nid| Name::get_by_id(&tx, nid).unwrap());
let mut authors = Vec::new();
for nid in form.authors {
authors.push(Name::get_by_id(&mut *tx, nid).await.unwrap());
}
let lines = form.lines.into_iter().zip(authors).collect();
let offset = form
.tz_offset
.and_then(|mins| chrono::FixedOffset::west_opt(mins * 60))
.unwrap_or_else(|| chrono::FixedOffset::west_opt(0).unwrap());
let timestamp = chrono::NaiveDateTime::parse_from_str(&form.time, "%Y-%m-%dT%H:%M")
.map(|ndt| offset.from_local_datetime(&ndt).unwrap())
.unwrap_or_else(|_| Utc::now().with_timezone(&Warsaw).fixed_offset());
let timestamp = match NaiveDateTime::parse_from_str(&form.time, "%Y-%m-%dT%H:%M") {
Ok(ts) => ts,
Err(_) => return Ok("Time was formatted wrong.".into_response()),
};
let context = match form.context.trim() {
"" => None,
s => Some(s.to_string()),
@@ -170,9 +168,9 @@ pub async fn form(
s => Some(s.to_string()),
};
let q = Quote::create(&tx, lines, timestamp, context, location, u.id, false)?;
LogEntry::new(&tx, u, LogAction::CreateQuote { id: q.id })?;
tx.commit()?;
let q = Quote::create(&mut *tx, lines, timestamp, context, location, u.id, false).await?;
LogEntry::new(&mut *tx, u, LogAction::CreateQuote { id: q.id }).await?;
tx.commit().await?;
Ok(Redirect::to("/dashboard").into_response())
}

View File

@@ -9,23 +9,38 @@ use serde::Deserialize;
use uuid::Uuid;
use crate::{
database::{self},
error::CompositeError,
logs::{LogAction, LogEntry},
tags::{Tag, TagName},
users::{
User,
auth::{AuthError, UserAuthRequired, UserAuthenticate},
auth::{UserAuthRequired, UserAuthenticate},
},
web::{components::nav::nav, icons, pages::base},
};
pub async fn page(req: Request) -> Result<Response, AuthError> {
let u = match User::authenticate(req.headers())? {
pub async fn page(
axum::extract::State(state): axum::extract::State<crate::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) => u,
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
};
let conn = database::conn()?;
let total_tags = Tag::total_count(&mut *conn).await;
let mut tags_with_counts = Vec::new();
let tags = Tag::get_all(&mut *conn).await;
let mut is_tags_ok = false;
let mut is_tags_empty = true;
if let Ok(ts) = tags {
is_tags_ok = true;
is_tags_empty = ts.is_empty();
for tag in ts {
let count = tag.get_tagged_quotes_count(&mut *conn).await;
tags_with_counts.push((tag, count));
}
}
Ok(base(
"Tags | Mnemosyne",
@@ -38,23 +53,23 @@ pub async fn page(req: Request) -> Result<Response, AuthError> {
span class="text-2xl font-semibold font-lora" {"Tags"}
}
p class="text-neutral-500 text-sm font-light" {
@if let Ok(c) = Tag::total_count(&conn) {
@if let Ok(c) = total_tags {
(c) " tags in total."
} @else {
"Could not get total tag count."
}
}
}
@if let Ok(tags) = Tag::get_all(&conn) {
@if is_tags_ok {
div class="max-w-4xl mx-auto px-2 mt-4 flex flex-wrap gap-2" {
@for tag in &tags {
@for (tag, count) in tags_with_counts {
div class="rounded-full px-3 py-1 bg-neutral-200/10 border border-neutral-200/15 flex" {
span class="text-neutral-400 text-sm" {"#"}
span class="text-sm" {(tag.name)}
div class="w-px h-2/3 my-auto mx-2 bg-neutral-200/15" {}
div class="text-xs flex items-center" {
(
if let Ok(i) = tag.get_tagged_quotes_count(&conn) {
if let Ok(i) = &count {
i.to_string()
} else {
"?".to_string()
@@ -63,7 +78,7 @@ pub async fn page(req: Request) -> Result<Response, AuthError> {
// div class="ml-2" {}
// "0" span class="*:size-3 ml-1 text-neutral-400" {(PreEscaped(icons::FILE_IMAGE))}
}
@if let Ok(0) = tag.get_tagged_quotes_count(&conn) {
@if let Ok(0) = count {
form action=(format!("/tags/{}/delete", tag.id)) method="post" class="flex items-center ml-1" {
button type="submit" class="text-neutral-500 hover:text-red-400 text-sm flex items-center justify-center cursor-pointer" title="Delete" {
""
@@ -73,7 +88,7 @@ pub async fn page(req: Request) -> Result<Response, AuthError> {
}
}
}
@if tags.is_empty() {
@if is_tags_empty {
p class="text-center p-2" {"No tags yet. How about making one?"}
}
div class="mx-auto max-w-4xl mt-4 px-2" {
@@ -101,40 +116,41 @@ pub struct TagForm {
tagname: TagName,
}
pub async fn create(
axum::extract::State(state): axum::extract::State<crate::MnemoState>,
headers: HeaderMap,
Form(form): Form<TagForm>,
) -> Result<Response, CompositeError> {
let u = User::authenticate(&headers)?.required()?;
let mut conn = database::conn()?;
let tx = conn.transaction()?;
let mut tx = state.pool.begin().await?;
let u = User::authenticate(&mut *tx, &headers).await?.required()?;
let t = Tag::create(&tx, form.tagname)?;
let t = Tag::create(&mut *tx, form.tagname).await?;
LogEntry::new(
&tx,
&mut *tx,
u,
LogAction::CreateTag {
id: t.id,
name: t.name.to_string(),
},
)?;
tx.commit()?;
)
.await?;
tx.commit().await?;
Ok(Redirect::to("/tags").into_response())
}
pub async fn delete_tag(
axum::extract::State(state): axum::extract::State<crate::MnemoState>,
Path(id): Path<Uuid>,
headers: HeaderMap,
) -> Result<Response, CompositeError> {
let u = User::authenticate(&headers)?.required()?;
let mut conn = database::conn()?;
let tx = conn.transaction()?;
let mut tx = state.pool.begin().await?;
let u = User::authenticate(&mut *tx, &headers).await?.required()?;
let t = Tag::get_by_id(&tx, id)?;
let t = Tag::get_by_id(&mut *tx, id).await?;
let name = t.name.as_str().to_string();
t.delete(&tx)?;
t.delete(&mut *tx).await?;
LogEntry::new(&tx, u, LogAction::DeleteTag { id, name })?;
tx.commit()?;
LogEntry::new(&mut *tx, u, LogAction::DeleteTag { id, name }).await?;
tx.commit().await?;
Ok(Redirect::to("/tags").into_response())
}

View File

@@ -5,12 +5,8 @@ use axum::{
use maud::{PreEscaped, html};
use crate::{
database,
users::{
User,
auth::{AuthError, UserAuthenticate},
permissions::Permission,
},
error::CompositeError,
users::{User, auth::UserAuthenticate, permissions::Permission},
web::{
components::{nav::nav, user_miniprofile::user_miniprofile},
icons,
@@ -21,13 +17,19 @@ use crate::{
pub mod create;
pub mod profile;
pub async fn page(req: Request) -> Result<Response, AuthError> {
let u = match User::authenticate(req.headers())? {
pub async fn page(
axum::extract::State(state): axum::extract::State<crate::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) => u,
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
};
let conn = database::conn()?;
let us = User::get_all(&conn);
let us = User::get_all(&mut *conn).await;
let can_create_users = u
.has_permission(&mut *conn, Permission::ManuallyCreateUsers)
.await;
Ok(base(
"Users | Mnemosyne",
@@ -45,7 +47,7 @@ pub async fn page(req: Request) -> Result<Response, AuthError> {
} @else {
"Could not fetch user count."
}
@if let Ok(true) = u.has_permission(&conn, Permission::ManuallyCreateUsers) {
@if let Ok(true) = can_create_users {
" "
a href="/users/create" class="text-blue-500 hover:text-blue-400 hover:underline" {
"Create a new user"

View File

@@ -8,24 +8,29 @@ use maud::{PreEscaped, html};
use serde::Deserialize;
use crate::{
database::{self},
error::CompositeError,
logs::{LogAction, LogEntry},
users::{
User,
auth::{AuthError, UserAuthRequired, UserAuthenticate},
auth::{UserAuthRequired, UserAuthenticate},
handle::UserHandle,
permissions::Permission,
},
web::{components::nav::nav, icons, pages::base},
};
pub async fn page(req: Request) -> Result<Response, AuthError> {
let u = match User::authenticate(req.headers())? {
pub async fn page(
axum::extract::State(state): axum::extract::State<crate::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) => u,
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
};
let conn = database::conn()?;
let can_create = u
.has_permission(&mut *conn, Permission::ManuallyCreateUsers)
.await;
Ok(base(
"Create User | Mnemosyne",
@@ -38,7 +43,7 @@ pub async fn page(req: Request) -> Result<Response, AuthError> {
span class="text-2xl font-semibold font-lora" {"Create a new user"}
}
}
@if let Ok(true) = u.has_permission(&conn, Permission::ManuallyCreateUsers) {
@if let Ok(true) = can_create {
div class="mx-auto max-w-4xl px-2 mt-4" {
form action="/users/create-form" method="post" class="flex flex-col" {
label for="handle" class="font-light text-neutral-500" {"Handle"}
@@ -68,26 +73,30 @@ pub struct CreateUserWithPasswordForm {
password: String,
}
pub async fn create_user(
axum::extract::State(state): axum::extract::State<crate::MnemoState>,
headers: HeaderMap,
Form(form): Form<CreateUserWithPasswordForm>,
) -> Result<Response, CompositeError> {
let u = User::authenticate(&headers)?.required()?;
let mut conn = database::conn()?;
let tx = conn.transaction()?;
let mut tx = state.pool.begin().await?;
let u = User::authenticate(&mut *tx, &headers).await?.required()?;
if !u.has_permission(&tx, Permission::ManuallyCreateUsers)? {
if !u
.has_permission(&mut *tx, Permission::ManuallyCreateUsers)
.await?
{
return Ok((StatusCode::FORBIDDEN).into_response());
}
let mut nu = User::create(&tx, form.handle)?;
nu.set_password(&tx, Some(&form.password))?;
let mut nu = User::create(&mut *tx, form.handle).await?;
nu.set_password(&mut *tx, Some(&form.password)).await?;
LogEntry::new(
&tx,
&mut *tx,
u,
LogAction::CreateUser {
id: nu.id,
handle: nu.handle.as_str().to_string(),
},
)?;
tx.commit()?;
)
.await?;
tx.commit().await?;
Ok(Redirect::to("/users").into_response())
}

View File

@@ -2,15 +2,11 @@ use axum::{
extract::{Path, Request},
response::{IntoResponse, Redirect, Response},
};
use chrono::{DateTime, Utc};
use maud::{PreEscaped, html};
use uuid::Uuid;
use crate::{
database::{self},
error::CompositeError,
persons::Name,
quotes::{Quote, QuoteLine},
users::{User, UserError, auth::UserAuthenticate},
web::{
components::{nav::nav, quote::quote},
@@ -19,15 +15,18 @@ use crate::{
},
};
pub async fn page(Path(id): Path<Uuid>, req: Request) -> Result<Response, CompositeError> {
let u = match User::authenticate(req.headers())? {
pub async fn page(
axum::extract::State(state): axum::extract::State<crate::MnemoState>,
Path(id): Path<Uuid>,
req: Request,
) -> Result<Response, CompositeError> {
let mut tx = state.pool.begin().await?;
let u = match User::authenticate(&mut *tx, req.headers()).await? {
Some(u) => u,
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
};
let mut conn = database::conn()?;
let tx = conn.transaction()?;
let user = match User::get_by_id(&tx, id) {
let user = match User::get_by_id(&mut *tx, id).await {
Ok(u) => u,
Err(UserError::NoUserWithId(_)) => {
return Ok(base(
@@ -65,7 +64,7 @@ pub async fn page(Path(id): Path<Uuid>, req: Request) -> Result<Response, Compos
.to_uppercase()
.to_string();
let joined_str = user.created_at().map(|d| d.format("%Y-%m-%d").to_string());
let sample_quotes = sample_quotes_for_display();
let sample_quotes = vec![];
Ok(base(
&format!("@{} | Mnemosyne", user.handle),
@@ -199,72 +198,3 @@ pub async fn page(Path(id): Path<Uuid>, req: Request) -> Result<Response, Compos
)
.into_response())
}
fn sample_quotes_for_display() -> Vec<Quote> {
vec![
Quote {
id: Uuid::now_v7(),
public: true,
location: Some(String::from("Poznań")),
context: Some(String::from("Wykład z językoznawstwa")),
created_by: Uuid::max(),
timestamp: DateTime::from(Utc::now()),
lines: vec![
QuoteLine {
id: Uuid::now_v7(),
content: String::from("Nie wiem, czy są tutaj osoby fanowskie zipline-ów?"),
attribution: Name {
id: Uuid::nil(),
created_by: Uuid::max(),
person_id: Uuid::now_v7(),
is_primary: true,
name: String::from("dr. Barbara Konat"),
},
},
QuoteLine {
id: Uuid::now_v7(),
content: String::from("Taka uprząż co robi pziuuum!"),
attribution: Name {
id: Uuid::nil(),
created_by: Uuid::max(),
person_id: Uuid::now_v7(),
is_primary: true,
name: String::from("dr. Barbara Konat"),
},
},
],
},
Quote {
id: Uuid::now_v7(),
public: true,
location: Some(String::from("Discord VC")),
context: Some(String::from("O narysowanej dziewczynie")),
created_by: Uuid::max(),
timestamp: DateTime::from(Utc::now()),
lines: vec![
QuoteLine {
id: Uuid::now_v7(),
content: String::from("Czy tu proporcje są zachowane?"),
attribution: Name {
id: Uuid::now_v7(),
created_by: Uuid::max(),
person_id: Uuid::now_v7(),
is_primary: true,
name: String::from("Adam"),
},
},
QuoteLine {
id: Uuid::now_v7(),
content: String::from("Adam, ona nie ma kolan."),
attribution: Name {
id: Uuid::nil(),
created_by: Uuid::max(),
person_id: Uuid::now_v7(),
is_primary: true,
name: String::from("Mollin"),
},
},
],
},
]
}

View File

@@ -1,6 +1,6 @@
use axum::{
Form,
extract::Request,
extract::{Request, State},
http::HeaderMap,
response::{IntoResponse, Redirect, Response},
};
@@ -8,19 +8,23 @@ use maud::{PreEscaped, html};
use serde::Deserialize;
use crate::{
database::{self},
MnemoState,
error::CompositeError,
logs::{LogAction, LogEntry},
users::{
User,
auth::{AuthError, UserAuthRequired, UserAuthenticate},
auth::{UserAuthRequired, UserAuthenticate},
handle::UserHandle,
},
web::{components::nav::nav, icons, pages::base},
};
pub async fn page(req: Request) -> Result<Response, AuthError> {
let u = match User::authenticate(req.headers())? {
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) => u,
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
};
@@ -77,25 +81,26 @@ pub struct HandleForm {
handle: UserHandle,
}
pub async fn change_handle(
State(state): State<MnemoState>,
headers: HeaderMap,
Form(form): Form<HandleForm>,
) -> Result<Response, CompositeError> {
let mut u = User::authenticate(&headers)?.required()?;
let mut conn = database::conn()?;
let tx = conn.transaction()?;
let mut tx = state.pool.begin().await?;
let mut u = User::authenticate(&mut *tx, &headers).await?.required()?;
let oldhandle = u.handle.as_str().to_string();
u.set_handle(&tx, form.handle)?;
u.set_handle(&mut *tx, form.handle).await?;
LogEntry::new(
&tx,
&mut *tx,
u.clone(),
LogAction::ChangeUserHandle {
id: u.id,
old: oldhandle,
new: u.handle.as_str().to_string(),
},
)?;
tx.commit()?;
)
.await?;
tx.commit().await?;
Ok(Redirect::to("/user-settings").into_response())
}
@@ -104,13 +109,14 @@ pub struct PasswordForm {
password: String,
}
pub async fn change_password(
State(state): State<MnemoState>,
headers: HeaderMap,
Form(form): Form<PasswordForm>,
) -> Result<Response, CompositeError> {
let mut u = User::authenticate(&headers)?.required()?;
let mut conn = database::conn()?;
let tx = conn.transaction()?;
u.set_password(&tx, Some(&form.password))?;
tx.commit()?;
let mut tx = state.pool.begin().await?;
let mut u = User::authenticate(&mut *tx, &headers).await?.required()?;
u.set_password(&mut *tx, Some(&form.password)).await?;
tx.commit().await?;
Ok(Redirect::to("/user-settings").into_response())
}