work
This commit is contained in:
parent
02b9e34258
commit
7b7e1a4014
10 changed files with 942 additions and 270 deletions
761
Cargo.lock
generated
761
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -9,12 +9,12 @@ rocket = { version = "0.5.1", features = ["json"] }
|
|||
rocket_cors = "0.6.0"
|
||||
serde = { version = "1.0.203", features = ["derive"] }
|
||||
serde_json = "1.0.117"
|
||||
uuid = { version = "1.9.1", features = ["v4"] }
|
||||
walkdir = "2.5.0"
|
||||
mongod = { git = "https://git.hydrar.de/jmarya/mongod", features = ["cache"] }
|
||||
bcrypt = "0.15.1"
|
||||
data-encoding = "2.6.0"
|
||||
rand = "0.8.5"
|
||||
env_logger = "0.11.5"
|
||||
log = "0.4.22"
|
||||
chrono = "0.4.38"
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
uuid = { version = "1.8.0", features = ["v4", "serde"] }
|
||||
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-native-tls", "derive", "uuid", "chrono", "json"] }
|
|
@ -5,20 +5,19 @@ services:
|
|||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
- mongodb
|
||||
- postgres
|
||||
volumes:
|
||||
- ./data:/data # Runtime data (optional)
|
||||
- ./media:/media # Audio files
|
||||
environment:
|
||||
- "DB_URI=mongodb://user:pass@mongodb:27017"
|
||||
- "DB=synthwrld"
|
||||
- "DATABASE_URL=postgres://user:pass@postgres/synthwave"
|
||||
|
||||
mongodb:
|
||||
image: mongo:latest
|
||||
ports:
|
||||
- "27017:27017"
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: user
|
||||
MONGO_INITDB_ROOT_PASSWORD: pass
|
||||
postgres:
|
||||
image: timescale/timescaledb:latest-pg16
|
||||
restart: always
|
||||
volumes:
|
||||
- ./db:/data/db
|
||||
- ./db:/var/lib/postgresql/data/
|
||||
environment:
|
||||
- POSTGRES_USER=user
|
||||
- POSTGRES_PASSWORD=pass
|
||||
- POSTGRES_DB=synthwave
|
||||
|
|
49
migrations/0000_init.sql
Normal file
49
migrations/0000_init.sql
Normal file
|
@ -0,0 +1,49 @@
|
|||
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
username varchar(255) NOT NULL PRIMARY KEY,
|
||||
password text NOT NULL,
|
||||
user_role text NOT NULL DEFAULT 'Regular' CHECK (user_role IN ('regular', 'admin'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_session (
|
||||
id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
token text NOT NULL,
|
||||
user varchar(255) NOT NULL,
|
||||
FOREIGN KEY(user) REFERENCES user(username)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS artist (
|
||||
id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name text NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS album (
|
||||
id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
title text NOT NULL,
|
||||
artist UUID
|
||||
FOREIGN KEY(artist) REFERENCES artist(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS track (
|
||||
id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
path text NOT NULL,
|
||||
title text,
|
||||
date_added timestampz NOT NULL DEFAULT current_timestamp,
|
||||
album UUID,
|
||||
artist UUID,
|
||||
meta jsonb,
|
||||
FOREIGN KEY(album) REFERENCES album(id),
|
||||
FOREIGN KEY(artist) REFERENCES artist(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
time timestampz NOT NULL DEFAULT current_timestamp,
|
||||
kind text CHECK (kind IN ('play', 'played', 'stop')),
|
||||
user VARCHAR(255) NOT NULL,
|
||||
track UUID NOT NULL,
|
||||
FOREIGN KEY(user) REFERENCES user(username),
|
||||
FOREIGN KEY(track) REFERENCES track(id)
|
||||
);
|
||||
|
||||
SELECT create_hypertable('events', by_range('time'));
|
|
@ -1,41 +1,33 @@
|
|||
use mongod::{
|
||||
assert_reference_of,
|
||||
derive::{Model, Referencable},
|
||||
Model, Referencable, Reference, Validate,
|
||||
};
|
||||
use mongodb::bson::doc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::FromRow;
|
||||
|
||||
use crate::library::artist::Artist;
|
||||
use mongod::ToAPI;
|
||||
use crate::{get_pg, library::artist::Artist};
|
||||
|
||||
use super::track::Track;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct Album {
|
||||
pub _id: String,
|
||||
pub id: uuid::Uuid,
|
||||
pub title: String,
|
||||
pub artist_id: Option<Reference>,
|
||||
pub artist: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
impl Album {
|
||||
pub async fn create(title: &str, artist: Option<&str>) -> Self {
|
||||
let a = Self {
|
||||
_id: uuid::Uuid::new_v4().to_string(),
|
||||
title: title.to_string(),
|
||||
artist_id: if let Some(artist) = artist {
|
||||
Some(Reference::new_raw(artist))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
a.insert().await.unwrap();
|
||||
a
|
||||
sqlx::query_as("INSERT INTO album (title, artist) VALUES ($1, $2) RETURNING *")
|
||||
.bind(title)
|
||||
.bind(artist)
|
||||
.fetch_one(get_pg!())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn get_tracks_of_album(album: &str) -> Vec<Track> {
|
||||
Track::find(doc! { "album_id": album}, None, None)
|
||||
pub async fn get_tracks_of_album(album: &uuid::Uuid) -> Vec<Track> {
|
||||
sqlx::query_as("SELECT * FROM track WHERE album = $1")
|
||||
.bind(album)
|
||||
.fetch_all(get_pg!())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
@ -44,7 +36,7 @@ impl Album {
|
|||
///
|
||||
/// This method first retrieves the list of tracks in the album, then looks for a file named "cover.{ext}" where {ext} is one of png, jpg, jpeg, avif. The first existing cover file found will be returned.
|
||||
pub async fn get_cover(&self) -> Option<String> {
|
||||
let track_path = Self::get_tracks_of_album(&format!("album::{}", self._id))
|
||||
let track_path = Self::get_tracks_of_album(&self.id)
|
||||
.await
|
||||
.first()?
|
||||
.path
|
||||
|
@ -63,27 +55,17 @@ impl Album {
|
|||
}
|
||||
}
|
||||
|
||||
impl ToAPI for Album {
|
||||
impl Album {
|
||||
async fn api(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"id": &self._id,
|
||||
"id": &self.id,
|
||||
"title": &self.title,
|
||||
"artist": self.artist_id.as_ref().map(mongod::Reference::id),
|
||||
"artist": self.artist,
|
||||
"cover_url": if self.get_cover().await.is_some() {
|
||||
Some(format!("/album/{}/cover", self._id))
|
||||
Some(format!("/album/{}/cover", self.id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for Album {
|
||||
async fn validate(&self) -> Result<(), String> {
|
||||
if let Some(artist_id) = &self.artist_id {
|
||||
assert_reference_of!(artist_id, Artist);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +1,31 @@
|
|||
use mongod::{
|
||||
derive::{Model, Referencable},
|
||||
reference_of, Model, Referencable, Validate,
|
||||
};
|
||||
use mongodb::bson::doc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::FromRow;
|
||||
|
||||
use mongod::ToAPI;
|
||||
use crate::get_pg;
|
||||
|
||||
use super::track::Track;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct Artist {
|
||||
pub _id: String,
|
||||
pub id: uuid::Uuid,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Artist {
|
||||
pub async fn create(name: &str) -> Self {
|
||||
let a = Artist {
|
||||
_id: uuid::Uuid::new_v4().to_string(),
|
||||
name: name.to_string(),
|
||||
};
|
||||
a.insert().await.unwrap();
|
||||
a
|
||||
sqlx::query_as("INSERT INTO artist (name) VALUES ($1) RETURNING id, name")
|
||||
.bind(name)
|
||||
.fetch_one(get_pg!())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Gets the image of an artist or `None` if it can't be found.
|
||||
///
|
||||
/// This function gets a track from the artist. It then expects the folder structure to be `Artist/Album/Track.ext` and searches for an image file named `artist` in the artist folder.
|
||||
pub async fn get_image_of(id: &str) -> Option<String> {
|
||||
pub async fn get_image_of(id: &uuid::Uuid) -> Option<String> {
|
||||
// todo : fix
|
||||
let track_path = Track::find_one(doc! { "artist_id": reference_of!(Artist, id)})
|
||||
.await?
|
||||
.path;
|
||||
|
@ -49,22 +45,16 @@ impl Artist {
|
|||
}
|
||||
}
|
||||
|
||||
impl ToAPI for Artist {
|
||||
impl Artist {
|
||||
async fn api(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"id": &self._id,
|
||||
"id": &self.id,
|
||||
"name": &self.name,
|
||||
"image": if Artist::get_image_of(self.id()).await.is_some() {
|
||||
Some(format!("/artist/{}/image", self.id()))
|
||||
"image": if Artist::get_image_of(&self.id).await.is_some() {
|
||||
Some(format!("/artist/{}/image", self.id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for Artist {
|
||||
async fn validate(&self) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,19 @@
|
|||
use mongod::{
|
||||
assert_reference_of,
|
||||
derive::{Model, Referencable},
|
||||
Model, Referencable, Reference, Validate,
|
||||
};
|
||||
use mongodb::bson::doc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::prelude::FromRow;
|
||||
|
||||
use crate::library::user::User;
|
||||
use crate::{get_pg, library::user::User};
|
||||
|
||||
use super::track::Track;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct Event {
|
||||
pub _id: String,
|
||||
pub id: uuid::Uuid,
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
pub kind: EventKind,
|
||||
pub user: Reference,
|
||||
pub track: Reference,
|
||||
pub timestamp: i64,
|
||||
pub user: String,
|
||||
pub track: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)]
|
||||
#[sqlx(type_name = "event_kind", rename_all = "lowercase")]
|
||||
pub enum EventKind {
|
||||
Play,
|
||||
Played,
|
||||
|
@ -27,36 +21,21 @@ pub enum EventKind {
|
|||
}
|
||||
|
||||
impl Event {
|
||||
pub async fn create(kind: EventKind, user: &User, track: Reference) -> Self {
|
||||
let event = Self {
|
||||
_id: uuid::Uuid::new_v4().to_string(),
|
||||
kind: kind,
|
||||
user: user.reference(),
|
||||
track,
|
||||
timestamp: chrono::Utc::now().timestamp(),
|
||||
};
|
||||
|
||||
event.insert().await.unwrap();
|
||||
|
||||
event
|
||||
pub async fn create(kind: EventKind, user: &User, track: uuid::Uuid) -> Self {
|
||||
sqlx::query_as("INSERT INTO events (kind, user, track) VALUES ($1, $2, $3) RETURNING *")
|
||||
.bind(kind)
|
||||
.bind(&user.username)
|
||||
.bind(track)
|
||||
.fetch_one(get_pg!())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn get_latest_events_of(u: &User) -> Vec<Self> {
|
||||
Self::find(
|
||||
doc! { "user": u.reference() },
|
||||
Some(300),
|
||||
Some(doc! { "timestamp": -1 }),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for Event {
|
||||
async fn validate(&self) -> Result<(), String> {
|
||||
assert_reference_of!(self.user, User);
|
||||
assert_reference_of!(self.track, Track);
|
||||
|
||||
Ok(())
|
||||
sqlx::query_as("SELECT * FROM events WHERE user = $1 ORDER BY time DESC LIMIT 300")
|
||||
.bind(&u.username)
|
||||
.fetch_all(get_pg!())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,61 +1,50 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use mongod::{
|
||||
assert_reference_of,
|
||||
derive::{Model, Referencable},
|
||||
Model, Referencable, Reference, Validate,
|
||||
};
|
||||
use mongodb::bson::doc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::prelude::FromRow;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::library::{album::Album, artist::Artist};
|
||||
use mongod::ToAPI;
|
||||
use crate::{
|
||||
get_pg,
|
||||
library::{album::Album, artist::Artist},
|
||||
};
|
||||
|
||||
use super::{event::Event, metadata::AudioMetadata, user::User};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct Track {
|
||||
pub _id: String,
|
||||
pub id: uuid::Uuid,
|
||||
pub path: String,
|
||||
pub title: String,
|
||||
pub date_added: i64,
|
||||
pub album_id: Option<Reference>,
|
||||
pub artist_id: Option<Reference>,
|
||||
pub meta: Option<AudioMetadata>,
|
||||
pub date_added: chrono::DateTime<chrono::Utc>,
|
||||
pub album: Option<uuid::Uuid>,
|
||||
pub artist: Option<uuid::Uuid>,
|
||||
pub meta: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl Track {
|
||||
pub async fn create(data: &serde_json::Map<String, serde_json::Value>) {
|
||||
let mut t = Self {
|
||||
_id: uuid::Uuid::new_v4().to_string(),
|
||||
path: data.get("path").unwrap().as_str().unwrap().to_string(),
|
||||
title: data.get("title").unwrap().as_str().unwrap().to_string(),
|
||||
date_added: chrono::Utc::now().timestamp(),
|
||||
album_id: None,
|
||||
artist_id: None,
|
||||
meta: data.get("meta").map(|x| AudioMetadata(x.clone())),
|
||||
};
|
||||
t.insert().await.unwrap();
|
||||
t.update(&serde_json::to_value(data).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query("INSERT INTO track (path, title, meta) VALUES ($1, $2, $4)")
|
||||
.bind(data.get("path").unwrap().as_str().unwrap().to_string())
|
||||
.bind(data.get("title").unwrap().as_str().unwrap().to_string())
|
||||
.bind(data.get("meta"))
|
||||
.fetch(get_pg!());
|
||||
}
|
||||
|
||||
pub async fn get_latest_of_user(u: &User) -> Vec<Self> {
|
||||
let latest_events = Event::get_latest_events_of(u).await;
|
||||
let mut ids = HashSet::new();
|
||||
let mut tracks = vec![];
|
||||
|
||||
for event in latest_events {
|
||||
let track: Track = event.track.get().await;
|
||||
if !ids.contains(&track._id) {
|
||||
ids.insert(track._id.clone());
|
||||
tracks.push(track);
|
||||
if !ids.contains(&event.track) {
|
||||
ids.insert(event.track.clone());
|
||||
}
|
||||
}
|
||||
|
||||
tracks
|
||||
sqlx::query_as("SELECT * FROM track WHERE id = ANY($1)")
|
||||
.bind(ids.into_iter().collect::<Vec<_>>())
|
||||
.fetch_all(get_pg!())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Transcode audio to OPUS with `bitrate`
|
||||
|
@ -100,6 +89,7 @@ impl Track {
|
|||
|
||||
/// Find tracks with no album or artist
|
||||
pub async fn get_orphans() -> Vec<Track> {
|
||||
// todo : fix
|
||||
Self::find(
|
||||
doc! {
|
||||
"artist_id": None::<String>,
|
||||
|
@ -113,9 +103,10 @@ impl Track {
|
|||
}
|
||||
}
|
||||
|
||||
impl ToAPI for Track {
|
||||
impl Track {
|
||||
async fn api(&self) -> serde_json::Value {
|
||||
let (cover, album_title, album_id) = if let Some(album_ref) = &self.album_id {
|
||||
// todo : fix
|
||||
let (cover, album_title, album_id) = if let Some(album_ref) = &self.album {
|
||||
let album = album_ref.get::<Album>().await;
|
||||
|
||||
(album.get_cover().await.is_some(), album.title, album._id)
|
||||
|
@ -123,7 +114,7 @@ impl ToAPI for Track {
|
|||
(false, String::new(), String::new())
|
||||
};
|
||||
|
||||
let artist_title = if let Some(artist_ref) = &self.artist_id {
|
||||
let artist_title = if let Some(artist_ref) = &self.artist {
|
||||
artist_ref
|
||||
.get_partial::<Artist>(json!({"name": 1}))
|
||||
.await
|
||||
|
@ -133,33 +124,19 @@ impl ToAPI for Track {
|
|||
};
|
||||
|
||||
json!({
|
||||
"id": self._id,
|
||||
"id": self.id,
|
||||
"title": self.title,
|
||||
"track_number": self.meta.as_ref().map(super::metadata::AudioMetadata::track_number),
|
||||
"track_number": self.meta.as_ref().map(|x| AudioMetadata(*x).track_number()),
|
||||
"meta": serde_json::to_value(&self.meta).unwrap(),
|
||||
"album_id": self.album_id.as_ref().map(|x| x.id().to_string()),
|
||||
"album_id": self.album,
|
||||
"album": album_title,
|
||||
"cover": if cover {
|
||||
Some(format!("/album/{album_id}/cover"))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
"artist_id": self.artist_id.as_ref().map(|x| x.id().to_string()),
|
||||
"artist_id": self.artist,
|
||||
"artist": artist_title
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for Track {
|
||||
async fn validate(&self) -> Result<(), String> {
|
||||
if let Some(artist_id) = &self.artist_id {
|
||||
assert_reference_of!(artist_id, Artist);
|
||||
}
|
||||
|
||||
if let Some(album_id) = &self.artist_id {
|
||||
assert_reference_of!(album_id, Artist);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
use data_encoding::HEXUPPER;
|
||||
use mongod::{
|
||||
assert_reference_of,
|
||||
derive::{Model, Referencable},
|
||||
Model, Referencable, Reference, Validate,
|
||||
};
|
||||
use mongodb::bson::doc;
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::FromRow;
|
||||
|
||||
use mongod::ToAPI;
|
||||
use crate::get_pg;
|
||||
|
||||
fn gen_token(token_length: usize) -> String {
|
||||
let mut token_bytes = vec![0u8; token_length];
|
||||
|
@ -19,65 +14,66 @@ fn gen_token(token_length: usize) -> String {
|
|||
HEXUPPER.encode(&token_bytes)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct User {
|
||||
pub _id: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub role: UserRole,
|
||||
pub user_role: UserRole,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)]
|
||||
#[sqlx(type_name = "user_role", rename_all = "lowercase")]
|
||||
pub enum UserRole {
|
||||
Regular,
|
||||
Admin,
|
||||
}
|
||||
|
||||
impl Validate for User {
|
||||
async fn validate(&self) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub async fn create(username: &str, password: &str, role: UserRole) -> Option<Self> {
|
||||
if Self::find_one_partial(doc! { "username": username }, json!({}))
|
||||
pub async fn find(username: &str) -> Option<Self> {
|
||||
sqlx::query_as("SELECT * FROM user WHERE username = $1")
|
||||
.bind(username)
|
||||
.fetch_optional(get_pg!())
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn create(username: &str, password: &str, role: UserRole) -> Option<Self> {
|
||||
if Self::find(username).await.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let u = Self {
|
||||
_id: uuid::Uuid::new_v4().to_string(),
|
||||
username: username.to_string(),
|
||||
password: bcrypt::hash(password, bcrypt::DEFAULT_COST).unwrap(),
|
||||
role,
|
||||
user_role: role,
|
||||
};
|
||||
|
||||
u.insert().await.ok()?;
|
||||
sqlx::query("INSERT INTO user (username, password, user_role) VALUES ($1, $2, $3)")
|
||||
.bind(&u.username)
|
||||
.bind(&u.password)
|
||||
.bind(&u.user_role)
|
||||
.fetch(get_pg!());
|
||||
|
||||
Some(u)
|
||||
}
|
||||
|
||||
pub async fn login(username: &str, password: &str) -> Option<(Session, UserRole)> {
|
||||
let u = Self::find_one(doc! { "username": username }).await?;
|
||||
let u = Self::find(username).await?;
|
||||
|
||||
if !u.verify_pw(password) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((u.session().await, u.role))
|
||||
Some((u.session().await, u.user_role))
|
||||
}
|
||||
|
||||
/// Change the password of a `User`
|
||||
pub async fn passwd(self, old: &str, new: &str) -> Result<(), ()> {
|
||||
if self.verify_pw(old) {
|
||||
self.change()
|
||||
.password(bcrypt::hash(new, bcrypt::DEFAULT_COST).unwrap())
|
||||
.update()
|
||||
.await
|
||||
.map_err(|_| ())?;
|
||||
sqlx::query("UPDATE user SET password = $1 WHERE username = $2;")
|
||||
.bind(bcrypt::hash(new, bcrypt::DEFAULT_COST).unwrap())
|
||||
.bind(&self.username)
|
||||
.fetch(get_pg!());
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
@ -86,15 +82,14 @@ impl User {
|
|||
}
|
||||
|
||||
pub async fn session(&self) -> Session {
|
||||
let s = Session {
|
||||
_id: uuid::Uuid::new_v4().to_string(),
|
||||
token: gen_token(60),
|
||||
user: self.reference(),
|
||||
};
|
||||
|
||||
s.insert().await.unwrap();
|
||||
|
||||
s
|
||||
sqlx::query_as(
|
||||
"INSERT INTO user_session (token, user) VALUES ($1, $2) RETURNING id, token, user",
|
||||
)
|
||||
.bind(gen_token(64))
|
||||
.bind(&self.username)
|
||||
.fetch_one(get_pg!())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub const fn is_admin(&self) -> bool {
|
||||
|
@ -106,26 +101,18 @@ impl User {
|
|||
}
|
||||
}
|
||||
|
||||
impl ToAPI for User {
|
||||
impl User {
|
||||
async fn api(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"username": self.username,
|
||||
"role": self.role
|
||||
"role": self.user_role
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct Session {
|
||||
pub _id: String,
|
||||
pub id: uuid::Uuid,
|
||||
pub token: String,
|
||||
pub user: Reference,
|
||||
}
|
||||
|
||||
impl Validate for Session {
|
||||
async fn validate(&self) -> Result<(), String> {
|
||||
assert_reference_of!(self.user, User);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub user: String,
|
||||
}
|
||||
|
|
30
src/main.rs
30
src/main.rs
|
@ -5,11 +5,30 @@ mod library;
|
|||
mod route;
|
||||
|
||||
use library::user::{User, UserRole};
|
||||
use mongod::Model;
|
||||
use mongodb::bson::doc;
|
||||
use rocket::routes;
|
||||
use rocket::tokio::sync::OnceCell;
|
||||
use rocket::{http::Method, launch};
|
||||
|
||||
pub static PG: OnceCell<sqlx::PgPool> = OnceCell::const_new();
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! get_pg {
|
||||
() => {
|
||||
if let Some(client) = $crate::PG.get() {
|
||||
client
|
||||
} else {
|
||||
let client = sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&std::env::var("DATABASE_URL").unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
$crate::PG.set(client).unwrap();
|
||||
$crate::PG.get().unwrap()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[launch]
|
||||
async fn rocket() -> _ {
|
||||
env_logger::init();
|
||||
|
@ -27,16 +46,17 @@ async fn rocket() -> _ {
|
|||
.to_cors()
|
||||
.expect("error creating CORS options");
|
||||
|
||||
let pg = get_pg!();
|
||||
|
||||
sqlx::migrate!("./migrations").run(pg).await.unwrap();
|
||||
|
||||
let lib = Libary::new("./media".into());
|
||||
let cache = cache::RouteCache::new();
|
||||
|
||||
lib.rescan(&cache).await;
|
||||
|
||||
// create initial admin user
|
||||
if User::find(doc! { "username": "admin" }, None, None)
|
||||
.await
|
||||
.is_none()
|
||||
{
|
||||
if User::find("admin").await.is_none() {
|
||||
User::create("admin", "admin", UserRole::Admin).await;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue