Various code improvements

This commit is contained in:
Sofía Aritz 2024-10-16 20:53:15 +02:00
parent d99b0344df
commit ef6a1cbc9e
Signed by: sofia
GPG key ID: 90B5116E3542B28F
8 changed files with 745 additions and 206 deletions

View file

@ -16,6 +16,7 @@
use diesel::{ use diesel::{
r2d2::{ConnectionManager, PooledConnection}, r2d2::{ConnectionManager, PooledConnection},
result::QueryResult,
ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper,
SqliteConnection, SqliteConnection,
}; };
@ -24,12 +25,12 @@ use super::models::{DateEntry, Entry, FullDatabaseEntry, Heir, LocationEntry, Mu
type Connection<'a> = &'a mut PooledConnection<ConnectionManager<SqliteConnection>>; type Connection<'a> = &'a mut PooledConnection<ConnectionManager<SqliteConnection>>;
pub fn user(user_id: &str, conn: Connection) -> diesel::result::QueryResult<User> { pub fn user(user_id: &str, conn: Connection) -> QueryResult<User> {
use crate::database::schema::users::dsl::users; use crate::database::schema::users::dsl::users;
users.find(user_id).select(User::as_select()).first(conn) users.find(user_id).select(User::as_select()).first(conn)
} }
pub fn user_by_email(email: &str, conn: Connection) -> diesel::result::QueryResult<Option<User>> { pub fn user_by_email(email: &str, conn: Connection) -> QueryResult<Option<User>> {
use crate::database::schema::users::dsl as users; use crate::database::schema::users::dsl as users;
users::users users::users
.filter(users::email.eq(email)) .filter(users::email.eq(email))
@ -39,7 +40,7 @@ pub fn user_by_email(email: &str, conn: Connection) -> diesel::result::QueryResu
.optional() .optional()
} }
pub fn list_heirs(user_id: &str, conn: Connection) -> diesel::result::QueryResult<Vec<Heir>> { pub fn list_heirs(user_id: &str, conn: Connection) -> QueryResult<Vec<Heir>> {
use crate::database::schema::heirs::dsl as heirs; use crate::database::schema::heirs::dsl as heirs;
heirs::heirs heirs::heirs
.filter(heirs::user_id.eq(user_id)) .filter(heirs::user_id.eq(user_id))
@ -47,8 +48,42 @@ pub fn list_heirs(user_id: &str, conn: Connection) -> diesel::result::QueryResul
.load(conn) .load(conn)
} }
pub fn insert_music_entry(music_entry: &MusicEntry, conn: Connection) -> QueryResult<()> {
use crate::database::schema::music_entries::dsl::*;
diesel::insert_into(music_entries)
.values((
id.eq(&music_entry.id),
artist.eq(&music_entry.artist),
title.eq(&music_entry.title),
links.eq(music_entry.links.to_string()),
universal_ids.eq(music_entry.universal_ids.to_string()),
))
.execute(conn)?;
Ok(())
}
pub fn insert_location_entry(location_entry: &LocationEntry, conn: Connection) -> QueryResult<()> {
use crate::database::schema::location_entries::dsl::*;
diesel::insert_into(location_entries)
.values(location_entry)
.execute(conn)?;
Ok(())
}
pub fn insert_date_entry(date_entry: &DateEntry, conn: Connection) -> QueryResult<()> {
use crate::database::schema::date_entries::dsl::*;
diesel::insert_into(date_entries)
.values(date_entry)
.execute(conn)?;
Ok(())
}
macro_rules! retrieve_sub_entry { macro_rules! retrieve_sub_entry {
(($model:ident, $conn:ident) from $dsl:ident with id $id:expr) => {{ (($model:ident, $conn:ident) from $dsl:ident with id $id:expr) => {{
use $crate::database::schema::$dsl::dsl::$dsl;
let value = $id let value = $id
.as_ref() .as_ref()
.map(|id| $dsl.find(id).select($model::as_select()).first($conn)); .map(|id| $dsl.find(id).select($model::as_select()).first($conn));
@ -63,11 +98,8 @@ macro_rules! retrieve_sub_entry {
pub fn entry_recursive( pub fn entry_recursive(
entry_id: &str, entry_id: &str,
conn: Connection, conn: Connection,
) -> diesel::result::QueryResult<FullDatabaseEntry> { ) -> QueryResult<FullDatabaseEntry> {
use crate::database::schema::date_entries::dsl::date_entries;
use crate::database::schema::entries::dsl::entries; use crate::database::schema::entries::dsl::entries;
use crate::database::schema::location_entries::dsl::location_entries;
use crate::database::schema::music_entries::dsl::music_entries;
let entry: Entry = entries let entry: Entry = entries
.find(entry_id) .find(entry_id)
@ -83,7 +115,12 @@ pub fn entry_recursive(
Ok((entry, music_entry, location_entry, date_entry)) Ok((entry, music_entry, location_entry, date_entry))
} }
pub fn list_entries_recursive(user_id: &str, offset: i64, limit: i64, conn: Connection) -> diesel::result::QueryResult<Vec<FullDatabaseEntry>> { pub fn list_entries_recursive(
user_id: &str,
offset: i64,
limit: i64,
conn: Connection,
) -> QueryResult<Vec<FullDatabaseEntry>> {
use crate::database::schema::entries::dsl as entries; use crate::database::schema::entries::dsl as entries;
let entry_ids = entries::entries let entry_ids = entries::entries
@ -92,5 +129,8 @@ pub fn list_entries_recursive(user_id: &str, offset: i64, limit: i64, conn: Conn
.offset(offset) .offset(offset)
.select(entries::id) .select(entries::id)
.load::<String>(conn)?; .load::<String>(conn)?;
entry_ids.iter().map(|id| entry_recursive(id, conn)).collect() entry_ids
.iter()
.map(|id| entry_recursive(id, conn))
.collect()
} }

View file

@ -0,0 +1,220 @@
// Identity. Store your memories and mental belongings
// Copyright (C) 2024 Sofía Aritz
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use serde::de::Error as DeError;
use serde::{Deserialize, Serialize};
use crate::database::models::{
DateEntry, FullDatabaseEntry, LocationEntry, MusicEntry, UniversalId,
};
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(untagged)]
pub enum HttpEntryFeeling {
Builtin(String),
#[serde(rename_all = "camelCase")]
Custom {
identifier: String,
description: String,
background_color: String,
text_color: String,
},
}
// FIXME(sofia): Improve this impl
impl TryFrom<&str> for HttpEntryFeeling {
type Error = serde_json::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
if value.contains('\"') || value.contains('{') {
let json_value: serde_json::Value = serde_json::from_str(value)?;
let identifier = json_value
.get("identifier")
.and_then(|v| v.as_str())
.ok_or_else(|| serde_json::Error::custom("Missing or invalid 'identifier' field"))?
.to_owned();
let description = json_value
.get("description")
.and_then(|v| v.as_str())
.ok_or_else(|| serde_json::Error::custom("Missing or invalid 'description' field"))?
.to_owned();
let background_color = json_value
.get("background_color")
.and_then(|v| v.as_str())
.ok_or_else(|| {
serde_json::Error::custom("Missing or invalid 'background_color' field")
})?
.to_owned();
let text_color = json_value
.get("text_color")
.and_then(|v| v.as_str())
.ok_or_else(|| serde_json::Error::custom("Missing or invalid 'text_color' field"))?
.to_owned();
Ok(Self::Custom {
identifier,
description,
background_color,
text_color,
})
} else {
Ok(Self::Builtin(value.to_owned()))
}
}
}
#[derive(Debug, Deserialize, Clone, Serialize)]
#[serde(untagged)]
pub enum HttpEntryLocation {
Description(String),
Exact { latitude: f64, longitude: f64 },
}
#[derive(Debug, Deserialize, Clone, Serialize)]
#[serde(tag = "kind")]
pub enum HttpEntryBase {
#[serde(rename = "event")]
Event,
#[serde(rename = "memory")]
Memory,
#[serde(rename = "feeling")]
Feeling,
#[serde(rename = "environment")]
Environment { location: Option<HttpEntryLocation> },
#[serde(rename = "date")]
#[serde(rename_all = "camelCase")]
Date { referenced_date: String },
#[serde(rename = "song")]
Song {
artist: String,
title: String,
links: Vec<String>,
id: Vec<UniversalId>,
},
#[serde(rename = "album")]
Album {
artist: String,
title: String,
links: Vec<String>,
id: Vec<UniversalId>,
},
}
impl HttpEntryBase {
pub fn kind(&self) -> &'static str {
match self {
Self::Event => "event",
Self::Memory => "memory",
Self::Feeling => "feeling",
Self::Environment { .. } => "environment",
Self::Date { .. } => "date",
Self::Song { .. } => "song",
Self::Album { .. } => "album",
}
}
pub fn from_kind(
kind: &str,
(music_entry, location_entry, date_entry): (
Option<MusicEntry>,
Option<LocationEntry>,
Option<DateEntry>,
),
) -> Option<Self> {
match kind {
"event" => Some(Self::Event),
"memory" => Some(Self::Memory),
"feeling" => Some(Self::Feeling),
"environment" => Some(Self::Environment {
location: location_entry.map(|v| {
if let Some(text) = v.location_text {
HttpEntryLocation::Description(text)
} else {
let coords = v.location_coordinates().unwrap();
HttpEntryLocation::Exact {
latitude: coords.latitude,
longitude: coords.longitude,
}
}
}),
}),
"date" => Some(Self::Date {
referenced_date: date_entry.unwrap().referenced_date.to_string(),
}),
"song" => {
let music_entry = music_entry.unwrap();
Some(Self::Song {
artist: music_entry.artist,
title: music_entry.title,
links: music_entry.links.0,
id: music_entry.universal_ids.0,
})
}
"album" => {
let music_entry = music_entry.unwrap();
Some(Self::Album {
artist: music_entry.artist,
title: music_entry.title,
links: music_entry.links.0,
id: music_entry.universal_ids.0,
})
}
_ => None,
}
}
}
#[derive(Debug, Deserialize, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HttpEntry {
/// Only `Some` when built by the server
pub id: Option<String>,
pub title: Option<String>,
pub description: Option<String>,
pub creation_date: String,
pub assets: Vec<String>,
pub feelings: Vec<HttpEntryFeeling>,
pub base: HttpEntryBase,
}
impl TryFrom<FullDatabaseEntry> for HttpEntry {
type Error = &'static str;
fn try_from(
(entry, music_entry, location_entry, date_entry): FullDatabaseEntry,
) -> Result<Self, Self::Error> {
if let Some(base) =
HttpEntryBase::from_kind(&entry.kind, (music_entry, location_entry, date_entry))
{
Ok(Self {
id: Some(entry.id),
title: entry.title,
description: entry.description,
creation_date: entry.created_at.to_string(),
assets: entry.assets.0,
feelings: entry
.feelings
.0
.iter()
.filter_map(|v| v.as_str().try_into().ok())
.collect(),
base,
})
} else {
Err("invalid data stored in the database")
}
}
}

View file

@ -23,6 +23,9 @@ use axum::{
http::{header::AUTHORIZATION, request::Parts, StatusCode}, http::{header::AUTHORIZATION, request::Parts, StatusCode},
}; };
use tracing::{error, warn}; use tracing::{error, warn};
use super::database::Database;
pub struct ExtractJwtUser(pub JwtUser); pub struct ExtractJwtUser(pub JwtUser);
#[async_trait] #[async_trait]
@ -64,18 +67,14 @@ impl FromRequestParts<AppState> for ExtractUser {
parts: &mut Parts, parts: &mut Parts,
state: &AppState, state: &AppState,
) -> Result<Self, Self::Rejection> { ) -> Result<Self, Self::Rejection> {
let jwt_user = ExtractJwtUser::from_request_parts(parts, state).await?; let ExtractJwtUser(jwt_user) = ExtractJwtUser::from_request_parts(parts, state).await?;
let Database(mut conn) = Database::from_request_parts(parts, state).await?;
if let Ok(mut conn) = state.pool.get() { if let Ok(user) = actions::user(&jwt_user.uid, &mut conn) {
if let Ok(user) = actions::user(&jwt_user.0.uid, &mut conn) {
Ok(Self(user)) Ok(Self(user))
} else { } else {
error!("JWT user does not exist in database"); error!("JWT user does not exist in database");
Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")) Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"))
} }
} else {
error!("failed to obtain pooled connection");
Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"))
}
} }
} }

