Initial impl of entry endpoints

This commit is contained in:
Sofía Aritz 2024-10-16 18:37:31 +02:00
parent e48f74b970
commit d99b0344df
Signed by: sofia
GPG key ID: 90B5116E3542B28F
18 changed files with 303 additions and 156 deletions

View file

@ -6,7 +6,7 @@ edition = "2021"
[dependencies] [dependencies]
argon2 = "0.5.3" argon2 = "0.5.3"
axum = { version = "0.7", features = ["macros", "tracing"] } 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 = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }

View file

@ -17,8 +17,8 @@
use std::time::SystemTime; use std::time::SystemTime;
use crate::env; use crate::env;
use jsonwebtoken::{TokenData, Header, Validation}; use jsonwebtoken::{Header, TokenData, Validation};
use serde::{Serialize, Deserialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct JwtUser { pub struct JwtUser {
@ -40,5 +40,6 @@ pub fn expiration_time() -> u64 {
SystemTime::now() SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
.expect("time went backwards") .expect("time went backwards")
.as_secs() + 30 * 24 * 3600 .as_secs()
+ 30 * 24 * 3600
} }

View file

@ -14,19 +14,19 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use diesel::{SqliteConnection, r2d2::{ConnectionManager, PooledConnection}, RunQueryDsl, QueryDsl, SelectableHelper, ExpressionMethods, OptionalExtension}; use diesel::{
use crate::database::models::User; 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<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) -> diesel::result::QueryResult<User> {
use crate::database::schema::users::dsl::users; use crate::database::schema::users::dsl::users;
users users.find(user_id).select(User::as_select()).first(conn)
.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) -> diesel::result::QueryResult<Option<User>> {
@ -46,3 +46,51 @@ pub fn list_heirs(user_id: &str, conn: Connection) -> diesel::result::QueryResul
.select(Heir::as_select()) .select(Heir::as_select())
.load(conn) .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<FullDatabaseEntry> {
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<Vec<FullDatabaseEntry>> {
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::<String>(conn)?;
entry_ids.iter().map(|id| entry_recursive(id, conn)).collect()
}

View file

@ -14,11 +14,15 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fmt::Display;
use diesel::{ 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 serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::fmt::Display;
#[derive(FromSqlRow, Deserialize, Serialize, Debug, Clone)] #[derive(FromSqlRow, Deserialize, Serialize, Debug, Clone)]
#[serde(transparent)] #[serde(transparent)]
@ -50,16 +54,20 @@ where
} }
} }
impl<V: std::fmt::Debug + std::clone::Clone + DeserializeOwned> FromSql<Text, Sqlite> for List<V> impl<A: std::fmt::Debug + std::clone::Clone> From<Vec<A>> for List<A> {
{ fn from(value: Vec<A>) -> Self {
Self(value)
}
}
impl<V: std::fmt::Debug + std::clone::Clone + DeserializeOwned> FromSql<Text, Sqlite> for List<V> {
fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> { fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> {
let str = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?; let str = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
Ok(List::from(str)) Ok(List::from(str))
} }
} }
impl<V: std::fmt::Debug + std::clone::Clone + Serialize> ToSql<Text, Sqlite> for List<V> impl<V: std::fmt::Debug + std::clone::Clone + Serialize> ToSql<Text, Sqlite> for List<V> {
{
fn to_sql<'b>( fn to_sql<'b>(
&'b self, &'b self,
out: &mut diesel::serialize::Output<'b, '_, Sqlite>, out: &mut diesel::serialize::Output<'b, '_, Sqlite>,

View file

@ -20,11 +20,10 @@ use diesel::r2d2::Pool;
use crate::env; use crate::env;
pub mod actions;
pub mod list;
pub mod models; pub mod models;
pub mod schema; pub mod schema;
pub mod list;
pub mod actions;
pub fn create_connection_pool() -> Result<Pool<ConnectionManager<SqliteConnection>>, r2d2::Error> { pub fn create_connection_pool() -> Result<Pool<ConnectionManager<SqliteConnection>>, r2d2::Error> {
let url = env::database_url(); let url = env::database_url();

View file

@ -18,8 +18,15 @@ use chrono::NaiveDateTime;
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::database::schema;
use crate::database::list::List; use crate::database::list::List;
use crate::database::schema;
pub type FullDatabaseEntry = (
Entry,
Option<MusicEntry>,
Option<LocationEntry>,
Option<DateEntry>,
);
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UniversalId { pub struct UniversalId {
@ -29,36 +36,36 @@ pub struct UniversalId {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocationCoordinates { pub struct LocationCoordinates {
latitude: f64, pub latitude: f64,
longitude: f64, pub longitude: f64,
} }
#[derive(Queryable, Selectable, Serialize, Deserialize)] #[derive(Queryable, Selectable, Insertable, Serialize, Deserialize)]
#[diesel(table_name = schema::date_entries)] #[diesel(table_name = schema::date_entries)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct DateEntry { pub struct DateEntry {
id: String, pub id: String,
referenced_date: NaiveDateTime, pub referenced_date: NaiveDateTime,
} }
#[derive(Queryable, Selectable, Serialize, Deserialize)] #[derive(Queryable, Selectable, Serialize, Deserialize)]
#[diesel(table_name = schema::entries)] #[diesel(table_name = schema::entries)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Entry { pub struct Entry {
id: String, pub id: String,
user_id: String, pub user_id: String,
created_at: NaiveDateTime, pub created_at: NaiveDateTime,
feelings: List<String>, pub feelings: List<String>,
assets: List<String>, pub assets: List<String>,
title: Option<String>, pub title: Option<String>,
description: Option<String>, pub description: Option<String>,
kind: String, pub kind: String,
music_entry: Option<String>, pub music_entry: Option<String>,
location_entry: Option<String>, pub location_entry: Option<String>,
date_entry: Option<String>, pub date_entry: Option<String>,
} }
#[derive(Queryable, Selectable, Serialize, Deserialize)] #[derive(Queryable, Selectable, Insertable, Serialize, Deserialize)]
#[diesel(table_name = schema::heirs)] #[diesel(table_name = schema::heirs)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Heir { pub struct Heir {
@ -69,7 +76,7 @@ pub struct Heir {
pub email: Option<String>, pub email: Option<String>,
} }
#[derive(Queryable, Selectable, Serialize, Deserialize)] #[derive(Queryable, Selectable, Insertable, Serialize, Deserialize)]
#[diesel(table_name = schema::limits)] #[diesel(table_name = schema::limits)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Limit { pub struct Limit {
@ -78,19 +85,21 @@ pub struct Limit {
max_asset_count: i32, max_asset_count: i32,
} }
#[derive(Queryable, Selectable, Serialize, Deserialize)] #[derive(Queryable, Selectable, Insertable, Serialize, Deserialize)]
#[diesel(table_name = schema::location_entries)] #[diesel(table_name = schema::location_entries)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct LocationEntry { pub struct LocationEntry {
id: String, pub id: String,
location_text: Option<String>, pub location_text: Option<String>,
/// JSON value: { latitude: number, longitude: number } /// JSON value: { latitude: number, longitude: number }
location_coordinates: Option<String>, pub location_coordinates: Option<String>,
} }
impl LocationEntry { impl LocationEntry {
pub fn location_coordinates(&self) -> Option<LocationCoordinates> { pub fn location_coordinates(&self) -> Option<LocationCoordinates> {
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(table_name = schema::music_entries)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct MusicEntry { pub struct MusicEntry {
id: String, pub id: String,
artist: String, pub artist: String,
title: String, pub title: String,
links: List<String>, pub links: List<String>,
universal_ids: List<UniversalId>, pub universal_ids: List<UniversalId>,
} }
#[derive(Queryable, Selectable, Serialize, Deserialize)] #[derive(Queryable, Selectable, Insertable, Serialize, Deserialize)]
#[diesel(table_name = schema::session_keys)] #[diesel(table_name = schema::session_keys)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct SessionKey { pub struct SessionKey {
@ -113,7 +122,7 @@ pub struct SessionKey {
user_id: String, user_id: String,
} }
#[derive(Queryable, Selectable, Serialize, Deserialize)] #[derive(Queryable, Selectable, Insertable, Serialize, Deserialize)]
#[diesel(table_name = schema::users)] #[diesel(table_name = schema::users)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct User { pub struct User {

View file

@ -14,9 +14,9 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::{env, str::FromStr};
use std::sync::OnceLock; use std::sync::OnceLock;
use std::time::Duration; use std::time::Duration;
use std::{env, str::FromStr};
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey}; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey};
@ -89,14 +89,18 @@ pub fn jwt_secret() -> &'static (EncodingKey, DecodingKey) {
let secret = env::var("IDENTITY_API_JWT_SECRET") let secret = env::var("IDENTITY_API_JWT_SECRET")
.expect("environment variables were not loaded correctly"); .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 { pub fn jwt_alg() -> &'static Algorithm {
static IDENTITY_API_JWT_ALG: OnceLock<Algorithm> = OnceLock::new(); static IDENTITY_API_JWT_ALG: OnceLock<Algorithm> = OnceLock::new();
IDENTITY_API_JWT_ALG.get_or_init(|| { 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") Algorithm::from_str(&algo).expect("invalid JWT algorithm")
}) })
} }

