removing rbac cos its not needed at all
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -5071,6 +5071,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
|
||||
@@ -32,6 +32,7 @@ sea-orm = { version = "1.1", features = [
|
||||
"macros",
|
||||
] }
|
||||
chrono = { version = "0.4" }
|
||||
time = { version = "0.3" }
|
||||
validator = { version = "0.20" }
|
||||
uuid = { version = "1.6", features = ["v4"] }
|
||||
include_dir = { version = "0.7" }
|
||||
|
||||
@@ -93,7 +93,14 @@ database:
|
||||
auth:
|
||||
# JWT authentication
|
||||
jwt:
|
||||
location:
|
||||
- from: Cookie
|
||||
name: auth_token
|
||||
- from: Bearer
|
||||
# Secret key for token generation and verification
|
||||
secret: A6ECni63rt2Jb00tX9Hf
|
||||
# Token expiration time in seconds
|
||||
expiration: 604800 # 7 days
|
||||
|
||||
settings:
|
||||
admin_email: {{ get_env(name="ADMIN_EMAIL", default="admin@example.com") }}
|
||||
|
||||
@@ -90,7 +90,14 @@ database:
|
||||
auth:
|
||||
# JWT authentication
|
||||
jwt:
|
||||
location:
|
||||
- from: Cookie
|
||||
name: auth_token
|
||||
- from: Bearer
|
||||
# Secret key for token generation and verification
|
||||
secret: 0yWwoflcGiAhonIzhQyQ
|
||||
# Token expiration time in seconds
|
||||
expiration: 604800 # 7 days
|
||||
|
||||
settings:
|
||||
admin_email: admin@example.com
|
||||
|
||||
@@ -10,6 +10,8 @@ mod m20260517_000005_audio_albums;
|
||||
mod m20260517_000006_audio_tracks;
|
||||
mod m20260517_000007_audio_tags;
|
||||
mod m20260517_000008_audio_track_tags;
|
||||
mod m20260517_000009_simple_constraints;
|
||||
mod m20260517_000010_drop_user_roles;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -26,6 +28,8 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260517_000006_audio_tracks::Migration),
|
||||
Box::new(m20260517_000007_audio_tags::Migration),
|
||||
Box::new(m20260517_000008_audio_track_tags::Migration),
|
||||
Box::new(m20260517_000009_simple_constraints::Migration),
|
||||
Box::new(m20260517_000010_drop_user_roles::Migration),
|
||||
// inject-above (do not remove this comment)
|
||||
]
|
||||
}
|
||||
|
||||
60
migration/src/m20260517_000009_simple_constraints.rs
Normal file
60
migration/src/m20260517_000009_simple_constraints.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use sea_orm_migration::{prelude::*, sea_orm::ConnectionTrait};
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum AudioTrackTags {
|
||||
Table,
|
||||
TagId,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
|
||||
if matches!(
|
||||
m.get_database_backend(),
|
||||
sea_orm_migration::sea_orm::DatabaseBackend::Postgres
|
||||
) {
|
||||
m.get_connection()
|
||||
.execute_unprepared(
|
||||
"ALTER TABLE users \
|
||||
ADD CONSTRAINT chk_users_theme \
|
||||
CHECK (theme IN ('light', 'dark'))",
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
m.create_index(
|
||||
Index::create()
|
||||
.name("idx-audio_track_tags-tag_id")
|
||||
.table(AudioTrackTags::Table)
|
||||
.col(AudioTrackTags::TagId)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
|
||||
m.drop_index(
|
||||
Index::drop()
|
||||
.name("idx-audio_track_tags-tag_id")
|
||||
.table(AudioTrackTags::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if matches!(
|
||||
m.get_database_backend(),
|
||||
sea_orm_migration::sea_orm::DatabaseBackend::Postgres
|
||||
) {
|
||||
m.get_connection()
|
||||
.execute_unprepared("ALTER TABLE users DROP CONSTRAINT chk_users_theme")
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
28
migration/src/m20260517_000010_drop_user_roles.rs
Normal file
28
migration/src/m20260517_000010_drop_user_roles.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum UserRoles {
|
||||
Table,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
|
||||
m.drop_table(
|
||||
Table::drop()
|
||||
.table(UserRoles::Table)
|
||||
.if_exists()
|
||||
.cascade()
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, _m: &SchemaManager) -> Result<(), DbErr> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ impl Hooks for App {
|
||||
fn routes(_ctx: &AppContext) -> AppRoutes {
|
||||
AppRoutes::with_default_routes() // controller routes below
|
||||
.add_route(controllers::auth::routes())
|
||||
.add_route(controllers::admin::routes())
|
||||
}
|
||||
async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {
|
||||
queue.register(DownloadWorker::build(ctx)).await?;
|
||||
|
||||
57
src/controllers/admin.rs
Normal file
57
src/controllers/admin.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use crate::models::{
|
||||
_entities::{audio_albums, audio_tracks, audit_logs, blog_articles, users},
|
||||
users as users_model,
|
||||
};
|
||||
use loco_rs::prelude::*;
|
||||
use sea_orm::{EntityTrait, PaginatorTrait};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct DashboardResponse {
|
||||
users: u64,
|
||||
blog_articles: u64,
|
||||
audio_albums: u64,
|
||||
audio_tracks: u64,
|
||||
audit_logs: u64,
|
||||
}
|
||||
|
||||
fn admin_email(ctx: &AppContext) -> Option<&str> {
|
||||
ctx.config
|
||||
.settings
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.get("admin_email"))
|
||||
.and_then(|email| email.as_str())
|
||||
}
|
||||
|
||||
fn is_admin(ctx: &AppContext, user: &users::Model) -> bool {
|
||||
admin_email(ctx).is_some_and(|email| user.email.eq_ignore_ascii_case(email))
|
||||
}
|
||||
|
||||
async fn current_admin(auth: auth::JWT, ctx: &AppContext) -> Result<users::Model> {
|
||||
let user = users_model::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;
|
||||
|
||||
if !is_admin(ctx, &user) {
|
||||
return unauthorized("admin only");
|
||||
}
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn dashboard(auth: auth::JWT, State(ctx): State<AppContext>) -> Result<Response> {
|
||||
current_admin(auth, &ctx).await?;
|
||||
|
||||
format::json(DashboardResponse {
|
||||
users: users::Entity::find().count(&ctx.db).await?,
|
||||
blog_articles: blog_articles::Entity::find().count(&ctx.db).await?,
|
||||
audio_albums: audio_albums::Entity::find().count(&ctx.db).await?,
|
||||
audio_tracks: audio_tracks::Entity::find().count(&ctx.db).await?,
|
||||
audit_logs: audit_logs::Entity::find().count(&ctx.db).await?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn routes() -> Routes {
|
||||
Routes::new()
|
||||
.prefix("/api/admin")
|
||||
.add("/dashboard", get(dashboard))
|
||||
}
|
||||
@@ -6,12 +6,15 @@ use crate::{
|
||||
},
|
||||
views::auth::{CurrentResponse, LoginResponse},
|
||||
};
|
||||
use axum_extra::extract::cookie::{Cookie, SameSite};
|
||||
use loco_rs::prelude::*;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::OnceLock;
|
||||
use time::Duration as TimeDuration;
|
||||
|
||||
pub static EMAIL_DOMAIN_RE: OnceLock<Regex> = OnceLock::new();
|
||||
const AUTH_COOKIE: &str = "auth_token";
|
||||
|
||||
fn get_allow_email_domain_re() -> &'static Regex {
|
||||
EMAIL_DOMAIN_RE.get_or_init(|| {
|
||||
@@ -19,6 +22,36 @@ fn get_allow_email_domain_re() -> &'static Regex {
|
||||
})
|
||||
}
|
||||
|
||||
fn admin_email(ctx: &AppContext) -> Option<&str> {
|
||||
ctx.config
|
||||
.settings
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.get("admin_email"))
|
||||
.and_then(|email| email.as_str())
|
||||
}
|
||||
|
||||
fn is_admin(ctx: &AppContext, user: &users::Model) -> bool {
|
||||
admin_email(ctx).is_some_and(|email| user.email.eq_ignore_ascii_case(email))
|
||||
}
|
||||
|
||||
fn auth_cookie(token: &str, max_age_seconds: u64) -> Cookie<'static> {
|
||||
Cookie::build((AUTH_COOKIE, token.to_string()))
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
.same_site(SameSite::Lax)
|
||||
.max_age(TimeDuration::seconds(max_age_seconds as i64))
|
||||
.build()
|
||||
}
|
||||
|
||||
fn clear_auth_cookie() -> Cookie<'static> {
|
||||
Cookie::build((AUTH_COOKIE, ""))
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
.same_site(SameSite::Lax)
|
||||
.max_age(TimeDuration::seconds(0))
|
||||
.build()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ForgotParams {
|
||||
pub email: String,
|
||||
@@ -155,13 +188,20 @@ async fn login(State(ctx): State<AppContext>, Json(params): Json<LoginParams>) -
|
||||
.generate_jwt(&jwt_secret.secret, jwt_secret.expiration)
|
||||
.or_else(|_| unauthorized("unauthorized!"))?;
|
||||
|
||||
format::json(LoginResponse::new(&user, &token))
|
||||
format::render()
|
||||
.cookies(&[auth_cookie(&token, jwt_secret.expiration)])?
|
||||
.json(LoginResponse::new(&user, &token, is_admin(&ctx, &user)))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn current(auth: auth::JWT, State(ctx): State<AppContext>) -> Result<Response> {
|
||||
let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;
|
||||
format::json(CurrentResponse::new(&user))
|
||||
format::json(CurrentResponse::new(&user, is_admin(&ctx, &user)))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn logout() -> Result<Response> {
|
||||
format::render().cookies(&[clear_auth_cookie()])?.json(())
|
||||
}
|
||||
|
||||
/// Magic link authentication provides a secure and passwordless way to log in to the application.
|
||||
@@ -223,7 +263,9 @@ async fn magic_link_verify(
|
||||
.generate_jwt(&jwt_secret.secret, jwt_secret.expiration)
|
||||
.or_else(|_| unauthorized("unauthorized!"))?;
|
||||
|
||||
format::json(LoginResponse::new(&user, &token))
|
||||
format::render()
|
||||
.cookies(&[auth_cookie(&token, jwt_secret.expiration)])?
|
||||
.json(LoginResponse::new(&user, &token, is_admin(&ctx, &user)))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
@@ -264,6 +306,7 @@ pub fn routes() -> Routes {
|
||||
.add("/register", post(register))
|
||||
.add("/verify/{token}", get(verify))
|
||||
.add("/login", post(login))
|
||||
.add("/logout", post(logout))
|
||||
.add("/forgot", post(forgot))
|
||||
.add("/reset", post(reset))
|
||||
.add("/current", get(current))
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod admin;
|
||||
pub mod auth;
|
||||
|
||||
@@ -8,5 +8,4 @@ pub mod audio_track_tags;
|
||||
pub mod audio_tracks;
|
||||
pub mod audit_logs;
|
||||
pub mod blog_articles;
|
||||
pub mod user_roles;
|
||||
pub mod users;
|
||||
|
||||
@@ -6,5 +6,4 @@ pub use super::audio_track_tags::Entity as AudioTrackTags;
|
||||
pub use super::audio_tracks::Entity as AudioTracks;
|
||||
pub use super::audit_logs::Entity as AuditLogs;
|
||||
pub use super::blog_articles::Entity as BlogArticles;
|
||||
pub use super::user_roles::Entity as UserRoles;
|
||||
pub use super::users::Entity as Users;
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "user_roles")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub user_id: i32,
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub role: String,
|
||||
pub assigned_by: Option<i32>,
|
||||
pub assigned_at: DateTimeWithTimeZone,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::users::Entity",
|
||||
from = "Column::AssignedBy",
|
||||
to = "super::users::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "SetNull"
|
||||
)]
|
||||
Users2,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::users::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::users::Column::Id",
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Users1,
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
pub mod _entities;
|
||||
pub mod users;
|
||||
pub mod user_roles;
|
||||
pub mod audio_tags;
|
||||
pub mod audio_tracks;
|
||||
pub mod audio_track_tags;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
pub use super::_entities::user_roles::{ActiveModel, Model, Entity};
|
||||
pub type UserRoles = 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,
|
||||
{
|
||||
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 {}
|
||||
@@ -8,16 +8,18 @@ pub struct LoginResponse {
|
||||
pub pid: String,
|
||||
pub name: String,
|
||||
pub is_verified: bool,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
impl LoginResponse {
|
||||
#[must_use]
|
||||
pub fn new(user: &users::Model, token: &String) -> Self {
|
||||
pub fn new(user: &users::Model, token: &String, is_admin: bool) -> Self {
|
||||
Self {
|
||||
token: token.to_string(),
|
||||
pid: user.pid.to_string(),
|
||||
name: user.name.clone(),
|
||||
is_verified: user.email_verified_at.is_some(),
|
||||
is_admin,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,15 +29,17 @@ pub struct CurrentResponse {
|
||||
pub pid: String,
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
impl CurrentResponse {
|
||||
#[must_use]
|
||||
pub fn new(user: &users::Model) -> Self {
|
||||
pub fn new(user: &users::Model, is_admin: bool) -> Self {
|
||||
Self {
|
||||
pid: user.pid.to_string(),
|
||||
name: user.name.clone(),
|
||||
email: user.email.clone(),
|
||||
is_admin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user