feat: add permission control

This commit is contained in:
2025-02-22 20:26:14 +08:00
parent ae40a3a7f8
commit c2f74dc369
33 changed files with 707 additions and 226 deletions

View File

@@ -1,7 +1,13 @@
use async_trait::async_trait;
use sea_orm::entity::prelude::*;
use loco_rs::{
app::AppContext,
model::{ModelError, ModelResult},
};
use sea_orm::{Set, TransactionTrait, entity::prelude::*};
use serde::{Deserialize, Serialize};
use super::subscribers::{self, SEED_SUBSCRIBER};
#[derive(
Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay, Serialize, Deserialize,
)]
@@ -17,14 +23,16 @@ pub enum AuthType {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, DeriveEntityModel)]
#[sea_orm(table_name = "auth")]
pub struct Model {
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub created_at: DateTime,
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub pid: String,
pub subscriber_id: i32,
pub auth_type: AuthType,
pub avatar_url: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@@ -47,3 +55,52 @@ impl Related<super::subscribers::Entity> for Entity {
#[async_trait]
impl ActiveModelBehavior for ActiveModel {}
impl Model {
pub async fn find_by_pid(ctx: &AppContext, pid: &str) -> ModelResult<Self> {
let db = &ctx.db;
let subscriber_auth = Entity::find()
.filter(Column::Pid.eq(pid))
.one(db)
.await?
.ok_or_else(|| ModelError::EntityNotFound)?;
Ok(subscriber_auth)
}
pub async fn create_from_oidc(ctx: &AppContext, sub: String) -> ModelResult<Self> {
let db = &ctx.db;
let txn = db.begin().await?;
let subscriber_id = if let Some(seed_subscriber_id) = Entity::find()
.filter(
Column::AuthType
.eq(AuthType::Basic)
.and(Column::Pid.eq(SEED_SUBSCRIBER)),
)
.one(&txn)
.await?
.map(|m| m.subscriber_id)
{
seed_subscriber_id
} else {
let new_subscriber = subscribers::ActiveModel {
..Default::default()
};
let new_subscriber: subscribers::Model = new_subscriber.save(&txn).await?.try_into()?;
new_subscriber.id
};
let new_item = ActiveModel {
pid: Set(sub),
auth_type: Set(AuthType::Oidc),
subscriber_id: Set(subscriber_id),
..Default::default()
};
let new_item: Model = new_item.save(&txn).await?.try_into()?;
Ok(new_item)
}
}

View File

@@ -1,7 +1,7 @@
use async_graphql::SimpleObject;
use async_trait::async_trait;
use loco_rs::app::AppContext;
use sea_orm::{entity::prelude::*, sea_query::OnConflict, ActiveValue, FromJsonQueryResult};
use sea_orm::{ActiveValue, FromJsonQueryResult, entity::prelude::*, sea_query::OnConflict};
use serde::{Deserialize, Serialize};
use super::subscription_bangumi;
@@ -9,7 +9,6 @@ use super::subscription_bangumi;
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult, SimpleObject,
)]
#[graphql(name = "BangumiFilter")]
pub struct BangumiFilter {
pub name: Option<Vec<String>>,
pub group: Option<Vec<String>>,
@@ -18,7 +17,6 @@ pub struct BangumiFilter {
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult, SimpleObject,
)]
#[graphql(name = "BangumiExtra")]
pub struct BangumiExtra {
pub name_zh: Option<String>,
pub s_name_zh: Option<String>,
@@ -30,14 +28,14 @@ pub struct BangumiExtra {
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, SimpleObject)]
#[sea_orm(table_name = "bangumi")]
#[graphql(name = "Bangumi")]
pub struct Model {
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub created_at: DateTime,
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
pub mikan_bangumi_id: Option<String>,
#[graphql(default_with = "default_subscriber_id")]
pub subscriber_id: i32,
pub display_name: String,
pub raw_name: String,

View File

@@ -22,9 +22,9 @@ pub enum DownloaderCategory {
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "downloaders")]
pub struct Model {
#[sea_orm(column_type = "Timestamp")]
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub created_at: DateTime,
#[sea_orm(column_type = "Timestamp")]
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,

View File

@@ -38,7 +38,9 @@ pub enum DownloadMime {
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "downloads")]
pub struct Model {
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub created_at: DateTime,
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,

View File

@@ -2,14 +2,14 @@ use std::sync::Arc;
use async_trait::async_trait;
use loco_rs::app::AppContext;
use sea_orm::{entity::prelude::*, sea_query::OnConflict, ActiveValue, FromJsonQueryResult};
use sea_orm::{ActiveValue, FromJsonQueryResult, entity::prelude::*, sea_query::OnConflict};
use serde::{Deserialize, Serialize};
use super::{bangumi, query::InsertManyReturningExt, subscription_episode};
use crate::{
app::AppContextExt,
extract::{
mikan::{build_mikan_episode_homepage, MikanEpisodeMeta},
mikan::{MikanEpisodeMeta, build_mikan_episode_homepage},
rawname::parse_episode_meta_from_raw_name,
},
};
@@ -27,7 +27,9 @@ pub struct EpisodeExtra {
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "episodes")]
pub struct Model {
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub created_at: DateTime,
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
@@ -135,6 +137,7 @@ pub struct MikanEpsiodeCreation {
impl Model {
pub async fn add_episodes(
ctx: &AppContext,
subscriber_id: i32,
subscription_id: i32,
creations: impl IntoIterator<Item = MikanEpsiodeCreation>,
) -> color_eyre::eyre::Result<()> {
@@ -162,6 +165,7 @@ impl Model {
let insert_subscription_episode_links = inserted_episodes.into_iter().map(|episode_id| {
subscription_episode::ActiveModel::from_subscription_and_episode(
subscriber_id,
subscription_id,
episode_id,
)

View File

@@ -4,7 +4,7 @@ use loco_rs::{
app::AppContext,
model::{ModelError, ModelResult},
};
use sea_orm::{entity::prelude::*, ActiveValue, FromJsonQueryResult, TransactionTrait};
use sea_orm::{ActiveValue, FromJsonQueryResult, TransactionTrait, entity::prelude::*};
use serde::{Deserialize, Serialize};
pub const SEED_SUBSCRIBER: &str = "konobangu";
@@ -16,15 +16,15 @@ pub struct SubscriberBangumiConfig {
pub leading_group_tag: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, SimpleObject)]
#[sea_orm(table_name = "subscribers")]
pub struct Model {
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub created_at: DateTime,
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub pid: String,
pub display_name: String,
pub bangumi_conf: Option<SubscriberBangumiConfig>,
}
@@ -91,59 +91,22 @@ pub struct SubscriberIdParams {
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {
async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
if insert {
let mut this = self;
this.pid = ActiveValue::Set(Uuid::new_v4().to_string());
Ok(this)
} else {
Ok(self)
}
}
}
impl ActiveModelBehavior for ActiveModel {}
impl Model {
pub async fn find_by_pid(ctx: &AppContext, pid: &str) -> ModelResult<Self> {
let db = &ctx.db;
let parse_uuid = Uuid::parse_str(pid).map_err(|e| ModelError::Any(e.into()))?;
let subscriber = Entity::find()
.filter(Column::Pid.eq(parse_uuid))
.one(db)
.await?;
subscriber.ok_or_else(|| ModelError::EntityNotFound)
pub async fn find_seed_subscriber_id(ctx: &AppContext) -> ModelResult<i32> {
let subscriber_auth = crate::models::auth::Model::find_by_pid(ctx, SEED_SUBSCRIBER).await?;
Ok(subscriber_auth.subscriber_id)
}
pub async fn find_by_id(ctx: &AppContext, id: i32) -> ModelResult<Self> {
let db = &ctx.db;
let subscriber = Entity::find_by_id(id).one(db).await?;
subscriber.ok_or_else(|| ModelError::EntityNotFound)
}
pub async fn find_pid_by_id_with_cache(
ctx: &AppContext,
id: i32,
) -> color_eyre::eyre::Result<String> {
let db = &ctx.db;
let cache = &ctx.cache;
let pid = cache
.get_or_insert(&format!("subscriber-id2pid::{}", id), async {
let subscriber = Entity::find_by_id(id)
.one(db)
.await?
.ok_or_else(|| loco_rs::Error::string(&format!("No such pid for id {}", id)))?;
Ok(subscriber.pid)
})
.await?;
Ok(pid)
}
pub async fn find_root(ctx: &AppContext) -> ModelResult<Self> {
Self::find_by_pid(ctx, SEED_SUBSCRIBER).await
let subscriber = Entity::find_by_id(id)
.one(db)
.await?
.ok_or_else(|| ModelError::EntityNotFound)?;
Ok(subscriber)
}
pub async fn create_root(ctx: &AppContext) -> ModelResult<Self> {
@@ -152,7 +115,6 @@ impl Model {
let user = ActiveModel {
display_name: ActiveValue::set(SEED_SUBSCRIBER.to_string()),
pid: ActiveValue::set(SEED_SUBSCRIBER.to_string()),
..Default::default()
}
.insert(&txn)

View File

@@ -1,5 +1,5 @@
use async_trait::async_trait;
use sea_orm::{entity::prelude::*, ActiveValue};
use sea_orm::{ActiveValue, entity::prelude::*};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
@@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub subscriber_id: i32,
pub subscription_id: i32,
pub bangumi_id: i32,
}
@@ -55,8 +56,13 @@ pub enum RelatedEntity {
impl ActiveModelBehavior for ActiveModel {}
impl ActiveModel {
pub fn from_subscription_and_bangumi(subscription_id: i32, bangumi_id: i32) -> Self {
pub fn from_subscription_and_bangumi(
subscriber_id: i32,
subscription_id: i32,
bangumi_id: i32,
) -> Self {
Self {
subscriber_id: ActiveValue::Set(subscriber_id),
subscription_id: ActiveValue::Set(subscription_id),
bangumi_id: ActiveValue::Set(bangumi_id),
..Default::default()

View File

@@ -1,5 +1,5 @@
use async_trait::async_trait;
use sea_orm::{entity::prelude::*, ActiveValue};
use sea_orm::{ActiveValue, entity::prelude::*};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
@@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub subscriber_id: i32,
pub subscription_id: i32,
pub episode_id: i32,
}
@@ -55,8 +56,13 @@ pub enum RelatedEntity {
impl ActiveModelBehavior for ActiveModel {}
impl ActiveModel {
pub fn from_subscription_and_episode(subscription_id: i32, episode_id: i32) -> Self {
pub fn from_subscription_and_episode(
subscriber_id: i32,
subscription_id: i32,
episode_id: i32,
) -> Self {
Self {
subscriber_id: ActiveValue::Set(subscriber_id),
subscription_id: ActiveValue::Set(subscription_id),
episode_id: ActiveValue::Set(episode_id),
..Default::default()

View File

@@ -3,7 +3,7 @@ use std::{collections::HashSet, sync::Arc};
use async_trait::async_trait;
use itertools::Itertools;
use loco_rs::app::AppContext;
use sea_orm::{entity::prelude::*, ActiveValue};
use sea_orm::{ActiveValue, entity::prelude::*};
use serde::{Deserialize, Serialize};
use super::{bangumi, episodes, query::filter_values_in};
@@ -15,8 +15,8 @@ use crate::{
parse_mikan_bangumi_meta_from_mikan_homepage,
parse_mikan_episode_meta_from_mikan_homepage, parse_mikan_rss_channel_from_rss_link,
web_parser::{
parse_mikan_bangumi_poster_from_origin_poster_src_with_cache,
MikanBangumiPosterMeta,
parse_mikan_bangumi_poster_from_origin_poster_src_with_cache,
},
},
rawname::extract_season_from_title_body,
@@ -43,9 +43,9 @@ pub enum SubscriptionCategory {
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "subscriptions")]
pub struct Model {
#[sea_orm(column_type = "Timestamp")]
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub created_at: DateTime,
#[sea_orm(column_type = "Timestamp")]
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
@@ -325,6 +325,7 @@ impl Model {
);
episodes::Model::add_episodes(
ctx,
self.subscriber_id,
self.id,
new_ep_metas.into_iter().map(|item| MikanEpsiodeCreation {
episode: item,