diff --git a/identity-api-rs/Cargo.toml b/identity-api-rs/Cargo.toml index 3ba8ac9..01fd704 100644 --- a/identity-api-rs/Cargo.toml +++ b/identity-api-rs/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] argon2 = "0.5.3" axum = { version = "0.7", features = ["macros", "tracing"] } -tower-http = { version = "0.6", features = ["trace"] } +tower-http = { version = "0.6", features = ["trace", "cors"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } chrono = { version = "0.4", features = ["serde"] } diff --git a/identity-api-rs/src/auth.rs b/identity-api-rs/src/auth.rs index acaea49..d1fb2f6 100644 --- a/identity-api-rs/src/auth.rs +++ b/identity-api-rs/src/auth.rs @@ -17,8 +17,8 @@ use std::time::SystemTime; use crate::env; -use jsonwebtoken::{TokenData, Header, Validation}; -use serde::{Serialize, Deserialize}; +use jsonwebtoken::{Header, TokenData, Validation}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] pub struct JwtUser { @@ -40,5 +40,6 @@ pub fn expiration_time() -> u64 { SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .expect("time went backwards") - .as_secs() + 30 * 24 * 3600 -} \ No newline at end of file + .as_secs() + + 30 * 24 * 3600 +} diff --git a/identity-api-rs/src/database/actions.rs b/identity-api-rs/src/database/actions.rs index 99eaa7a..d9ec6d1 100644 --- a/identity-api-rs/src/database/actions.rs +++ b/identity-api-rs/src/database/actions.rs @@ -14,19 +14,19 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use diesel::{SqliteConnection, r2d2::{ConnectionManager, PooledConnection}, RunQueryDsl, QueryDsl, SelectableHelper, ExpressionMethods, OptionalExtension}; -use crate::database::models::User; +use diesel::{ + r2d2::{ConnectionManager, PooledConnection}, + ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper, + SqliteConnection, +}; -use super::models::Heir; +use super::models::{DateEntry, Entry, FullDatabaseEntry, Heir, LocationEntry, MusicEntry, User}; type Connection<'a> = &'a mut PooledConnection>; pub fn user(user_id: &str, conn: Connection) -> diesel::result::QueryResult { 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> { @@ -45,4 +45,52 @@ pub fn list_heirs(user_id: &str, conn: Connection) -> diesel::result::QueryResul .filter(heirs::user_id.eq(user_id)) .select(Heir::as_select()) .load(conn) +} + +macro_rules! retrieve_sub_entry { + (($model:ident, $conn:ident) from $dsl:ident with id $id:expr) => {{ + let value = $id + .as_ref() + .map(|id| $dsl.find(id).select($model::as_select()).first($conn)); + + match value { + Some(result) => Some(result?), + None => None, + } + }}; +} + +pub fn entry_recursive( + entry_id: &str, + conn: Connection, +) -> diesel::result::QueryResult { + use crate::database::schema::date_entries::dsl::date_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 + .find(entry_id) + .select(Entry::as_select()) + .first(conn)?; + + let music_entry = + retrieve_sub_entry!((MusicEntry, conn) from music_entries with id entry.music_entry); + let location_entry = retrieve_sub_entry!((LocationEntry, conn) from location_entries with id entry.location_entry); + let date_entry = + retrieve_sub_entry!((DateEntry, conn) from date_entries with id 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> { + use crate::database::schema::entries::dsl as entries; + + let entry_ids = entries::entries + .filter(entries::user_id.eq(user_id)) + .limit(limit) + .offset(offset) + .select(entries::id) + .load::(conn)?; + entry_ids.iter().map(|id| entry_recursive(id, conn)).collect() } \ No newline at end of file diff --git a/identity-api-rs/src/database/list.rs b/identity-api-rs/src/database/list.rs index fc5623e..74ac982 100644 --- a/identity-api-rs/src/database/list.rs +++ b/identity-api-rs/src/database/list.rs @@ -14,11 +14,15 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use std::fmt::Display; use diesel::{ - backend::Backend, deserialize::{FromSql, FromSqlRow}, serialize::ToSql, sql_types::Text, sqlite::Sqlite + backend::Backend, + deserialize::{FromSql, FromSqlRow}, + serialize::ToSql, + sql_types::Text, + sqlite::Sqlite, }; use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::fmt::Display; #[derive(FromSqlRow, Deserialize, Serialize, Debug, Clone)] #[serde(transparent)] @@ -35,7 +39,7 @@ impl List { } } -impl Display for List { +impl Display for List { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&serde_json::to_string(&self).unwrap()) } @@ -50,16 +54,20 @@ where } } -impl FromSql for List -{ +impl From> for List { + fn from(value: Vec) -> Self { + Self(value) + } +} + +impl FromSql for List { fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { let str = >::from_sql(bytes)?; Ok(List::from(str)) } } -impl ToSql for List -{ +impl ToSql for List { fn to_sql<'b>( &'b self, out: &mut diesel::serialize::Output<'b, '_, Sqlite>, diff --git a/identity-api-rs/src/database/mod.rs b/identity-api-rs/src/database/mod.rs index dcc8989..93e378b 100644 --- a/identity-api-rs/src/database/mod.rs +++ b/identity-api-rs/src/database/mod.rs @@ -20,11 +20,10 @@ use diesel::r2d2::Pool; use crate::env; +pub mod actions; +pub mod list; pub mod models; pub mod schema; -pub mod list; -pub mod actions; - pub fn create_connection_pool() -> Result>, r2d2::Error> { let url = env::database_url(); diff --git a/identity-api-rs/src/database/models.rs b/identity-api-rs/src/database/models.rs index edccdf4..f7f2bdb 100644 --- a/identity-api-rs/src/database/models.rs +++ b/identity-api-rs/src/database/models.rs @@ -18,8 +18,15 @@ use chrono::NaiveDateTime; use diesel::prelude::*; use serde::{Deserialize, Serialize}; -use crate::database::schema; use crate::database::list::List; +use crate::database::schema; + +pub type FullDatabaseEntry = ( + Entry, + Option, + Option, + Option, +); #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UniversalId { @@ -29,36 +36,36 @@ pub struct UniversalId { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LocationCoordinates { - latitude: f64, - longitude: f64, + pub latitude: f64, + pub longitude: f64, } -#[derive(Queryable, Selectable, Serialize, Deserialize)] +#[derive(Queryable, Selectable, Insertable, Serialize, Deserialize)] #[diesel(table_name = schema::date_entries)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct DateEntry { - id: String, - referenced_date: NaiveDateTime, + pub id: String, + pub referenced_date: NaiveDateTime, } #[derive(Queryable, Selectable, Serialize, Deserialize)] #[diesel(table_name = schema::entries)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct Entry { - id: String, - user_id: String, - created_at: NaiveDateTime, - feelings: List, - assets: List, - title: Option, - description: Option, - kind: String, - music_entry: Option, - location_entry: Option, - date_entry: Option, + pub id: String, + pub user_id: String, + pub created_at: NaiveDateTime, + pub feelings: List, + pub assets: List, + pub title: Option, + pub description: Option, + pub kind: String, + pub music_entry: Option, + pub location_entry: Option, + pub date_entry: Option, } -#[derive(Queryable, Selectable, Serialize, Deserialize)] +#[derive(Queryable, Selectable, Insertable, Serialize, Deserialize)] #[diesel(table_name = schema::heirs)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct Heir { @@ -69,7 +76,7 @@ pub struct Heir { pub email: Option, } -#[derive(Queryable, Selectable, Serialize, Deserialize)] +#[derive(Queryable, Selectable, Insertable, Serialize, Deserialize)] #[diesel(table_name = schema::limits)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct Limit { @@ -78,19 +85,21 @@ pub struct Limit { max_asset_count: i32, } -#[derive(Queryable, Selectable, Serialize, Deserialize)] +#[derive(Queryable, Selectable, Insertable, Serialize, Deserialize)] #[diesel(table_name = schema::location_entries)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct LocationEntry { - id: String, - location_text: Option, + pub id: String, + pub location_text: Option, /// JSON value: { latitude: number, longitude: number } - location_coordinates: Option, + pub location_coordinates: Option, } impl LocationEntry { pub fn location_coordinates(&self) -> Option { - self.location_coordinates.as_ref().map(|v| serde_json::from_str(v).unwrap()) + self.location_coordinates + .as_ref() + .map(|v| serde_json::from_str(v).unwrap()) } } @@ -98,14 +107,14 @@ impl LocationEntry { #[diesel(table_name = schema::music_entries)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct MusicEntry { - id: String, - artist: String, - title: String, - links: List, - universal_ids: List, + pub id: String, + pub artist: String, + pub title: String, + pub links: List, + pub universal_ids: List, } -#[derive(Queryable, Selectable, Serialize, Deserialize)] +#[derive(Queryable, Selectable, Insertable, Serialize, Deserialize)] #[diesel(table_name = schema::session_keys)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct SessionKey { @@ -113,7 +122,7 @@ pub struct SessionKey { user_id: String, } -#[derive(Queryable, Selectable, Serialize, Deserialize)] +#[derive(Queryable, Selectable, Insertable, Serialize, Deserialize)] #[diesel(table_name = schema::users)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct User { diff --git a/identity-api-rs/src/env.rs b/identity-api-rs/src/env.rs index 1eb7b19..7532e93 100644 --- a/identity-api-rs/src/env.rs +++ b/identity-api-rs/src/env.rs @@ -14,9 +14,9 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use std::{env, str::FromStr}; use std::sync::OnceLock; use std::time::Duration; +use std::{env, str::FromStr}; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey}; @@ -89,14 +89,18 @@ pub fn jwt_secret() -> &'static (EncodingKey, DecodingKey) { let secret = env::var("IDENTITY_API_JWT_SECRET") .expect("environment variables were not loaded correctly"); - (EncodingKey::from_secret(secret.as_bytes()), DecodingKey::from_secret(secret.as_bytes())) + ( + EncodingKey::from_secret(secret.as_bytes()), + DecodingKey::from_secret(secret.as_bytes()), + ) }) } pub fn jwt_alg() -> &'static Algorithm { static IDENTITY_API_JWT_ALG: OnceLock = OnceLock::new(); IDENTITY_API_JWT_ALG.get_or_init(|| { - let algo = env::var("IDENTITY_API_JWT_ALG").expect("environment variables were not loaded correctly"); + let algo = env::var("IDENTITY_API_JWT_ALG") + .expect("environment variables were not loaded correctly"); Algorithm::from_str(&algo).expect("invalid JWT algorithm") }) } diff --git a/identity-api-rs/src/http/extractors/auth.rs b/identity-api-rs/src/http/extractors/auth.rs index c56adaf..28f5996 100644 --- a/identity-api-rs/src/http/extractors/auth.rs +++ b/identity-api-rs/src/http/extractors/auth.rs @@ -14,11 +14,15 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use axum::{async_trait, extract::FromRequestParts, http::{header::AUTHORIZATION, request::Parts, StatusCode}}; -use tracing::{warn, error}; +use crate::auth::JwtUser; use crate::database::{actions, models::User}; use crate::AppState; -use crate::auth::JwtUser; +use axum::{ + async_trait, + extract::FromRequestParts, + http::{header::AUTHORIZATION, request::Parts, StatusCode}, +}; +use tracing::{error, warn}; pub struct ExtractJwtUser(pub JwtUser); #[async_trait] @@ -53,11 +57,13 @@ where pub struct ExtractUser(pub User); #[async_trait] -impl FromRequestParts for ExtractUser -{ +impl FromRequestParts for ExtractUser { type Rejection = (StatusCode, &'static str); - async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result { + async fn from_request_parts( + parts: &mut Parts, + state: &AppState, + ) -> Result { let jwt_user = ExtractJwtUser::from_request_parts(parts, state).await?; if let Ok(mut conn) = state.pool.get() { @@ -72,4 +78,4 @@ impl FromRequestParts for ExtractUser Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")) } } -} \ No newline at end of file +} diff --git a/identity-api-rs/src/http/extractors/mod.rs b/identity-api-rs/src/http/extractors/mod.rs index fedf3fe..df68f84 100644 --- a/identity-api-rs/src/http/extractors/mod.rs +++ b/identity-api-rs/src/http/extractors/mod.rs @@ -14,4 +14,4 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -pub mod auth; \ No newline at end of file +pub mod auth; diff --git a/identity-api-rs/src/http/mod.rs b/identity-api-rs/src/http/mod.rs index 6c29d39..cbafd54 100644 --- a/identity-api-rs/src/http/mod.rs +++ b/identity-api-rs/src/http/mod.rs @@ -14,5 +14,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +mod entry; pub mod extractors; -pub mod routes; \ No newline at end of file +pub mod routes; diff --git a/identity-api-rs/src/http/routes/auth.rs b/identity-api-rs/src/http/routes/auth.rs index 5ac3c28..ec550ec 100644 --- a/identity-api-rs/src/http/routes/auth.rs +++ b/identity-api-rs/src/http/routes/auth.rs @@ -14,14 +14,27 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use argon2::{password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; -use axum::{extract::State, http::StatusCode, routing::{get, post, put, delete}, Json, Router}; -use chrono::{Utc, NaiveDateTime}; -use diesel::{QueryDsl, RunQueryDsl, ExpressionMethods}; +use crate::{ + auth::{encode_jwt, expiration_time, JwtUser}, + database::actions, + http::extractors::auth::{ExtractJwtUser, ExtractUser}, + AppState, +}; +use argon2::{ + password_hash::{rand_core::OsRng, SaltString}, + Argon2, PasswordHash, PasswordHasher, PasswordVerifier, +}; +use axum::{ + extract::State, + http::StatusCode, + routing::{delete, get, post, put}, + Json, Router, +}; +use chrono::{NaiveDateTime, Utc}; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; +use serde::{Deserialize, Serialize}; use tracing::{error, info}; -use serde::{Serialize, Deserialize}; use uuid::Uuid; -use crate::{auth::{encode_jwt, expiration_time, JwtUser}, database::actions, http::extractors::auth::{ExtractJwtUser, ExtractUser}, AppState}; pub fn auth_router() -> Router { Router::new() @@ -58,31 +71,32 @@ struct GenkeyResponse { session_key: String, } -async fn genkey(State(state): State, ExtractJwtUser(user): ExtractJwtUser) -> Result, StatusCode> { +async fn genkey( + State(state): State, + 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), - )) + .values((user_id.eq(&user.uid), key.eq(&session_key))) .execute(&mut conn); if result.is_ok() { - Ok(Json(GenkeyResponse { - session_key, - })) + Ok(Json(GenkeyResponse { session_key })) } else { - error!("failed to insert into session_keys {}, error: {:?}", user.uid, result.err()); + error!( + "failed to insert into session_keys {}, error: {:?}", + user.uid, + result.err() + ); Err(StatusCode::INTERNAL_SERVER_ERROR) } } else { error!("failed to obtain pooled connection"); Err(StatusCode::INTERNAL_SERVER_ERROR) } - } #[derive(Debug, Deserialize)] @@ -96,11 +110,18 @@ struct LoginResponse { token: String, } -async fn login(State(state): State, Json(req): Json) -> Result, StatusCode> { +async fn login( + State(state): State, + 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() { + 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 { @@ -140,15 +161,22 @@ struct RegisterResponse { token: String, } -async fn register(State(state): State, Json(req): Json) -> Result, StatusCode> { - use crate::database::schema::users::dsl as users; +async fn register( + State(state): State, + 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); if user.is_err() { - error!("failed to retrieve potential existing user from database: {}, error: {:?}", &req.email, user.err()); + error!( + "failed to retrieve potential existing user from database: {}, error: {:?}", + &req.email, + user.err() + ); return Err(StatusCode::INTERNAL_SERVER_ERROR); } @@ -167,7 +195,11 @@ async fn register(State(state): State, Json(req): Json, Json(req): Json, Json(req): Json for HttpHeir { // Only e-mail is implemented right now contact_method: "email".into(), name: value.name, - value: value.email.unwrap() + value: value.email.unwrap(), } } } -async fn list_heirs(State(state): State, ExtractJwtUser(user): ExtractJwtUser) -> Result>, StatusCode> { +async fn list_heirs( + State(state): State, + 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()); + error!( + "failed to obtain heirs: {}, error: {:?}", + user.uid, + result.err() + ); Err(StatusCode::INTERNAL_SERVER_ERROR) } } else { @@ -258,12 +300,16 @@ async fn list_heirs(State(state): State, ExtractJwtUser(user): Extract #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct InsertHeirRequest { - contact_method: String, - name: String, - value: String, + contact_method: String, + name: String, + value: String, } -async fn insert_heir(State(state): State, ExtractJwtUser(user): ExtractJwtUser, Json(req): Json) -> Result>, StatusCode> { +async fn insert_heir( + State(state): State, + 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(); @@ -278,37 +324,12 @@ async fn insert_heir(State(state): State, ExtractJwtUser(user): Extrac )) .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 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) - } - } else { - error!("failed to obtain pooled connection"); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } -} - - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct DeleteHeirRequest { - id: String, -} - -async fn delete_heir(State(state): State, 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()); + error!( + "failed to insert into heirs: {}, error: {:?}", + user.uid, + result.err() + ); return Err(StatusCode::INTERNAL_SERVER_ERROR); } @@ -316,11 +337,55 @@ async fn delete_heir(State(state): State, ExtractJwtUser(user): Extrac 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()); + error!( + "failed to obtain heirs: {}, error: {:?}", + user.uid, + result.err() + ); Err(StatusCode::INTERNAL_SERVER_ERROR) } } else { error!("failed to obtain pooled connection"); Err(StatusCode::INTERNAL_SERVER_ERROR) } -} \ No newline at end of file +} + +#[derive(Debug, Deserialize)] +struct DeleteHeirRequest { + id: String, +} + +async fn delete_heir( + State(state): State, + 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) + } + } else { + error!("failed to obtain pooled connection"); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } +} diff --git a/identity-api-rs/src/http/routes/mod.rs b/identity-api-rs/src/http/routes/mod.rs index fedf3fe..71d219c 100644 --- a/identity-api-rs/src/http/routes/mod.rs +++ b/identity-api-rs/src/http/routes/mod.rs @@ -14,4 +14,5 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -pub mod auth; \ No newline at end of file +pub mod auth; +pub mod entry; diff --git a/identity-api-rs/src/main.rs b/identity-api-rs/src/main.rs index 42bb4e8..90aecd4 100644 --- a/identity-api-rs/src/main.rs +++ b/identity-api-rs/src/main.rs @@ -14,21 +14,23 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use axum::{extract::{MatchedPath, Request}, response::Response, routing::get, Router}; +use axum::{ + 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}; use env::{listen_port, LoadEnvError}; -use http::routes::auth::auth_router; +use http::routes::{auth::auth_router, entry::entry_router}; use r2d2::Pool; -use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer}; -use tracing::{info, info_span, warn, error, Span}; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use tokio::time::Duration; +use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer, cors::{Any, CorsLayer}}; +use tracing::{error, info, info_span, warn, Span}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +mod auth; mod database; mod env; mod http; -mod auth; #[derive(Clone)] struct AppState { @@ -59,6 +61,13 @@ async fn main() { .with(tracing_subscriber::fmt::layer()) .init(); + // FIXME(sofia): Add an cors config in env vars + let cors = CorsLayer::new() + .allow_methods(vec![Method::GET, Method::POST, Method::PUT, Method::DELETE]) + .allow_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE]) + .allow_origin(Any) + .allow_credentials(false); + let state = AppState { pool: create_connection_pool().expect("failed to create database connection pool"), }; @@ -66,6 +75,7 @@ async fn main() { let app = Router::new() .route("/", get(landing)) .merge(auth_router()) + .merge(entry_router()) .with_state(state) .layer( TraceLayer::new_for_http() @@ -83,23 +93,18 @@ async fn main() { }) .on_response(|response: &Response, _latency: Duration, _span: &Span| { if response.status().is_client_error() { - warn!( - "client error: {}", - response.status().to_string() - ); + warn!("client error: {}", response.status().to_string()); } else { info!("finished processing request"); } }) .on_failure( |error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| { - error!( - "internal server error: {}", - error.to_string(), - ); + error!("internal server error: {}", error.to_string(),); }, ), - ); + ) + .layer(cors); // FIXME(sofia): Add an env var to change the bind addr let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", listen_port())) diff --git a/identity-api/src/database.ts b/identity-api/src/database.ts index f9ece36..25fd96a 100644 --- a/identity-api/src/database.ts +++ b/identity-api/src/database.ts @@ -215,7 +215,7 @@ export async function startDatabase() { let musicDetails = ( await database.select().from(musicEntries).where(eq(musicEntries.id, entry.musicEntry)) )[0]; - (musicDetails["link"] as any) = fromDBList(musicDetails.links); + (musicDetails["links"] as any) = fromDBList(musicDetails.links); (musicDetails["id"] as any) = fromDBList(musicDetails.universalIDs); musicDetails["links"] = undefined; diff --git a/identity-api/src/routes/entry/index.ts b/identity-api/src/routes/entry/index.ts index 16aec97..cae4d64 100644 --- a/identity-api/src/routes/entry/index.ts +++ b/identity-api/src/routes/entry/index.ts @@ -53,7 +53,7 @@ const PutEntryBody = Type.Object({ kind: Type.String(), artist: Type.String(), title: Type.String(), - link: Type.Array(Type.String()), + links: Type.Array(Type.String()), id: Type.Array( Type.Object({ provider: Type.String(), @@ -111,7 +111,7 @@ export default function registerRoutes(app: AppInterface, auth: AuthInterface, d id: randomUUID(), title: entry.base.title, artist: entry.base.artist, - links: toDBList(entry.base.link), + links: toDBList(entry.base.links), universalIDs: toDBList(entry.base.id), }; } else if (entry.base.kind === "environment" && "location" in entry.base) { diff --git a/identity-web/src/lib/entry.ts b/identity-web/src/lib/entry.ts index 88ca8f5..b038e72 100644 --- a/identity-web/src/lib/entry.ts +++ b/identity-web/src/lib/entry.ts @@ -115,7 +115,7 @@ export type SongEntry = { kind: 'song'; artist: string; title: string; - link: string[]; + links: string[]; id: UniversalID[]; }; @@ -123,7 +123,7 @@ export type AlbumEntry = { kind: 'album'; artist: string; title: string; - link: string[]; + links: string[]; id: UniversalID[]; }; diff --git a/identity-web/src/routes/dashboard/Entries.svelte b/identity-web/src/routes/dashboard/Entries.svelte index e6a5c25..98fb450 100644 --- a/identity-web/src/routes/dashboard/Entries.svelte +++ b/identity-web/src/routes/dashboard/Entries.svelte @@ -121,8 +121,8 @@ >
{#if entry.base.kind === 'song' || entry.base.kind === 'album'} - {#if entry.base.link[0] != null} - + {#if entry.base.links[0] != null} + {entry.base.artist} ‐ {entry.base.title} {:else} @@ -163,8 +163,8 @@
{#if entry.base.kind === 'song' || entry.base.kind === 'album'} - {#if entry.base.link[0] != null} - + {#if entry.base.links[0] != null} + {entry.base.artist} ‐ {entry.base.title} {:else} diff --git a/identity-web/src/routes/entry/new/+page.svelte b/identity-web/src/routes/entry/new/+page.svelte index 680dc4c..e16d982 100644 --- a/identity-web/src/routes/entry/new/+page.svelte +++ b/identity-web/src/routes/entry/new/+page.svelte @@ -38,7 +38,7 @@ kind: values.kind, artist: values.artist, title: values.musicTitle, - link: [values.spotify, values.yt, values.otherProvider].filter( + links: [values.spotify, values.yt, values.otherProvider].filter( (v) => v != null && v.length > 0 ), // FIXME: Infer Universal IDs (Spotify URL, etc)