View file

@ -14,11 +14,15 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use axum::{async_trait, extract::FromRequestParts, http::{header::AUTHORIZATION, request::Parts, StatusCode}}; use crate::auth::JwtUser;
use tracing::{warn, error};
use crate::database::{actions, models::User}; use crate::database::{actions, models::User};
use crate::AppState; 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); pub struct ExtractJwtUser(pub JwtUser);
#[async_trait] #[async_trait]
@ -53,11 +57,13 @@ where
pub struct ExtractUser(pub User); pub struct ExtractUser(pub User);
#[async_trait] #[async_trait]
impl FromRequestParts<AppState> for ExtractUser impl FromRequestParts<AppState> for ExtractUser {
{
type Rejection = (StatusCode, &'static str); type Rejection = (StatusCode, &'static str);
async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> { async fn from_request_parts(
parts: &mut Parts,
state: &AppState,
) -> Result<Self, Self::Rejection> {
let jwt_user = ExtractJwtUser::from_request_parts(parts, state).await?; let jwt_user = ExtractJwtUser::from_request_parts(parts, state).await?;
if let Ok(mut conn) = state.pool.get() { if let Ok(mut conn) = state.pool.get() {

View file

@ -14,5 +14,6 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
mod entry;
pub mod extractors; pub mod extractors;
pub mod routes; pub mod routes;

View file

@ -14,14 +14,27 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use argon2::{password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; use crate::{
use axum::{extract::State, http::StatusCode, routing::{get, post, put, delete}, Json, Router}; auth::{encode_jwt, expiration_time, JwtUser},
use chrono::{Utc, NaiveDateTime}; database::actions,
use diesel::{QueryDsl, RunQueryDsl, ExpressionMethods}; 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 tracing::{error, info};
use serde::{Serialize, Deserialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{auth::{encode_jwt, expiration_time, JwtUser}, database::actions, http::extractors::auth::{ExtractJwtUser, ExtractUser}, AppState};
pub fn auth_router() -> Router<AppState> { pub fn auth_router() -> Router<AppState> {
Router::new() Router::new()
@ -58,31 +71,32 @@ struct GenkeyResponse {
session_key: String, session_key: String,
} }
async fn genkey(State(state): State<AppState>, ExtractJwtUser(user): ExtractJwtUser) -> Result<Json<GenkeyResponse>, StatusCode> { async fn genkey(
State(state): State<AppState>,
ExtractJwtUser(user): ExtractJwtUser,
) -> 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() { 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(( .values((user_id.eq(&user.uid), key.eq(&session_key)))
user_id.eq(&user.uid),
key.eq(&session_key),
))
.execute(&mut conn); .execute(&mut conn);
if result.is_ok() { if result.is_ok() {
Ok(Json(GenkeyResponse { Ok(Json(GenkeyResponse { session_key }))
session_key,
}))
} else { } 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) Err(StatusCode::INTERNAL_SERVER_ERROR)
} }
} else { } else {
error!("failed to obtain pooled connection"); error!("failed to obtain pooled connection");
Err(StatusCode::INTERNAL_SERVER_ERROR) Err(StatusCode::INTERNAL_SERVER_ERROR)
} }
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -96,11 +110,18 @@ struct LoginResponse {
token: String, token: String,
} }
async fn login(State(state): State<AppState>, Json(req): Json<LoginRequest>) -> Result<Json<LoginResponse>, StatusCode> { async fn login(
State(state): State<AppState>,
Json(req): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, StatusCode> {
if let Ok(mut conn) = state.pool.get() { 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 = PasswordHash::new(&user.password).expect("invalid argon2 password hash"); let parsed_hash =
if Argon2::default().verify_password(req.password.as_bytes(), &parsed_hash).is_err() { 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); info!("failed login attempt, invalid password: {}", &req.email);
Err(StatusCode::UNAUTHORIZED) Err(StatusCode::UNAUTHORIZED)
} else { } else {
@ -140,15 +161,22 @@ struct RegisterResponse {
token: String, token: String,
} }
async fn register(State(state): State<AppState>, Json(req): Json<RegisterRequest>) -> Result<Json<RegisterResponse>, StatusCode> { async fn register(
use crate::database::schema::users::dsl as users; State(state): State<AppState>,
Json(req): Json<RegisterRequest>,
) -> 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;
if let Ok(mut conn) = state.pool.get() { 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() {
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); return Err(StatusCode::INTERNAL_SERVER_ERROR);
} }
@ -167,7 +195,11 @@ async fn register(State(state): State<AppState>, Json(req): Json<RegisterRequest
.execute(&mut conn); .execute(&mut conn);
if result.is_err() { if result.is_err() {
error!("failed to insert into limits: {}, error: {:?}", &req.email, result.err()); error!(
"failed to insert into limits: {}, error: {:?}",
&req.email,
result.err()
);
return Err(StatusCode::INTERNAL_SERVER_ERROR); return Err(StatusCode::INTERNAL_SERVER_ERROR);
} }
@ -186,13 +218,17 @@ async fn register(State(state): State<AppState>, Json(req): Json<RegisterRequest
users::password.eq(password_hash.to_string()), users::password.eq(password_hash.to_string()),
users::name.eq(&req.name), users::name.eq(&req.name),
users::limits.eq(&limit_id), users::limits.eq(&limit_id),
// TODO: Implement diesel::Expression for List // FIXME(sofia): Implement diesel::Expression for List
users::assets.eq("[]"), users::assets.eq("[]"),
)) ))
.execute(&mut conn); .execute(&mut conn);
if result.is_err() { if result.is_err() {
error!("failed to insert into users: {}, error: {:?}", req.email, result.err()); error!(
"failed to insert into users: {}, error: {:?}",
req.email,
result.err()
);
return Err(StatusCode::INTERNAL_SERVER_ERROR); return Err(StatusCode::INTERNAL_SERVER_ERROR);
} }
@ -212,7 +248,6 @@ async fn register(State(state): State<AppState>, Json(req): Json<RegisterRequest
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 { } else {
error!("failed to obtain pooled connection"); error!("failed to obtain pooled connection");
Err(StatusCode::INTERNAL_SERVER_ERROR) Err(StatusCode::INTERNAL_SERVER_ERROR)
@ -235,18 +270,25 @@ impl From<crate::database::models::Heir> for HttpHeir {
// Only e-mail is implemented right now // Only e-mail is implemented right now
contact_method: "email".into(), contact_method: "email".into(),
name: value.name, name: value.name,
value: value.email.unwrap() value: value.email.unwrap(),
} }
} }
} }
async fn list_heirs(State(state): State<AppState>, ExtractJwtUser(user): ExtractJwtUser) -> Result<Json<Vec<HttpHeir>>, StatusCode> { async fn list_heirs(
State(state): State<AppState>,
ExtractJwtUser(user): ExtractJwtUser,
) -> Result<Json<Vec<HttpHeir>>, StatusCode> {
if let Ok(mut conn) = state.pool.get() { 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()))
} else { } 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) Err(StatusCode::INTERNAL_SERVER_ERROR)
} }
} else { } else {
@ -258,12 +300,16 @@ async fn list_heirs(State(state): State<AppState>, ExtractJwtUser(user): Extract
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct InsertHeirRequest { struct InsertHeirRequest {
contact_method: String, contact_method: String,
name: String, name: String,
value: String, value: String,
} }
async fn insert_heir(State(state): State<AppState>, ExtractJwtUser(user): ExtractJwtUser, Json(req): Json<InsertHeirRequest>) -> Result<Json<Vec<HttpHeir>>, StatusCode> { async fn insert_heir(
State(state): State<AppState>,
ExtractJwtUser(user): ExtractJwtUser,
Json(req): Json<InsertHeirRequest>,
) -> 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() { if let Ok(mut conn) = state.pool.get() {
let heir_id = Uuid::new_v4().to_string(); let heir_id = Uuid::new_v4().to_string();
@ -278,37 +324,12 @@ async fn insert_heir(State(state): State<AppState>, ExtractJwtUser(user): Extrac
)) ))
.execute(&mut conn); .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<AppState>, ExtractJwtUser(user): ExtractJwtUser, Json(req): Json<DeleteHeirRequest>) -> Result<Json<Vec<HttpHeir>>, 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() { 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); return Err(StatusCode::INTERNAL_SERVER_ERROR);
} }
@ -316,7 +337,51 @@ async fn delete_heir(State(state): State<AppState>, ExtractJwtUser(user): Extrac
if let Ok(heirs_list) = result { if let Ok(heirs_list) = result {
Ok(Json(heirs_list.into_iter().map(HttpHeir::from).collect())) Ok(Json(heirs_list.into_iter().map(HttpHeir::from).collect()))
} else { } 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)
}
}
#[derive(Debug, Deserialize)]
struct DeleteHeirRequest {
id: String,
}
async fn delete_heir(
State(state): State<AppState>,
ExtractJwtUser(user): ExtractJwtUser,
Json(req): Json<DeleteHeirRequest>,
) -> Result<Json<Vec<HttpHeir>>, 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) Err(StatusCode::INTERNAL_SERVER_ERROR)
} }
} else { } else {

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 entry;

View file

@ -14,21 +14,23 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
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 database::create_connection_pool;
use diesel::{r2d2::ConnectionManager, SqliteConnection}; use diesel::{r2d2::ConnectionManager, SqliteConnection};
use env::{listen_port, LoadEnvError}; use env::{listen_port, LoadEnvError};
use http::routes::auth::auth_router; use http::routes::{auth::auth_router, entry::entry_router};
use r2d2::Pool; 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 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 database;
mod env; mod env;
mod http; mod http;
mod auth;
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
@ -59,6 +61,13 @@ async fn main() {
.with(tracing_subscriber::fmt::layer()) .with(tracing_subscriber::fmt::layer())
.init(); .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 { let state = AppState {
pool: create_connection_pool().expect("failed to create database connection pool"), pool: create_connection_pool().expect("failed to create database connection pool"),
}; };
@ -66,6 +75,7 @@ async fn main() {
let app = Router::new() let app = Router::new()
.route("/", get(landing)) .route("/", get(landing))
.merge(auth_router()) .merge(auth_router())
.merge(entry_router())
.with_state(state) .with_state(state)
.layer( .layer(
TraceLayer::new_for_http() TraceLayer::new_for_http()
@ -83,23 +93,18 @@ async fn main() {
}) })
.on_response(|response: &Response, _latency: Duration, _span: &Span| { .on_response(|response: &Response, _latency: Duration, _span: &Span| {
if response.status().is_client_error() { if response.status().is_client_error() {
warn!( warn!("client error: {}", response.status().to_string());
"client error: {}",
response.status().to_string()
);
} else { } else {
info!("finished processing request"); info!("finished processing request");
} }
}) })
.on_failure( .on_failure(
|error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| { |error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| {
error!( error!("internal server error: {}", error.to_string(),);
"internal server error: {}",
error.to_string(),
);
}, },
), ),
); )
.layer(cors);
// FIXME(sofia): Add an env var to change the bind addr // FIXME(sofia): Add an env var to change the bind addr
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", listen_port())) let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", listen_port()))

