fix: add basic auth and oidc auth
This commit is contained in:
parent
4c6cc1116b
commit
abd399aacd
@ -1,6 +1,6 @@
|
|||||||
[alias]
|
[alias]
|
||||||
recorder = "run -p recorder --bin recorder_cli -- --environment recorder/development"
|
recorder = "run -p recorder --bin recorder_cli -- --environment development"
|
||||||
recorder-playground = "run -p recorder --example playground -- --environment recorder/development"
|
recorder-playground = "run -p recorder --example playground -- --environment development"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
rustflags = ["-Zthreads=8"]
|
rustflags = ["-Zthreads=8"]
|
||||||
|
106
Cargo.lock
generated
106
Cargo.lock
generated
@ -299,6 +299,18 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-auth"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8169113a185f54f68614fcfc3581df585d30bf8542bcb99496990e1025e4120a"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"axum-core",
|
||||||
|
"base64 0.21.7",
|
||||||
|
"http 1.2.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-core"
|
name = "axum-core"
|
||||||
version = "0.4.5"
|
version = "0.4.5"
|
||||||
@ -1864,6 +1876,30 @@ dependencies = [
|
|||||||
"hashbrown 0.14.5",
|
"hashbrown 0.14.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headers"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"bytes",
|
||||||
|
"headers-core",
|
||||||
|
"http 1.2.0",
|
||||||
|
"httpdate",
|
||||||
|
"mime",
|
||||||
|
"sha1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headers-core"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
|
||||||
|
dependencies = [
|
||||||
|
"http 1.2.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@ -2528,6 +2564,33 @@ dependencies = [
|
|||||||
"simple_asn1",
|
"simple_asn1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jwt-authorizer"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87d72c47f96cdd849689e1a417a2b7206399b238dd8e2b1dfcea16f27185d00e"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"chrono",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"headers",
|
||||||
|
"http 1.2.0",
|
||||||
|
"http-body-util",
|
||||||
|
"jsonwebtoken",
|
||||||
|
"pin-project",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tokio",
|
||||||
|
"tower-http 0.5.2",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jxl-bitstream"
|
name = "jxl-bitstream"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@ -2946,7 +3009,7 @@ dependencies = [
|
|||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml",
|
"toml",
|
||||||
"tower 0.4.13",
|
"tower 0.4.13",
|
||||||
"tower-http",
|
"tower-http 0.6.2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-appender",
|
"tracing-appender",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@ -3714,6 +3777,26 @@ dependencies = [
|
|||||||
"siphasher",
|
"siphasher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project"
|
||||||
|
version = "1.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project-internal",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-internal"
|
||||||
|
version = "1.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.92",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
@ -4094,6 +4177,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
|
"axum-auth",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"eyre",
|
"eyre",
|
||||||
@ -4101,6 +4185,7 @@ dependencies = [
|
|||||||
"html-escape",
|
"html-escape",
|
||||||
"insta",
|
"insta",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
|
"jwt-authorizer",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"leaky-bucket",
|
"leaky-bucket",
|
||||||
"lightningcss",
|
"lightningcss",
|
||||||
@ -6091,6 +6176,25 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"bytes",
|
||||||
|
"http 1.2.0",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-http"
|
name = "tower-http"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
# Server
|
# Server
|
||||||
BETTER_AUTH_SECRET="konobangu"
|
|
||||||
DATABASE_URL="postgres://konobangu:konobangu@127.0.0.1:5432/konobangu"
|
DATABASE_URL="postgres://konobangu:konobangu@127.0.0.1:5432/konobangu"
|
||||||
BETTERSTACK_API_KEY=""
|
BETTERSTACK_API_KEY=""
|
||||||
BETTERSTACK_URL=""
|
BETTERSTACK_URL=""
|
||||||
|
@ -1,5 +1,19 @@
|
|||||||
# Server
|
# Server
|
||||||
BETTER_AUTH_SECRET="konobangu"
|
AUTH_TYPE="basic" #
|
||||||
|
|
||||||
|
BASIC_USER="konobangu"
|
||||||
|
BASIC_PASSWORD="konobangu"
|
||||||
|
|
||||||
|
OIDC_PROVIDER_ENDPOINT="https://some-oidc-auth.com/oidc/.well-known/openid-configuration"
|
||||||
|
OIDC_CLIENT_ID=""
|
||||||
|
OIDC_CLIENT_SECRET=""
|
||||||
|
OIDC_API_ISSUER="https://some-oidc-auth.com/oidc"
|
||||||
|
OIDC_API_AUDIENCE="https://konobangu.com/api"
|
||||||
|
OIDC_ICON_URL=""
|
||||||
|
OIDC_EXTRA_SCOPE_REGEX=""
|
||||||
|
OIDC_EXTRA_CLAIM_KEY=""
|
||||||
|
OIDC_EXTRA_CLAIM_VALUE=""
|
||||||
|
|
||||||
DATABASE_URL="postgres://konobangu:konobangu@127.0.0.1:5432/konobangu"
|
DATABASE_URL="postgres://konobangu:konobangu@127.0.0.1:5432/konobangu"
|
||||||
BETTERSTACK_API_KEY=""
|
BETTERSTACK_API_KEY=""
|
||||||
BETTERSTACK_URL=""
|
BETTERSTACK_URL=""
|
||||||
|
@ -1,6 +1,20 @@
|
|||||||
# Server
|
# AUTH
|
||||||
BETTER_AUTH_SECRET=""
|
AUTH_TYPE="basic"
|
||||||
DATABASE_URL=""
|
|
||||||
|
NEXT_PUBLIC_OIDC_PROVIDER_ENDPOINT="https://some-oidc-auth.com/oidc/.well-known/openid-configuration"
|
||||||
|
NEXT_PUBLIC_OIDC_CLIENT_ID=""
|
||||||
|
NEXT_PUBLIC_OIDC_CLIENT_SECRET=""
|
||||||
|
NEXT_PUBLIC_OIDC_ICON_URL=""
|
||||||
|
OIDC_API_ISSUER="https://some-oidc-auth.com/oidc"
|
||||||
|
OIDC_API_AUDIENCE="https://konobangu.com/api"
|
||||||
|
OIDC_EXTRA_SCOPES="" # 如 "read:konobangu,write:konobangu"
|
||||||
|
OIDC_EXTRA_CLAIM_KEY=""
|
||||||
|
OIDC_EXTRA_CLAIM_VALUE=""
|
||||||
|
|
||||||
|
# DATABASE
|
||||||
|
DATABASE_URL="postgres://konobangu:konobangu@127.0.0.1:5432/konobangu"
|
||||||
|
|
||||||
|
# SERVER MISC
|
||||||
BETTERSTACK_API_KEY=""
|
BETTERSTACK_API_KEY=""
|
||||||
BETTERSTACK_URL=""
|
BETTERSTACK_URL=""
|
||||||
FLAGS_SECRET=""
|
FLAGS_SECRET=""
|
||||||
@ -8,8 +22,8 @@ ARCJET_KEY=""
|
|||||||
SVIX_TOKEN=""
|
SVIX_TOKEN=""
|
||||||
LIVEBLOCKS_SECRET=""
|
LIVEBLOCKS_SECRET=""
|
||||||
|
|
||||||
# Client
|
# WEBUI
|
||||||
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||||
NEXT_PUBLIC_WEB_URL="http://localhost:3001"
|
NEXT_PUBLIC_WEB_URL="http://localhost:3001"
|
||||||
NEXT_PUBLIC_DOCS_URL="http://localhost:3004"
|
NEXT_PUBLIC_DOCS_URL="http://localhost:3004"
|
||||||
NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3000"
|
NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="https://konobangu.com"
|
@ -57,6 +57,8 @@ reqwest-tracing = "0.5.5"
|
|||||||
scraper = "0.22.0"
|
scraper = "0.22.0"
|
||||||
leaky-bucket = "1.1.2"
|
leaky-bucket = "1.1.2"
|
||||||
serde_with = "3"
|
serde_with = "3"
|
||||||
|
jwt-authorizer = "0.15.0"
|
||||||
|
axum-auth = "0.7.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = "3"
|
serial_test = "3"
|
||||||
|
@ -12,7 +12,7 @@ use recorder::{
|
|||||||
extract::mikan::parse_mikan_rss_items_from_rss_link,
|
extract::mikan::parse_mikan_rss_items_from_rss_link,
|
||||||
migrations::Migrator,
|
migrations::Migrator,
|
||||||
models::{
|
models::{
|
||||||
subscribers::ROOT_SUBSCRIBER,
|
subscribers::SEED_SUBSCRIBER,
|
||||||
subscriptions::{self, SubscriptionCreateFromRssDto},
|
subscriptions::{self, SubscriptionCreateFromRssDto},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -15,6 +15,7 @@ use loco_rs::{
|
|||||||
use sea_orm::DatabaseConnection;
|
use sea_orm::DatabaseConnection;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
auth::service::AppAuthService,
|
||||||
controllers,
|
controllers,
|
||||||
dal::{AppDalClient, AppDalInitalizer},
|
dal::{AppDalClient, AppDalInitalizer},
|
||||||
extract::mikan::{client::AppMikanClientInitializer, AppMikanClient},
|
extract::mikan::{client::AppMikanClientInitializer, AppMikanClient},
|
||||||
@ -25,11 +26,15 @@ use crate::{
|
|||||||
|
|
||||||
pub trait AppContextExt {
|
pub trait AppContextExt {
|
||||||
fn get_dal_client(&self) -> &AppDalClient {
|
fn get_dal_client(&self) -> &AppDalClient {
|
||||||
AppDalClient::global()
|
AppDalClient::app_instance()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_mikan_client(&self) -> &AppMikanClient {
|
fn get_mikan_client(&self) -> &AppMikanClient {
|
||||||
AppMikanClient::global()
|
AppMikanClient::app_instance()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_auth_service(&self) -> &AppAuthService {
|
||||||
|
&AppAuthService::app_instance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
31
apps/recorder/src/auth/basic.rs
Normal file
31
apps/recorder/src/auth/basic.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use axum::{http::request::Parts, RequestPartsExt};
|
||||||
|
use axum_auth::AuthBasic;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
config::BasicAuthConfig,
|
||||||
|
errors::AuthError,
|
||||||
|
service::{AuthService, AuthUserInfo},
|
||||||
|
};
|
||||||
|
use crate::models::{auth::AuthType, subscribers::SEED_SUBSCRIBER};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BasicAuthService {
|
||||||
|
pub config: BasicAuthConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl AuthService for BasicAuthService {
|
||||||
|
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError> {
|
||||||
|
if let Ok(AuthBasic((found_user, found_password))) = request.extract().await {
|
||||||
|
if self.config.user == found_user
|
||||||
|
&& self.config.password == found_password.unwrap_or_default()
|
||||||
|
{
|
||||||
|
return Ok(AuthUserInfo {
|
||||||
|
user_pid: SEED_SUBSCRIBER.to_string(),
|
||||||
|
auth_type: AuthType::Basic,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(AuthError::BasicInvalidCredentials)
|
||||||
|
}
|
||||||
|
}
|
31
apps/recorder/src/auth/config.rs
Normal file
31
apps/recorder/src/auth/config.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use jwt_authorizer::OneOrArray;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct BasicAuthConfig {
|
||||||
|
#[serde(rename = "basic_user")]
|
||||||
|
pub user: String,
|
||||||
|
#[serde(rename = "basic_password")]
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct OidcAuthConfig {
|
||||||
|
#[serde(rename = "oidc_api_issuer")]
|
||||||
|
pub issuer: String,
|
||||||
|
#[serde(rename = "oidc_api_audience")]
|
||||||
|
pub audience: String,
|
||||||
|
#[serde(rename = "oidc_extra_scopes")]
|
||||||
|
pub extra_scopes: Option<OneOrArray<String>>,
|
||||||
|
#[serde(rename = "oidc_extra_claim_key")]
|
||||||
|
pub extra_claim_key: Option<String>,
|
||||||
|
#[serde(rename = "oidc_extra_claim_value")]
|
||||||
|
pub extra_claim_value: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub enum AppAuthConfig {
|
||||||
|
Basic(BasicAuthConfig),
|
||||||
|
Oidc(OidcAuthConfig),
|
||||||
|
}
|
35
apps/recorder/src/auth/errors.rs
Normal file
35
apps/recorder/src/auth/errors.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AuthError {
|
||||||
|
#[error(transparent)]
|
||||||
|
OidcInitError(#[from] jwt_authorizer::error::InitError),
|
||||||
|
#[error("Invalid credentials")]
|
||||||
|
BasicInvalidCredentials,
|
||||||
|
#[error(transparent)]
|
||||||
|
OidcJwtAuthError(#[from] jwt_authorizer::AuthError),
|
||||||
|
#[error("Extra scopes {expected} do not match found scopes {found}")]
|
||||||
|
OidcExtraScopesMatchError { expected: String, found: String },
|
||||||
|
#[error("Extra claim {key} does not match expected value {expected}, found {found}")]
|
||||||
|
OidcExtraClaimMatchError {
|
||||||
|
key: String,
|
||||||
|
expected: String,
|
||||||
|
found: String,
|
||||||
|
},
|
||||||
|
#[error("Extra claim {0} missing")]
|
||||||
|
OidcExtraClaimMissingError(String),
|
||||||
|
#[error("Audience {0} missing")]
|
||||||
|
OidcAudMissingError(String),
|
||||||
|
#[error("Subject missing")]
|
||||||
|
OidcSubMissingError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for AuthError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
(StatusCode::UNAUTHORIZED, self.to_string()).into_response()
|
||||||
|
}
|
||||||
|
}
|
9
apps/recorder/src/auth/mod.rs
Normal file
9
apps/recorder/src/auth/mod.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
pub mod basic;
|
||||||
|
pub mod config;
|
||||||
|
pub mod errors;
|
||||||
|
pub mod oidc;
|
||||||
|
pub mod service;
|
||||||
|
|
||||||
|
pub use config::{AppAuthConfig, BasicAuthConfig, OidcAuthConfig};
|
||||||
|
pub use errors::AuthError;
|
||||||
|
pub use service::{AppAuthService, AuthService, AuthUserInfo};
|
137
apps/recorder/src/auth/oidc.rs
Normal file
137
apps/recorder/src/auth/oidc.rs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use axum::http::request::Parts;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use jwt_authorizer::{authorizer::Authorizer, NumericDate, OneOrArray};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
config::OidcAuthConfig,
|
||||||
|
errors::AuthError,
|
||||||
|
service::{AuthService, AuthUserInfo},
|
||||||
|
};
|
||||||
|
use crate::models::auth::AuthType;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
|
pub struct OidcAuthClaims {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub iss: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub sub: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub aud: Option<OneOrArray<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub exp: Option<NumericDate>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub nbf: Option<NumericDate>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub iat: Option<NumericDate>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub jti: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub scope: Option<String>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub custom: HashMap<String, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OidcAuthClaims {
|
||||||
|
pub fn scopes(&self) -> std::str::Split<'_, char> {
|
||||||
|
self.scope.as_deref().unwrap_or_default().split(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_claim(&self, key: &str) -> Option<String> {
|
||||||
|
match key {
|
||||||
|
"iss" => self.iss.clone(),
|
||||||
|
"sub" => self.sub.clone(),
|
||||||
|
"aud" => self.aud.as_ref().map(|s| s.iter().join(",")),
|
||||||
|
"exp" => self.exp.clone().map(|s| s.0.to_string()),
|
||||||
|
"nbf" => self.nbf.clone().map(|s| s.0.to_string()),
|
||||||
|
"iat" => self.iat.clone().map(|s| s.0.to_string()),
|
||||||
|
"jti" => self.jti.clone(),
|
||||||
|
"scope" => self.scope.clone(),
|
||||||
|
key => self.custom.get(key).map(|s| s.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_claim(&self, key: &str) -> bool {
|
||||||
|
match key {
|
||||||
|
"iss" => self.iss.is_some(),
|
||||||
|
"sub" => self.sub.is_some(),
|
||||||
|
"aud" => self.aud.is_some(),
|
||||||
|
"exp" => self.exp.is_some(),
|
||||||
|
"nbf" => self.nbf.is_some(),
|
||||||
|
"iat" => self.iat.is_some(),
|
||||||
|
"jti" => self.jti.is_some(),
|
||||||
|
"scope" => self.scope.is_some(),
|
||||||
|
key => self.custom.contains_key(key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains_audience(&self, aud: &str) -> bool {
|
||||||
|
self.aud
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|arr| arr.iter().any(|s| s == aud))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OidcAuthService {
|
||||||
|
pub config: OidcAuthConfig,
|
||||||
|
pub authorizer: Authorizer<OidcAuthClaims>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl AuthService for OidcAuthService {
|
||||||
|
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError> {
|
||||||
|
let config = &self.config;
|
||||||
|
let token =
|
||||||
|
self.authorizer
|
||||||
|
.extract_token(&request.headers)
|
||||||
|
.ok_or(AuthError::OidcJwtAuthError(
|
||||||
|
jwt_authorizer::AuthError::MissingToken(),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let token_data = self.authorizer.check_auth(&token).await?;
|
||||||
|
let claims = token_data.claims;
|
||||||
|
if !claims.sub.as_deref().is_some_and(|s| !s.trim().is_empty()) {
|
||||||
|
return Err(AuthError::OidcSubMissingError);
|
||||||
|
}
|
||||||
|
if !claims.contains_audience(&config.audience) {
|
||||||
|
return Err(AuthError::OidcAudMissingError(config.audience.clone()));
|
||||||
|
}
|
||||||
|
if let Some(expected_scopes) = config.extra_scopes.as_ref() {
|
||||||
|
let found_scopes = claims.scopes().collect::<HashSet<_>>();
|
||||||
|
if !expected_scopes
|
||||||
|
.iter()
|
||||||
|
.all(|es| found_scopes.contains(&es as &str))
|
||||||
|
{
|
||||||
|
return Err(AuthError::OidcExtraScopesMatchError {
|
||||||
|
expected: expected_scopes.iter().join(","),
|
||||||
|
found: claims.scope.unwrap_or_default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(key) = config.extra_claim_key.as_ref() {
|
||||||
|
if !claims.has_claim(key) {
|
||||||
|
return Err(AuthError::OidcExtraClaimMissingError(key.clone()));
|
||||||
|
}
|
||||||
|
if let Some(value) = config.extra_claim_value.as_ref() {
|
||||||
|
if claims.get_claim(key).is_none_or(|v| &v != value) {
|
||||||
|
return Err(AuthError::OidcExtraClaimMatchError {
|
||||||
|
expected: value.clone(),
|
||||||
|
found: claims.get_claim(key).unwrap_or_default().to_string(),
|
||||||
|
key: key.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(AuthUserInfo {
|
||||||
|
user_pid: claims
|
||||||
|
.sub
|
||||||
|
.as_deref()
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.unwrap_or_else(|| unreachable!("sub should be present and validated")),
|
||||||
|
auth_type: AuthType::Oidc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
116
apps/recorder/src/auth/service.rs
Normal file
116
apps/recorder/src/auth/service.rs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::FromRequestParts,
|
||||||
|
http::request::Parts,
|
||||||
|
response::{IntoResponse as _, Response},
|
||||||
|
Extension,
|
||||||
|
};
|
||||||
|
use jwt_authorizer::{JwtAuthorizer, Validation};
|
||||||
|
use loco_rs::app::{AppContext, Initializer};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
basic::BasicAuthService,
|
||||||
|
errors::AuthError,
|
||||||
|
oidc::{OidcAuthClaims, OidcAuthService},
|
||||||
|
AppAuthConfig,
|
||||||
|
};
|
||||||
|
use crate::{app::AppContextExt as _, config::AppConfigExt, models::auth::AuthType};
|
||||||
|
|
||||||
|
pub struct AuthUserInfo {
|
||||||
|
pub user_pid: String,
|
||||||
|
pub auth_type: AuthType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for AuthUserInfo
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = Response;
|
||||||
|
|
||||||
|
async fn from_request_parts(req: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let Extension(ctx) = Extension::<AppContext>::from_request_parts(req, state)
|
||||||
|
.await
|
||||||
|
.expect("AppContext should be present");
|
||||||
|
|
||||||
|
let auth_service = ctx.get_auth_service();
|
||||||
|
|
||||||
|
auth_service
|
||||||
|
.extract_user_info(req)
|
||||||
|
.await
|
||||||
|
.map_err(|err| err.into_response())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait AuthService {
|
||||||
|
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum AppAuthService {
|
||||||
|
Basic(BasicAuthService),
|
||||||
|
Oidc(OidcAuthService),
|
||||||
|
}
|
||||||
|
|
||||||
|
static APP_AUTH_SERVICE: OnceCell<AppAuthService> = OnceCell::new();
|
||||||
|
|
||||||
|
impl AppAuthService {
|
||||||
|
pub fn app_instance() -> &'static Self {
|
||||||
|
APP_AUTH_SERVICE
|
||||||
|
.get()
|
||||||
|
.expect("AppAuthService is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn from_conf(config: AppAuthConfig) -> Result<Self, AuthError> {
|
||||||
|
let result = match config {
|
||||||
|
AppAuthConfig::Basic(config) => AppAuthService::Basic(BasicAuthService { config }),
|
||||||
|
AppAuthConfig::Oidc(config) => {
|
||||||
|
let validation = Validation::new()
|
||||||
|
.iss(&[&config.issuer])
|
||||||
|
.aud(&[&config.audience]);
|
||||||
|
|
||||||
|
let jwt_auth = JwtAuthorizer::<OidcAuthClaims>::from_oidc(&config.issuer)
|
||||||
|
.validation(validation)
|
||||||
|
.build()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
AppAuthService::Oidc(OidcAuthService {
|
||||||
|
config,
|
||||||
|
authorizer: jwt_auth,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl AuthService for AppAuthService {
|
||||||
|
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError> {
|
||||||
|
match self {
|
||||||
|
AppAuthService::Basic(service) => service.extract_user_info(request).await,
|
||||||
|
AppAuthService::Oidc(service) => service.extract_user_info(request).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppAuthServiceInitializer;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Initializer for AppAuthServiceInitializer {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
String::from("AppAuthServiceInitializer")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn before_run(&self, ctx: &AppContext) -> Result<(), loco_rs::Error> {
|
||||||
|
let auth_conf = ctx.config.get_app_conf()?.auth;
|
||||||
|
|
||||||
|
let service = AppAuthService::from_conf(auth_conf)
|
||||||
|
.await
|
||||||
|
.map_err(|e| loco_rs::Error::wrap(e))?;
|
||||||
|
|
||||||
|
APP_AUTH_SERVICE.get_or_init(|| service);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,13 @@
|
|||||||
use serde::de::DeserializeOwned;
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{auth::AppAuthConfig, dal::config::AppDalConfig, extract::mikan::AppMikanConfig};
|
||||||
dal::{config::AppDalConfig, DAL_CONF_KEY},
|
|
||||||
extract::mikan::{AppMikanConfig, MIKAN_CONF_KEY},
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
};
|
pub struct AppConfig {
|
||||||
|
pub auth: AppAuthConfig,
|
||||||
|
pub dal: Option<AppDalConfig>,
|
||||||
|
pub mikan: Option<AppMikanConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deserialize_key_path_from_json_value<T: DeserializeOwned>(
|
pub fn deserialize_key_path_from_json_value<T: DeserializeOwned>(
|
||||||
value: &serde_json::Value,
|
value: &serde_json::Value,
|
||||||
@ -37,12 +41,11 @@ pub fn deserialize_key_path_from_app_config<T: DeserializeOwned>(
|
|||||||
pub trait AppConfigExt {
|
pub trait AppConfigExt {
|
||||||
fn get_root_conf(&self) -> &loco_rs::config::Config;
|
fn get_root_conf(&self) -> &loco_rs::config::Config;
|
||||||
|
|
||||||
fn get_dal_conf(&self) -> loco_rs::Result<Option<AppDalConfig>> {
|
fn get_app_conf(&self) -> loco_rs::Result<AppConfig> {
|
||||||
deserialize_key_path_from_app_config(self.get_root_conf(), &[DAL_CONF_KEY])
|
Ok(
|
||||||
}
|
deserialize_key_path_from_app_config(self.get_root_conf(), &[])?
|
||||||
|
.expect("app config must be present"),
|
||||||
fn get_mikan_conf(&self) -> loco_rs::Result<Option<AppMikanConfig>> {
|
)
|
||||||
deserialize_key_path_from_app_config(self.get_root_conf(), &[MIKAN_CONF_KEY])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
0
apps/recorder/src/controllers/episodes.rs
Normal file
0
apps/recorder/src/controllers/episodes.rs
Normal file
0
apps/recorder/src/controllers/subscriptions.rs
Normal file
0
apps/recorder/src/controllers/subscriptions.rs
Normal file
@ -27,11 +27,6 @@ impl AsRef<str> for DalContentCategory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct AppDalClient {
|
|
||||||
pub config: AppDalConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
static APP_DAL_CLIENT: OnceCell<AppDalClient> = OnceCell::new();
|
static APP_DAL_CLIENT: OnceCell<AppDalClient> = OnceCell::new();
|
||||||
|
|
||||||
pub enum DalStoredUrl {
|
pub enum DalStoredUrl {
|
||||||
@ -54,15 +49,20 @@ impl fmt::Display for DalStoredUrl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AppDalClient {
|
||||||
|
pub config: AppDalConfig,
|
||||||
|
}
|
||||||
|
|
||||||
impl AppDalClient {
|
impl AppDalClient {
|
||||||
pub fn new(config: AppDalConfig) -> Self {
|
pub fn new(config: AppDalConfig) -> Self {
|
||||||
Self { config }
|
Self { config }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn global() -> &'static AppDalClient {
|
pub fn app_instance() -> &'static AppDalClient {
|
||||||
APP_DAL_CLIENT
|
APP_DAL_CLIENT
|
||||||
.get()
|
.get()
|
||||||
.expect("Global app dal client is not initialized")
|
.expect("AppDalClient is not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_fs(&self) -> Fs {
|
pub fn get_fs(&self) -> Fs {
|
||||||
@ -192,7 +192,7 @@ impl Initializer for AppDalInitalizer {
|
|||||||
|
|
||||||
async fn before_run(&self, app_context: &AppContext) -> loco_rs::Result<()> {
|
async fn before_run(&self, app_context: &AppContext) -> loco_rs::Result<()> {
|
||||||
let config = &app_context.config;
|
let config = &app_context.config;
|
||||||
let app_dal_conf = config.get_dal_conf()?;
|
let app_dal_conf = config.get_app_conf()?.dal;
|
||||||
|
|
||||||
APP_DAL_CLIENT.get_or_init(|| AppDalClient::new(app_dal_conf.unwrap_or_default()));
|
APP_DAL_CLIENT.get_or_init(|| AppDalClient::new(app_dal_conf.unwrap_or_default()));
|
||||||
|
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub const DAL_CONF_KEY: &str = "dal";
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
pub struct AppDalConfig {
|
pub struct AppDalConfig {
|
||||||
pub data_dir: Option<String>,
|
pub data_dir: Option<String>,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub use client::{AppDalClient, AppDalInitalizer, DalContentCategory};
|
pub use client::{AppDalClient, AppDalInitalizer, DalContentCategory};
|
||||||
pub use config::{AppDalConfig, DAL_CONF_KEY};
|
pub use config::AppDalConfig;
|
||||||
|
@ -26,10 +26,10 @@ impl AppMikanClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn global() -> &'static AppMikanClient {
|
pub fn app_instance() -> &'static AppMikanClient {
|
||||||
APP_MIKAN_CLIENT
|
APP_MIKAN_CLIENT
|
||||||
.get()
|
.get()
|
||||||
.expect("Global mikan http client is not initialized")
|
.expect("AppMikanClient is not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn base_url(&self) -> &str {
|
pub fn base_url(&self) -> &str {
|
||||||
@ -55,7 +55,7 @@ impl Initializer for AppMikanClientInitializer {
|
|||||||
|
|
||||||
async fn before_run(&self, app_context: &AppContext) -> loco_rs::Result<()> {
|
async fn before_run(&self, app_context: &AppContext) -> loco_rs::Result<()> {
|
||||||
let config = &app_context.config;
|
let config = &app_context.config;
|
||||||
let app_mikan_conf = config.get_mikan_conf()?.unwrap_or_default();
|
let app_mikan_conf = config.get_app_conf()?.mikan.unwrap_or_default();
|
||||||
|
|
||||||
APP_MIKAN_CLIENT.get_or_try_init(|| AppMikanClient::new(app_mikan_conf))?;
|
APP_MIKAN_CLIENT.get_or_try_init(|| AppMikanClient::new(app_mikan_conf))?;
|
||||||
|
|
||||||
|
@ -2,8 +2,6 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::fetch::HttpClientConfig;
|
use crate::fetch::HttpClientConfig;
|
||||||
|
|
||||||
pub const MIKAN_CONF_KEY: &str = "mikan";
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
pub struct AppMikanConfig {
|
pub struct AppMikanConfig {
|
||||||
pub http_client: Option<HttpClientConfig>,
|
pub http_client: Option<HttpClientConfig>,
|
||||||
|
@ -5,7 +5,7 @@ pub mod rss_parser;
|
|||||||
pub mod web_parser;
|
pub mod web_parser;
|
||||||
|
|
||||||
pub use client::{AppMikanClient, AppMikanClientInitializer};
|
pub use client::{AppMikanClient, AppMikanClientInitializer};
|
||||||
pub use config::{AppMikanConfig, MIKAN_CONF_KEY};
|
pub use config::AppMikanConfig;
|
||||||
pub use constants::{MIKAN_BASE_URL, MIKAN_BUCKET_KEY};
|
pub use constants::{MIKAN_BASE_URL, MIKAN_BUCKET_KEY};
|
||||||
pub use rss_parser::{
|
pub use rss_parser::{
|
||||||
build_mikan_bangumi_rss_link, build_mikan_subscriber_aggregation_rss_link,
|
build_mikan_bangumi_rss_link, build_mikan_subscriber_aggregation_rss_link,
|
||||||
|
@ -11,3 +11,4 @@ pub mod models;
|
|||||||
pub mod tasks;
|
pub mod tasks;
|
||||||
pub mod views;
|
pub mod views;
|
||||||
pub mod workers;
|
pub mod workers;
|
||||||
|
pub mod auth;
|
||||||
|
@ -107,6 +107,16 @@ pub enum Downloaders {
|
|||||||
SavePath,
|
SavePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
pub enum Auth {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Pid,
|
||||||
|
SubscriberId,
|
||||||
|
AvatarUrl,
|
||||||
|
AuthType,
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! create_postgres_enum_for_active_enum {
|
macro_rules! create_postgres_enum_for_active_enum {
|
||||||
($manager: expr, $active_enum: expr, $($enum_value:expr),+) => {
|
($manager: expr, $active_enum: expr, $($enum_value:expr),+) => {
|
||||||
{
|
{
|
||||||
|
@ -5,7 +5,7 @@ use super::defs::{
|
|||||||
Bangumi, CustomSchemaManagerExt, Episodes, GeneralIds, Subscribers, Subscriptions,
|
Bangumi, CustomSchemaManagerExt, Episodes, GeneralIds, Subscribers, Subscriptions,
|
||||||
};
|
};
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
subscribers::ROOT_SUBSCRIBER,
|
subscribers::SEED_SUBSCRIBER,
|
||||||
subscriptions::{self, SubscriptionCategoryEnum},
|
subscriptions::{self, SubscriptionCategoryEnum},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ impl MigrationTrait for Migration {
|
|||||||
let insert = Query::insert()
|
let insert = Query::insert()
|
||||||
.into_table(Subscribers::Table)
|
.into_table(Subscribers::Table)
|
||||||
.columns([Subscribers::Pid, Subscribers::DisplayName])
|
.columns([Subscribers::Pid, Subscribers::DisplayName])
|
||||||
.values_panic([ROOT_SUBSCRIBER.into(), ROOT_SUBSCRIBER.into()])
|
.values_panic([SEED_SUBSCRIBER.into(), SEED_SUBSCRIBER.into()])
|
||||||
.to_owned();
|
.to_owned();
|
||||||
manager.exec_stmt(insert).await?;
|
manager.exec_stmt(insert).await?;
|
||||||
|
|
||||||
|
83
apps/recorder/src/migrations/m20241231_000001_auth.rs
Normal file
83
apps/recorder/src/migrations/m20241231_000001_auth.rs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
|
use super::defs::Auth;
|
||||||
|
use crate::{
|
||||||
|
migrations::defs::{CustomSchemaManagerExt, GeneralIds, Subscribers},
|
||||||
|
models::auth::{AuthType, AuthTypeEnum},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
create_postgres_enum_for_active_enum!(
|
||||||
|
manager,
|
||||||
|
AuthTypeEnum,
|
||||||
|
AuthType::Basic,
|
||||||
|
AuthType::Oidc
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
table_auto(Auth::Table)
|
||||||
|
.col(pk_auto(Auth::Id))
|
||||||
|
.col(text(Auth::Pid))
|
||||||
|
.col(enumeration(
|
||||||
|
Auth::AuthType,
|
||||||
|
AuthTypeEnum,
|
||||||
|
AuthType::iden_values(),
|
||||||
|
))
|
||||||
|
.col(string_null(Auth::AvatarUrl))
|
||||||
|
.col(integer(Auth::SubscriberId))
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_auth_subscriber_id")
|
||||||
|
.from_tbl(Auth::Table)
|
||||||
|
.from_col(Auth::SubscriberId)
|
||||||
|
.to_tbl(Subscribers::Table)
|
||||||
|
.to_col(Subscribers::Id)
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Restrict),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_auth_pid_auth_type")
|
||||||
|
.unique()
|
||||||
|
.table(Auth::Table)
|
||||||
|
.col(Auth::Pid)
|
||||||
|
.col(Auth::AuthType)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_postgres_auto_update_ts_trigger_for_col(Auth::Table, GeneralIds::UpdatedAt)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_postgres_auto_update_ts_trigger_for_col(Auth::Table, GeneralIds::UpdatedAt)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(Auth::Table).to_owned())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.drop_postgres_enum_for_active_enum(AuthTypeEnum)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ pub mod defs;
|
|||||||
pub mod m20220101_000001_init;
|
pub mod m20220101_000001_init;
|
||||||
pub mod m20240224_082543_add_downloads;
|
pub mod m20240224_082543_add_downloads;
|
||||||
pub mod m20240225_060853_subscriber_add_downloader;
|
pub mod m20240225_060853_subscriber_add_downloader;
|
||||||
|
pub mod m20241231_000001_auth;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20220101_000001_init::Migration),
|
Box::new(m20220101_000001_init::Migration),
|
||||||
Box::new(m20240224_082543_add_downloads::Migration),
|
Box::new(m20240224_082543_add_downloads::Migration),
|
||||||
Box::new(m20240225_060853_subscriber_add_downloader::Migration),
|
Box::new(m20240225_060853_subscriber_add_downloader::Migration),
|
||||||
|
Box::new(m20241231_000001_auth::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
apps/recorder/src/models/auth.rs
Normal file
6
apps/recorder/src/models/auth.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
|
pub use super::entities::auth::*;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
43
apps/recorder/src/models/entities/auth.rs
Normal file
43
apps/recorder/src/models/entities/auth.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
|
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "auth_type")]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum AuthType {
|
||||||
|
#[sea_orm(string_value = "basic")]
|
||||||
|
Basic,
|
||||||
|
#[sea_orm(string_value = "oidc")]
|
||||||
|
Oidc,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, DeriveEntityModel)]
|
||||||
|
#[sea_orm(table_name = "auth")]
|
||||||
|
pub struct Model {
|
||||||
|
pub created_at: DateTime,
|
||||||
|
pub updated_at: DateTime,
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: i32,
|
||||||
|
pub pid: String,
|
||||||
|
pub subscriber_id: i32,
|
||||||
|
pub auth_type: AuthType,
|
||||||
|
pub avatar_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::subscribers::Entity",
|
||||||
|
from = "Column::SubscriberId",
|
||||||
|
to = "super::subscribers::Column::Id"
|
||||||
|
)]
|
||||||
|
SubscriberId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::subscribers::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::SubscriberId.def()
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4
|
||||||
|
pub mod auth;
|
||||||
pub mod bangumi;
|
pub mod bangumi;
|
||||||
|
pub mod downloaders;
|
||||||
pub mod downloads;
|
pub mod downloads;
|
||||||
pub mod episodes;
|
pub mod episodes;
|
||||||
pub mod subscribers;
|
pub mod subscribers;
|
||||||
pub mod subscriptions;
|
pub mod subscriptions;
|
||||||
pub mod downloaders;
|
|
||||||
|
@ -36,6 +36,8 @@ pub enum Relation {
|
|||||||
Bangumi,
|
Bangumi,
|
||||||
#[sea_orm(has_many = "super::episodes::Entity")]
|
#[sea_orm(has_many = "super::episodes::Entity")]
|
||||||
Episode,
|
Episode,
|
||||||
|
#[sea_orm(has_many = "super::auth::Entity")]
|
||||||
|
Auth,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Related<super::subscriptions::Entity> for Entity {
|
impl Related<super::subscriptions::Entity> for Entity {
|
||||||
@ -61,3 +63,9 @@ impl Related<super::episodes::Entity> for Entity {
|
|||||||
Relation::Episode.def()
|
Relation::Episode.def()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Related<super::auth::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Auth.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
pub mod auth;
|
||||||
pub mod bangumi;
|
pub mod bangumi;
|
||||||
pub mod downloaders;
|
pub mod downloaders;
|
||||||
pub mod downloads;
|
pub mod downloads;
|
||||||
|
@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
pub use super::entities::subscribers::*;
|
pub use super::entities::subscribers::*;
|
||||||
|
|
||||||
pub const ROOT_SUBSCRIBER: &str = "konobangu";
|
pub const SEED_SUBSCRIBER: &str = "konobangu";
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct SubscriberIdParams {
|
pub struct SubscriberIdParams {
|
||||||
@ -69,7 +69,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn find_root(ctx: &AppContext) -> ModelResult<Self> {
|
pub async fn find_root(ctx: &AppContext) -> ModelResult<Self> {
|
||||||
Self::find_by_pid(ctx, ROOT_SUBSCRIBER).await
|
Self::find_by_pid(ctx, SEED_SUBSCRIBER).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Asynchronously creates a user with a password and saves it to the
|
/// Asynchronously creates a user with a password and saves it to the
|
||||||
@ -83,8 +83,8 @@ impl Model {
|
|||||||
let txn = db.begin().await?;
|
let txn = db.begin().await?;
|
||||||
|
|
||||||
let user = ActiveModel {
|
let user = ActiveModel {
|
||||||
display_name: ActiveValue::set(ROOT_SUBSCRIBER.to_string()),
|
display_name: ActiveValue::set(SEED_SUBSCRIBER.to_string()),
|
||||||
pid: ActiveValue::set(ROOT_SUBSCRIBER.to_string()),
|
pid: ActiveValue::set(SEED_SUBSCRIBER.to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
.insert(&txn)
|
.insert(&txn)
|
||||||
|
@ -112,8 +112,10 @@ redis:
|
|||||||
dangerously_flush: false
|
dangerously_flush: false
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
|
|
||||||
dal:
|
dal:
|
||||||
data_dir: ./data
|
data_dir: ./data
|
||||||
|
|
||||||
mikan:
|
mikan:
|
||||||
http_client:
|
http_client:
|
||||||
exponential_backoff_max_retries: 3
|
exponential_backoff_max_retries: 3
|
||||||
@ -123,3 +125,13 @@ settings:
|
|||||||
leaky_bucket_refill_interval: 500
|
leaky_bucket_refill_interval: 500
|
||||||
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"
|
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"
|
||||||
base_url: "https://mikanani.me/"
|
base_url: "https://mikanani.me/"
|
||||||
|
|
||||||
|
auth:
|
||||||
|
auth_type: "oidc" # or "basic"
|
||||||
|
basic_user: "konobangu"
|
||||||
|
basic_password: "konobangu"
|
||||||
|
oidc_api_issuer: "https://some-oidc-auth.com/oidc"
|
||||||
|
oidc_api_audience: "https://konobangu.com/api"
|
||||||
|
oidc_extra_scopes: "read:konobangu,write:konobangu"
|
||||||
|
oidc_extra_claim_key: ""
|
||||||
|
oidc_extra_claim_value: ""
|
0
config/production.yaml
Normal file
0
config/production.yaml
Normal file
2
justfile
2
justfile
@ -13,7 +13,7 @@ dev-recorder:
|
|||||||
cargo watch -w apps/recorder -w config -x 'recorder start'
|
cargo watch -w apps/recorder -w config -x 'recorder start'
|
||||||
|
|
||||||
down-recorder:
|
down-recorder:
|
||||||
cargo run -p recorder --bin recorder_cli -- db down 999 --environment recorder/development
|
cargo run -p recorder --bin recorder_cli -- db down 999 --environment development
|
||||||
|
|
||||||
play-recorder:
|
play-recorder:
|
||||||
cargo recorder-playground
|
cargo recorder-playground
|
Loading…
Reference in New Issue
Block a user