From ef6a1cbc9e7fe2c1a4d191d221f8cb78c997206b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sof=C3=ADa=20Aritz?= Date: Wed, 16 Oct 2024 20:53:15 +0200 Subject: [PATCH] Various code improvements --- identity-api-rs/src/database/actions.rs | 60 ++- identity-api-rs/src/http/entry.rs | 220 +++++++++++ identity-api-rs/src/http/extractors/auth.rs | 17 +- .../src/http/extractors/database.rs | 30 ++ identity-api-rs/src/http/extractors/mod.rs | 1 + identity-api-rs/src/http/routes/auth.rs | 343 ++++++++---------- identity-api-rs/src/http/routes/entry.rs | 265 ++++++++++++++ identity-api-rs/src/main.rs | 15 +- 8 files changed, 745 insertions(+), 206 deletions(-) create mode 100644 identity-api-rs/src/http/entry.rs create mode 100644 identity-api-rs/src/http/extractors/database.rs create mode 100644 identity-api-rs/src/http/routes/entry.rs diff --git a/identity-api-rs/src/database/actions.rs b/identity-api-rs/src/database/actions.rs index d9ec6d1..88de978 100644 --- a/identity-api-rs/src/database/actions.rs +++ b/identity-api-rs/src/database/actions.rs @@ -16,6 +16,7 @@ use diesel::{ r2d2::{ConnectionManager, PooledConnection}, + result::QueryResult, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection, }; @@ -24,12 +25,12 @@ use super::models::{DateEntry, Entry, FullDatabaseEntry, Heir, LocationEntry, Mu type Connection<'a> = &'a mut PooledConnection>; -pub fn user(user_id: &str, conn: Connection) -> diesel::result::QueryResult { +pub fn user(user_id: &str, conn: Connection) -> QueryResult { use crate::database::schema::users::dsl::users; users.find(user_id).select(User::as_select()).first(conn) } -pub fn user_by_email(email: &str, conn: Connection) -> diesel::result::QueryResult> { +pub fn user_by_email(email: &str, conn: Connection) -> QueryResult> { use crate::database::schema::users::dsl as users; users::users .filter(users::email.eq(email)) @@ -39,7 +40,7 @@ pub fn user_by_email(email: &str, conn: Connection) -> diesel::result::QueryResu .optional() } -pub fn list_heirs(user_id: &str, conn: Connection) -> diesel::result::QueryResult> { +pub fn list_heirs(user_id: &str, conn: Connection) -> QueryResult> { use crate::database::schema::heirs::dsl as heirs; heirs::heirs .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) } +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 { (($model:ident, $conn:ident) from $dsl:ident with id $id:expr) => {{ + use $crate::database::schema::$dsl::dsl::$dsl; let value = $id .as_ref() .map(|id| $dsl.find(id).select($model::as_select()).first($conn)); @@ -63,11 +98,8 @@ macro_rules! retrieve_sub_entry { pub fn entry_recursive( entry_id: &str, conn: Connection, -) -> diesel::result::QueryResult { - use crate::database::schema::date_entries::dsl::date_entries; +) -> QueryResult { 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 .find(entry_id) @@ -83,7 +115,12 @@ pub fn entry_recursive( 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> { +pub fn list_entries_recursive( + user_id: &str, + offset: i64, + limit: i64, + conn: Connection, +) -> QueryResult> { use crate::database::schema::entries::dsl as 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) .select(entries::id) .load::(conn)?; - entry_ids.iter().map(|id| entry_recursive(id, conn)).collect() -} \ No newline at end of file + entry_ids + .iter() + .map(|id| entry_recursive(id, conn)) + .collect() +} diff --git a/identity-api-rs/src/http/entry.rs b/identity-api-rs/src/http/entry.rs new file mode 100644 index 0000000..bbf6a35 --- /dev/null +++ b/identity-api-rs/src/http/entry.rs @@ -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 . + +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 { + 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 }, + #[serde(rename = "date")] + #[serde(rename_all = "camelCase")] + Date { referenced_date: String }, + #[serde(rename = "song")] + Song { + artist: String, + title: String, + links: Vec, + id: Vec, + }, + #[serde(rename = "album")] + Album { + artist: String, + title: String, + links: Vec, + id: Vec, + }, +} + +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, + Option, + Option, + ), + ) -> Option { + 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, + pub title: Option, + pub description: Option, + pub creation_date: String, + pub assets: Vec, + pub feelings: Vec, + pub base: HttpEntryBase, +} + +impl TryFrom for HttpEntry { + type Error = &'static str; + + fn try_from( + (entry, music_entry, location_entry, date_entry): FullDatabaseEntry, + ) -> Result { + 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") + } + } +} diff --git a/identity-api-rs/src/http/extractors/auth.rs b/identity-api-rs/src/http/extractors/auth.rs index 28f5996..70b166c 100644 --- a/identity-api-rs/src/http/extractors/auth.rs +++ b/identity-api-rs/src/http/extractors/auth.rs @@ -23,6 +23,9 @@ use axum::{ http::{header::AUTHORIZATION, request::Parts, StatusCode}, }; use tracing::{error, warn}; + +use super::database::Database; + pub struct ExtractJwtUser(pub JwtUser); #[async_trait] @@ -64,17 +67,13 @@ impl FromRequestParts for ExtractUser { parts: &mut Parts, state: &AppState, ) -> Result { - 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.0.uid, &mut conn) { - Ok(Self(user)) - } else { - error!("JWT user does not exist in database"); - Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")) - } + if let Ok(user) = actions::user(&jwt_user.uid, &mut conn) { + Ok(Self(user)) } else { - error!("failed to obtain pooled connection"); + error!("JWT user does not exist in database"); Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")) } } diff --git a/identity-api-rs/src/http/extractors/database.rs b/identity-api-rs/src/http/extractors/database.rs new file mode 100644 index 0000000..24e0ecf --- /dev/null +++ b/identity-api-rs/src/http/extractors/database.rs @@ -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>); + +#[async_trait] +impl FromRequestParts for Database { + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts( + _parts: &mut Parts, + state: &AppState, + ) -> Result { + 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)) + } +} diff --git a/identity-api-rs/src/http/extractors/mod.rs b/identity-api-rs/src/http/extractors/mod.rs index df68f84..0b82fe5 100644 --- a/identity-api-rs/src/http/extractors/mod.rs +++ b/identity-api-rs/src/http/extractors/mod.rs @@ -15,3 +15,4 @@ // along with this program. If not, see . pub mod auth; +pub mod database; diff --git a/identity-api-rs/src/http/routes/auth.rs b/identity-api-rs/src/http/routes/auth.rs index ec550ec..2350a18 100644 --- a/identity-api-rs/src/http/routes/auth.rs +++ b/identity-api-rs/src/http/routes/auth.rs @@ -17,7 +17,10 @@ use crate::{ auth::{encode_jwt, expiration_time, JwtUser}, database::actions, - http::extractors::auth::{ExtractJwtUser, ExtractUser}, + http::extractors::{ + auth::{ExtractJwtUser, ExtractUser}, + database::Database, + }, AppState, }; use argon2::{ @@ -25,7 +28,6 @@ use argon2::{ Argon2, PasswordHash, PasswordHasher, PasswordVerifier, }; use axum::{ - extract::State, http::StatusCode, routing::{delete, get, post, put}, Json, Router, @@ -72,29 +74,24 @@ struct GenkeyResponse { } async fn genkey( - State(state): State, + Database(mut conn): Database, ExtractJwtUser(user): ExtractJwtUser, ) -> Result, StatusCode> { use crate::database::schema::session_keys::dsl::*; - if let Ok(mut conn) = state.pool.get() { - let session_key = Uuid::new_v4().to_string(); - let result = diesel::insert_into(session_keys) - .values((user_id.eq(&user.uid), key.eq(&session_key))) - .execute(&mut conn); + let session_key = Uuid::new_v4().to_string(); + let result = diesel::insert_into(session_keys) + .values((user_id.eq(&user.uid), key.eq(&session_key))) + .execute(&mut conn); - if result.is_ok() { - Ok(Json(GenkeyResponse { session_key })) - } else { - error!( - "failed to insert into session_keys {}, error: {:?}", - user.uid, - result.err() - ); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } + if result.is_ok() { + Ok(Json(GenkeyResponse { session_key })) } else { - error!("failed to obtain pooled connection"); + error!( + "failed to insert into session_keys {}, error: {:?}", + user.uid, + result.err() + ); Err(StatusCode::INTERNAL_SERVER_ERROR) } } @@ -111,41 +108,35 @@ struct LoginResponse { } async fn login( - State(state): State, + Database(mut conn): Database, Json(req): Json, ) -> Result, StatusCode> { - if let Ok(mut conn) = state.pool.get() { - if let Ok(Some(user)) = actions::user_by_email(&req.email, &mut conn) { - let parsed_hash = - PasswordHash::new(&user.password).expect("invalid argon2 password hash"); - if Argon2::default() - .verify_password(req.password.as_bytes(), &parsed_hash) - .is_err() - { - info!("failed login attempt, invalid password: {}", &req.email); - Err(StatusCode::UNAUTHORIZED) - } else { - info!("valid login attempt: {}", req.email); - match encode_jwt(&JwtUser { - uid: user.id, - email: user.email, - name: user.name, - exp: expiration_time(), - }) { - Ok(token) => Ok(Json(LoginResponse { token })), - Err(err) => { - error!("token couldn't be encoded: {:?}", err); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } + if let Ok(Some(user)) = actions::user_by_email(&req.email, &mut conn) { + let parsed_hash = PasswordHash::new(&user.password).expect("invalid argon2 password hash"); + if Argon2::default() + .verify_password(req.password.as_bytes(), &parsed_hash) + .is_err() + { + info!("failed login attempt, invalid password: {}", &req.email); + Err(StatusCode::UNAUTHORIZED) + } else { + info!("valid login attempt: {}", req.email); + match encode_jwt(&JwtUser { + uid: user.id, + email: user.email, + name: user.name, + exp: expiration_time(), + }) { + Ok(token) => Ok(Json(LoginResponse { token })), + Err(err) => { + error!("token couldn't be encoded: {:?}", err); + Err(StatusCode::INTERNAL_SERVER_ERROR) } } - } else { - info!("failed login attempt, email does not exist: {}", &req.email); - Err(StatusCode::UNAUTHORIZED) } } else { - error!("failed to obtain pooled connection"); - Err(StatusCode::INTERNAL_SERVER_ERROR) + info!("failed login attempt, email does not exist: {}", &req.email); + Err(StatusCode::UNAUTHORIZED) } } @@ -162,94 +153,89 @@ struct RegisterResponse { } async fn register( - State(state): State, + Database(mut conn): Database, Json(req): Json, ) -> Result, StatusCode> { use crate::database::schema::limits::dsl as limits; 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() { - error!( - "failed to retrieve potential existing user from database: {}, error: {:?}", - &req.email, - user.err() - ); - return Err(StatusCode::INTERNAL_SERVER_ERROR); - } + if user.is_err() { + error!( + "failed to retrieve potential existing user from database: {}, error: {:?}", + &req.email, + user.err() + ); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } - if user.is_ok_and(|v| v.is_some()) { - info!("tried to register existing user: {}", &req.email); - return Err(StatusCode::BAD_REQUEST); - } + if user.is_ok_and(|v| v.is_some()) { + info!("tried to register existing user: {}", &req.email); + return Err(StatusCode::BAD_REQUEST); + } - let limit_id = Uuid::new_v4().to_string(); - let result = diesel::insert_into(limits::limits) + let limit_id = Uuid::new_v4().to_string(); + let result = diesel::insert_into(limits::limits) + .values(( + limits::id.eq(&limit_id), + limits::current_asset_count.eq(0), + limits::max_asset_count.eq(10), + )) + .execute(&mut conn); + + if result.is_err() { + error!( + "failed to insert into limits: {}, error: {:?}", + &req.email, + result.err() + ); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + let password_hash = argon2.hash_password(req.password.as_bytes(), &salt); + + if let Ok(password_hash) = password_hash { + let user_id = Uuid::new_v4().to_string(); + let result = diesel::insert_into(users::users) .values(( - limits::id.eq(&limit_id), - limits::current_asset_count.eq(0), - limits::max_asset_count.eq(10), + users::id.eq(&user_id), + users::created_at.eq(Utc::now().naive_utc()), + users::last_connected_at.eq(Utc::now().naive_utc()), + users::email.eq(&req.email), + users::password.eq(password_hash.to_string()), + users::name.eq(&req.name), + users::limits.eq(&limit_id), + // FIXME(sofia): Implement diesel::Expression for List + users::assets.eq("[]"), )) .execute(&mut conn); if result.is_err() { error!( - "failed to insert into limits: {}, error: {:?}", - &req.email, + "failed to insert into users: {}, error: {:?}", + req.email, result.err() ); return Err(StatusCode::INTERNAL_SERVER_ERROR); } - let salt = SaltString::generate(&mut OsRng); - let argon2 = Argon2::default(); - let password_hash = argon2.hash_password(req.password.as_bytes(), &salt); - - if let Ok(password_hash) = password_hash { - let user_id = Uuid::new_v4().to_string(); - let result = diesel::insert_into(users::users) - .values(( - users::id.eq(&user_id), - users::created_at.eq(Utc::now().naive_utc()), - users::last_connected_at.eq(Utc::now().naive_utc()), - users::email.eq(&req.email), - users::password.eq(password_hash.to_string()), - users::name.eq(&req.name), - users::limits.eq(&limit_id), - // FIXME(sofia): Implement diesel::Expression for List - users::assets.eq("[]"), - )) - .execute(&mut conn); - - if result.is_err() { - error!( - "failed to insert into users: {}, error: {:?}", - req.email, - result.err() - ); - return Err(StatusCode::INTERNAL_SERVER_ERROR); + match crate::auth::encode_jwt(&JwtUser { + uid: user_id, + email: req.email, + name: req.name, + exp: expiration_time(), + }) { + Ok(token) => Ok(Json(RegisterResponse { token })), + Err(err) => { + error!("token couldn't be encoded: {:?}", err); + Err(StatusCode::INTERNAL_SERVER_ERROR) } - - match crate::auth::encode_jwt(&JwtUser { - uid: user_id, - email: req.email, - name: req.name, - exp: expiration_time(), - }) { - Ok(token) => Ok(Json(RegisterResponse { token })), - Err(err) => { - error!("token couldn't be encoded: {:?}", err); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } - } - } else { - error!("failed to hash password: {:?}", password_hash.err()); - Err(StatusCode::INTERNAL_SERVER_ERROR) } } else { - error!("failed to obtain pooled connection"); + error!("failed to hash password: {:?}", password_hash.err()); Err(StatusCode::INTERNAL_SERVER_ERROR) } } @@ -276,23 +262,18 @@ impl From for HttpHeir { } async fn list_heirs( - State(state): State, + Database(mut conn): Database, ExtractJwtUser(user): ExtractJwtUser, ) -> Result>, StatusCode> { - if let Ok(mut conn) = state.pool.get() { - let result = actions::list_heirs(&user.uid, &mut conn); - if let Ok(heirs) = result { - Ok(Json(heirs.into_iter().map(HttpHeir::from).collect())) - } else { - error!( - "failed to obtain heirs: {}, error: {:?}", - user.uid, - result.err() - ); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } + let result = actions::list_heirs(&user.uid, &mut conn); + if let Ok(heirs) = result { + Ok(Json(heirs.into_iter().map(HttpHeir::from).collect())) } else { - error!("failed to obtain pooled connection"); + error!( + "failed to obtain heirs: {}, error: {:?}", + user.uid, + result.err() + ); Err(StatusCode::INTERNAL_SERVER_ERROR) } } @@ -306,46 +287,42 @@ struct InsertHeirRequest { } async fn insert_heir( - State(state): State, + Database(mut conn): Database, ExtractJwtUser(user): ExtractJwtUser, Json(req): Json, ) -> Result>, StatusCode> { use crate::database::schema::heirs::dsl::*; - if let Ok(mut conn) = state.pool.get() { - let heir_id = Uuid::new_v4().to_string(); - let result = diesel::insert_into(heirs) - .values(( - id.eq(heir_id), - created_at.eq(Utc::now().naive_utc()), - user_id.eq(&user.uid), - name.eq(req.name), - // Only e-mail is implemented right now - email.eq(req.value), - )) - .execute(&mut conn); - if result.is_err() { - error!( - "failed to insert into heirs: {}, error: {:?}", - user.uid, - result.err() - ); - return Err(StatusCode::INTERNAL_SERVER_ERROR); - } + let heir_id = Uuid::new_v4().to_string(); + let result = diesel::insert_into(heirs) + .values(( + id.eq(heir_id), + created_at.eq(Utc::now().naive_utc()), + user_id.eq(&user.uid), + name.eq(req.name), + // Only e-mail is implemented right now + email.eq(req.value), + )) + .execute(&mut conn); - let result = actions::list_heirs(&user.uid, &mut conn); - if let Ok(heirs_list) = result { - Ok(Json(heirs_list.into_iter().map(HttpHeir::from).collect())) - } else { - error!( - "failed to obtain heirs: {}, error: {:?}", - user.uid, - result.err() - ); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } + if result.is_err() { + error!( + "failed to insert into heirs: {}, error: {:?}", + user.uid, + result.err() + ); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + + let result = actions::list_heirs(&user.uid, &mut conn); + if let Ok(heirs_list) = result { + Ok(Json(heirs_list.into_iter().map(HttpHeir::from).collect())) } else { - error!("failed to obtain pooled connection"); + error!( + "failed to obtain heirs: {}, error: {:?}", + user.uid, + result.err() + ); Err(StatusCode::INTERNAL_SERVER_ERROR) } } @@ -356,36 +333,32 @@ struct DeleteHeirRequest { } async fn delete_heir( - State(state): State, + Database(mut conn): Database, ExtractJwtUser(user): ExtractJwtUser, Json(req): Json, ) -> Result>, StatusCode> { 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); - if result.is_err() { - error!( - "failed to delete from heirs: {}, heir_id: {}, error: {:?}", - user.uid, - req.id, - result.err() - ); - return Err(StatusCode::INTERNAL_SERVER_ERROR); - } - let result = actions::list_heirs(&user.uid, &mut conn); - if let Ok(heirs_list) = result { - Ok(Json(heirs_list.into_iter().map(HttpHeir::from).collect())) - } else { - error!( - "failed to obtain heirs: {}, error: {:?}", - user.uid, - result.err() - ); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } + let result = diesel::delete(heirs.filter(id.eq(&req.id))).execute(&mut conn); + if result.is_err() { + error!( + "failed to delete from heirs: {}, heir_id: {}, error: {:?}", + user.uid, + req.id, + result.err() + ); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + + let result = actions::list_heirs(&user.uid, &mut conn); + if let Ok(heirs_list) = result { + Ok(Json(heirs_list.into_iter().map(HttpHeir::from).collect())) } else { - error!("failed to obtain pooled connection"); + error!( + "failed to obtain heirs: {}, error: {:?}", + user.uid, + result.err() + ); Err(StatusCode::INTERNAL_SERVER_ERROR) } } diff --git a/identity-api-rs/src/http/routes/entry.rs b/identity-api-rs/src/http/routes/entry.rs new file mode 100644 index 0000000..4cfd978 --- /dev/null +++ b/identity-api-rs/src/http/routes/entry.rs @@ -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 . + +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 { + 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, + 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, + ExtractUser(user): ExtractUser, +) -> Result>, 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, +) -> Result<(), StatusCode> { + let mut music_entry: Option = None; + let mut location_entry: Option = None; + let mut date_entry: Option = 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(()) +} diff --git a/identity-api-rs/src/main.rs b/identity-api-rs/src/main.rs index 90aecd4..79d52ec 100644 --- a/identity-api-rs/src/main.rs +++ b/identity-api-rs/src/main.rs @@ -15,7 +15,14 @@ // along with this program. If not, see . 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 diesel::{r2d2::ConnectionManager, SqliteConnection}; @@ -23,7 +30,11 @@ use env::{listen_port, LoadEnvError}; use http::routes::{auth::auth_router, entry::entry_router}; use r2d2::Pool; 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_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};