View file

@ -0,0 +1,30 @@
use crate::AppState;
use axum::{
async_trait,
extract::FromRequestParts,
http::{header::AUTHORIZATION, request::Parts, StatusCode},
};
use diesel::{
r2d2::{ConnectionManager, PooledConnection},
SqliteConnection,
};
use tracing::{error, warn};
pub struct Database(pub PooledConnection<ConnectionManager<SqliteConnection>>);
#[async_trait]
impl FromRequestParts<AppState> for Database {
type Rejection = (StatusCode, &'static str);
async fn from_request_parts(
_parts: &mut Parts,
state: &AppState,
) -> Result<Self, Self::Rejection> {
let conn = state.pool.get().map_err(|err| {
error!("failed to obtain pooled connection: {:?}", err);
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
})?;
Ok(Self(conn))
}
}

View file

@ -15,3 +15,4 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod auth; pub mod auth;
pub mod database;

View file

@ -17,7 +17,10 @@
use crate::{ use crate::{
auth::{encode_jwt, expiration_time, JwtUser}, auth::{encode_jwt, expiration_time, JwtUser},
database::actions, database::actions,
http::extractors::auth::{ExtractJwtUser, ExtractUser}, http::extractors::{
auth::{ExtractJwtUser, ExtractUser},
database::Database,
},
AppState, AppState,
}; };
use argon2::{ use argon2::{
@ -25,7 +28,6 @@ use argon2::{
Argon2, PasswordHash, PasswordHasher, PasswordVerifier, Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
}; };
use axum::{ use axum::{
extract::State,
http::StatusCode, http::StatusCode,
routing::{delete, get, post, put}, routing::{delete, get, post, put},
Json, Router, Json, Router,
@ -72,12 +74,11 @@ struct GenkeyResponse {
} }
async fn genkey( async fn genkey(
State(state): State<AppState>, Database(mut conn): Database,
ExtractJwtUser(user): ExtractJwtUser, ExtractJwtUser(user): ExtractJwtUser,
) -> Result<Json<GenkeyResponse>, StatusCode> { ) -> Result<Json<GenkeyResponse>, StatusCode> {
use crate::database::schema::session_keys::dsl::*; use crate::database::schema::session_keys::dsl::*;
if let Ok(mut conn) = state.pool.get() {
let session_key = Uuid::new_v4().to_string(); let session_key = Uuid::new_v4().to_string();
let result = diesel::insert_into(session_keys) let result = diesel::insert_into(session_keys)
.values((user_id.eq(&user.uid), key.eq(&session_key))) .values((user_id.eq(&user.uid), key.eq(&session_key)))
@ -93,10 +94,6 @@ async fn genkey(
); );
Err(StatusCode::INTERNAL_SERVER_ERROR) Err(StatusCode::INTERNAL_SERVER_ERROR)
} }
} else {
error!("failed to obtain pooled connection");
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -111,13 +108,11 @@ struct LoginResponse {
} }
async fn login( async fn login(
State(state): State<AppState>, Database(mut conn): Database,
Json(req): Json<LoginRequest>, Json(req): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, StatusCode> { ) -> Result<Json<LoginResponse>, StatusCode> {
if let Ok(mut conn) = state.pool.get() {
if let Ok(Some(user)) = actions::user_by_email(&req.email, &mut conn) { if let Ok(Some(user)) = actions::user_by_email(&req.email, &mut conn) {
let parsed_hash = let parsed_hash = PasswordHash::new(&user.password).expect("invalid argon2 password hash");
PasswordHash::new(&user.password).expect("invalid argon2 password hash");
if Argon2::default() if Argon2::default()
.verify_password(req.password.as_bytes(), &parsed_hash) .verify_password(req.password.as_bytes(), &parsed_hash)
.is_err() .is_err()
@ -143,10 +138,6 @@ async fn login(
info!("failed login attempt, email does not exist: {}", &req.email); info!("failed login attempt, email does not exist: {}", &req.email);
Err(StatusCode::UNAUTHORIZED) Err(StatusCode::UNAUTHORIZED)
} }
} else {
error!("failed to obtain pooled connection");
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -162,13 +153,12 @@ struct RegisterResponse {
} }
async fn register( async fn register(
State(state): State<AppState>, Database(mut conn): Database,
Json(req): Json<RegisterRequest>, Json(req): Json<RegisterRequest>,
) -> Result<Json<RegisterResponse>, StatusCode> { ) -> Result<Json<RegisterResponse>, StatusCode> {
use crate::database::schema::limits::dsl as limits; use crate::database::schema::limits::dsl as limits;
use crate::database::schema::users::dsl as users; use crate::database::schema::users::dsl as users;
if let Ok(mut conn) = state.pool.get() {
let user = actions::user_by_email(&req.email, &mut conn); let user = actions::user_by_email(&req.email, &mut conn);
if user.is_err() { if user.is_err() {
@ -248,10 +238,6 @@ async fn register(
error!("failed to hash password: {:?}", password_hash.err()); error!("failed to hash password: {:?}", password_hash.err());
Err(StatusCode::INTERNAL_SERVER_ERROR) Err(StatusCode::INTERNAL_SERVER_ERROR)
} }
} else {
error!("failed to obtain pooled connection");
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -276,10 +262,9 @@ impl From<crate::database::models::Heir> for HttpHeir {
} }
async fn list_heirs( async fn list_heirs(
State(state): State<AppState>, Database(mut conn): Database,
ExtractJwtUser(user): ExtractJwtUser, ExtractJwtUser(user): ExtractJwtUser,
) -> Result<Json<Vec<HttpHeir>>, StatusCode> { ) -> Result<Json<Vec<HttpHeir>>, StatusCode> {
if let Ok(mut conn) = state.pool.get() {
let result = actions::list_heirs(&user.uid, &mut conn); let result = actions::list_heirs(&user.uid, &mut conn);
if let Ok(heirs) = result { if let Ok(heirs) = result {
Ok(Json(heirs.into_iter().map(HttpHeir::from).collect())) Ok(Json(heirs.into_iter().map(HttpHeir::from).collect()))
@ -291,10 +276,6 @@ async fn list_heirs(
); );
Err(StatusCode::INTERNAL_SERVER_ERROR) Err(StatusCode::INTERNAL_SERVER_ERROR)
} }
} else {
error!("failed to obtain pooled connection");
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -306,12 +287,12 @@ struct InsertHeirRequest {
} }
async fn insert_heir( async fn insert_heir(
State(state): State<AppState>, Database(mut conn): Database,
ExtractJwtUser(user): ExtractJwtUser, ExtractJwtUser(user): ExtractJwtUser,
Json(req): Json<InsertHeirRequest>, Json(req): Json<InsertHeirRequest>,
) -> Result<Json<Vec<HttpHeir>>, StatusCode> { ) -> Result<Json<Vec<HttpHeir>>, StatusCode> {
use crate::database::schema::heirs::dsl::*; use crate::database::schema::heirs::dsl::*;
if let Ok(mut conn) = state.pool.get() {
let heir_id = Uuid::new_v4().to_string(); let heir_id = Uuid::new_v4().to_string();
let result = diesel::insert_into(heirs) let result = diesel::insert_into(heirs)
.values(( .values((
@ -344,10 +325,6 @@ async fn insert_heir(
); );
Err(StatusCode::INTERNAL_SERVER_ERROR) Err(StatusCode::INTERNAL_SERVER_ERROR)
} }
} else {
error!("failed to obtain pooled connection");
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -356,12 +333,12 @@ struct DeleteHeirRequest {
} }
async fn delete_heir( async fn delete_heir(
State(state): State<AppState>, Database(mut conn): Database,
ExtractJwtUser(user): ExtractJwtUser, ExtractJwtUser(user): ExtractJwtUser,
Json(req): Json<DeleteHeirRequest>, Json(req): Json<DeleteHeirRequest>,
) -> Result<Json<Vec<HttpHeir>>, StatusCode> { ) -> Result<Json<Vec<HttpHeir>>, StatusCode> {
use crate::database::schema::heirs::dsl::*; use crate::database::schema::heirs::dsl::*;
if let Ok(mut conn) = state.pool.get() {
let result = diesel::delete(heirs.filter(id.eq(&req.id))).execute(&mut conn); let result = diesel::delete(heirs.filter(id.eq(&req.id))).execute(&mut conn);
if result.is_err() { if result.is_err() {
error!( error!(
@ -384,8 +361,4 @@ async fn delete_heir(
); );
Err(StatusCode::INTERNAL_SERVER_ERROR) Err(StatusCode::INTERNAL_SERVER_ERROR)
} }
} else {
error!("failed to obtain pooled connection");
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
} }

View file

@ -0,0 +1,265 @@
// Identity. Store your memories and mental belongings
// Copyright (C) 2024 Sofía Aritz
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::{
database::{
actions,
list::List,
models::{DateEntry, LocationEntry, MusicEntry},
},
http::{
entry::*,
extractors::{
auth::{ExtractJwtUser, ExtractUser},
database::Database,
},
},
AppState,
};
use axum::{
extract::{Query, State},
http::StatusCode,
routing::{delete, get, put},
Json, Router,
};
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, Utc};
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use serde::Deserialize;
use serde_json::json;
use tracing::{error, info, warn};
use uuid::Uuid;
pub fn entry_router() -> Router<AppState> {
Router::new()
.route("/entry", delete(delete_entry))
.route("/entry", put(insert_entry))
.route("/entry/list", get(list_entries))
}
#[derive(Debug, Deserialize)]
struct DeleteEntryQuery {
entry_id: String,
}
// FIXME(sofia): Error on non existent entry_id
async fn delete_entry(
Database(mut conn): Database,
Query(query): Query<DeleteEntryQuery>,
ExtractJwtUser(user): ExtractJwtUser,
) -> Result<(), StatusCode> {
use crate::database::schema::entries::dsl::*;
if let Err(err) = diesel::delete(entries.filter(id.eq(&query.entry_id))).execute(&mut conn) {
error!(
"failed to delete from heirs: {}, entry_id: {}, error: {:?}",
user.uid, query.entry_id, err
);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
info!("deleted entry {}", query.entry_id);
Ok(())
}
#[derive(Debug, Deserialize)]
struct ListEntriesQuery {
offset: i64,
limit: i64,
}
async fn list_entries(
Database(mut conn): Database,
Query(query): Query<ListEntriesQuery>,
ExtractUser(user): ExtractUser,
) -> Result<Json<Vec<HttpEntry>>, StatusCode> {
let result = actions::list_entries_recursive(&user.id, query.offset, query.limit, &mut conn);
if let Ok(entries) = result {
Ok(Json(
entries
.into_iter()
.filter_map(|v| HttpEntry::try_from(v).ok())
.collect(),
))
} else {
error!("failed to obtain entries {}: {:?}", user.id, result.err());
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
#[derive(Debug, Deserialize)]
struct InsertEntryBody {
entry: HttpEntry,
}
async fn insert_entry(
Database(mut conn): Database,
ExtractUser(user): ExtractUser,
Json(entry): Json<InsertEntryBody>,
) -> Result<(), StatusCode> {
let mut music_entry: Option<MusicEntry> = None;
let mut location_entry: Option<LocationEntry> = None;
let mut date_entry: Option<DateEntry> = None;
let entry = entry.entry;
match entry.base {
HttpEntryBase::Album { ref artist, ref title, ref links, ref id }
| HttpEntryBase::Song { ref artist, ref title, ref links, ref id } => {
music_entry = Some(MusicEntry {
id: Uuid::new_v4().to_string(),
// FIXME(sofia): These clones seems unnecesary
title: title.to_owned(),
links: links.clone().into(),
artist: artist.clone(),
universal_ids: id.clone().into(),
})
},
HttpEntryBase::Environment { ref location } => {
if entry.title.as_ref().is_none_or(|v| v.is_empty()) {
warn!(
"no title in request for inserting environment entry: {}",
user.id
);
return Err(StatusCode::BAD_REQUEST);
}
if let Some(location) = location {
match location {
HttpEntryLocation::Description(description) => {
location_entry = Some(LocationEntry {
id: Uuid::new_v4().to_string(),
// FIXME(sofia): This clone seems unnecesary
location_text: Some(description.clone()),
location_coordinates: None,
})
}
HttpEntryLocation::Exact {
latitude,
longitude,
} => {
location_entry = Some(LocationEntry {
id: Uuid::new_v4().to_string(),
location_text: None,
location_coordinates: Some(
json!({
"latitude": latitude,
"longitude": longitude,
})
.to_string(),
),
})
}
}
}
}
HttpEntryBase::Date {
ref referenced_date,
} => {
let naive_date = NaiveDate::parse_from_str(referenced_date, "%Y-%m-%d");
if let Err(err) = naive_date {
warn!(
"invalid date in request for inserting entry: {}, err: {err:?}",
user.id
);
return Err(StatusCode::BAD_REQUEST);
}
date_entry = Some(DateEntry {
id: Uuid::new_v4().to_string(),
referenced_date: NaiveDateTime::new(
naive_date.unwrap(),
NaiveTime::from_hms_milli_opt(0, 0, 0, 0).unwrap(),
),
});
}
HttpEntryBase::Event => {
if entry.description.as_ref().is_none_or(|v| v.is_empty()) {
warn!(
"no description or title in request for inserting event entry: {}",
user.id
);
return Err(StatusCode::BAD_REQUEST);
}
}
HttpEntryBase::Memory => {
if entry.description.as_ref().is_none_or(|v| v.is_empty())
|| entry.title.as_ref().is_none_or(|v| v.is_empty())
{
warn!(
"no description or title in request for inserting memory entry: {}",
user.id
);
return Err(StatusCode::BAD_REQUEST);
}
}
HttpEntryBase::Feeling => {
if entry.feelings.is_empty() {
warn!(
"no feelings in request for inserting feeling entry: {}",
user.id
);
return Err(StatusCode::BAD_REQUEST);
}
}
}
let music_entry_id = music_entry.as_ref().map(|v| v.id.clone());
music_entry.map(|music_entry| actions::insert_music_entry(&music_entry, &mut conn).map_err(|err| {
error!("failed to insert into music_entries: {}, error: {err:?}",user.id);
StatusCode::INTERNAL_SERVER_ERROR
})).transpose()?;
let location_entry_id = location_entry.as_ref().map(|v| v.id.clone());
location_entry.map(|location_entry| actions::insert_location_entry(&location_entry, &mut conn).map_err(|err| {
error!("failed to insert into location_entries: {}, error: {err:?}",user.id);
StatusCode::INTERNAL_SERVER_ERROR
})).transpose()?;
let date_entry_id = date_entry.as_ref().map(|v| v.id.clone());
date_entry.map(|date_entry| actions::insert_date_entry(&date_entry, &mut conn).map_err(|err| {
error!("failed to insert into date_entries: {}, error: {err:?}",user.id);
StatusCode::INTERNAL_SERVER_ERROR
})).transpose()?;
{
use crate::database::schema::entries::dsl as entries;
let result = diesel::insert_into(entries::entries)
.values((
entries::id.eq(Uuid::new_v4().to_string()),
entries::user_id.eq(&user.id),
entries::created_at.eq(Utc::now().naive_utc()),
entries::feelings.eq(List::from(entry.feelings).to_string()),
// FIXME(sofia): Check that the assets exists
entries::assets.eq(List::from(entry.assets).to_string()),
entries::title.eq(&entry.title),
entries::description.eq(&entry.description),
entries::kind.eq(&entry.base.kind()),
entries::date_entry.eq(date_entry_id),
entries::music_entry.eq(music_entry_id),
entries::location_entry.eq(location_entry_id),
))
.execute(&mut conn);
if let Err(err) = result {
error!(
"failed to insert into entries: {}, error: {:?}",
user.id, err
);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
}
Ok(())
}

View file

@ -15,7 +15,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use axum::{ use axum::{
extract::{MatchedPath, Request}, http::{header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}, Method}, response::Response, routing::get, Router extract::{MatchedPath, Request},
http::{
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
Method,
},
response::Response,
routing::get,
Router,
}; };
use database::create_connection_pool; use database::create_connection_pool;
use diesel::{r2d2::ConnectionManager, SqliteConnection}; use diesel::{r2d2::ConnectionManager, SqliteConnection};
@ -23,7 +30,11 @@ use env::{listen_port, LoadEnvError};
use http::routes::{auth::auth_router, entry::entry_router}; use http::routes::{auth::auth_router, entry::entry_router};
use r2d2::Pool; use r2d2::Pool;
use tokio::time::Duration; use tokio::time::Duration;
use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer, cors::{Any, CorsLayer}}; use tower_http::{
classify::ServerErrorsFailureClass,
cors::{Any, CorsLayer},
trace::TraceLayer,
};
use tracing::{error, info, info_span, warn, Span}; use tracing::{error, info, info_span, warn, Span};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};