Initial impl of entry endpoints
This commit is contained in:
parent
e48f74b970
commit
d99b0344df
18 changed files with 303 additions and 156 deletions
|
@ -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"] }
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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>,
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
|
@ -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 {
|
||||||
|
@ -263,7 +305,11 @@ struct InsertHeirRequest {
|
||||||
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();
|
||||||
|
@ -279,7 +325,11 @@ async fn insert_heir(State(state): State<AppState>, ExtractJwtUser(user): Extrac
|
||||||
.execute(&mut conn);
|
.execute(&mut conn);
|
||||||
|
|
||||||
if result.is_err() {
|
if result.is_err() {
|
||||||
error!("failed to insert into heirs: {}, error: {:?}", user.uid, 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,7 +337,11 @@ async fn insert_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)
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -296,19 +350,26 @@ async fn insert_heir(State(state): State<AppState>, ExtractJwtUser(user): Extrac
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct DeleteHeirRequest {
|
struct DeleteHeirRequest {
|
||||||
id: String,
|
id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_heir(State(state): State<AppState>, ExtractJwtUser(user): ExtractJwtUser, Json(req): Json<DeleteHeirRequest>) -> Result<Json<Vec<HttpHeir>>, StatusCode> {
|
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::*;
|
use crate::database::schema::heirs::dsl::*;
|
||||||
if let Ok(mut conn) = state.pool.get() {
|
if let Ok(mut conn) = state.pool.get() {
|
||||||
let result = diesel::delete(heirs.filter(id.eq(&req.id))).execute(&mut conn);
|
let result = diesel::delete(heirs.filter(id.eq(&req.id))).execute(&mut conn);
|
||||||
if result.is_err() {
|
if result.is_err() {
|
||||||
error!("failed to delete from heirs: {}, heir_id: {}, error: {:?}", user.uid, req.id, result.err());
|
error!(
|
||||||
|
"failed to delete from heirs: {}, heir_id: {}, error: {:?}",
|
||||||
|
user.uid,
|
||||||
|
req.id,
|
||||||
|
result.err()
|
||||||
|
);
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,7 +377,11 @@ 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)
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()))
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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} ‐ {entry.base.title}
|
{entry.base.artist} ‐ {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} ‐ {entry.base.title}
|
{entry.base.artist} ‐ {entry.base.title}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue