mirror of
https://github.com/euzu/tuliprox.git
synced 2026-09-29 20:42:31 +02:00
role based content
This commit is contained in:
Generated
+1
-1
@@ -987,6 +987,7 @@ name = "frontend"
|
||||
version = "3.1.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"futures",
|
||||
@@ -997,7 +998,6 @@ dependencies = [
|
||||
"implicit-clone",
|
||||
"js-sys",
|
||||
"log",
|
||||
"paste",
|
||||
"prost",
|
||||
"regex",
|
||||
"reqwasm",
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::api::model::{
|
||||
ProviderStreamFactoryOptions, ProviderStreamInfo, ProviderStreamResponse, SharedStreamManager,
|
||||
StreamError, ThrottledStream, UserApiRequest,
|
||||
};
|
||||
use crate::auth::Claims;
|
||||
use crate::model::ConfigInput;
|
||||
use crate::model::{ConfigTarget, ProxyUserCredentials};
|
||||
use crate::tools::atomic_once_flag::AtomicOnceFlag;
|
||||
@@ -25,10 +24,7 @@ use chrono::{DateTime, Utc};
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
||||
use log::{debug, error, log_enabled, trace};
|
||||
use shared::model::{
|
||||
InputFetchMethod, PlaylistEntry, PlaylistItemType, TargetType, UserConnectionPermission,
|
||||
XtreamCluster,
|
||||
};
|
||||
use shared::model::{Claims, InputFetchMethod, PlaylistEntry, PlaylistItemType, TargetType, UserConnectionPermission, XtreamCluster};
|
||||
use shared::utils::{default_grace_period_millis, human_readable_byte_size, trim_slash};
|
||||
use shared::utils::{
|
||||
extract_extension_from_url, replace_url_extension, sanitize_sensitive_info, DASH_EXT, HLS_EXT,
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::auth::{create_jwt_admin, create_jwt_user, is_admin, verify_password,
|
||||
use axum::response::IntoResponse;
|
||||
use log::error;
|
||||
use serde_json::json;
|
||||
use shared::model::{TokenResponse, UserCredential};
|
||||
use shared::model::{TokenResponse, UserCredential, TOKEN_NO_AUTH};
|
||||
use shared::utils::{concat_path_leading_slash, CONSTANTS};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
@@ -21,7 +21,7 @@ use lol_html::{element, RewriteStrSettings};
|
||||
|
||||
fn no_web_auth_token() -> impl axum::response::IntoResponse + Send {
|
||||
axum::Json(TokenResponse {
|
||||
token: "authorized".to_string(),
|
||||
token: TOKEN_NO_AUTH.to_string(),
|
||||
username: "admin".to_string(),
|
||||
}).into_response()
|
||||
}
|
||||
|
||||
@@ -6,18 +6,7 @@ use crate::model::WebAuthConfig;
|
||||
use crate::api::model::AppState;
|
||||
use crate::auth::AuthBearer;
|
||||
use shared::error::to_io_error;
|
||||
|
||||
const ROLE_ADMIN: &str = "ADMIN";
|
||||
const ROLE_USER: &str = "USER";
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Claims {
|
||||
pub(crate) username: String,
|
||||
iss: String,
|
||||
iat: i64,
|
||||
exp: i64,
|
||||
roles: Vec<String>,
|
||||
}
|
||||
use shared::model::{Claims, ROLE_ADMIN, ROLE_USER};
|
||||
|
||||
pub fn create_jwt_admin(web_auth_config: &WebAuthConfig, username: &str) -> Result<String, std::io::Error> {
|
||||
create_jwt(web_auth_config, username, vec![ROLE_ADMIN.to_string()])
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ prost = "0"
|
||||
wasm-bindgen-futures = "0"
|
||||
bytes = "1"
|
||||
regex = "1.11.1"
|
||||
paste = "1.0.15"
|
||||
base64 = "0.22.1"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"tabTitle": "tuliprox",
|
||||
"appTitle": "Tuliprox",
|
||||
"tabTitle": "Tuliprox",
|
||||
"appLogo": "/assets/tuliprox-logo.svg",
|
||||
"api": {
|
||||
"apiUrl": "/api/v1/",
|
||||
|
||||
@@ -67,4 +67,5 @@
|
||||
@forward "components/config/config_view";
|
||||
@forward "components/tabset";
|
||||
@forward "components/select";
|
||||
@forward "components/form";
|
||||
@forward "components/form";
|
||||
@forward "components/api_user/api_user_view";
|
||||
@@ -0,0 +1,3 @@
|
||||
div {
|
||||
color: var(--text-color);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use yew::{function_component, html, Callback, Html};
|
||||
use crate::app::components::loading_indicator::BusyIndicator;
|
||||
use crate::app::components::{IconButton, ToastrView};
|
||||
use crate::hooks::use_service_context;
|
||||
use crate::provider::DialogProvider;
|
||||
|
||||
#[function_component]
|
||||
pub fn ApiUserView() -> Html {
|
||||
let services = use_service_context();
|
||||
|
||||
let handle_logout = {
|
||||
let services_ctx = services.clone();
|
||||
Callback::from(move |_| services_ctx.auth.logout())
|
||||
};
|
||||
|
||||
html! {
|
||||
<DialogProvider>
|
||||
<ToastrView />
|
||||
<div class="tp__app">
|
||||
<BusyIndicator />
|
||||
|
||||
<div class="tp__app-main">
|
||||
<div class="tp__app-main__header tp__app-header">
|
||||
<div class="tp__app-main__header-left">
|
||||
{
|
||||
if let Some(ref title) = services.config.ui_config.app_title {
|
||||
html! { title.as_str() }
|
||||
} else {
|
||||
html! { /*<AppIcon name="AppTitle" /> */ }
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class={"tp__app-header-toolbar"}>
|
||||
<IconButton name="Logout" icon="Logout" onclick={handle_logout} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="tp__app-main__body">
|
||||
{" TODO "}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogProvider>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod api_user_view;
|
||||
|
||||
pub use api_user_view::*;
|
||||
@@ -61,6 +61,7 @@ pub fn Login() -> Html {
|
||||
let login = do_login.clone();
|
||||
Callback::from(move |e: KeyboardEvent| {
|
||||
if e.key() == "Enter" {
|
||||
e.prevent_default();
|
||||
login.emit(());
|
||||
}
|
||||
})
|
||||
|
||||
@@ -38,6 +38,8 @@ mod tabset;
|
||||
mod select;
|
||||
mod number_input;
|
||||
mod date_input;
|
||||
mod role_based_content;
|
||||
mod api_user;
|
||||
// pub use self::input::*;
|
||||
// pub use self::menu_item::*;
|
||||
// pub use self::popup_menu::*;
|
||||
@@ -74,5 +76,6 @@ pub use self::accordion::*;
|
||||
pub use self::accordion_panel::*;
|
||||
pub use self::csv_table::*;
|
||||
pub use self::tabset::*;
|
||||
pub use self::role_based_content::*;
|
||||
//pub use self::number_input::*;
|
||||
//pub use self::date_input::*;
|
||||
@@ -0,0 +1,18 @@
|
||||
use yew::prelude::*;
|
||||
use yew_router::Switch;
|
||||
use crate::app::{switch, AppRoute};
|
||||
use crate::app::components::api_user::ApiUserView;
|
||||
use crate::hooks::use_service_context;
|
||||
|
||||
#[function_component]
|
||||
pub fn RoleBasedContent() -> Html {
|
||||
let services = use_service_context();
|
||||
|
||||
if services.auth.is_admin() {
|
||||
html! { <Switch<AppRoute> render={switch} /> }
|
||||
} else if services.auth.is_user() {
|
||||
html! { <ApiUserView /> }
|
||||
} else {
|
||||
html! { "Not authorized" }
|
||||
}
|
||||
}
|
||||
+14
-4
@@ -6,13 +6,14 @@ use std::rc::Rc;
|
||||
use futures::future::join_all;
|
||||
use log::error;
|
||||
use serde_json::Value;
|
||||
use web_sys::window;
|
||||
use crate::provider::IconContextProvider;
|
||||
use crate::provider::ServiceContextProvider;
|
||||
use yew_i18n::I18nProvider;
|
||||
use yew::prelude::*;
|
||||
use yew_hooks::{use_async_with_options, UseAsyncOptions};
|
||||
use yew_router::prelude::*;
|
||||
use crate::app::components::{Authentication, Home, Login};
|
||||
use crate::app::components::{Authentication, Home, Login, RoleBasedContent};
|
||||
use crate::error::Error;
|
||||
use crate::hooks::{IconDefinition};
|
||||
use crate::model::WebConfig;
|
||||
@@ -101,8 +102,17 @@ pub fn App() -> Html {
|
||||
{
|
||||
let config_state = configuration_state.clone();
|
||||
use_async_with_options::<_, (), Error>(async move {
|
||||
match request_get("config.json", None, None).await {
|
||||
Ok(cfg) => config_state.set(Some(cfg)),
|
||||
match request_get::<WebConfig>("config.json", None, None).await {
|
||||
Ok(cfg) => {
|
||||
if let Some(tab_title) = cfg.tab_title.as_deref() {
|
||||
if let Some(win) = window() {
|
||||
if let Some(doc) = win.document() {
|
||||
doc.set_title(tab_title);
|
||||
}
|
||||
}
|
||||
}
|
||||
config_state.set(Some(cfg));
|
||||
},
|
||||
Err(err) => error!("Failed to load config {err}"),
|
||||
}
|
||||
Ok(())
|
||||
@@ -134,7 +144,7 @@ pub fn App() -> Html {
|
||||
<IconContextProvider icons={icons.clone()}>
|
||||
<I18nProvider supported_languages={supported_languages} translations={transl.clone()}>
|
||||
<Authentication>
|
||||
<Switch<AppRoute> render={switch} />
|
||||
<RoleBasedContent />
|
||||
</Authentication>
|
||||
</I18nProvider>
|
||||
</IconContextProvider>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::rc::Rc;
|
||||
use yew::prelude::*;
|
||||
use crate::model::WebConfig;
|
||||
use crate::services::{AuthService, ConfigService, EventService, PlaylistService, StatusService, ToastrService, UserService, WebSocketService};
|
||||
use crate::services::{AuthService, ConfigService, EventService, PlaylistService, StatusService, ToastrService,
|
||||
UserService, WebSocketService};
|
||||
|
||||
pub struct Services {
|
||||
pub auth: Rc<AuthService>,
|
||||
@@ -15,9 +16,9 @@ pub struct Services {
|
||||
}
|
||||
|
||||
impl Services {
|
||||
pub fn new(config: &WebConfig) -> Self {
|
||||
pub fn new(web_config: &WebConfig) -> Self {
|
||||
let config = Rc::new(ConfigService::new(web_config));
|
||||
let auth = Rc::new(AuthService::new());
|
||||
let config = Rc::new(ConfigService::new(config));
|
||||
let status = Rc::new(StatusService::new());
|
||||
let event = Rc::new(EventService::new());
|
||||
let playlist = Rc::new(PlaylistService::new());
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
use std::cell::RefCell;
|
||||
use super::{get_base_href, request_post};
|
||||
use super::{get_base_href, request_post, ConfigService};
|
||||
use crate::error::Error;
|
||||
use crate::services::requests::set_token;
|
||||
use futures_signals::signal::Mutable;
|
||||
use futures_signals::signal::SignalExt;
|
||||
use shared::model::{TokenResponse, UserCredential};
|
||||
use shared::model::{Claims, TokenResponse, UserCredential, ROLE_ADMIN, ROLE_USER, TOKEN_NO_AUTH};
|
||||
use std::future::Future;
|
||||
use shared::utils::{concat_path, concat_path_leading_slash};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use log::warn;
|
||||
|
||||
fn decode_jwt_payload(token: &str) -> Option<Claims> {
|
||||
let payload_enc = token.split('.').nth(1)?;
|
||||
let payload_bytes = general_purpose::URL_SAFE_NO_PAD.decode(payload_enc).ok()?;
|
||||
serde_json::from_slice::<Claims>(&payload_bytes).ok()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthService {
|
||||
auth_path: String,
|
||||
username: RefCell<String>,
|
||||
roles: RefCell<Vec<String>>,
|
||||
auth_channel: Mutable<bool>,
|
||||
}
|
||||
|
||||
@@ -22,12 +30,24 @@ impl AuthService {
|
||||
auth_path: concat_path_leading_slash(&base_href, "auth"),
|
||||
username: RefCell::new(String::new()),
|
||||
auth_channel: Mutable::new(false),
|
||||
roles: RefCell::new(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_username(&self) -> String {
|
||||
self.username.borrow().to_string()
|
||||
}
|
||||
pub fn is_admin(&self) -> bool {
|
||||
self.roles.borrow().iter().any(|r| r == ROLE_ADMIN)
|
||||
}
|
||||
|
||||
pub fn is_user(&self) -> bool {
|
||||
self.roles.borrow().iter().any(|r| r == ROLE_USER)
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
self.auth_channel.get()
|
||||
}
|
||||
|
||||
pub async fn auth_subscribe<F, U>(&self, callback: &mut F)
|
||||
where
|
||||
@@ -54,6 +74,7 @@ impl AuthService {
|
||||
self.username.replace(token.username.to_string());
|
||||
self.auth_channel.set(true);
|
||||
set_token(Some(&token.token));
|
||||
self.handle_token(&token.token);
|
||||
Ok(token)
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -71,6 +92,7 @@ impl AuthService {
|
||||
self.username.replace(token.username.to_string());
|
||||
self.auth_channel.set(true);
|
||||
set_token(Some(&token.token));
|
||||
self.handle_token(&token.token);
|
||||
Ok(token)
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -81,10 +103,21 @@ impl AuthService {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AuthService {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
fn handle_token(&self, token: &str) {
|
||||
let mut roles = self.roles.borrow_mut();
|
||||
roles.clear();
|
||||
|
||||
if token == TOKEN_NO_AUTH {
|
||||
roles.push(ROLE_ADMIN.to_string());
|
||||
}
|
||||
|
||||
if let Some(claims) = decode_jwt_payload(token) {
|
||||
for role in claims.roles.iter() {
|
||||
roles.push(role.clone());
|
||||
}
|
||||
} else {
|
||||
warn!("no claims");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
use zeroize::Zeroize;
|
||||
|
||||
pub const TOKEN_NO_AUTH: &str = "authorized";
|
||||
|
||||
pub const ROLE_ADMIN: &str = "ADMIN";
|
||||
pub const ROLE_USER: &str = "USER";
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Claims {
|
||||
pub username: String,
|
||||
pub iss: String,
|
||||
pub iat: i64,
|
||||
pub exp: i64,
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UserCredential {
|
||||
pub username: String,
|
||||
|
||||
Reference in New Issue
Block a user