mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
feat: support user appearance preferences with persisted theme and language #85
This commit is contained in:
@@ -28,7 +28,7 @@ use crate::{
|
||||
users::{
|
||||
permissions::Permission,
|
||||
role::{RoleType, UserRole},
|
||||
BichonUser,
|
||||
UserModel,
|
||||
},
|
||||
},
|
||||
raise_error, utc_now,
|
||||
@@ -68,7 +68,7 @@ impl BatchAccountRoleRequest {
|
||||
}
|
||||
|
||||
for id in &self.user_ids {
|
||||
let exists = BichonUser::find(*id).await?; // Assuming an exists helper
|
||||
let exists = UserModel::find(*id).await?; // Assuming an exists helper
|
||||
if exists.is_none() {
|
||||
return Err(raise_error!(
|
||||
format!("User ID {} not found", id),
|
||||
@@ -90,7 +90,7 @@ impl BatchAccountRoleRequest {
|
||||
// Fetch the current user record from the database
|
||||
let user = rw
|
||||
.get()
|
||||
.primary::<BichonUser>(uid)
|
||||
.primary::<UserModel>(uid)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
|
||||
.ok_or_else(|| {
|
||||
raise_error!(
|
||||
|
||||
@@ -36,7 +36,7 @@ use crate::{
|
||||
database::{list_all_impl, with_transaction},
|
||||
error::BichonResult,
|
||||
indexer::manager::{EML_INDEX_MANAGER, ENVELOPE_INDEX_MANAGER},
|
||||
users::{role::DEFAULT_ACCOUNT_MANAGER_ROLE_ID, BichonUser, DEFAULT_ADMIN_USER_ID},
|
||||
users::{role::DEFAULT_ACCOUNT_MANAGER_ROLE_ID, UserModel, DEFAULT_ADMIN_USER_ID},
|
||||
},
|
||||
utc_now,
|
||||
};
|
||||
@@ -238,7 +238,7 @@ impl AccountV3 {
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
let user = rw
|
||||
.get()
|
||||
.primary::<BichonUser>(user_id)
|
||||
.primary::<UserModel>(user_id)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
|
||||
.ok_or_else(|| {
|
||||
raise_error!(
|
||||
@@ -312,7 +312,7 @@ impl AccountV3 {
|
||||
MAIL_CONTEXT.clean_account(account.id).await?;
|
||||
}
|
||||
OAuth2AccessToken::try_delete(account.id).await?;
|
||||
BichonUser::cleanup_account(account.id).await?;
|
||||
UserModel::cleanup_account(account.id).await?;
|
||||
MailBox::clean(account.id).await?;
|
||||
ENVELOPE_INDEX_MANAGER
|
||||
.delete_account_envelopes(account.id)
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::modules::{
|
||||
migration::{AccountModel, AccountType},
|
||||
since::{DateSince, RelativeDate},
|
||||
},
|
||||
users::BichonUser,
|
||||
users::UserModel,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
|
||||
@@ -57,7 +57,7 @@ pub struct AccountResp {
|
||||
}
|
||||
|
||||
impl AccountResp {
|
||||
pub fn from_model(account: AccountModel, user_map: &HashMap<u64, BichonUser>) -> AccountResp {
|
||||
pub fn from_model(account: AccountModel, user_map: &HashMap<u64, UserModel>) -> AccountResp {
|
||||
let user = user_map.get(&account.created_by);
|
||||
AccountResp {
|
||||
id: account.id,
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::{
|
||||
modules::{
|
||||
error::{code::ErrorCode, BichonResult},
|
||||
token::AccessTokenModel,
|
||||
users::{permissions::Permission, role::UserRole, BichonUser},
|
||||
users::{permissions::Permission, role::UserRole, UserModel},
|
||||
utils::rate_limit::RATE_LIMITER_MANAGER,
|
||||
},
|
||||
raise_error,
|
||||
@@ -74,7 +74,7 @@ impl<E: Endpoint> Endpoint for ApiGuardEndpoint<E> {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ClientContext {
|
||||
pub ip_addr: Option<IpAddr>,
|
||||
pub user: BichonUser,
|
||||
pub user: UserModel,
|
||||
}
|
||||
|
||||
impl ClientContext {
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::modules::cache::imap::MAILBOX_MODELS;
|
||||
use crate::modules::error::{code::ErrorCode, BichonError};
|
||||
use crate::modules::settings::cli::SETTINGS;
|
||||
use crate::modules::settings::dir::DATA_DIR_MANAGER;
|
||||
use crate::modules::users::UserModel;
|
||||
use crate::modules::{database::META_MODELS, error::BichonResult};
|
||||
use crate::raise_error;
|
||||
use native_db::{Builder, Database};
|
||||
@@ -73,6 +74,8 @@ impl DatabaseManager {
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
rw.migrate::<AccountModel>()
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
rw.migrate::<UserModel>()
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
rw.commit()
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::modules::settings::proxy::Proxy;
|
||||
use crate::modules::settings::system::SystemSetting;
|
||||
use crate::modules::token::AccessTokenModel;
|
||||
use crate::modules::users::role::UserRole;
|
||||
use crate::modules::users::BichonUser;
|
||||
use crate::modules::users::{BichonUser, BichonUserV2};
|
||||
use crate::raise_error;
|
||||
use db_type::{KeyOptions, ToKeyDefinition};
|
||||
use itertools::Itertools;
|
||||
@@ -73,6 +73,7 @@ impl ModelsAdapter {
|
||||
self.register_model::<Proxy>();
|
||||
self.register_model::<UserRole>();
|
||||
self.register_model::<BichonUser>();
|
||||
self.register_model::<BichonUserV2>();
|
||||
self.register_model::<AccessTokenModel>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ use crate::modules::rest::api::ApiTags;
|
||||
use crate::modules::rest::response::DataPage;
|
||||
use crate::modules::rest::ApiResult;
|
||||
use crate::modules::users::permissions::Permission;
|
||||
use crate::modules::users::BichonUser;
|
||||
use crate::modules::users::UserModel;
|
||||
use crate::raise_error;
|
||||
use poem_openapi::param::{Path, Query};
|
||||
use poem_openapi::payload::Json;
|
||||
@@ -131,7 +131,7 @@ impl AccountApi {
|
||||
let is_admin = context.user.is_admin().await;
|
||||
let sort_desc = desc.0.unwrap_or(true);
|
||||
|
||||
let user_map: HashMap<u64, BichonUser> = BichonUser::list_all()
|
||||
let user_map: HashMap<u64, UserModel> = UserModel::list_all()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|u| (u.id, u))
|
||||
|
||||
@@ -29,7 +29,7 @@ use crate::modules::users::payload::{
|
||||
use crate::modules::users::permissions::Permission;
|
||||
use crate::modules::users::role::UserRole;
|
||||
use crate::modules::users::view::UserView;
|
||||
use crate::modules::users::BichonUser;
|
||||
use crate::modules::users::UserModel;
|
||||
use poem::web::Path;
|
||||
use poem_openapi::payload::Json;
|
||||
use poem_openapi::OpenApi;
|
||||
@@ -100,10 +100,10 @@ impl UsersApi {
|
||||
.await?;
|
||||
let roles = UserRole::list_all().await?;
|
||||
let role_lookup: BTreeMap<u64, UserRole> = roles.into_iter().map(|r| (r.id, r)).collect();
|
||||
let users = BichonUser::list_all().await?;
|
||||
let users = UserModel::list_all().await?;
|
||||
let users = users
|
||||
.into_iter()
|
||||
.map(|u| u.to_current_user(&role_lookup))
|
||||
.map(|u| u.to_view(&role_lookup))
|
||||
.collect();
|
||||
Ok(Json(users))
|
||||
}
|
||||
@@ -140,7 +140,7 @@ impl UsersApi {
|
||||
context
|
||||
.require_permission(None, Permission::USER_MANAGE)
|
||||
.await?;
|
||||
Ok(BichonUser::remove(id).await?)
|
||||
Ok(UserModel::remove(id).await?)
|
||||
}
|
||||
|
||||
#[oai(path = "/users", method = "post", operation_id = "create_user")]
|
||||
@@ -152,10 +152,10 @@ impl UsersApi {
|
||||
context
|
||||
.require_permission(None, Permission::USER_MANAGE)
|
||||
.await?;
|
||||
let user = BichonUser::create(payload.0).await?;
|
||||
let user = UserModel::create(payload.0).await?;
|
||||
let roles = UserRole::list_all().await?;
|
||||
let role_lookup: BTreeMap<u64, UserRole> = roles.into_iter().map(|r| (r.id, r)).collect();
|
||||
Ok(Json(user.to_current_user(&role_lookup)))
|
||||
Ok(Json(user.to_view(&role_lookup)))
|
||||
}
|
||||
|
||||
#[oai(path = "/users/:id", method = "post", operation_id = "update_user")]
|
||||
@@ -180,7 +180,7 @@ impl UsersApi {
|
||||
update_data.account_access_map = None;
|
||||
update_data.acl = None;
|
||||
}
|
||||
Ok(BichonUser::update(target_id, update_data).await?)
|
||||
Ok(UserModel::update(target_id, update_data).await?)
|
||||
}
|
||||
|
||||
#[oai(
|
||||
@@ -191,7 +191,7 @@ impl UsersApi {
|
||||
async fn get_current_user(&self, context: ClientContext) -> ApiResult<Json<UserView>> {
|
||||
let roles = UserRole::list_all().await?;
|
||||
let role_lookup: BTreeMap<u64, UserRole> = roles.into_iter().map(|r| (r.id, r)).collect();
|
||||
Ok(Json(context.user.to_current_user(&role_lookup)))
|
||||
Ok(Json(context.user.to_view(&role_lookup)))
|
||||
}
|
||||
|
||||
#[oai(
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::modules::users::BichonUser;
|
||||
use crate::modules::users::UserModel;
|
||||
use poem::{handler, web::Json, IntoResponse, Response};
|
||||
use serde::Deserialize;
|
||||
use tracing::error;
|
||||
@@ -34,7 +34,7 @@ pub struct LoginPayload {
|
||||
#[handler]
|
||||
pub async fn login(payload: Json<LoginPayload>) -> Response {
|
||||
let payload = payload.0;
|
||||
match BichonUser::authenticate_user(payload.username, payload.password).await {
|
||||
match UserModel::authenticate_user(payload.username, payload.password).await {
|
||||
Ok(result) => match serde_json::to_string(&result) {
|
||||
Ok(json_string) => Response::builder()
|
||||
.status(http::StatusCode::OK)
|
||||
|
||||
@@ -26,7 +26,7 @@ use crate::modules::database::{
|
||||
use crate::modules::database::{insert_impl, list_all_impl, update_impl};
|
||||
use crate::modules::settings::cli::SETTINGS;
|
||||
use crate::modules::token::view::AccessTokenResp;
|
||||
use crate::modules::users::BichonUser;
|
||||
use crate::modules::users::UserModel;
|
||||
use crate::raise_error;
|
||||
use crate::{
|
||||
generate_token, modules::error::BichonResult,
|
||||
@@ -180,7 +180,7 @@ impl AccessTokenModel {
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn resolve_user_from_token(token: &str) -> BichonResult<BichonUser> {
|
||||
pub async fn resolve_user_from_token(token: &str) -> BichonResult<UserModel> {
|
||||
let token = token.to_string();
|
||||
let token_option = async_find_impl::<AccessTokenModel>(DB_MANAGER.meta_db(), token).await?;
|
||||
let token = match token_option {
|
||||
@@ -237,7 +237,7 @@ impl AccessTokenModel {
|
||||
.await?;
|
||||
}
|
||||
|
||||
let user = BichonUser::find(token.user_id)
|
||||
let user = UserModel::find(token.user_id)
|
||||
.await?
|
||||
.ok_or_else(|| raise_error!("The user associated with this access token does not exist or may have been deleted.".into(), ErrorCode::ResourceNotFound))?;
|
||||
Ok(user)
|
||||
@@ -287,11 +287,11 @@ impl AccessTokenModel {
|
||||
}
|
||||
|
||||
pub async fn list_all_api_tokens() -> BichonResult<Vec<AccessTokenResp>> {
|
||||
let users = BichonUser::list_all().await?;
|
||||
let users = UserModel::list_all().await?;
|
||||
let mut all = list_all_impl::<AccessTokenModel>(DB_MANAGER.meta_db()).await?;
|
||||
|
||||
all.retain(|t| t.token_type == TokenType::Api);
|
||||
let user_map: HashMap<u64, BichonUser> = users.into_iter().map(|u| (u.id, u)).collect();
|
||||
let user_map: HashMap<u64, UserModel> = users.into_iter().map(|u| (u.id, u)).collect();
|
||||
|
||||
let resp = all
|
||||
.into_iter()
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
use crate::modules::{
|
||||
context::Initialize,
|
||||
error::BichonResult,
|
||||
users::{role::UserRole, BichonUser},
|
||||
users::{role::UserRole, UserModel},
|
||||
};
|
||||
|
||||
pub struct UserManager;
|
||||
@@ -27,6 +27,6 @@ pub struct UserManager;
|
||||
impl Initialize for UserManager {
|
||||
async fn initialize() -> BichonResult<()> {
|
||||
UserRole::ensure_default_roles_exists().await?;
|
||||
BichonUser::ensure_default_admin_exists().await
|
||||
UserModel::ensure_default_admin_exists().await
|
||||
}
|
||||
}
|
||||
|
||||
+125
-25
@@ -51,11 +51,15 @@ pub mod permissions;
|
||||
pub mod role;
|
||||
pub mod view;
|
||||
|
||||
pub type UserModel = BichonUserV2;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
|
||||
pub struct LoginResult {
|
||||
pub success: bool,
|
||||
pub error_message: Option<String>,
|
||||
pub access_token: Option<String>,
|
||||
pub theme: Option<String>,
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
pub const DEFAULT_ADMIN_USER_ID: u64 = 100000000000000;
|
||||
@@ -92,9 +96,44 @@ pub struct BichonUser {
|
||||
pub acl: Option<AccessControl>,
|
||||
}
|
||||
|
||||
impl BichonUser {
|
||||
pub async fn list_all() -> BichonResult<Vec<BichonUser>> {
|
||||
Ok(list_all_impl::<BichonUser>(DB_MANAGER.meta_db()).await?)
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
|
||||
#[native_model(id = 10, version = 2, from = BichonUser)]
|
||||
#[native_db]
|
||||
pub struct BichonUserV2 {
|
||||
#[primary_key]
|
||||
pub id: u64,
|
||||
#[secondary_key(unique)]
|
||||
pub username: String,
|
||||
#[secondary_key(unique)]
|
||||
pub email: String,
|
||||
|
||||
pub password: Option<String>,
|
||||
|
||||
/// Scoped Access: Defines per-account permissions.
|
||||
/// Example:
|
||||
/// { account_id: 1, role_id: role_manager_id } -> Manager on Account 1
|
||||
/// { account_id: 2, role_id: role_viewer_id } -> Viewer on Account 2
|
||||
pub account_access_map: BTreeMap<u64, u64>,
|
||||
|
||||
pub description: Option<String>,
|
||||
|
||||
/// System Roles: Permissions that apply to the whole system
|
||||
/// (e.g., system settings, creating new users).
|
||||
pub global_roles: Vec<u64>,
|
||||
|
||||
pub avatar: Option<String>,
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
/// Optional access control settings
|
||||
pub acl: Option<AccessControl>,
|
||||
|
||||
pub theme: Option<String>,
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
impl BichonUserV2 {
|
||||
pub async fn list_all() -> BichonResult<Vec<UserModel>> {
|
||||
Ok(list_all_impl::<UserModel>(DB_MANAGER.meta_db()).await?)
|
||||
}
|
||||
|
||||
async fn get_all_permissions(&self) -> HashSet<String> {
|
||||
@@ -111,7 +150,7 @@ impl BichonUser {
|
||||
all_perms
|
||||
}
|
||||
|
||||
pub fn to_current_user(self, role_lookup: &BTreeMap<u64, UserRole>) -> UserView {
|
||||
pub fn to_view(self, role_lookup: &BTreeMap<u64, UserRole>) -> UserView {
|
||||
let global_roles_names = self
|
||||
.global_roles
|
||||
.iter()
|
||||
@@ -173,6 +212,8 @@ impl BichonUser {
|
||||
acl: self.acl,
|
||||
account_permissions,
|
||||
global_permissions,
|
||||
theme: self.theme,
|
||||
language: self.language,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,12 +228,12 @@ impl BichonUser {
|
||||
// 1. Try to get the existing admin user
|
||||
let admin = rw
|
||||
.get()
|
||||
.primary::<BichonUser>(DEFAULT_ADMIN_USER_ID)
|
||||
.primary::<UserModel>(DEFAULT_ADMIN_USER_ID)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
|
||||
if admin.is_none() {
|
||||
// 2. Insert the BichonUser with the updated schema
|
||||
rw.insert(BichonUser {
|
||||
rw.insert(UserModel {
|
||||
id: DEFAULT_ADMIN_USER_ID,
|
||||
username: "admin".into(),
|
||||
email: "placeholder@example.com".into(),
|
||||
@@ -209,6 +250,8 @@ impl BichonUser {
|
||||
updated_at: now,
|
||||
description: Some("System default administrator".into()),
|
||||
acl: None,
|
||||
theme: None,
|
||||
language: None,
|
||||
})
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
|
||||
@@ -239,9 +282,9 @@ impl BichonUser {
|
||||
username: String,
|
||||
password: String,
|
||||
) -> BichonResult<LoginResult> {
|
||||
let user_option = secondary_find_impl::<BichonUser>(
|
||||
let user_option = secondary_find_impl::<UserModel>(
|
||||
DB_MANAGER.meta_db(),
|
||||
BichonUserKey::username,
|
||||
BichonUserV2Key::username,
|
||||
username.clone(),
|
||||
)
|
||||
.await?;
|
||||
@@ -249,9 +292,9 @@ impl BichonUser {
|
||||
let user = match user_option {
|
||||
Some(u) => u,
|
||||
None => {
|
||||
match secondary_find_impl::<BichonUser>(
|
||||
match secondary_find_impl::<UserModel>(
|
||||
DB_MANAGER.meta_db(),
|
||||
BichonUserKey::email,
|
||||
BichonUserV2Key::email,
|
||||
username,
|
||||
)
|
||||
.await?
|
||||
@@ -262,6 +305,8 @@ impl BichonUser {
|
||||
success: false,
|
||||
error_message: Some("User or email not found.".to_string()),
|
||||
access_token: None,
|
||||
theme: None,
|
||||
language: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -277,6 +322,8 @@ impl BichonUser {
|
||||
success: true,
|
||||
error_message: None,
|
||||
access_token: Some(new_token),
|
||||
theme: user.theme,
|
||||
language: user.language,
|
||||
})
|
||||
} else {
|
||||
warn!(
|
||||
@@ -287,6 +334,8 @@ impl BichonUser {
|
||||
success: false,
|
||||
error_message: Some("Incorrect password.".to_string()),
|
||||
access_token: None,
|
||||
theme: None,
|
||||
language: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -304,20 +353,22 @@ impl BichonUser {
|
||||
)
|
||||
),
|
||||
access_token: None,
|
||||
theme: None,
|
||||
language: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find(user_id: u64) -> BichonResult<Option<BichonUser>> {
|
||||
pub async fn find(user_id: u64) -> BichonResult<Option<UserModel>> {
|
||||
async_find_impl(DB_MANAGER.meta_db(), user_id).await
|
||||
}
|
||||
|
||||
pub async fn check_username_conflict(username: &str) -> BichonResult<()> {
|
||||
// Check username duplicate
|
||||
if secondary_find_impl::<BichonUser>(
|
||||
if secondary_find_impl::<UserModel>(
|
||||
DB_MANAGER.meta_db(),
|
||||
BichonUserKey::username,
|
||||
BichonUserV2Key::username,
|
||||
username.to_string(),
|
||||
)
|
||||
.await?
|
||||
@@ -334,9 +385,9 @@ impl BichonUser {
|
||||
|
||||
pub async fn check_email_conflict(email: &str) -> BichonResult<()> {
|
||||
// Check email duplicate
|
||||
if secondary_find_impl::<BichonUser>(
|
||||
if secondary_find_impl::<UserModel>(
|
||||
DB_MANAGER.meta_db(),
|
||||
BichonUserKey::email,
|
||||
BichonUserV2Key::email,
|
||||
email.to_string(),
|
||||
)
|
||||
.await?
|
||||
@@ -351,7 +402,7 @@ impl BichonUser {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create(request: UserCreateRequest) -> BichonResult<BichonUser> {
|
||||
pub async fn create(request: UserCreateRequest) -> BichonResult<UserModel> {
|
||||
request.validate().await?;
|
||||
Self::check_username_conflict(&request.username).await?;
|
||||
Self::check_email_conflict(&request.email).await?;
|
||||
@@ -359,7 +410,7 @@ impl BichonUser {
|
||||
let password_hash = Some(encrypt!(&request.password)?);
|
||||
let now = utc_now!();
|
||||
|
||||
let user = BichonUser {
|
||||
let user = UserModel {
|
||||
id: id!(96),
|
||||
username: request.username,
|
||||
email: request.email,
|
||||
@@ -371,6 +422,8 @@ impl BichonUser {
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
account_access_map: request.account_access_map,
|
||||
theme: request.theme,
|
||||
language: request.language,
|
||||
};
|
||||
|
||||
let user_clone = user.clone();
|
||||
@@ -454,9 +507,9 @@ impl BichonUser {
|
||||
}
|
||||
|
||||
if let Some(username) = &request.username {
|
||||
let user_option = secondary_find_impl::<BichonUser>(
|
||||
let user_option = secondary_find_impl::<UserModel>(
|
||||
DB_MANAGER.meta_db(),
|
||||
BichonUserKey::username,
|
||||
BichonUserV2Key::username,
|
||||
username.to_string(),
|
||||
)
|
||||
.await?;
|
||||
@@ -472,9 +525,9 @@ impl BichonUser {
|
||||
}
|
||||
|
||||
if let Some(email) = &request.email {
|
||||
let user_option = secondary_find_impl::<BichonUser>(
|
||||
let user_option = secondary_find_impl::<UserModel>(
|
||||
DB_MANAGER.meta_db(),
|
||||
BichonUserKey::email,
|
||||
BichonUserV2Key::email,
|
||||
email.to_string(),
|
||||
)
|
||||
.await?;
|
||||
@@ -493,7 +546,7 @@ impl BichonUser {
|
||||
DB_MANAGER.meta_db(),
|
||||
move |rw| {
|
||||
rw.get()
|
||||
.primary::<BichonUser>(id)
|
||||
.primary::<UserModel>(id)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
|
||||
.ok_or_else(|| {
|
||||
raise_error!(
|
||||
@@ -532,6 +585,15 @@ impl BichonUser {
|
||||
if let Some(avatar_base64) = request.avatar_base64 {
|
||||
updated.avatar = Some(avatar_base64);
|
||||
}
|
||||
|
||||
if let Some(theme) = request.theme {
|
||||
updated.theme = Some(theme);
|
||||
}
|
||||
|
||||
if let Some(language) = request.language {
|
||||
updated.language = Some(language);
|
||||
}
|
||||
|
||||
updated.updated_at = utc_now!();
|
||||
|
||||
Ok(updated)
|
||||
@@ -546,9 +608,9 @@ impl BichonUser {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_authorized_users(account_id: u64) -> BichonResult<Vec<BichonUser>> {
|
||||
async fn list_authorized_users(account_id: u64) -> BichonResult<Vec<UserModel>> {
|
||||
let all = Self::list_all().await?;
|
||||
let result: Vec<BichonUser> = all
|
||||
let result: Vec<UserModel> = all
|
||||
.into_iter()
|
||||
.filter(|e| e.account_access_map.contains_key(&account_id))
|
||||
.collect();
|
||||
@@ -566,7 +628,7 @@ impl BichonUser {
|
||||
for user in users {
|
||||
let current = rw
|
||||
.get()
|
||||
.primary::<BichonUser>(user.id)
|
||||
.primary::<UserModel>(user.id)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
|
||||
.ok_or_else(|| {
|
||||
raise_error!(
|
||||
@@ -590,3 +652,41 @@ impl BichonUser {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BichonUserV2> for BichonUser {
|
||||
fn from(value: BichonUserV2) -> Self {
|
||||
BichonUser {
|
||||
id: value.id,
|
||||
username: value.username,
|
||||
email: value.email,
|
||||
password: value.password,
|
||||
account_access_map: value.account_access_map,
|
||||
description: value.description,
|
||||
global_roles: value.global_roles,
|
||||
avatar: value.avatar,
|
||||
created_at: value.created_at,
|
||||
updated_at: value.updated_at,
|
||||
acl: value.acl,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BichonUser> for BichonUserV2 {
|
||||
fn from(value: BichonUser) -> Self {
|
||||
BichonUserV2 {
|
||||
id: value.id,
|
||||
username: value.username,
|
||||
email: value.email,
|
||||
password: value.password,
|
||||
account_access_map: value.account_access_map,
|
||||
description: value.description,
|
||||
global_roles: value.global_roles,
|
||||
avatar: value.avatar,
|
||||
created_at: value.created_at,
|
||||
updated_at: value.updated_at,
|
||||
acl: value.acl,
|
||||
theme: None,
|
||||
language: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,44 @@ use crate::{
|
||||
};
|
||||
use poem_openapi::Object;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
|
||||
fn allowed_themes() -> HashSet<&'static str> {
|
||||
["light", "dark"].into_iter().collect()
|
||||
}
|
||||
|
||||
fn allowed_languages() -> HashSet<&'static str> {
|
||||
[
|
||||
"ar", "da", "de", "en", "es", "fi", "fr", "it", "jp", "ko", "nl", "no", "pl", "pt", "ru",
|
||||
"sv", "zh", "zh-tw",
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn validate_option_in_set(
|
||||
value: &Option<String>,
|
||||
allowed: &std::collections::HashSet<&'static str>,
|
||||
field_name: &str,
|
||||
) -> BichonResult<()> {
|
||||
if let Some(v) = value {
|
||||
if !allowed.contains(v.as_str()) {
|
||||
return Err(raise_error!(
|
||||
format!("invalid {} value: '{}'", field_name, v),
|
||||
ErrorCode::InvalidParameter
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_theme(theme: &Option<String>) -> BichonResult<()> {
|
||||
validate_option_in_set(theme, &allowed_themes(), "theme")
|
||||
}
|
||||
|
||||
fn validate_language(language: &Option<String>) -> BichonResult<()> {
|
||||
validate_option_in_set(language, &allowed_languages(), "language")
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
|
||||
pub struct RoleCreateRequest {
|
||||
@@ -176,6 +213,8 @@ pub struct UserCreateRequest {
|
||||
pub acl: Option<AccessControl>,
|
||||
pub avatar_base64: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub theme: Option<String>,
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
impl UserCreateRequest {
|
||||
@@ -219,6 +258,9 @@ impl UserCreateRequest {
|
||||
));
|
||||
}
|
||||
|
||||
validate_theme(&self.theme)?;
|
||||
validate_language(&self.language)?;
|
||||
|
||||
let all_roles = UserRole::list_all().await?;
|
||||
let role_type_map: HashMap<u64, RoleType> =
|
||||
all_roles.into_iter().map(|r| (r.id, r.role_type)).collect();
|
||||
@@ -301,6 +343,8 @@ pub struct UserUpdateRequest {
|
||||
pub account_access_map: Option<BTreeMap<u64, u64>>,
|
||||
pub acl: Option<AccessControl>,
|
||||
pub description: Option<String>,
|
||||
pub theme: Option<String>,
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
impl UserUpdateRequest {
|
||||
@@ -325,6 +369,9 @@ impl UserUpdateRequest {
|
||||
}
|
||||
}
|
||||
|
||||
validate_theme(&self.theme)?;
|
||||
validate_language(&self.language)?;
|
||||
|
||||
let all_roles = UserRole::list_all().await?;
|
||||
let role_type_map: HashMap<u64, RoleType> =
|
||||
all_roles.into_iter().map(|r| (r.id, r.role_type)).collect();
|
||||
|
||||
@@ -49,4 +49,6 @@ pub struct UserView {
|
||||
pub updated_at: i64,
|
||||
/// Optional access control settings
|
||||
pub acl: Option<AccessControl>,
|
||||
pub theme: Option<String>,
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
+31
-25
@@ -14,33 +14,33 @@ export interface UserRole {
|
||||
}
|
||||
|
||||
export function getPermissions(t: (key: string) => string) {
|
||||
return [
|
||||
// 1. Global Management
|
||||
{ label: t('permission.system.access'), value: 'system:access' },
|
||||
{ label: t('permission.system.root'), value: 'system:root' },
|
||||
{ label: t('permission.user.manage'), value: 'user:manage' },
|
||||
{ label: t('permission.user.view'), value: 'user:view' },
|
||||
{ label: t('permission.token.manage'), value: 'token:manage' },
|
||||
{ label: t('permission.account.create'), value: 'account:create' },
|
||||
return [
|
||||
// 1. Global Management
|
||||
{ label: t('permission.system.access'), value: 'system:access' },
|
||||
{ label: t('permission.system.root'), value: 'system:root' },
|
||||
{ label: t('permission.user.manage'), value: 'user:manage' },
|
||||
{ label: t('permission.user.view'), value: 'user:view' },
|
||||
{ label: t('permission.token.manage'), value: 'token:manage' },
|
||||
{ label: t('permission.account.create'), value: 'account:create' },
|
||||
|
||||
// 2. Global "ALL" Scoped (Admin)
|
||||
{ label: t('permission.account.manage_all'), value: 'account:manage:all' },
|
||||
{ label: t('permission.data.read_all'), value: 'data:read:all' },
|
||||
{ label: t('permission.data.manage_all'), value: 'data:manage:all' },
|
||||
{ label: t('permission.data.raw_download_all'), value: 'data:raw:download:all' },
|
||||
{ label: t('permission.data.delete_all'), value: 'data:delete:all' },
|
||||
{ label: t('permission.data.export_batch_all'), value: 'data:export:batch:all' },
|
||||
// 2. Global "ALL" Scoped (Admin)
|
||||
{ label: t('permission.account.manage_all'), value: 'account:manage:all' },
|
||||
{ label: t('permission.data.read_all'), value: 'data:read:all' },
|
||||
{ label: t('permission.data.manage_all'), value: 'data:manage:all' },
|
||||
{ label: t('permission.data.raw_download_all'), value: 'data:raw:download:all' },
|
||||
{ label: t('permission.data.delete_all'), value: 'data:delete:all' },
|
||||
{ label: t('permission.data.export_batch_all'), value: 'data:export:batch:all' },
|
||||
|
||||
// 3. Scoped / Limited
|
||||
{ label: t('permission.account.manage'), value: 'account:manage' },
|
||||
{ label: t('permission.account.read_details'), value: 'account:read_details' },
|
||||
{ label: t('permission.data.read'), value: 'data:read' },
|
||||
{ label: t('permission.data.manage'), value: 'data:manage' },
|
||||
{ label: t('permission.data.raw_download'), value: 'data:raw:download' },
|
||||
{ label: t('permission.data.delete'), value: 'data:delete' },
|
||||
{ label: t('permission.data.export_batch'), value: 'data:export:batch' },
|
||||
{ label: t('permission.data.import_batch'), value: 'data:import:batch' },
|
||||
]
|
||||
// 3. Scoped / Limited
|
||||
{ label: t('permission.account.manage'), value: 'account:manage' },
|
||||
{ label: t('permission.account.read_details'), value: 'account:read_details' },
|
||||
{ label: t('permission.data.read'), value: 'data:read' },
|
||||
{ label: t('permission.data.manage'), value: 'data:manage' },
|
||||
{ label: t('permission.data.raw_download'), value: 'data:raw:download' },
|
||||
{ label: t('permission.data.delete'), value: 'data:delete' },
|
||||
{ label: t('permission.data.export_batch'), value: 'data:export:batch' },
|
||||
{ label: t('permission.data.import_batch'), value: 'data:import:batch' },
|
||||
]
|
||||
}
|
||||
|
||||
export interface RateLimit {
|
||||
@@ -85,10 +85,16 @@ export interface User {
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
type Theme = 'dark' | 'light'
|
||||
|
||||
|
||||
export interface LoginResult {
|
||||
success: boolean;
|
||||
error_message?: string | null;
|
||||
access_token?: string | null;
|
||||
theme?: Theme,
|
||||
language?: string,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import i18n from '@/i18n'
|
||||
import { Loader2, LogIn } from 'lucide-react'
|
||||
import { login } from '@/api/users/api'
|
||||
import { useTheme } from '@/context/theme-context'
|
||||
|
||||
type UserAuthFormProps = HTMLAttributes<HTMLDivElement>
|
||||
|
||||
@@ -59,6 +60,7 @@ const getFormSchema = (t: (key: string, options?: Record<string, any>) => string
|
||||
|
||||
export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { setTheme } = useTheme();
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -86,6 +88,15 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
||||
onSuccess: (result) => {
|
||||
if (result.success) {
|
||||
setToken(result);
|
||||
|
||||
if (result.theme) {
|
||||
setTheme(result.theme);
|
||||
}
|
||||
|
||||
if (result.language) {
|
||||
i18n.changeLanguage(result.language);
|
||||
}
|
||||
|
||||
navigate({ to: redirect });
|
||||
} else {
|
||||
toast({
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
import { z } from 'zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { CaretSortIcon, CheckIcon } from '@radix-ui/react-icons'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { useTheme } from '@/context/theme-context'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCurrentUser } from '@/hooks/use-current-user'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { update_user } from '@/api/users/api'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
import { AxiosError } from 'axios'
|
||||
|
||||
|
||||
const languages = [
|
||||
{ value: 'ar', label: 'العربية' },
|
||||
{ value: 'da', label: 'Dansk' },
|
||||
{ value: 'de', label: 'Deutsch' },
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'es', label: 'Español' },
|
||||
{ value: 'fi', label: 'Suomi' },
|
||||
{ value: 'fr', label: 'Français' },
|
||||
{ value: 'it', label: 'Italiano' },
|
||||
{ value: 'jp', label: '日本語' },
|
||||
{ value: 'ko', label: '한국어' },
|
||||
{ value: 'nl', label: 'Nederlands' },
|
||||
{ value: 'no', label: 'Norsk' },
|
||||
{ value: 'pl', label: 'Polski' },
|
||||
{ value: 'pt', label: 'Português' },
|
||||
{ value: 'ru', label: 'Русский' },
|
||||
{ value: 'sv', label: 'Svenska' },
|
||||
{ value: 'zh', label: '中文' },
|
||||
{ value: 'zh-tw', label: '繁體中文' },
|
||||
]
|
||||
|
||||
const appearanceSchema = (t: (key: string) => string) => z.object({
|
||||
theme: z.enum(['light', 'dark'], {
|
||||
required_error: t('settings.appearance.validation.theme.required'),
|
||||
}),
|
||||
language: z.string({
|
||||
required_error: t('settings.appearance.validation.language.required'),
|
||||
})
|
||||
})
|
||||
|
||||
type AppearanceFormValues = z.infer<ReturnType<typeof appearanceSchema>>
|
||||
|
||||
|
||||
export function AppearanceForm() {
|
||||
const { data: user } = useCurrentUser();
|
||||
const queryClient = useQueryClient();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
|
||||
const form = useForm<AppearanceFormValues>({
|
||||
resolver: zodResolver(appearanceSchema(t)),
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
theme: (theme as 'light' | 'dark') || 'light',
|
||||
language: i18n.language || 'en',
|
||||
},
|
||||
})
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: AppearanceFormValues) => {
|
||||
return update_user(user!.id, values)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['current-user'] })
|
||||
toast({ title: t('settings.profile.toast.updated') })
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: t('settings.profile.toast.update_failed'),
|
||||
description: (err.response?.data as any)?.message || err.message,
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(data: AppearanceFormValues) {
|
||||
i18n.changeLanguage(data.language);
|
||||
setTheme(data.theme);
|
||||
mutation.mutate(data);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-6xl ml-0 px-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8 w-full max-w-screen-xl mx-auto px-4 md:px-6"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='language'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex flex-col'>
|
||||
<FormLabel>{t('settings.appearance.field.language')}</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
className={cn(
|
||||
'w-[400px] justify-between',
|
||||
!field.value && 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{field.value
|
||||
? languages.find((l) => l.value === field.value)?.label
|
||||
: t('settings.appearance.placeholder.select_language')}
|
||||
<CaretSortIcon className='ms-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-[400px] p-0' align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder={t('settings.appearance.command.search')} />
|
||||
<CommandEmpty>{t('settings.appearance.command.no_results')}</CommandEmpty>
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
{languages.map((language) => (
|
||||
<CommandItem
|
||||
value={language.label}
|
||||
key={language.value}
|
||||
onSelect={() => {
|
||||
form.setValue('language', language.value)
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
language.value === field.value ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
{language.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormDescription>
|
||||
{t('settings.appearance.description.language')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='theme'
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-1">
|
||||
<FormLabel>{t('settings.appearance.field.theme')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t('settings.appearance.description.theme')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
className='grid max-w-md grid-cols-2 gap-8 pt-2'
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel className='[&:has([data-state=checked])>div]:border-primary cursor-pointer'>
|
||||
<FormControl>
|
||||
<RadioGroupItem value='light' className='sr-only' />
|
||||
</FormControl>
|
||||
<div className='items-center rounded-md border-2 border-muted p-1 hover:border-accent'>
|
||||
<div className='space-y-2 rounded-sm bg-[#ecedef] p-2'>
|
||||
<div className='space-y-2 rounded-md bg-white p-2 shadow-sm'>
|
||||
<div className='h-2 w-[80px] rounded-lg bg-[#ecedef]' />
|
||||
<div className='h-2 w-[100px] rounded-lg bg-[#ecedef]' />
|
||||
</div>
|
||||
<div className='flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm'>
|
||||
<div className='h-4 w-4 rounded-full bg-[#ecedef]' />
|
||||
<div className='h-2 w-[100px] rounded-lg bg-[#ecedef]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className='block w-full p-2 text-center font-normal'>
|
||||
{t('settings.appearance.theme.light')}
|
||||
</span>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<FormLabel className='[&:has([data-state=checked])>div]:border-primary cursor-pointer'>
|
||||
<FormControl>
|
||||
<RadioGroupItem value='dark' className='sr-only' />
|
||||
</FormControl>
|
||||
<div className='items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground'>
|
||||
<div className='space-y-2 rounded-sm bg-slate-950 p-2'>
|
||||
<div className='space-y-2 rounded-md bg-slate-800 p-2 shadow-sm'>
|
||||
<div className='h-2 w-[80px] rounded-lg bg-slate-400' />
|
||||
<div className='h-2 w-[100px] rounded-lg bg-slate-400' />
|
||||
</div>
|
||||
<div className='flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm'>
|
||||
<div className='h-4 w-4 rounded-full bg-slate-400' />
|
||||
<div className='h-2 w-[100px] rounded-lg bg-slate-400' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className='block w-full p-2 text-center font-normal'>
|
||||
{t('settings.appearance.theme.dark')}
|
||||
</span>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-start pt-4">
|
||||
<Button type='submit'>
|
||||
{t('settings.appearance.button.update')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { AppearanceForm } from './appearance-form'
|
||||
|
||||
export function SettingsAppearance() {
|
||||
return (
|
||||
<AppearanceForm />
|
||||
)
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
import { Outlet } from '@tanstack/react-router'
|
||||
import { Main } from '@/components/layout/main'
|
||||
import SidebarNav from './components/sidebar-nav'
|
||||
import { KeyRound, SettingsIcon, UserCog, Waypoints } from 'lucide-react'
|
||||
import { KeyRound, Palette, SettingsIcon, UserCog, Waypoints } from 'lucide-react'
|
||||
import { FixedHeader } from '@/components/layout/fixed-header'
|
||||
import { useCurrentUser } from '@/hooks/use-current-user'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -36,6 +36,11 @@ export default function Settings() {
|
||||
href: '/settings/profile',
|
||||
icon: <UserCog size={18} />,
|
||||
},
|
||||
{
|
||||
title: t('settings.appearance.title'),
|
||||
href: '/settings/appearance',
|
||||
icon: <Palette size={18} />,
|
||||
},
|
||||
{
|
||||
title: t('settings.sidebar.apiTokens'),
|
||||
href: '/settings/api-tokens',
|
||||
|
||||
@@ -718,6 +718,31 @@
|
||||
"interval": "الفاصل الزمني"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "المظهر",
|
||||
"field": {
|
||||
"language": "لغة الواجهة",
|
||||
"theme": "سمة الواجهة"
|
||||
},
|
||||
"description": {
|
||||
"language": "اختر اللغة المستخدمة في واجهة الويب.",
|
||||
"theme": "اختر بين الوضع الفاتح أو الداكن."
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "اختر اللغة"
|
||||
},
|
||||
"command": {
|
||||
"search": "بحث عن لغة...",
|
||||
"no_results": "لم يتم العثور على لغة."
|
||||
},
|
||||
"theme": {
|
||||
"light": "فاتح",
|
||||
"dark": "داكن"
|
||||
},
|
||||
"button": {
|
||||
"update": "تحديث التفضيلات"
|
||||
}
|
||||
},
|
||||
"title": "الإعدادات",
|
||||
"general": "عام",
|
||||
"proxy": "وكيل",
|
||||
|
||||
@@ -718,6 +718,39 @@
|
||||
"interval": "Interval"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "Udseende",
|
||||
"field": {
|
||||
"language": "Grænsefladesprog",
|
||||
"theme": "Grænsefladetema"
|
||||
},
|
||||
"description": {
|
||||
"language": "Vælg det sprog, der skal bruges i webbrugerfladen.",
|
||||
"theme": "Vælg mellem lys eller mørk tilstand for brugerfladen."
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "Vælg sprog"
|
||||
},
|
||||
"command": {
|
||||
"search": "Søg efter sprog...",
|
||||
"no_results": "Intet sprog fundet."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Lys",
|
||||
"dark": "Mørk"
|
||||
},
|
||||
"button": {
|
||||
"update": "Opdater indstillinger"
|
||||
},
|
||||
"validation": {
|
||||
"language": {
|
||||
"required": "Vælg venligst et sprog."
|
||||
},
|
||||
"theme": {
|
||||
"required": "Vælg venligst et tema."
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Indstillinger",
|
||||
"general": "Generelt",
|
||||
"proxy": "Proxy",
|
||||
|
||||
@@ -718,6 +718,31 @@
|
||||
"interval": "Intervall"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "Erscheinungsbild",
|
||||
"field": {
|
||||
"language": "Sprache der Benutzeroberfläche",
|
||||
"theme": "Design"
|
||||
},
|
||||
"description": {
|
||||
"language": "Wählen Sie die Sprache für die Web-Oberfläche.",
|
||||
"theme": "Wählen Sie zwischen hellem oder dunklem Modus."
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "Sprache wählen"
|
||||
},
|
||||
"command": {
|
||||
"search": "Sprache suchen...",
|
||||
"no_results": "Keine Sprache gefunden."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Hell",
|
||||
"dark": "Dunkel"
|
||||
},
|
||||
"button": {
|
||||
"update": "Einstellungen aktualisieren"
|
||||
}
|
||||
},
|
||||
"title": "Einstellungen",
|
||||
"general": "Allgemein",
|
||||
"proxy": "Proxy",
|
||||
|
||||
@@ -718,6 +718,39 @@
|
||||
"interval": "Interval"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "Appearance",
|
||||
"field": {
|
||||
"language": "Language",
|
||||
"theme": "Interface Theme"
|
||||
},
|
||||
"description": {
|
||||
"language": "Select the language used in the Web UI.",
|
||||
"theme": "Choose between light or dark mode for the interface."
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "Select language"
|
||||
},
|
||||
"command": {
|
||||
"search": "Search language...",
|
||||
"no_results": "No language found."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Light",
|
||||
"dark": "Dark"
|
||||
},
|
||||
"button": {
|
||||
"update": "Update preferences"
|
||||
},
|
||||
"validation": {
|
||||
"language": {
|
||||
"required": "Please select a language."
|
||||
},
|
||||
"theme": {
|
||||
"required": "Please select a theme."
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Settings",
|
||||
"general": "General",
|
||||
"proxy": "Proxy",
|
||||
|
||||
@@ -718,6 +718,31 @@
|
||||
"interval": "Intervalo"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "Apariencia",
|
||||
"field": {
|
||||
"language": "Idioma de la interfaz",
|
||||
"theme": "Tema de la interfaz"
|
||||
},
|
||||
"description": {
|
||||
"language": "Seleccione el idioma de la interfaz web.",
|
||||
"theme": "Elija entre el modo claro u oscuro."
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "Seleccionar idioma"
|
||||
},
|
||||
"command": {
|
||||
"search": "Buscar idioma...",
|
||||
"no_results": "No se encontró el idioma."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Claro",
|
||||
"dark": "Oscuro"
|
||||
},
|
||||
"button": {
|
||||
"update": "Actualizar preferencias"
|
||||
}
|
||||
},
|
||||
"title": "Configuración",
|
||||
"general": "General",
|
||||
"proxy": "Proxy",
|
||||
|
||||
@@ -718,6 +718,39 @@
|
||||
"interval": "Väli"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "Ulkoasu",
|
||||
"field": {
|
||||
"language": "Käyttöliittymän kieli",
|
||||
"theme": "Käyttöliittymän teema"
|
||||
},
|
||||
"description": {
|
||||
"language": "Valitse Web-käyttöliittymässä käytettävä kieli.",
|
||||
"theme": "Valitse käyttöliittymän vaalea tai tumma tila."
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "Valitse kieli"
|
||||
},
|
||||
"command": {
|
||||
"search": "Hae kieltä...",
|
||||
"no_results": "Kieltä ei löytynyt."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Vaalea",
|
||||
"dark": "Tumma"
|
||||
},
|
||||
"button": {
|
||||
"update": "Päivitä asetukset"
|
||||
},
|
||||
"validation": {
|
||||
"language": {
|
||||
"required": "Valitse kieli."
|
||||
},
|
||||
"theme": {
|
||||
"required": "Valitse teema."
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Asetukset",
|
||||
"general": "Yleinen",
|
||||
"proxy": "Välityspalvelin",
|
||||
|
||||
@@ -718,6 +718,31 @@
|
||||
"interval": "Intervalle"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "Apparence",
|
||||
"field": {
|
||||
"language": "Langue de l'interface",
|
||||
"theme": "Thème de l'interface"
|
||||
},
|
||||
"description": {
|
||||
"language": "Choisissez la langue de l'interface web.",
|
||||
"theme": "Choisissez entre le mode clair ou sombre."
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "Choisir une langue"
|
||||
},
|
||||
"command": {
|
||||
"search": "Rechercher une langue...",
|
||||
"no_results": "Aucune langue trouvée."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Clair",
|
||||
"dark": "Sombre"
|
||||
},
|
||||
"button": {
|
||||
"update": "Mettre à jour les préférences"
|
||||
}
|
||||
},
|
||||
"title": "Paramètres",
|
||||
"general": "Général",
|
||||
"proxy": "Proxy",
|
||||
|
||||
@@ -718,6 +718,31 @@
|
||||
"interval": "Intervallo"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "Aspetto",
|
||||
"field": {
|
||||
"language": "Lingua dell'interfaccia",
|
||||
"theme": "Tema dell'interfaccia"
|
||||
},
|
||||
"description": {
|
||||
"language": "Seleziona la lingua per l'interfaccia web.",
|
||||
"theme": "Scegli tra la modalità chiara o scura."
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "Seleziona lingua"
|
||||
},
|
||||
"command": {
|
||||
"search": "Cerca lingua...",
|
||||
"no_results": "Nessuna lingua trovata."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Chiaro",
|
||||
"dark": "Scuro"
|
||||
},
|
||||
"button": {
|
||||
"update": "Aggiorna preferenze"
|
||||
}
|
||||
},
|
||||
"title": "Impostazioni",
|
||||
"general": "Generale",
|
||||
"proxy": "Proxy",
|
||||
|
||||
@@ -718,6 +718,31 @@
|
||||
"interval": "間隔"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "外観",
|
||||
"field": {
|
||||
"language": "表示言語",
|
||||
"theme": "インターフェーステーマ"
|
||||
},
|
||||
"description": {
|
||||
"language": "Web UIで使用する言語を選択します。",
|
||||
"theme": "ライトモードまたはダークモードを選択します。"
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "言語を選択"
|
||||
},
|
||||
"command": {
|
||||
"search": "言語を検索...",
|
||||
"no_results": "言語が見つかりません。"
|
||||
},
|
||||
"theme": {
|
||||
"light": "ライト",
|
||||
"dark": "ダーク"
|
||||
},
|
||||
"button": {
|
||||
"update": "設定を更新"
|
||||
}
|
||||
},
|
||||
"title": "設定",
|
||||
"general": "一般",
|
||||
"proxy": "プロキシ",
|
||||
|
||||
@@ -718,6 +718,31 @@
|
||||
"interval": "간격"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "외관",
|
||||
"field": {
|
||||
"language": "인터페이스 언어",
|
||||
"theme": "인터페이스 테마"
|
||||
},
|
||||
"description": {
|
||||
"language": "웹 UI에서 사용할 언어를 선택하세요.",
|
||||
"theme": "라이트 모드와 다크 모드 중에서 선택하세요."
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "언어 선택"
|
||||
},
|
||||
"command": {
|
||||
"search": "언어 검색...",
|
||||
"no_results": "언어를 찾을 수 없습니다."
|
||||
},
|
||||
"theme": {
|
||||
"light": "라이트",
|
||||
"dark": "다크"
|
||||
},
|
||||
"button": {
|
||||
"update": "설정 업데이트"
|
||||
}
|
||||
},
|
||||
"title": "설정",
|
||||
"general": "일반",
|
||||
"proxy": "프록시",
|
||||
|
||||
@@ -718,6 +718,39 @@
|
||||
"interval": "Interval"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "Uiterlijk",
|
||||
"field": {
|
||||
"language": "Interfacetaal",
|
||||
"theme": "Interfacethema"
|
||||
},
|
||||
"description": {
|
||||
"language": "Selecteer de taal voor de webinterface.",
|
||||
"theme": "Kies tussen de lichte of donkere modus voor de interface."
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "Selecteer taal"
|
||||
},
|
||||
"command": {
|
||||
"search": "Taal zoeken...",
|
||||
"no_results": "Geen taal gevonden."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Licht",
|
||||
"dark": "Donker"
|
||||
},
|
||||
"button": {
|
||||
"update": "Voorkeuren bijwerken"
|
||||
},
|
||||
"validation": {
|
||||
"language": {
|
||||
"required": "Selecteer een taal."
|
||||
},
|
||||
"theme": {
|
||||
"required": "Selecteer een thema."
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Instellingen",
|
||||
"general": "Algemeen",
|
||||
"proxy": "Proxy",
|
||||
|
||||
@@ -718,6 +718,39 @@
|
||||
"interval": "Intervall"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "Utseende",
|
||||
"field": {
|
||||
"language": "Grensesnittspråk",
|
||||
"theme": "Grensesnittema"
|
||||
},
|
||||
"description": {
|
||||
"language": "Velg språket som skal brukes i webgrensesnittet.",
|
||||
"theme": "Velg mellom lys eller mørk modus for grensesnittet."
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "Velg språk"
|
||||
},
|
||||
"command": {
|
||||
"search": "Søk etter språk...",
|
||||
"no_results": "Fant ingen språk."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Lys",
|
||||
"dark": "Mørk"
|
||||
},
|
||||
"button": {
|
||||
"update": "Oppdater innstillinger"
|
||||
},
|
||||
"validation": {
|
||||
"language": {
|
||||
"required": "Vennligst velg et språk."
|
||||
},
|
||||
"theme": {
|
||||
"required": "Vennligst velg et tema."
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Innstillinger",
|
||||
"general": "Generelt",
|
||||
"proxy": "Proxy",
|
||||
|
||||
@@ -718,6 +718,31 @@
|
||||
"interval": "Interval"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "Wygląd",
|
||||
"field": {
|
||||
"language": "Język interfejsu",
|
||||
"theme": "Motyw interfejsu"
|
||||
},
|
||||
"description": {
|
||||
"language": "Wybierz język interfejsu webowego.",
|
||||
"theme": "Wybierz tryb jasny lub ciemny."
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "Wybierz język"
|
||||
},
|
||||
"command": {
|
||||
"search": "Szukaj języka...",
|
||||
"no_results": "Nie znaleziono języka."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Jasny",
|
||||
"dark": "Ciemny"
|
||||
},
|
||||
"button": {
|
||||
"update": "Aktualizuj preferencje"
|
||||
}
|
||||
},
|
||||
"title": "Ustawienia",
|
||||
"general": "Ogólne",
|
||||
"proxy": "Proxy",
|
||||
|
||||
@@ -718,6 +718,31 @@
|
||||
"interval": "Intervalo"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "Aparência",
|
||||
"field": {
|
||||
"language": "Idioma da interface",
|
||||
"theme": "Tema da interface"
|
||||
},
|
||||
"description": {
|
||||
"language": "Selecione o idioma da interface web.",
|
||||
"theme": "Escolha entre o modo claro ou escuro."
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "Selecionar idioma"
|
||||
},
|
||||
"command": {
|
||||
"search": "Pesquisar idioma...",
|
||||
"no_results": "Nenhum idioma encontrado."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Claro",
|
||||
"dark": "Escuro"
|
||||
},
|
||||
"button": {
|
||||
"update": "Atualizar preferências"
|
||||
}
|
||||
},
|
||||
"title": "Configurações",
|
||||
"general": "Geral",
|
||||
"proxy": "Proxy",
|
||||
|
||||
@@ -718,6 +718,31 @@
|
||||
"interval": "Интервал"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "Внешний вид",
|
||||
"field": {
|
||||
"language": "Язык интерфейса",
|
||||
"theme": "Тема интерфейса"
|
||||
},
|
||||
"description": {
|
||||
"language": "Выберите язык веб-интерфейса.",
|
||||
"theme": "Выберите светлый или темный режим."
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "Выберите язык"
|
||||
},
|
||||
"command": {
|
||||
"search": "Поиск языка...",
|
||||
"no_results": "Язык не найден."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Светлая",
|
||||
"dark": "Темная"
|
||||
},
|
||||
"button": {
|
||||
"update": "Обновить настройки"
|
||||
}
|
||||
},
|
||||
"title": "Настройки",
|
||||
"general": "Общие",
|
||||
"proxy": "Прокси",
|
||||
|
||||
@@ -718,6 +718,39 @@
|
||||
"interval": "Intervall"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "Utseende",
|
||||
"field": {
|
||||
"language": "Gränssnittsspråk",
|
||||
"theme": "Gränssnittstema"
|
||||
},
|
||||
"description": {
|
||||
"language": "Välj språk för webbanvändargränssnittet.",
|
||||
"theme": "Välj mellan ljust eller mörkt läge för gränssnittet."
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "Välj språk"
|
||||
},
|
||||
"command": {
|
||||
"search": "Sök språk...",
|
||||
"no_results": "Inget språk hittades."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Ljust",
|
||||
"dark": "Mörkt"
|
||||
},
|
||||
"button": {
|
||||
"update": "Uppdatera inställningar"
|
||||
},
|
||||
"validation": {
|
||||
"language": {
|
||||
"required": "Välj ett språk."
|
||||
},
|
||||
"theme": {
|
||||
"required": "Välj ett tema."
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Inställningar",
|
||||
"general": "Allmänt",
|
||||
"proxy": "Proxy",
|
||||
|
||||
@@ -711,6 +711,39 @@
|
||||
"interval": "間隔"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "外觀設置",
|
||||
"field": {
|
||||
"language": "介面语言",
|
||||
"theme": "介面主題"
|
||||
},
|
||||
"description": {
|
||||
"language": "選擇網頁介面顯示的语言。",
|
||||
"theme": "為介面選擇淺色或深色顯示模式。"
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "選擇語言"
|
||||
},
|
||||
"command": {
|
||||
"search": "搜尋語言...",
|
||||
"no_results": "未找到相關語言。"
|
||||
},
|
||||
"theme": {
|
||||
"light": "淺色",
|
||||
"dark": "深色"
|
||||
},
|
||||
"button": {
|
||||
"update": "更新偏好設置"
|
||||
},
|
||||
"validation": {
|
||||
"language": {
|
||||
"required": "請選擇一種語言。"
|
||||
},
|
||||
"theme": {
|
||||
"required": "請選擇一個主題。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "設定",
|
||||
"general": "一般",
|
||||
"proxy": "代理",
|
||||
|
||||
@@ -718,6 +718,39 @@
|
||||
"interval": "间隔"
|
||||
},
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "外观设置",
|
||||
"field": {
|
||||
"language": "语言",
|
||||
"theme": "界面主题"
|
||||
},
|
||||
"description": {
|
||||
"language": "选择 Web 界面显示的语言。",
|
||||
"theme": "为界面选择浅色或深色显示模式。"
|
||||
},
|
||||
"placeholder": {
|
||||
"select_language": "选择语言"
|
||||
},
|
||||
"command": {
|
||||
"search": "搜索语言...",
|
||||
"no_results": "未找到相关语言。"
|
||||
},
|
||||
"theme": {
|
||||
"light": "浅色",
|
||||
"dark": "深色"
|
||||
},
|
||||
"button": {
|
||||
"update": "更新偏好设置"
|
||||
},
|
||||
"validation": {
|
||||
"language": {
|
||||
"required": "请选择一种语言。"
|
||||
},
|
||||
"theme": {
|
||||
"required": "请选择一个主题。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "设置",
|
||||
"general": "常规",
|
||||
"proxy": "网络代理",
|
||||
|
||||
@@ -70,6 +70,9 @@ const AuthenticatedSettingsProfileLazyImport = createFileRoute(
|
||||
const AuthenticatedSettingsConfigurationsLazyImport = createFileRoute(
|
||||
'/_authenticated/settings/configurations',
|
||||
)()
|
||||
const AuthenticatedSettingsAppearanceLazyImport = createFileRoute(
|
||||
'/_authenticated/settings/appearance',
|
||||
)()
|
||||
const AuthenticatedSettingsApiTokensLazyImport = createFileRoute(
|
||||
'/_authenticated/settings/api-tokens',
|
||||
)()
|
||||
@@ -282,6 +285,17 @@ const AuthenticatedSettingsConfigurationsLazyRoute =
|
||||
),
|
||||
)
|
||||
|
||||
const AuthenticatedSettingsAppearanceLazyRoute =
|
||||
AuthenticatedSettingsAppearanceLazyImport.update({
|
||||
id: '/appearance',
|
||||
path: '/appearance',
|
||||
getParentRoute: () => AuthenticatedSettingsRouteLazyRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/_authenticated/settings/appearance.lazy').then(
|
||||
(d) => d.Route,
|
||||
),
|
||||
)
|
||||
|
||||
const AuthenticatedSettingsApiTokensLazyRoute =
|
||||
AuthenticatedSettingsApiTokensLazyImport.update({
|
||||
id: '/api-tokens',
|
||||
@@ -381,6 +395,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticatedSettingsApiTokensLazyImport
|
||||
parentRoute: typeof AuthenticatedSettingsRouteLazyImport
|
||||
}
|
||||
'/_authenticated/settings/appearance': {
|
||||
id: '/_authenticated/settings/appearance'
|
||||
path: '/appearance'
|
||||
fullPath: '/settings/appearance'
|
||||
preLoaderRoute: typeof AuthenticatedSettingsAppearanceLazyImport
|
||||
parentRoute: typeof AuthenticatedSettingsRouteLazyImport
|
||||
}
|
||||
'/_authenticated/settings/configurations': {
|
||||
id: '/_authenticated/settings/configurations'
|
||||
path: '/configurations'
|
||||
@@ -479,6 +500,7 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
interface AuthenticatedSettingsRouteLazyRouteChildren {
|
||||
AuthenticatedSettingsApiTokensLazyRoute: typeof AuthenticatedSettingsApiTokensLazyRoute
|
||||
AuthenticatedSettingsAppearanceLazyRoute: typeof AuthenticatedSettingsAppearanceLazyRoute
|
||||
AuthenticatedSettingsConfigurationsLazyRoute: typeof AuthenticatedSettingsConfigurationsLazyRoute
|
||||
AuthenticatedSettingsProfileLazyRoute: typeof AuthenticatedSettingsProfileLazyRoute
|
||||
AuthenticatedSettingsProxyLazyRoute: typeof AuthenticatedSettingsProxyLazyRoute
|
||||
@@ -489,6 +511,8 @@ const AuthenticatedSettingsRouteLazyRouteChildren: AuthenticatedSettingsRouteLaz
|
||||
{
|
||||
AuthenticatedSettingsApiTokensLazyRoute:
|
||||
AuthenticatedSettingsApiTokensLazyRoute,
|
||||
AuthenticatedSettingsAppearanceLazyRoute:
|
||||
AuthenticatedSettingsAppearanceLazyRoute,
|
||||
AuthenticatedSettingsConfigurationsLazyRoute:
|
||||
AuthenticatedSettingsConfigurationsLazyRoute,
|
||||
AuthenticatedSettingsProfileLazyRoute:
|
||||
@@ -562,6 +586,7 @@ export interface FileRoutesByFullPath {
|
||||
'/503': typeof errors503LazyRoute
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute
|
||||
'/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute
|
||||
'/settings/configurations': typeof AuthenticatedSettingsConfigurationsLazyRoute
|
||||
'/settings/profile': typeof AuthenticatedSettingsProfileLazyRoute
|
||||
'/settings/proxy': typeof AuthenticatedSettingsProxyLazyRoute
|
||||
@@ -586,6 +611,7 @@ export interface FileRoutesByTo {
|
||||
'/503': typeof errors503LazyRoute
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute
|
||||
'/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute
|
||||
'/settings/configurations': typeof AuthenticatedSettingsConfigurationsLazyRoute
|
||||
'/settings/profile': typeof AuthenticatedSettingsProfileLazyRoute
|
||||
'/settings/proxy': typeof AuthenticatedSettingsProxyLazyRoute
|
||||
@@ -615,6 +641,7 @@ export interface FileRoutesById {
|
||||
'/(errors)/503': typeof errors503LazyRoute
|
||||
'/_authenticated/': typeof AuthenticatedIndexRoute
|
||||
'/_authenticated/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute
|
||||
'/_authenticated/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute
|
||||
'/_authenticated/settings/configurations': typeof AuthenticatedSettingsConfigurationsLazyRoute
|
||||
'/_authenticated/settings/profile': typeof AuthenticatedSettingsProfileLazyRoute
|
||||
'/_authenticated/settings/proxy': typeof AuthenticatedSettingsProxyLazyRoute
|
||||
@@ -644,6 +671,7 @@ export interface FileRouteTypes {
|
||||
| '/503'
|
||||
| '/'
|
||||
| '/settings/api-tokens'
|
||||
| '/settings/appearance'
|
||||
| '/settings/configurations'
|
||||
| '/settings/profile'
|
||||
| '/settings/proxy'
|
||||
@@ -667,6 +695,7 @@ export interface FileRouteTypes {
|
||||
| '/503'
|
||||
| '/'
|
||||
| '/settings/api-tokens'
|
||||
| '/settings/appearance'
|
||||
| '/settings/configurations'
|
||||
| '/settings/profile'
|
||||
| '/settings/proxy'
|
||||
@@ -694,6 +723,7 @@ export interface FileRouteTypes {
|
||||
| '/(errors)/503'
|
||||
| '/_authenticated/'
|
||||
| '/_authenticated/settings/api-tokens'
|
||||
| '/_authenticated/settings/appearance'
|
||||
| '/_authenticated/settings/configurations'
|
||||
| '/_authenticated/settings/profile'
|
||||
| '/_authenticated/settings/proxy'
|
||||
@@ -777,6 +807,7 @@ export const routeTree = rootRoute
|
||||
"parent": "/_authenticated",
|
||||
"children": [
|
||||
"/_authenticated/settings/api-tokens",
|
||||
"/_authenticated/settings/appearance",
|
||||
"/_authenticated/settings/configurations",
|
||||
"/_authenticated/settings/profile",
|
||||
"/_authenticated/settings/proxy",
|
||||
@@ -815,6 +846,10 @@ export const routeTree = rootRoute
|
||||
"filePath": "_authenticated/settings/api-tokens.lazy.tsx",
|
||||
"parent": "/_authenticated/settings"
|
||||
},
|
||||
"/_authenticated/settings/appearance": {
|
||||
"filePath": "_authenticated/settings/appearance.lazy.tsx",
|
||||
"parent": "/_authenticated/settings"
|
||||
},
|
||||
"/_authenticated/settings/configurations": {
|
||||
"filePath": "_authenticated/settings/configurations.lazy.tsx",
|
||||
"parent": "/_authenticated/settings"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { SettingsAppearance } from '@/features/settings/appearance'
|
||||
|
||||
export const Route = createLazyFileRoute('/_authenticated/settings/appearance')(
|
||||
{
|
||||
component: SettingsAppearance,
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user