From d846e8aa132dcca825b6a579856febbb0eccdcf2 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:56:22 +0100 Subject: [PATCH] Fix enterprise settings not taking effect immediately, ehance gateway token error logging (#865) * fix bugs, including a bug related to enterprise settings * move delay * fix tests * log token errors --- src/bin/defguard.rs | 6 +- src/enterprise/handlers/mod.rs | 14 +- src/enterprise/handlers/openid_login.rs | 3 +- src/enterprise/limits.rs | 15 +- src/grpc/enrollment.rs | 4 +- src/grpc/interceptor.rs | 64 +++++--- src/handlers/settings.rs | 2 +- src/handlers/user.rs | 9 +- src/lib.rs | 6 +- web/src/components/AppLoader.tsx | 24 +-- web/src/pages/auth/AuthPage.tsx | 5 +- .../EnterpriseSettings/EnterpriseSettings.tsx | 27 +++- .../LicenseSettings/LicenseSettings.tsx | 150 ++++++++++-------- .../components/LicenseSettings/styles.scss | 6 + .../OpenIdSettings/OpenIdSettings.tsx | 26 ++- web/src/pages/settings/style.scss | 9 ++ .../ProfileDetailsForm/ProfileDetailsForm.tsx | 4 +- web/src/shared/hooks/useApi.tsx | 3 + web/src/shared/queries.ts | 1 + web/src/shared/types.ts | 5 + 20 files changed, 262 insertions(+), 121 deletions(-) diff --git a/src/bin/defguard.rs b/src/bin/defguard.rs index a52be806f..f33d50d8b 100644 --- a/src/bin/defguard.rs +++ b/src/bin/defguard.rs @@ -9,7 +9,7 @@ use defguard::{ db::{init_db, AppEvent, GatewayEvent, Settings, User}, enterprise::{ license::{run_periodic_license_check, set_cached_license, License}, - limits::update_counts, + limits::{run_periodic_count_update, update_counts}, }, grpc::{run_grpc_bidi_stream, run_grpc_server, GatewayMap, WorkerState}, init_dev_env, init_vpn_location, @@ -123,7 +123,9 @@ async fn main() -> Result<(), anyhow::Error> { res = run_mail_handler(mail_rx, pool.clone()) => error!("Mail handler returned early: {res:#?}"), res = run_periodic_peer_disconnect(pool.clone(), wireguard_tx) => error!("Periodic peer disconnect task returned early: {res:#?}"), res = run_periodic_stats_purge(pool.clone(), config.stats_purge_frequency.into(), config.stats_purge_threshold.into()), if !config.disable_stats_purge => error!("Periodic stats purge task returned early: {res:#?}"), - res = run_periodic_license_check(pool) => error!("Periodic license check task returned early: {res:#?}"), + res = run_periodic_license_check(pool.clone()) => error!("Periodic license check task returned early: {res:#?}"), + // Temporary. Change to a database trigger when they are implemented. + res = run_periodic_count_update(&pool) => error!("Periodic count update task returned early: {res:#?}"), } Ok(()) } diff --git a/src/enterprise/handlers/mod.rs b/src/enterprise/handlers/mod.rs index 651a175bd..5816a2d75 100644 --- a/src/enterprise/handlers/mod.rs +++ b/src/enterprise/handlers/mod.rs @@ -1,5 +1,5 @@ use crate::{ - auth::SessionInfo, + auth::{SessionInfo, UserAdminRole}, handlers::{ApiResponse, ApiResult}, }; @@ -45,7 +45,19 @@ where } } +/// Gets basic enterprise status. pub async fn check_enterprise_status() -> ApiResult { + let enterprise_enabled = is_enterprise_enabled(); + Ok(ApiResponse { + json: serde_json::json!({ + "enabled": enterprise_enabled, + }), + status: StatusCode::OK, + }) +} + +/// Gets full information about enterprise status. +pub async fn check_enterprise_info(_admin: UserAdminRole, _session: SessionInfo) -> ApiResult { let enterprise_enabled = is_enterprise_enabled(); let needs_license = needs_enterprise_license(); let license = get_cached_license(); diff --git a/src/enterprise/handlers/openid_login.rs b/src/enterprise/handlers/openid_login.rs index 3da1b72fc..20786fec2 100644 --- a/src/enterprise/handlers/openid_login.rs +++ b/src/enterprise/handlers/openid_login.rs @@ -26,7 +26,7 @@ use super::LicenseInfo; use crate::{ appstate::AppState, db::{Id, Settings, User}, - enterprise::db::models::openid_provider::OpenIdProvider, + enterprise::{db::models::openid_provider::OpenIdProvider, limits::update_counts}, error::WebError, handlers::{ auth::create_session, @@ -270,6 +270,7 @@ pub(crate) async fn user_from_claims( } }; + update_counts(pool).await?; Ok(user) } diff --git a/src/enterprise/limits.rs b/src/enterprise/limits.rs index a9ebfe724..bb44ff55c 100644 --- a/src/enterprise/limits.rs +++ b/src/enterprise/limits.rs @@ -1,5 +1,9 @@ use sqlx::{error::Error as SqlxError, query_as, PgPool}; -use std::sync::{RwLock, RwLockReadGuard}; +use std::{ + sync::{RwLock, RwLockReadGuard}, + time::Duration, +}; +use tokio::time::sleep; #[derive(Debug)] pub(crate) struct Counts { @@ -50,6 +54,15 @@ pub async fn update_counts(pool: &PgPool) -> Result<(), SqlxError> { Ok(()) } +// Just to make sure we don't miss any user/device/network count changes +pub async fn run_periodic_count_update(pool: &PgPool) -> Result<(), SqlxError> { + let delay = Duration::from_secs(60 * 60); + loop { + update_counts(pool).await?; + sleep(delay).await; + } +} + impl Counts { pub(crate) fn is_over_limit(&self) -> bool { self.user > 5 || self.device > 10 || self.wireguard_network > 1 diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index af88dd099..3f310bd58 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -20,7 +20,7 @@ use crate::{ }, Device, GatewayEvent, Id, Settings, User, }, - enterprise::db::models::enterprise_settings::EnterpriseSettings, + enterprise::{db::models::enterprise_settings::EnterpriseSettings, limits::update_counts}, grpc::utils::build_device_config_response, handlers::{mail::send_new_device_added_email, user::check_password_strength}, headers::get_device_info, @@ -302,6 +302,7 @@ impl EnrollmentServer { Status::internal("unexpected error") })?; debug!("Updating user details ended with success."); + let _ = update_counts(&self.pool).await; // sync with LDAP debug!("Add user to ldap: {}.", self.ldap_feature_active); @@ -468,6 +469,7 @@ impl EnrollmentServer { Status::internal("unexpected error") })?; info!("New device created: {device:?}."); + let _ = update_counts(&self.pool).await; debug!( "Adding device {} to all existing user networks for user {}({:?}).", diff --git a/src/grpc/interceptor.rs b/src/grpc/interceptor.rs index 190d0f09e..4b8bceef5 100644 --- a/src/grpc/interceptor.rs +++ b/src/grpc/interceptor.rs @@ -18,37 +18,57 @@ impl JwtInterceptor { impl Interceptor for JwtInterceptor { fn call(&mut self, mut req: tonic::Request<()>) -> Result, Status> { + // This is only used for logging purposes, so no proper error handling + let hostname = req + .metadata() + .get("hostname") + .map(|h| h.to_str().unwrap_or("UNKNOWN")) + .unwrap_or("UNKNOWN"); + let token = match req.metadata().get("authorization") { - Some(token) => token - .to_str() - .map_err(|_| Status::unauthenticated("Invalid token"))?, + Some(token) => token.to_str().map_err(|err| { + warn!("Failed to parse authorization header during handling gRPC request from hostname {}. \ + If you recognize this hostname, there may be an issue with the token used for authorization. \ + Cause of the failed parsing: {:?}", hostname, err); + Status::unauthenticated("Invalid token") + })?, None => return Err(Status::unauthenticated("Missing authorization header")), }; - if let Ok(claims) = Claims::from_jwt(self.claims_type, token) { - let request_metadata = req.metadata_mut(); - if let ClaimsType::Gateway = self.claims_type { + match Claims::from_jwt(self.claims_type, token) { + Ok(claims) => { + let request_metadata = req.metadata_mut(); + + if let ClaimsType::Gateway = self.claims_type { + request_metadata.insert( + "gateway_network_id", + claims + .client_id + .parse() + .map_err(|_| Status::unknown("Network ID parsing error"))?, + ); + } + + // FIXME: can we push whole Claims object into metadata? request_metadata.insert( - "gateway_network_id", + "username", claims - .client_id + .sub .parse() - .map_err(|_| Status::unknown("Network ID parsing error"))?, + .map_err(|_| Status::unknown("Username parsing error"))?, ); + debug!("Authorization successful for user {}", claims.sub); + Ok(req) + } + Err(err) => { + warn!( + "Failed to authorize a gRPC request from hostname {}. \ + If you recognize this hostname, there may be an issue with the token used for authorization. \ + Cause of the failed authorization: {:?}", + hostname, err + ); + Err(Status::unauthenticated("Invalid token")) } - - // FIXME: can we push whole Claims object into metadata? - request_metadata.insert( - "username", - claims - .sub - .parse() - .map_err(|_| Status::unknown("Username parsing error"))?, - ); - debug!("Authorization successful for user {}", claims.sub); - Ok(req) - } else { - Err(Status::unauthenticated("Invalid token")) } } } diff --git a/src/handlers/settings.rs b/src/handlers/settings.rs index 90ccc76bb..b3b1517bc 100644 --- a/src/handlers/settings.rs +++ b/src/handlers/settings.rs @@ -21,7 +21,7 @@ use crate::{ static DEFAULT_NAV_LOGO_URL: &str = "/svg/defguard-nav-logo.svg"; static DEFAULT_MAIN_LOGO_URL: &str = "/svg/logo-defguard-white.svg"; -pub async fn get_settings(State(appstate): State) -> ApiResult { +pub async fn get_settings(_admin: AdminRole, State(appstate): State) -> ApiResult { debug!("Retrieving settings"); if let Some(mut settings) = Settings::get(&appstate.pool).await? { if settings.nav_logo_url.is_empty() { diff --git a/src/handlers/user.rs b/src/handlers/user.rs index 8aff5cd94..bc0226401 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -21,7 +21,7 @@ use crate::{ AppEvent, GatewayEvent, OAuth2AuthorizedApp, Settings, User, UserDetails, UserInfo, Wallet, WebAuthn, WireguardNetwork, }, - enterprise::limits::update_counts, + enterprise::{db::models::enterprise_settings::EnterpriseSettings, limits::update_counts}, error::WebError, ldap::utils::{ldap_add_user, ldap_change_password, ldap_delete_user, ldap_modify_user}, mail::Mail, @@ -482,6 +482,13 @@ pub async fn start_remote_desktop_configuration( session.user.username ); + let settings = EnterpriseSettings::get(&appstate.pool).await?; + if settings.admin_device_management && !session.is_admin { + return Err(WebError::Forbidden( + "Only admin users can manage devices".into(), + )); + } + debug!("Verify that the user from the current session is an admin or only peforms desktop activation for self."); let user = user_for_admin_or_self(&appstate.pool, &session, &username).await?; debug!("Successfully fetched user data: {user:?}"); diff --git a/src/lib.rs b/src/lib.rs index c40fc74ce..329b4ffd3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ use axum::{ serve, Extension, Json, Router, }; use enterprise::handlers::{ - check_enterprise_status, + check_enterprise_info, check_enterprise_status, enterprise_settings::{get_enterprise_settings, patch_enterprise_settings}, openid_login::{auth_callback, get_auth_info}, openid_providers::{add_openid_provider, delete_openid_provider, get_current_openid_provider}, @@ -414,7 +414,9 @@ pub fn build_webapp( .route("/callback", post(auth_callback)) .route("/auth_info", get(get_auth_info)), ); - let webapp = webapp.route("/api/v1/enterprise_status", get(check_enterprise_status)); + let webapp = webapp + .route("/api/v1/enterprise_status", get(check_enterprise_status)) + .route("/api/v1/enterprise_info", get(check_enterprise_info)); #[cfg(feature = "openid")] let webapp = webapp diff --git a/web/src/components/AppLoader.tsx b/web/src/components/AppLoader.tsx index 55b94d063..eabf80489 100644 --- a/web/src/components/AppLoader.tsx +++ b/web/src/components/AppLoader.tsx @@ -28,8 +28,8 @@ export const AppLoader = () => { const appSettings = useAppStore((state) => state.settings); const { getAppInfo, - getEnterpriseStatus, user: { getMe }, + getEnterpriseStatus, settings: { getEssentialSettings, getEnterpriseSettings }, } = useApi(); const [userLoading, setUserLoading] = useState(true); @@ -68,6 +68,18 @@ export const AppLoader = () => { enabled: !isUndefined(currentUser), }); + useQuery([QueryKeys.FETCH_ENTERPRISE_SETTINGS], getEnterpriseSettings, { + onSuccess: (settings) => { + setAppStore({ enterprise_settings: settings }); + }, + onError: (err) => { + console.error(err); + }, + refetchOnWindowFocus: true, + retry: false, + enabled: !isUndefined(currentUser), + }); + useQuery([QueryKeys.FETCH_ENTERPRISE_STATUS], getEnterpriseStatus, { onSuccess: (status) => { setAppStore({ @@ -83,16 +95,6 @@ export const AppLoader = () => { retry: false, }); - useQuery([QueryKeys.FETCH_ENTERPRISE_SETTINGS], getEnterpriseSettings, { - onSuccess: (settings) => { - setAppStore({ enterprise_settings: settings }); - }, - onError: (err) => { - console.error(err); - }, - refetchOnWindowFocus: true, - retry: false, - }); const { isLoading: settingsLoading, data: essentialSettings } = useQuery( [QueryKeys.FETCH_ESSENTIAL_SETTINGS], getEssentialSettings, diff --git a/web/src/pages/auth/AuthPage.tsx b/web/src/pages/auth/AuthPage.tsx index 93a50c06f..503232488 100644 --- a/web/src/pages/auth/AuthPage.tsx +++ b/web/src/pages/auth/AuthPage.tsx @@ -116,7 +116,10 @@ export const AuthPage = () => { // check where to navigate administrator const appInfo = await getAppInfo(); const settings = await getSettings(); - setAppStore({ appInfo, settings }); + setAppStore({ + appInfo, + settings, + }); if (settings.wireguard_enabled) { if (!appInfo?.network_present) { navigateURL = '/admin/wizard'; diff --git a/web/src/pages/settings/components/EnterpriseSettings/EnterpriseSettings.tsx b/web/src/pages/settings/components/EnterpriseSettings/EnterpriseSettings.tsx index dda18754c..1df752203 100644 --- a/web/src/pages/settings/components/EnterpriseSettings/EnterpriseSettings.tsx +++ b/web/src/pages/settings/components/EnterpriseSettings/EnterpriseSettings.tsx @@ -1,23 +1,40 @@ +import { useQuery } from '@tanstack/react-query'; import parse from 'html-react-parser'; import { useI18nContext } from '../../../../i18n/i18n-react'; import { BigInfoBox } from '../../../../shared/defguard-ui/components/Layout/BigInfoBox/BigInfoBox'; -import { useAppStore } from '../../../../shared/hooks/store/useAppStore'; +import { LoaderSpinner } from '../../../../shared/defguard-ui/components/Layout/LoaderSpinner/LoaderSpinner'; +import useApi from '../../../../shared/hooks/useApi'; +import { QueryKeys } from '../../../../shared/queries'; import { EnterpriseForm } from './components/EnterpriseForm'; export const EnterpriseSettings = () => { - const enterpriseStatus = useAppStore((state) => state.enterprise_status); const { LL } = useI18nContext(); const localLL = LL.settingsPage.enterpriseOnly; + const { getEnterpriseInfo } = useApi(); + const { data: enterpriseInfo, isLoading } = useQuery({ + queryFn: getEnterpriseInfo, + queryKey: [QueryKeys.FETCH_ENTERPRISE_INFO], + refetchOnMount: true, + refetchOnWindowFocus: false, + }); + if (isLoading) { + return ( +
+ +
+ ); + } + return ( <> - {!enterpriseStatus?.enabled && ( + {!enterpriseInfo?.enabled && (

{localLL.title()}

{/* If enterprise is disabled but we have some license info, the license probably expired */} - {enterpriseStatus?.license_info &&

{localLL.currentExpired()}

} + {enterpriseInfo?.license_info &&

{localLL.currentExpired()}

}

{localLL.subtitle()}{' '} @@ -29,7 +46,7 @@ export const EnterpriseSettings = () => {

)} - {!enterpriseStatus?.needs_license && !enterpriseStatus?.license_info && ( + {!enterpriseInfo?.needs_license && !enterpriseInfo?.license_info && (
{ } = useApi(); const settings = useSettingsPage((state) => state.settings); - const enterpriseStatus = useAppStore((state) => state.enterprise_status); const queryClient = useQueryClient(); const { breakpoint } = useBreakpoint(deviceBreakpoints); @@ -56,6 +55,7 @@ export const LicenseSettings = () => { toaster.success(LL.settingsPage.messages.editSuccess()); queryClient.invalidateQueries([QueryKeys.FETCH_SETTINGS]); queryClient.invalidateQueries([QueryKeys.FETCH_ENTERPRISE_STATUS]); + queryClient.invalidateQueries([QueryKeys.FETCH_ENTERPRISE_INFO]); }, onError: (err: AxiosError) => { const errorResponse = err.response?.data as LicenseErrorResponse; @@ -66,6 +66,14 @@ export const LicenseSettings = () => { }, }); + const { getEnterpriseInfo } = useApi(); + const { data: enterpriseInfo, isLoading: enterpriseInfoLoading } = useQuery({ + queryFn: getEnterpriseInfo, + queryKey: [QueryKeys.FETCH_ENTERPRISE_INFO], + refetchOnMount: true, + refetchOnWindowFocus: false, + }); + const zodSchema = useMemo( () => z.object({ @@ -126,73 +134,85 @@ export const LicenseSettings = () => { type="submit" />
-
-
- - - - {enterpriseStatus?.license_info ? ( -
-
- - {enterpriseStatus?.enabled ? ( -
- -

{LL.settingsPage.license.licenseInfo.fields.status.active()}

- {enterpriseStatus?.license_info.subscription ? ( - - {LL.settingsPage.license.licenseInfo.fields.status.subscriptionHelper()} - - ) : null} -
- ) : ( -
- -

{LL.settingsPage.license.licenseInfo.fields.status.expired()}

+ {enterpriseInfoLoading ? ( +
+ +
+ ) : ( +
+
+ + + + {enterpriseInfo?.license_info ? ( +
+
+ + {enterpriseInfo?.enabled ? ( +
+ +

+ {LL.settingsPage.license.licenseInfo.fields.status.active()} +

+ {enterpriseInfo?.license_info.subscription ? ( + + {LL.settingsPage.license.licenseInfo.fields.status.subscriptionHelper()} + + ) : null} +
+ ) : ( +
+ +

+ {LL.settingsPage.license.licenseInfo.fields.status.expired()} +

+
+ )} +
+
+ +
+

+ {enterpriseInfo?.license_info.subscription + ? LL.settingsPage.license.licenseInfo.types.subscription.label() + : LL.settingsPage.license.licenseInfo.types.offline.label()} +

+ + {enterpriseInfo?.license_info.subscription + ? LL.settingsPage.license.licenseInfo.types.subscription.helper() + : LL.settingsPage.license.licenseInfo.types.offline.helper()} +
- )} -
-
- -
+
+
+

- {enterpriseStatus?.license_info.subscription - ? LL.settingsPage.license.licenseInfo.types.subscription.label() - : LL.settingsPage.license.licenseInfo.types.offline.label()} + {enterpriseInfo?.license_info.valid_until + ? new Date( + enterpriseInfo?.license_info.valid_until, + ).toLocaleString() + : '-'}

- - {enterpriseStatus?.license_info.subscription - ? LL.settingsPage.license.licenseInfo.types.subscription.helper() - : LL.settingsPage.license.licenseInfo.types.offline.helper()} -
-
- -

- {enterpriseStatus?.license_info.valid_until - ? new Date( - enterpriseStatus?.license_info.valid_until, - ).toLocaleString() - : '-'} -

-
-
- ) : ( - <> -

{LL.settingsPage.license.licenseInfo.noLicense()}

- - )} -
-
+ ) : ( + <> +

{LL.settingsPage.license.licenseInfo.noLicense()}

+ + )} + +
+ )} ); diff --git a/web/src/pages/settings/components/GlobalSettings/components/LicenseSettings/styles.scss b/web/src/pages/settings/components/GlobalSettings/components/LicenseSettings/styles.scss index a14d6b109..3d1ca3037 100644 --- a/web/src/pages/settings/components/GlobalSettings/components/LicenseSettings/styles.scss +++ b/web/src/pages/settings/components/GlobalSettings/components/LicenseSettings/styles.scss @@ -7,6 +7,12 @@ @include media-breakpoint-up(lg) { padding: 16px 15px; } + + .loading-license-info { + display: flex; + justify-content: center; + align-items: center; + } } } diff --git a/web/src/pages/settings/components/OpenIdSettings/OpenIdSettings.tsx b/web/src/pages/settings/components/OpenIdSettings/OpenIdSettings.tsx index 37a3f1a0f..dc5c44bc1 100644 --- a/web/src/pages/settings/components/OpenIdSettings/OpenIdSettings.tsx +++ b/web/src/pages/settings/components/OpenIdSettings/OpenIdSettings.tsx @@ -1,27 +1,43 @@ import './style.scss'; +import { useQuery } from '@tanstack/react-query'; import parse from 'html-react-parser'; import { useI18nContext } from '../../../../i18n/i18n-react'; import { BigInfoBox } from '../../../../shared/defguard-ui/components/Layout/BigInfoBox/BigInfoBox'; -import { useAppStore } from '../../../../shared/hooks/store/useAppStore'; +import { LoaderSpinner } from '../../../../shared/defguard-ui/components/Layout/LoaderSpinner/LoaderSpinner'; +import useApi from '../../../../shared/hooks/useApi'; +import { QueryKeys } from '../../../../shared/queries'; import { OpenIdGeneralSettings } from './components/OpenIdGeneralSettings'; import { OpenIdSettingsForm } from './components/OpenIdSettingsForm'; export const OpenIdSettings = () => { - const enterpriseStatus = useAppStore((state) => state.enterprise_status); const { LL } = useI18nContext(); const localLL = LL.settingsPage.enterpriseOnly; + const { getEnterpriseInfo } = useApi(); + const { data: enterpriseInfo, isLoading } = useQuery({ + queryFn: getEnterpriseInfo, + queryKey: [QueryKeys.FETCH_ENTERPRISE_INFO], + refetchOnMount: true, + refetchOnWindowFocus: false, + }); + if (isLoading) { + return ( +
+ +
+ ); + } return ( <> - {!enterpriseStatus?.enabled && ( + {!enterpriseInfo?.enabled && (
)} - {!enterpriseStatus?.needs_license && !enterpriseStatus?.license_info && ( + {!enterpriseInfo?.needs_license && !enterpriseInfo?.license_info && (
{ getGroups, { refetchOnWindowFocus: false, - enabled: fetchGroups, + enabled: fetchGroups && isAdmin, }, ); const toaster = useToaster(); @@ -330,7 +330,7 @@ export const ProfileDetailsForm = () => { options={groupsOptions} controller={{ control, name: 'groups' }} label={LL.userPage.userDetails.fields.groups.label()} - loading={groupsLoading || userEditLoading} + loading={isAdmin && (groupsLoading || userEditLoading)} disabled={!isAdmin} renderSelected={(val) => ({ key: val, diff --git a/web/src/shared/hooks/useApi.tsx b/web/src/shared/hooks/useApi.tsx index 84a961c41..14b8bcb01 100644 --- a/web/src/shared/hooks/useApi.tsx +++ b/web/src/shared/hooks/useApi.tsx @@ -362,6 +362,8 @@ const useApi = (props?: HookProps): ApiHook => { const getEnterpriseStatus = () => client.get('/enterprise_status').then(unpackRequest); + const getEnterpriseInfo = () => client.get('/enterprise_info').then(unpackRequest); + const mfaEnable = () => client.put('/auth/mfa').then(unpackRequest); const recovery: ApiHook['auth']['mfa']['recovery'] = (data) => @@ -504,6 +506,7 @@ const useApi = (props?: HookProps): ApiHook => { getAppInfo, changePasswordSelf, getEnterpriseStatus, + getEnterpriseInfo, oAuth: { consent: oAuthConsent, }, diff --git a/web/src/shared/queries.ts b/web/src/shared/queries.ts index 0d539d21a..c930639cd 100644 --- a/web/src/shared/queries.ts +++ b/web/src/shared/queries.ts @@ -30,4 +30,5 @@ export const QueryKeys = { FETCH_OPENID_INFO: 'FETCH_OPENID_INFO', FETCH_ENTERPRISE_STATUS: 'FETCH_ENTERPRISE_STATUS', FETCH_ENTERPRISE_SETTINGS: 'FETCH_ENTERPRISE_SETTINGS', + FETCH_ENTERPRISE_INFO: 'FETCH_ENTERPRISE_INFO', }; diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index f8d23147d..5274ead6d 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -436,6 +436,7 @@ export interface ApiHook { getAppInfo: () => Promise; changePasswordSelf: (data: ChangePasswordSelfRequest) => Promise; getEnterpriseStatus: () => Promise; + getEnterpriseInfo: () => Promise; oAuth: { consent: (params: unknown) => Promise; }; @@ -881,6 +882,10 @@ export type LicenseInfo = { export type EnterpriseStatus = { enabled: boolean; +}; + +export type EnterpriseInfo = { + enabled: boolean; // If there is no license, there is no license info license_info?: LicenseInfo; needs_license: boolean;