Compare commits
1 Commits
c6452609d6
...
a9d414bffa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9d414bffa |
@@ -3,6 +3,7 @@ pub mod artist;
|
|||||||
pub mod download_queue;
|
pub mod download_queue;
|
||||||
pub mod search_cache;
|
pub mod search_cache;
|
||||||
pub mod track;
|
pub mod track;
|
||||||
|
pub mod user;
|
||||||
pub mod wanted_item;
|
pub mod wanted_item;
|
||||||
|
|
||||||
pub use album::Entity as Albums;
|
pub use album::Entity as Albums;
|
||||||
@@ -10,4 +11,5 @@ pub use artist::Entity as Artists;
|
|||||||
pub use download_queue::Entity as DownloadQueue;
|
pub use download_queue::Entity as DownloadQueue;
|
||||||
pub use search_cache::Entity as SearchCache;
|
pub use search_cache::Entity as SearchCache;
|
||||||
pub use track::Entity as Tracks;
|
pub use track::Entity as Tracks;
|
||||||
|
pub use user::Entity as Users;
|
||||||
pub use wanted_item::Entity as WantedItems;
|
pub use wanted_item::Entity as WantedItems;
|
||||||
|
|||||||
39
src/entities/user.rs
Normal file
39
src/entities/user.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum)]
|
||||||
|
#[sea_orm(rs_type = "String", db_type = "Text")]
|
||||||
|
pub enum UserRole {
|
||||||
|
#[sea_orm(string_value = "admin")]
|
||||||
|
Admin,
|
||||||
|
#[sea_orm(string_value = "user")]
|
||||||
|
User,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "users")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: i32,
|
||||||
|
#[sea_orm(unique)]
|
||||||
|
pub username: String,
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
pub password_hash: String,
|
||||||
|
pub role: UserRole,
|
||||||
|
pub created_at: chrono::NaiveDateTime,
|
||||||
|
pub updated_at: chrono::NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(has_many = "super::wanted_item::Entity")]
|
||||||
|
WantedItems,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::wanted_item::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::WantedItems.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
@@ -37,6 +37,8 @@ pub struct Model {
|
|||||||
pub album_id: Option<i32>,
|
pub album_id: Option<i32>,
|
||||||
#[sea_orm(nullable)]
|
#[sea_orm(nullable)]
|
||||||
pub track_id: Option<i32>,
|
pub track_id: Option<i32>,
|
||||||
|
#[sea_orm(nullable)]
|
||||||
|
pub user_id: Option<i32>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[sea_orm(nullable)]
|
#[sea_orm(nullable)]
|
||||||
pub musicbrainz_id: Option<String>,
|
pub musicbrainz_id: Option<String>,
|
||||||
@@ -47,6 +49,13 @@ pub struct Model {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::user::Entity",
|
||||||
|
from = "Column::UserId",
|
||||||
|
to = "super::user::Column::Id",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
User,
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to = "super::artist::Entity",
|
belongs_to = "super::artist::Entity",
|
||||||
from = "Column::ArtistId",
|
from = "Column::ArtistId",
|
||||||
@@ -72,6 +81,12 @@ pub enum Relation {
|
|||||||
Downloads,
|
Downloads,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Related<super::user::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::User.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Related<super::artist::Entity> for Entity {
|
impl Related<super::artist::Entity> for Entity {
|
||||||
fn to() -> RelationDef {
|
fn to() -> RelationDef {
|
||||||
Relation::Artist.def()
|
Relation::Artist.def()
|
||||||
|
|||||||
67
src/migration/m20260319_000011_create_users.rs
Normal file
67
src/migration/m20260319_000011_create_users.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Users::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Users::Id)
|
||||||
|
.integer()
|
||||||
|
.not_null()
|
||||||
|
.auto_increment()
|
||||||
|
.primary_key(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Users::Username)
|
||||||
|
.text()
|
||||||
|
.not_null()
|
||||||
|
.unique_key(),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(Users::PasswordHash).text().not_null())
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Users::Role)
|
||||||
|
.text()
|
||||||
|
.not_null()
|
||||||
|
.default("user"),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Users::CreatedAt)
|
||||||
|
.date_time()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp()),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Users::UpdatedAt)
|
||||||
|
.date_time()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp()),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(Users::Table).to_owned())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
pub(crate) enum Users {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Username,
|
||||||
|
PasswordHash,
|
||||||
|
Role,
|
||||||
|
CreatedAt,
|
||||||
|
UpdatedAt,
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(WantedItems::Table)
|
||||||
|
.add_column(ColumnDef::new(WantedItems::UserId).integer())
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_wanted_items_user_id")
|
||||||
|
.table(WantedItems::Table)
|
||||||
|
.col(WantedItems::UserId)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(WantedItems::Table)
|
||||||
|
.drop_column(WantedItems::UserId)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum WantedItems {
|
||||||
|
Table,
|
||||||
|
UserId,
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ mod m20260317_000006_create_search_cache;
|
|||||||
mod m20260317_000007_unique_artist_album;
|
mod m20260317_000007_unique_artist_album;
|
||||||
mod m20260317_000009_add_wanted_name;
|
mod m20260317_000009_add_wanted_name;
|
||||||
mod m20260317_000010_add_wanted_mbid;
|
mod m20260317_000010_add_wanted_mbid;
|
||||||
|
mod m20260319_000011_create_users;
|
||||||
|
mod m20260319_000012_add_user_id_to_wanted_items;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@@ -25,6 +27,8 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20260317_000007_unique_artist_album::Migration),
|
Box::new(m20260317_000007_unique_artist_album::Migration),
|
||||||
Box::new(m20260317_000009_add_wanted_name::Migration),
|
Box::new(m20260317_000009_add_wanted_name::Migration),
|
||||||
Box::new(m20260317_000010_add_wanted_mbid::Migration),
|
Box::new(m20260317_000010_add_wanted_mbid::Migration),
|
||||||
|
Box::new(m20260319_000011_create_users::Migration),
|
||||||
|
Box::new(m20260319_000012_add_user_id_to_wanted_items::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ pub mod artists;
|
|||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod downloads;
|
pub mod downloads;
|
||||||
pub mod tracks;
|
pub mod tracks;
|
||||||
|
pub mod users;
|
||||||
pub mod wanted;
|
pub mod wanted;
|
||||||
|
|||||||
64
src/queries/users.rs
Normal file
64
src/queries/users.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use chrono::Utc;
|
||||||
|
use sea_orm::*;
|
||||||
|
|
||||||
|
use crate::entities::user::{self, ActiveModel, Entity as Users, Model as User, UserRole};
|
||||||
|
use crate::error::{DbError, DbResult};
|
||||||
|
|
||||||
|
pub async fn create(
|
||||||
|
db: &DatabaseConnection,
|
||||||
|
username: &str,
|
||||||
|
password_hash: &str,
|
||||||
|
role: UserRole,
|
||||||
|
) -> DbResult<User> {
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
|
let active = ActiveModel {
|
||||||
|
username: Set(username.to_string()),
|
||||||
|
password_hash: Set(password_hash.to_string()),
|
||||||
|
role: Set(role),
|
||||||
|
created_at: Set(now),
|
||||||
|
updated_at: Set(now),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
Ok(active.insert(db).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_username(db: &DatabaseConnection, username: &str) -> DbResult<Option<User>> {
|
||||||
|
Ok(Users::find()
|
||||||
|
.filter(user::Column::Username.eq(username))
|
||||||
|
.one(db)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_by_id(db: &DatabaseConnection, id: i32) -> DbResult<User> {
|
||||||
|
Users::find_by_id(id)
|
||||||
|
.one(db)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| DbError::NotFound(format!("user id={id}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list(db: &DatabaseConnection) -> DbResult<Vec<User>> {
|
||||||
|
Ok(Users::find().order_by_asc(user::Column::Username).all(db).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete(db: &DatabaseConnection, id: i32) -> DbResult<()> {
|
||||||
|
Users::delete_by_id(id).exec(db).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn count(db: &DatabaseConnection) -> DbResult<u64> {
|
||||||
|
Ok(Users::find().count(db).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assign all orphaned wanted items (user_id IS NULL) to the given user.
|
||||||
|
pub async fn adopt_orphaned_wanted_items(db: &DatabaseConnection, user_id: i32) -> DbResult<u64> {
|
||||||
|
use crate::entities::wanted_item;
|
||||||
|
let result = wanted_item::Entity::update_many()
|
||||||
|
.col_expr(
|
||||||
|
wanted_item::Column::UserId,
|
||||||
|
sea_orm::sea_query::Expr::value(user_id),
|
||||||
|
)
|
||||||
|
.filter(wanted_item::Column::UserId.is_null())
|
||||||
|
.exec(db)
|
||||||
|
.await?;
|
||||||
|
Ok(result.rows_affected)
|
||||||
|
}
|
||||||
@@ -7,23 +7,26 @@ use crate::entities::wanted_item::{
|
|||||||
};
|
};
|
||||||
use crate::error::{DbError, DbResult};
|
use crate::error::{DbError, DbResult};
|
||||||
|
|
||||||
pub async fn add(
|
pub struct AddWantedItem<'a> {
|
||||||
db: &DatabaseConnection,
|
pub item_type: ItemType,
|
||||||
item_type: ItemType,
|
pub name: &'a str,
|
||||||
name: &str,
|
pub musicbrainz_id: Option<&'a str>,
|
||||||
musicbrainz_id: Option<&str>,
|
pub artist_id: Option<i32>,
|
||||||
artist_id: Option<i32>,
|
pub album_id: Option<i32>,
|
||||||
album_id: Option<i32>,
|
pub track_id: Option<i32>,
|
||||||
track_id: Option<i32>,
|
pub user_id: Option<i32>,
|
||||||
) -> DbResult<WantedItem> {
|
}
|
||||||
|
|
||||||
|
pub async fn add(db: &DatabaseConnection, item: AddWantedItem<'_>) -> DbResult<WantedItem> {
|
||||||
let now = Utc::now().naive_utc();
|
let now = Utc::now().naive_utc();
|
||||||
let active = ActiveModel {
|
let active = ActiveModel {
|
||||||
item_type: Set(item_type),
|
item_type: Set(item.item_type),
|
||||||
name: Set(name.to_string()),
|
name: Set(item.name.to_string()),
|
||||||
musicbrainz_id: Set(musicbrainz_id.map(String::from)),
|
musicbrainz_id: Set(item.musicbrainz_id.map(String::from)),
|
||||||
artist_id: Set(artist_id),
|
artist_id: Set(item.artist_id),
|
||||||
album_id: Set(album_id),
|
album_id: Set(item.album_id),
|
||||||
track_id: Set(track_id),
|
track_id: Set(item.track_id),
|
||||||
|
user_id: Set(item.user_id),
|
||||||
status: Set(WantedStatus::Wanted),
|
status: Set(WantedStatus::Wanted),
|
||||||
added_at: Set(now),
|
added_at: Set(now),
|
||||||
updated_at: Set(now),
|
updated_at: Set(now),
|
||||||
@@ -35,11 +38,15 @@ pub async fn add(
|
|||||||
pub async fn list(
|
pub async fn list(
|
||||||
db: &DatabaseConnection,
|
db: &DatabaseConnection,
|
||||||
status_filter: Option<WantedStatus>,
|
status_filter: Option<WantedStatus>,
|
||||||
|
user_id: Option<i32>,
|
||||||
) -> DbResult<Vec<WantedItem>> {
|
) -> DbResult<Vec<WantedItem>> {
|
||||||
let mut query = WantedItems::find();
|
let mut query = WantedItems::find();
|
||||||
if let Some(status) = status_filter {
|
if let Some(status) = status_filter {
|
||||||
query = query.filter(wanted_item::Column::Status.eq(status));
|
query = query.filter(wanted_item::Column::Status.eq(status));
|
||||||
}
|
}
|
||||||
|
if let Some(uid) = user_id {
|
||||||
|
query = query.filter(wanted_item::Column::UserId.eq(uid));
|
||||||
|
}
|
||||||
Ok(query.all(db).await?)
|
Ok(query.all(db).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,12 +167,15 @@ async fn test_wanted_items_lifecycle() {
|
|||||||
// Add wanted item
|
// Add wanted item
|
||||||
let item = queries::wanted::add(
|
let item = queries::wanted::add(
|
||||||
conn,
|
conn,
|
||||||
ItemType::Artist,
|
queries::wanted::AddWantedItem {
|
||||||
"Radiohead",
|
item_type: ItemType::Artist,
|
||||||
None,
|
name: "Radiohead",
|
||||||
Some(artist.id),
|
musicbrainz_id: None,
|
||||||
None,
|
artist_id: Some(artist.id),
|
||||||
None,
|
album_id: None,
|
||||||
|
track_id: None,
|
||||||
|
user_id: None,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -180,12 +183,12 @@ async fn test_wanted_items_lifecycle() {
|
|||||||
assert_eq!(item.item_type, ItemType::Artist);
|
assert_eq!(item.item_type, ItemType::Artist);
|
||||||
|
|
||||||
// List with filter
|
// List with filter
|
||||||
let wanted = queries::wanted::list(conn, Some(WantedStatus::Wanted))
|
let wanted = queries::wanted::list(conn, Some(WantedStatus::Wanted), None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(wanted.len(), 1);
|
assert_eq!(wanted.len(), 1);
|
||||||
|
|
||||||
let downloaded = queries::wanted::list(conn, Some(WantedStatus::Downloaded))
|
let downloaded = queries::wanted::list(conn, Some(WantedStatus::Downloaded), None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(downloaded.is_empty());
|
assert!(downloaded.is_empty());
|
||||||
@@ -198,7 +201,7 @@ async fn test_wanted_items_lifecycle() {
|
|||||||
|
|
||||||
// Remove
|
// Remove
|
||||||
queries::wanted::remove(conn, item.id).await.unwrap();
|
queries::wanted::remove(conn, item.id).await.unwrap();
|
||||||
let all = queries::wanted::list(conn, None).await.unwrap();
|
let all = queries::wanted::list(conn, None, None).await.unwrap();
|
||||||
assert!(all.is_empty());
|
assert!(all.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user