init
This commit is contained in:
7
src/models/_entities/mod.rs
Normal file
7
src/models/_entities/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.8
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
pub mod music_files;
|
||||
pub mod music_libraries;
|
||||
pub mod users;
|
37
src/models/_entities/music_files.rs
Normal file
37
src/models/_entities/music_files.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.8
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "music_files")]
|
||||
pub struct Model {
|
||||
pub created_at: DateTimeWithTimeZone,
|
||||
pub updated_at: DateTimeWithTimeZone,
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub path: String,
|
||||
pub title: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub album: Option<String>,
|
||||
pub metadata: Option<Json>,
|
||||
pub music_library_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::music_libraries::Entity",
|
||||
from = "Column::MusicLibraryId",
|
||||
to = "super::music_libraries::Column::Id",
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
MusicLibraries,
|
||||
}
|
||||
|
||||
impl Related<super::music_libraries::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::MusicLibraries.def()
|
||||
}
|
||||
}
|
41
src/models/_entities/music_libraries.rs
Normal file
41
src/models/_entities/music_libraries.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.8
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "music_libraries")]
|
||||
pub struct Model {
|
||||
pub created_at: DateTimeWithTimeZone,
|
||||
pub updated_at: DateTimeWithTimeZone,
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub path: Option<String>,
|
||||
pub user_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::music_files::Entity")]
|
||||
MusicFiles,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::users::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::users::Column::Id",
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Users,
|
||||
}
|
||||
|
||||
impl Related<super::music_files::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::MusicFiles.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::users::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Users.def()
|
||||
}
|
||||
}
|
5
src/models/_entities/prelude.rs
Normal file
5
src/models/_entities/prelude.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.8
|
||||
|
||||
pub use super::music_files::Entity as MusicFiles;
|
||||
pub use super::music_libraries::Entity as MusicLibraries;
|
||||
pub use super::users::Entity as Users;
|
39
src/models/_entities/users.rs
Normal file
39
src/models/_entities/users.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.8
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "users")]
|
||||
pub struct Model {
|
||||
pub created_at: DateTimeWithTimeZone,
|
||||
pub updated_at: DateTimeWithTimeZone,
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub pid: Uuid,
|
||||
#[sea_orm(unique)]
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
#[sea_orm(unique)]
|
||||
pub api_key: String,
|
||||
pub name: String,
|
||||
pub reset_token: Option<String>,
|
||||
pub reset_sent_at: Option<DateTimeWithTimeZone>,
|
||||
pub email_verification_token: Option<String>,
|
||||
pub email_verification_sent_at: Option<DateTimeWithTimeZone>,
|
||||
pub email_verified_at: Option<DateTimeWithTimeZone>,
|
||||
pub magic_link_token: Option<String>,
|
||||
pub magic_link_expiration: Option<DateTimeWithTimeZone>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::music_libraries::Entity")]
|
||||
MusicLibraries,
|
||||
}
|
||||
|
||||
impl Related<super::music_libraries::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::MusicLibraries.def()
|
||||
}
|
||||
}
|
4
src/models/mod.rs
Normal file
4
src/models/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod _entities;
|
||||
pub mod users;
|
||||
pub mod music_libraries;
|
||||
pub mod music_files;
|
28
src/models/music_files.rs
Normal file
28
src/models/music_files.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
pub use super::_entities::music_files::{ActiveModel, Model, Entity};
|
||||
pub type MusicFiles = Entity;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C>(self, _db: &C, insert: bool) -> std::result::Result<Self, DbErr>
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
if !insert && self.updated_at.is_unchanged() {
|
||||
let mut this = self;
|
||||
this.updated_at = sea_orm::ActiveValue::Set(chrono::Utc::now().into());
|
||||
Ok(this)
|
||||
} else {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// implement your read-oriented logic here
|
||||
impl Model {}
|
||||
|
||||
// implement your write-oriented logic here
|
||||
impl ActiveModel {}
|
||||
|
||||
// implement your custom finders, selectors oriented logic here
|
||||
impl Entity {}
|
28
src/models/music_libraries.rs
Normal file
28
src/models/music_libraries.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
pub use super::_entities::music_libraries::{ActiveModel, Model, Entity};
|
||||
pub type MusicLibraries = Entity;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C>(self, _db: &C, insert: bool) -> std::result::Result<Self, DbErr>
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
if !insert && self.updated_at.is_unchanged() {
|
||||
let mut this = self;
|
||||
this.updated_at = sea_orm::ActiveValue::Set(chrono::Utc::now().into());
|
||||
Ok(this)
|
||||
} else {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// implement your read-oriented logic here
|
||||
impl Model {}
|
||||
|
||||
// implement your write-oriented logic here
|
||||
impl ActiveModel {}
|
||||
|
||||
// implement your custom finders, selectors oriented logic here
|
||||
impl Entity {}
|
367
src/models/users.rs
Normal file
367
src/models/users.rs
Normal file
@@ -0,0 +1,367 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::{offset::Local, Duration};
|
||||
use loco_rs::{auth::jwt, hash, prelude::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Map;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use super::_entities::users::{self, ActiveModel, Entity, Model};
|
||||
|
||||
pub const MAGIC_LINK_LENGTH: i8 = 32;
|
||||
pub const MAGIC_LINK_EXPIRATION_MIN: i8 = 5;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct LoginParams {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct RegisterParams {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Validate, Deserialize)]
|
||||
pub struct Validator {
|
||||
#[validate(length(min = 2, message = "Name must be at least 2 characters long."))]
|
||||
pub name: String,
|
||||
#[validate(custom(function = "validation::is_valid_email"))]
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl Validatable for ActiveModel {
|
||||
fn validator(&self) -> Box<dyn Validate> {
|
||||
Box::new(Validator {
|
||||
name: self.name.as_ref().to_owned(),
|
||||
email: self.email.as_ref().to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for super::_entities::users::ActiveModel {
|
||||
async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
self.validate()?;
|
||||
if insert {
|
||||
let mut this = self;
|
||||
this.pid = ActiveValue::Set(Uuid::new_v4());
|
||||
this.api_key = ActiveValue::Set(format!("lo-{}", Uuid::new_v4()));
|
||||
Ok(this)
|
||||
} else {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Authenticable for Model {
|
||||
async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult<Self> {
|
||||
let user = users::Entity::find()
|
||||
.filter(
|
||||
model::query::condition()
|
||||
.eq(users::Column::ApiKey, api_key)
|
||||
.build(),
|
||||
)
|
||||
.one(db)
|
||||
.await?;
|
||||
user.ok_or_else(|| ModelError::EntityNotFound)
|
||||
}
|
||||
|
||||
async fn find_by_claims_key(db: &DatabaseConnection, claims_key: &str) -> ModelResult<Self> {
|
||||
Self::find_by_pid(db, claims_key).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// finds a user by the provided email
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// When could not find user by the given token or DB query error
|
||||
pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult<Self> {
|
||||
let user = users::Entity::find()
|
||||
.filter(
|
||||
model::query::condition()
|
||||
.eq(users::Column::Email, email)
|
||||
.build(),
|
||||
)
|
||||
.one(db)
|
||||
.await?;
|
||||
user.ok_or_else(|| ModelError::EntityNotFound)
|
||||
}
|
||||
|
||||
/// finds a user by the provided verification token
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// When could not find user by the given token or DB query error
|
||||
pub async fn find_by_verification_token(
|
||||
db: &DatabaseConnection,
|
||||
token: &str,
|
||||
) -> ModelResult<Self> {
|
||||
let user = users::Entity::find()
|
||||
.filter(
|
||||
model::query::condition()
|
||||
.eq(users::Column::EmailVerificationToken, token)
|
||||
.build(),
|
||||
)
|
||||
.one(db)
|
||||
.await?;
|
||||
user.ok_or_else(|| ModelError::EntityNotFound)
|
||||
}
|
||||
|
||||
/// finds a user by the magic token and verify and token expiration
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// When could not find user by the given token or DB query error ot token expired
|
||||
pub async fn find_by_magic_token(db: &DatabaseConnection, token: &str) -> ModelResult<Self> {
|
||||
let user = users::Entity::find()
|
||||
.filter(
|
||||
query::condition()
|
||||
.eq(users::Column::MagicLinkToken, token)
|
||||
.build(),
|
||||
)
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
let user = user.ok_or_else(|| ModelError::EntityNotFound)?;
|
||||
if let Some(expired_at) = user.magic_link_expiration {
|
||||
if expired_at >= Local::now() {
|
||||
Ok(user)
|
||||
} else {
|
||||
tracing::debug!(
|
||||
user_pid = user.pid.to_string(),
|
||||
token_expiration = expired_at.to_string(),
|
||||
"magic token expired for the user."
|
||||
);
|
||||
Err(ModelError::msg("magic token expired"))
|
||||
}
|
||||
} else {
|
||||
tracing::error!(
|
||||
user_pid = user.pid.to_string(),
|
||||
"magic link expiration time not exists"
|
||||
);
|
||||
Err(ModelError::msg("expiration token not exists"))
|
||||
}
|
||||
}
|
||||
|
||||
/// finds a user by the provided reset token
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// When could not find user by the given token or DB query error
|
||||
pub async fn find_by_reset_token(db: &DatabaseConnection, token: &str) -> ModelResult<Self> {
|
||||
let user = users::Entity::find()
|
||||
.filter(
|
||||
model::query::condition()
|
||||
.eq(users::Column::ResetToken, token)
|
||||
.build(),
|
||||
)
|
||||
.one(db)
|
||||
.await?;
|
||||
user.ok_or_else(|| ModelError::EntityNotFound)
|
||||
}
|
||||
|
||||
/// finds a user by the provided pid
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// When could not find user or DB query error
|
||||
pub async fn find_by_pid(db: &DatabaseConnection, pid: &str) -> ModelResult<Self> {
|
||||
let parse_uuid = Uuid::parse_str(pid).map_err(|e| ModelError::Any(e.into()))?;
|
||||
let user = users::Entity::find()
|
||||
.filter(
|
||||
model::query::condition()
|
||||
.eq(users::Column::Pid, parse_uuid)
|
||||
.build(),
|
||||
)
|
||||
.one(db)
|
||||
.await?;
|
||||
user.ok_or_else(|| ModelError::EntityNotFound)
|
||||
}
|
||||
|
||||
/// finds a user by the provided api key
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// When could not find user by the given token or DB query error
|
||||
pub async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult<Self> {
|
||||
let user = users::Entity::find()
|
||||
.filter(
|
||||
model::query::condition()
|
||||
.eq(users::Column::ApiKey, api_key)
|
||||
.build(),
|
||||
)
|
||||
.one(db)
|
||||
.await?;
|
||||
user.ok_or_else(|| ModelError::EntityNotFound)
|
||||
}
|
||||
|
||||
/// Verifies whether the provided plain password matches the hashed password
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// when could not verify password
|
||||
#[must_use]
|
||||
pub fn verify_password(&self, password: &str) -> bool {
|
||||
hash::verify_password(password, &self.password)
|
||||
}
|
||||
|
||||
/// Asynchronously creates a user with a password and saves it to the
|
||||
/// database.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// When could not save the user into the DB
|
||||
pub async fn create_with_password(
|
||||
db: &DatabaseConnection,
|
||||
params: &RegisterParams,
|
||||
) -> ModelResult<Self> {
|
||||
let txn = db.begin().await?;
|
||||
|
||||
if users::Entity::find()
|
||||
.filter(
|
||||
model::query::condition()
|
||||
.eq(users::Column::Email, ¶ms.email)
|
||||
.build(),
|
||||
)
|
||||
.one(&txn)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Err(ModelError::EntityAlreadyExists {});
|
||||
}
|
||||
|
||||
let password_hash =
|
||||
hash::hash_password(¶ms.password).map_err(|e| ModelError::Any(e.into()))?;
|
||||
let user = users::ActiveModel {
|
||||
email: ActiveValue::set(params.email.to_string()),
|
||||
password: ActiveValue::set(password_hash),
|
||||
name: ActiveValue::set(params.name.to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&txn)
|
||||
.await?;
|
||||
|
||||
txn.commit().await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
/// Creates a JWT
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// when could not convert user claims to jwt token
|
||||
pub fn generate_jwt(&self, secret: &str, expiration: u64) -> ModelResult<String> {
|
||||
Ok(jwt::JWT::new(secret).generate_token(expiration, self.pid.to_string(), Map::new())?)
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModel {
|
||||
/// Sets the email verification information for the user and
|
||||
/// updates it in the database.
|
||||
///
|
||||
/// This method is used to record the timestamp when the email verification
|
||||
/// was sent and generate a unique verification token for the user.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// when has DB query error
|
||||
pub async fn set_email_verification_sent(
|
||||
mut self,
|
||||
db: &DatabaseConnection,
|
||||
) -> ModelResult<Model> {
|
||||
self.email_verification_sent_at = ActiveValue::set(Some(Local::now().into()));
|
||||
self.email_verification_token = ActiveValue::Set(Some(Uuid::new_v4().to_string()));
|
||||
Ok(self.update(db).await?)
|
||||
}
|
||||
|
||||
/// Sets the information for a reset password request,
|
||||
/// generates a unique reset password token, and updates it in the
|
||||
/// database.
|
||||
///
|
||||
/// This method records the timestamp when the reset password token is sent
|
||||
/// and generates a unique token for the user.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// when has DB query error
|
||||
pub async fn set_forgot_password_sent(mut self, db: &DatabaseConnection) -> ModelResult<Model> {
|
||||
self.reset_sent_at = ActiveValue::set(Some(Local::now().into()));
|
||||
self.reset_token = ActiveValue::Set(Some(Uuid::new_v4().to_string()));
|
||||
Ok(self.update(db).await?)
|
||||
}
|
||||
|
||||
/// Records the verification time when a user verifies their
|
||||
/// email and updates it in the database.
|
||||
///
|
||||
/// This method sets the timestamp when the user successfully verifies their
|
||||
/// email.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// when has DB query error
|
||||
pub async fn verified(mut self, db: &DatabaseConnection) -> ModelResult<Model> {
|
||||
self.email_verified_at = ActiveValue::set(Some(Local::now().into()));
|
||||
Ok(self.update(db).await?)
|
||||
}
|
||||
|
||||
/// Resets the current user password with a new password and
|
||||
/// updates it in the database.
|
||||
///
|
||||
/// This method hashes the provided password and sets it as the new password
|
||||
/// for the user.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// when has DB query error or could not hashed the given password
|
||||
pub async fn reset_password(
|
||||
mut self,
|
||||
db: &DatabaseConnection,
|
||||
password: &str,
|
||||
) -> ModelResult<Model> {
|
||||
self.password =
|
||||
ActiveValue::set(hash::hash_password(password).map_err(|e| ModelError::Any(e.into()))?);
|
||||
self.reset_token = ActiveValue::Set(None);
|
||||
self.reset_sent_at = ActiveValue::Set(None);
|
||||
Ok(self.update(db).await?)
|
||||
}
|
||||
|
||||
/// Creates a magic link token for passwordless authentication.
|
||||
///
|
||||
/// Generates a random token with a specified length and sets an expiration time
|
||||
/// for the magic link. This method is used to initiate the magic link authentication flow.
|
||||
///
|
||||
/// # Errors
|
||||
/// - Returns an error if database update fails
|
||||
pub async fn create_magic_link(mut self, db: &DatabaseConnection) -> ModelResult<Model> {
|
||||
let random_str = hash::random_string(MAGIC_LINK_LENGTH as usize);
|
||||
let expired = Local::now() + Duration::minutes(MAGIC_LINK_EXPIRATION_MIN.into());
|
||||
|
||||
self.magic_link_token = ActiveValue::set(Some(random_str));
|
||||
self.magic_link_expiration = ActiveValue::set(Some(expired.into()));
|
||||
Ok(self.update(db).await?)
|
||||
}
|
||||
|
||||
/// Verifies and invalidates the magic link after successful authentication.
|
||||
///
|
||||
/// Clears the magic link token and expiration time after the user has
|
||||
/// successfully authenticated using the magic link.
|
||||
///
|
||||
/// # Errors
|
||||
/// - Returns an error if database update fails
|
||||
pub async fn clear_magic_link(mut self, db: &DatabaseConnection) -> ModelResult<Model> {
|
||||
self.magic_link_token = ActiveValue::set(None);
|
||||
self.magic_link_expiration = ActiveValue::set(None);
|
||||
Ok(self.update(db).await?)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user