View file

@ -215,7 +215,7 @@ export async function startDatabase() {
let musicDetails = ( let musicDetails = (
await database.select().from(musicEntries).where(eq(musicEntries.id, entry.musicEntry)) await database.select().from(musicEntries).where(eq(musicEntries.id, entry.musicEntry))
)[0]; )[0];
(musicDetails["link"] as any) = fromDBList(musicDetails.links); (musicDetails["links"] as any) = fromDBList(musicDetails.links);
(musicDetails["id"] as any) = fromDBList(musicDetails.universalIDs); (musicDetails["id"] as any) = fromDBList(musicDetails.universalIDs);
musicDetails["links"] = undefined; musicDetails["links"] = undefined;

View file

@ -53,7 +53,7 @@ const PutEntryBody = Type.Object({
kind: Type.String(), kind: Type.String(),
artist: Type.String(), artist: Type.String(),
title: Type.String(), title: Type.String(),
link: Type.Array(Type.String()), links: Type.Array(Type.String()),
id: Type.Array( id: Type.Array(
Type.Object({ Type.Object({
provider: Type.String(), provider: Type.String(),
@ -111,7 +111,7 @@ export default function registerRoutes(app: AppInterface, auth: AuthInterface, d
id: randomUUID(), id: randomUUID(),
title: entry.base.title, title: entry.base.title,
artist: entry.base.artist, artist: entry.base.artist,
links: toDBList(entry.base.link), links: toDBList(entry.base.links),
universalIDs: toDBList(entry.base.id), universalIDs: toDBList(entry.base.id),
}; };
} else if (entry.base.kind === "environment" && "location" in entry.base) { } else if (entry.base.kind === "environment" && "location" in entry.base) {

View file

@ -115,7 +115,7 @@ export type SongEntry = {
kind: 'song'; kind: 'song';
artist: string; artist: string;
title: string; title: string;
link: string[]; links: string[];
id: UniversalID[]; id: UniversalID[];
}; };
@ -123,7 +123,7 @@ export type AlbumEntry = {
kind: 'album'; kind: 'album';
artist: string; artist: string;
title: string; title: string;
link: string[]; links: string[];
id: UniversalID[]; id: UniversalID[];
}; };

View file

@ -121,8 +121,8 @@
> >
<div slot="contracted"> <div slot="contracted">
{#if entry.base.kind === 'song' || entry.base.kind === 'album'} {#if entry.base.kind === 'song' || entry.base.kind === 'album'}
{#if entry.base.link[0] != null} {#if entry.base.links[0] != null}
<ExternalLink href={entry.base.link[0]}> <ExternalLink href={entry.base.links[0]}>
{entry.base.artist} &dash; {entry.base.title} {entry.base.artist} &dash; {entry.base.title}
</ExternalLink> </ExternalLink>
{:else} {:else}
@ -163,8 +163,8 @@
</div> </div>
{#if entry.base.kind === 'song' || entry.base.kind === 'album'} {#if entry.base.kind === 'song' || entry.base.kind === 'album'}
{#if entry.base.link[0] != null} {#if entry.base.links[0] != null}
<ExternalLink href={entry.base.link[0]}> <ExternalLink href={entry.base.links[0]}>
{entry.base.artist} &dash; {entry.base.title} {entry.base.artist} &dash; {entry.base.title}
</ExternalLink> </ExternalLink>
{:else} {:else}

View file

@ -38,7 +38,7 @@
kind: values.kind, kind: values.kind,
artist: values.artist, artist: values.artist,
title: values.musicTitle, 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 (v) => v != null && v.length > 0
), ),
// FIXME: Infer Universal IDs (Spotify URL, etc) // FIXME: Infer Universal IDs (Spotify URL, etc)