From ad60b8242277e2af3b2aa21af38dce61d2e562b8 Mon Sep 17 00:00:00 2001 From: Maria Khrustaleva Date: Mon, 12 Feb 2024 15:34:23 +0100 Subject: [PATCH] Add the ability to disable sign-in/sign-up in the UI (#7348) Co-authored-by: Boris Sekachev --- cvat-core/src/api-implementation.ts | 2 + cvat-core/src/api.ts | 4 + cvat-core/src/index.ts | 1 + cvat-core/src/server-proxy.ts | 14 ++- cvat-core/src/server-response-types.ts | 30 +++++ cvat-ui/src/actions/auth-actions.ts | 28 ----- cvat-ui/src/actions/server-actions.ts | 37 ++++++ cvat-ui/src/components/cvat-app.tsx | 44 ++++--- cvat-ui/src/components/header/header.tsx | 6 +- .../src/components/login-page/login-form.tsx | 118 +++++++++--------- .../src/components/login-page/login-page.tsx | 8 +- .../src/containers/login-page/login-page.tsx | 6 +- cvat-ui/src/cvat-core-wrapper.ts | 3 +- cvat-ui/src/index.tsx | 34 +++-- cvat-ui/src/reducers/auth-reducer.ts | 25 ---- cvat-ui/src/reducers/index.ts | 22 +++- cvat-ui/src/reducers/notifications-reducer.ts | 15 ++- cvat-ui/src/reducers/root-reducer.ts | 2 + cvat-ui/src/reducers/server-api-reducer.ts | 68 ++++++++++ cvat/apps/iam/urls.py | 7 +- 20 files changed, 310 insertions(+), 164 deletions(-) create mode 100644 cvat-ui/src/actions/server-actions.ts create mode 100644 cvat-ui/src/reducers/server-api-reducer.ts diff --git a/cvat-core/src/api-implementation.ts b/cvat-core/src/api-implementation.ts index 1e365965edbb..8c3388055cf3 100644 --- a/cvat-core/src/api-implementation.ts +++ b/cvat-core/src/api-implementation.ts @@ -136,6 +136,8 @@ export default function implementAPI(cvat: CVATCore): CVATCore { return result; }); + implementationMixin(cvat.server.apiSchema, serverProxy.server.apiSchema); + implementationMixin(cvat.assets.create, async (file: File, guideId: number): Promise => { if (!(file instanceof File)) { throw new ArgumentError('Assets expect a file'); diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index 86a6ae827663..a4482f58f82d 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -129,6 +129,10 @@ function build(): CVATCore { const result = await PluginRegistry.apiWrapper(cvat.server.installedApps); return result; }, + async apiSchema() { + const result = await PluginRegistry.apiWrapper(cvat.server.apiSchema); + return result; + }, }, projects: { async get(filter = {}) { diff --git a/cvat-core/src/index.ts b/cvat-core/src/index.ts index 52d40553eba8..cbce9cb0ba5e 100644 --- a/cvat-core/src/index.ts +++ b/cvat-core/src/index.ts @@ -65,6 +65,7 @@ export default interface CVATCore { setAuthData: any; removeAuthData: any; installedApps: any; + apiSchema: typeof serverProxy.server.apiSchema; }; assets: { create: any; diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index 3e9fd8ddff6f..b440e81e8e60 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -16,7 +16,7 @@ import { SerializedAbout, SerializedRemoteFile, SerializedUserAgreement, SerializedRegister, JobsFilter, SerializedJob, SerializedGuide, SerializedAsset, SerializedQualitySettingsData, SerializedInvitationData, SerializedCloudStorage, - SerializedFramesMetaData, SerializedCollection, + SerializedFramesMetaData, SerializedCollection, SerializedAPISchema, } from './server-response-types'; import { SerializedQualityReportData } from './quality-report'; import { SerializedAnalyticsReport } from './analytics-report'; @@ -1899,6 +1899,17 @@ async function installedApps() { } } +async function getApiSchema(): Promise { + const { backendAPI } = config; + + try { + const response = await Axios.get(`${backendAPI}/schema/?scheme=json`); + return response.data; + } catch (errorData) { + throw generateError(errorData); + } +} + async function createCloudStorage(storageDetail) { const { backendAPI } = config; @@ -2410,6 +2421,7 @@ export default Object.freeze({ request: serverRequest, userAgreements, installedApps, + apiSchema: getApiSchema, }), projects: Object.freeze({ diff --git a/cvat-core/src/server-response-types.ts b/cvat-core/src/server-response-types.ts index 01b4faf6eb49..d5fff1e7ce9b 100644 --- a/cvat-core/src/server-response-types.ts +++ b/cvat-core/src/server-response-types.ts @@ -344,3 +344,33 @@ export interface SerializedFramesMetaData { start_frame: number; stop_frame: number; } + +export interface SerializedAPISchema { + openapi: string; + info: { + version: string; + description: string; + termsOfService: string; + contact: { + name: string; + url: string; + email: string; + }; + license: { + name: string; + url: string; + } + }; + paths: { + [path: string]: any; + }; + components: { + schemas: { + [component: string]: any; + } + } + externalDocs: { + description: string; + url: string; + }; +} diff --git a/cvat-ui/src/actions/auth-actions.ts b/cvat-ui/src/actions/auth-actions.ts index 3374907ac0ca..9d283cd5c76a 100644 --- a/cvat-ui/src/actions/auth-actions.ts +++ b/cvat-ui/src/actions/auth-actions.ts @@ -6,7 +6,6 @@ import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; import { RegisterData } from 'components/register-page/register-form'; import { getCore, User } from 'cvat-core-wrapper'; -import isReachable from 'utils/url-checker'; const cvat = getCore(); @@ -33,9 +32,6 @@ export enum AuthActionTypes { RESET_PASSWORD = 'RESET_PASSWORD_CONFIRM', RESET_PASSWORD_SUCCESS = 'RESET_PASSWORD_CONFIRM_SUCCESS', RESET_PASSWORD_FAILED = 'RESET_PASSWORD_CONFIRM_FAILED', - LOAD_AUTH_ACTIONS = 'LOAD_AUTH_ACTIONS', - LOAD_AUTH_ACTIONS_SUCCESS = 'LOAD_AUTH_ACTIONS_SUCCESS', - LOAD_AUTH_ACTIONS_FAILED = 'LOAD_AUTH_ACTIONS_FAILED', } export const authActions = { @@ -65,14 +61,6 @@ export const authActions = { resetPassword: () => createAction(AuthActionTypes.RESET_PASSWORD), resetPasswordSuccess: () => createAction(AuthActionTypes.RESET_PASSWORD_SUCCESS), resetPasswordFailed: (error: any) => createAction(AuthActionTypes.RESET_PASSWORD_FAILED, { error }), - loadServerAuthActions: () => createAction(AuthActionTypes.LOAD_AUTH_ACTIONS), - loadServerAuthActionsSuccess: (allowChangePassword: boolean, allowResetPassword: boolean) => ( - createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_SUCCESS, { - allowChangePassword, - allowResetPassword, - }) - ), - loadServerAuthActionsFailed: (error: any) => createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_FAILED, { error }), }; export type AuthActions = ActionUnion; @@ -188,19 +176,3 @@ export const resetPasswordAsync = ( dispatch(authActions.resetPasswordFailed(error)); } }; - -export const loadAuthActionsAsync = (): ThunkAction => async (dispatch) => { - dispatch(authActions.loadServerAuthActions()); - - try { - const promises: Promise[] = [ - isReachable(`${cvat.config.backendAPI}/auth/password/change`, 'OPTIONS'), - isReachable(`${cvat.config.backendAPI}/auth/password/reset`, 'OPTIONS'), - ]; - const [allowChangePassword, allowResetPassword] = await Promise.all(promises); - - dispatch(authActions.loadServerAuthActionsSuccess(allowChangePassword, allowResetPassword)); - } catch (error) { - dispatch(authActions.loadServerAuthActionsFailed(error)); - } -}; diff --git a/cvat-ui/src/actions/server-actions.ts b/cvat-ui/src/actions/server-actions.ts new file mode 100644 index 000000000000..b368853d04e2 --- /dev/null +++ b/cvat-ui/src/actions/server-actions.ts @@ -0,0 +1,37 @@ +// Copyright (C) 2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; +import { getCore, SerializedAPISchema } from 'cvat-core-wrapper'; + +const core = getCore(); + +export enum ServerAPIActionTypes { + GET_SERVER_API_SCHEMA = 'GET_SERVER_API_SCHEMA', + GET_SERVER_API_SCHEMA_SUCCESS = 'GET_SERVER_API_SCHEMA_SUCCESS', + GET_SERVER_API_SCHEMA_FAILED = 'GET_SERVER_API_SCHEMA_FAILED', +} + +const serverAPIActions = { + getServerAPISchema: () => createAction(ServerAPIActionTypes.GET_SERVER_API_SCHEMA), + getServerAPISchemaSuccess: (schema: SerializedAPISchema) => ( + createAction(ServerAPIActionTypes.GET_SERVER_API_SCHEMA_SUCCESS, { schema }) + ), + getServerAPISchemaFailed: (error: any) => ( + createAction(ServerAPIActionTypes.GET_SERVER_API_SCHEMA_FAILED, { error }) + ), +}; + +export type ServerAPIActions = ActionUnion; + +export const getServerAPISchemaAsync = (): ThunkAction => async (dispatch): Promise => { + dispatch(serverAPIActions.getServerAPISchema()); + + try { + const schema = await core.server.apiSchema(); + dispatch(serverAPIActions.getServerAPISchemaSuccess(schema)); + } catch (error) { + dispatch(serverAPIActions.getServerAPISchemaFailed(error)); + } +}; diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 97416ea6b3ca..9c60ae5adf80 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -89,9 +89,9 @@ interface CVATAppProps { initModels: () => void; resetErrors: () => void; resetMessages: () => void; - loadAuthActions: () => void; loadOrganization: () => void; initInvitations: () => void; + loadServerAPISchema: () => void; userInitialized: boolean; userFetching: boolean; organizationFetching: boolean; @@ -106,14 +106,16 @@ interface CVATAppProps { aboutFetching: boolean; userAgreementsFetching: boolean; userAgreementsInitialized: boolean; - authActionsFetching: boolean; - authActionsInitialized: boolean; notifications: NotificationsState; user: any; isModelPluginActive: boolean; pluginComponents: PluginsState['components']; invitationsFetching: boolean; invitationsInitialized: boolean; + serverAPISchemaFetching: boolean; + serverAPISchemaInitialized: boolean; + isPasswordResetEnabled: boolean; + isRegistrationEnabled: boolean; } interface CVATAppState { @@ -261,7 +263,7 @@ class CVATApplication extends React.PureComponent <> - + {isRegistrationEnabled && ( + + )} @@ -558,12 +564,16 @@ class CVATApplication extends React.PureComponent - - + {isPasswordResetEnabled && ( + + )} + {isPasswordResetEnabled && ( + + )} { routesToRender } diff --git a/cvat-ui/src/components/header/header.tsx b/cvat-ui/src/components/header/header.tsx index 8926d60b2b45..124b94ba457a 100644 --- a/cvat-ui/src/components/header/header.tsx +++ b/cvat-ui/src/components/header/header.tsx @@ -79,13 +79,17 @@ function mapStateToProps(state: CombinedState): StateToProps { fetching: logoutFetching, fetching: changePasswordFetching, showChangePasswordDialog: changePasswordDialogShown, - allowChangePassword: renderChangePasswordItem, }, plugins: { list }, about, shortcuts: { normalizedKeyMap, keyMap, visibleShortcutsHelp: shortcutsModalVisible }, settings: { showDialog: settingsModalVisible }, organizations: { fetching: organizationFetching, current: currentOrganization }, + serverAPI: { + configuration: { + isPasswordChangeEnabled: renderChangePasswordItem, + }, + }, } = state; return { diff --git a/cvat-ui/src/components/login-page/login-form.tsx b/cvat-ui/src/components/login-page/login-form.tsx index 246f0ac55fad..c6f906c1f706 100644 --- a/cvat-ui/src/components/login-page/login-form.tsx +++ b/cvat-ui/src/components/login-page/login-form.tsx @@ -28,13 +28,15 @@ export interface LoginData { interface Props { renderResetPassword: boolean; + renderRegistrationComponent: boolean; + renderBasicLoginComponent: boolean; fetching: boolean; onSubmit(loginData: LoginData): void; } function LoginFormComponent(props: Props): JSX.Element { const { - fetching, onSubmit, renderResetPassword, + fetching, onSubmit, renderResetPassword, renderRegistrationComponent, renderBasicLoginComponent, } = props; const authQuery = useAuthQuery(); @@ -79,7 +81,7 @@ function LoginFormComponent(props: Props): JSX.Element { ) } { - !credential && ( + !credential && renderRegistrationComponent && ( @@ -110,65 +112,69 @@ function LoginFormComponent(props: Props): JSX.Element { onSubmit(loginData); }} > - - Email or username} - className={credential ? 'cvat-input-floating-label-above' : 'cvat-input-floating-label'} - suffix={credential && ( - { - setCredential(''); - form.setFieldsValue({ credential: '', password: '' }); - }} - /> - )} - onChange={(event) => { - const { value } = event.target; - setCredential(value); - if (!value) form.setFieldsValue({ credential: '', password: '' }); - }} - /> - - { - credential && ( + {renderBasicLoginComponent && ( + <> - Email or username} + className={credential ? 'cvat-input-floating-label-above' : 'cvat-input-floating-label'} + suffix={credential && ( + { + setCredential(''); + form.setFieldsValue({ credential: '', password: '' }); + }} + /> + )} + onChange={(event) => { + const { value } = event.target; + setCredential(value); + if (!value) form.setFieldsValue({ credential: '', password: '' }); + }} /> - ) - } - { - !!credential && ( - - - - ) - } + { + credential && ( + + + + ) + } + { + !!credential && ( + + + + ) + } + + )} { pluginsToRender.map(({ component: Component }, index) => ( diff --git a/cvat-ui/src/components/login-page/login-page.tsx b/cvat-ui/src/components/login-page/login-page.tsx index 0a0073a74d19..ba0948d18d78 100644 --- a/cvat-ui/src/components/login-page/login-page.tsx +++ b/cvat-ui/src/components/login-page/login-page.tsx @@ -14,6 +14,8 @@ import LoginForm, { LoginData } from './login-form'; interface LoginPageComponentProps { fetching: boolean; renderResetPassword: boolean; + renderRegistrationComponent: boolean; + renderBasicLoginComponent: boolean; hasEmailVerificationBeenSent: boolean; onLogin: (credential: string, password: string) => void; } @@ -21,13 +23,13 @@ interface LoginPageComponentProps { function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps): JSX.Element { const history = useHistory(); const { - fetching, renderResetPassword, hasEmailVerificationBeenSent, onLogin, + fetching, renderResetPassword, renderRegistrationComponent, renderBasicLoginComponent, + hasEmailVerificationBeenSent, onLogin, } = props; if (hasEmailVerificationBeenSent) { history.push('/auth/email-verification-sent'); } - return ( @@ -36,6 +38,8 @@ function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps { onLogin(loginData.credential, loginData.password); }} diff --git a/cvat-ui/src/containers/login-page/login-page.tsx b/cvat-ui/src/containers/login-page/login-page.tsx index 71c39939f952..8c8b10707c7e 100644 --- a/cvat-ui/src/containers/login-page/login-page.tsx +++ b/cvat-ui/src/containers/login-page/login-page.tsx @@ -11,6 +11,8 @@ interface StateToProps { fetching: boolean; renderResetPassword: boolean; hasEmailVerificationBeenSent: boolean; + renderRegistrationComponent: boolean; + renderBasicLoginComponent: boolean; } interface DispatchToProps { @@ -20,7 +22,9 @@ interface DispatchToProps { function mapStateToProps(state: CombinedState): StateToProps { return { fetching: state.auth.fetching, - renderResetPassword: state.auth.allowResetPassword, + renderResetPassword: state.serverAPI.configuration.isPasswordResetEnabled, + renderRegistrationComponent: state.serverAPI.configuration.isRegistrationEnabled, + renderBasicLoginComponent: state.serverAPI.configuration.isBasicLoginEnabled, hasEmailVerificationBeenSent: state.auth.hasEmailVerificationBeenSent, }; } diff --git a/cvat-ui/src/cvat-core-wrapper.ts b/cvat-ui/src/cvat-core-wrapper.ts index e8608c9baca0..95f341d90a64 100644 --- a/cvat-ui/src/cvat-core-wrapper.ts +++ b/cvat-ui/src/cvat-core-wrapper.ts @@ -14,7 +14,7 @@ import { ModelProvider } from 'cvat-core/src/lambda-manager'; import { Label, Attribute, } from 'cvat-core/src/labels'; -import { SerializedAttribute, SerializedLabel } from 'cvat-core/src/server-response-types'; +import { SerializedAttribute, SerializedLabel, SerializedAPISchema } from 'cvat-core/src/server-response-types'; import { Job, Task } from 'cvat-core/src/session'; import Project from 'cvat-core/src/project'; import QualityReport, { QualitySummary } from 'cvat-core/src/quality-report'; @@ -106,4 +106,5 @@ export type { APIWrapperEnterOptions, QualitySummary, CVATCore, + SerializedAPISchema, }; diff --git a/cvat-ui/src/index.tsx b/cvat-ui/src/index.tsx index 48009fc6f40c..a396eaf691e9 100644 --- a/cvat-ui/src/index.tsx +++ b/cvat-ui/src/index.tsx @@ -9,7 +9,7 @@ import { connect, Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; import { getAboutAsync } from 'actions/about-actions'; -import { authorizedAsync, loadAuthActionsAsync } from 'actions/auth-actions'; +import { authorizedAsync } from 'actions/auth-actions'; import { getFormatsAsync } from 'actions/formats-actions'; import { getModelsAsync } from 'actions/models-actions'; import { getPluginsAsync } from 'actions/plugins-actions'; @@ -23,6 +23,7 @@ import createRootReducer from 'reducers/root-reducer'; import { activateOrganizationAsync } from 'actions/organization-actions'; import { resetErrors, resetMessages } from 'actions/notification-actions'; import { getInvitationsAsync } from 'actions/invitations-actions'; +import { getServerAPISchemaAsync } from 'actions/server-actions'; import { CombinedState, NotificationsState, PluginsState } from './reducers'; createCVATStore(createRootReducer); @@ -44,16 +45,16 @@ interface StateToProps { formatsFetching: boolean; userAgreementsInitialized: boolean; userAgreementsFetching: boolean; - authActionsFetching: boolean; - authActionsInitialized: boolean; - allowChangePassword: boolean; - allowResetPassword: boolean; notifications: NotificationsState; user: any; isModelPluginActive: boolean; pluginComponents: PluginsState['components']; invitationsFetching: boolean; invitationsInitialized: boolean; + serverAPISchemaFetching: boolean; + serverAPISchemaInitialized: boolean; + isPasswordResetEnabled: boolean; + isRegistrationEnabled: boolean; } interface DispatchToProps { @@ -65,20 +66,15 @@ interface DispatchToProps { resetErrors: () => void; resetMessages: () => void; loadUserAgreements: () => void; - loadAuthActions: () => void; loadOrganization: () => void; initInvitations: () => void; + loadServerAPISchema: () => void; } function mapStateToProps(state: CombinedState): StateToProps { - const { plugins } = state; - const { auth } = state; - const { formats } = state; - const { about } = state; - const { userAgreements } = state; - const { models } = state; - const { organizations } = state; - const { invitations } = state; + const { + plugins, auth, formats, about, userAgreements, models, organizations, invitations, serverAPI, + } = state; return { userInitialized: auth.initialized, @@ -95,16 +91,16 @@ function mapStateToProps(state: CombinedState): StateToProps { formatsFetching: formats.fetching, userAgreementsInitialized: userAgreements.initialized, userAgreementsFetching: userAgreements.fetching, - authActionsFetching: auth.authActionsFetching, - authActionsInitialized: auth.authActionsInitialized, - allowChangePassword: auth.allowChangePassword, - allowResetPassword: auth.allowResetPassword, notifications: state.notifications, user: auth.user, pluginComponents: plugins.components, isModelPluginActive: plugins.list.MODELS, invitationsFetching: invitations.fetching, invitationsInitialized: invitations.initialized, + serverAPISchemaFetching: serverAPI.fetching, + serverAPISchemaInitialized: serverAPI.initialized, + isPasswordResetEnabled: serverAPI.configuration.isPasswordResetEnabled, + isRegistrationEnabled: serverAPI.configuration.isRegistrationEnabled, }; } @@ -118,9 +114,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { loadAbout: (): void => dispatch(getAboutAsync()), resetErrors: (): void => dispatch(resetErrors()), resetMessages: (): void => dispatch(resetMessages()), - loadAuthActions: (): void => dispatch(loadAuthActionsAsync()), loadOrganization: (): void => dispatch(activateOrganizationAsync()), initInvitations: (): void => dispatch(getInvitationsAsync({ page: 1 }, true)), + loadServerAPISchema: (): void => dispatch(getServerAPISchemaAsync()), }; } diff --git a/cvat-ui/src/reducers/auth-reducer.ts b/cvat-ui/src/reducers/auth-reducer.ts index 086ea5b2e1dc..362903fad49c 100644 --- a/cvat-ui/src/reducers/auth-reducer.ts +++ b/cvat-ui/src/reducers/auth-reducer.ts @@ -11,11 +11,7 @@ const defaultState: AuthState = { initialized: false, fetching: false, user: null, - authActionsFetching: false, - authActionsInitialized: false, - allowChangePassword: false, showChangePasswordDialog: false, - allowResetPassword: false, hasEmailVerificationBeenSent: false, }; @@ -140,27 +136,6 @@ export default function (state = defaultState, action: AuthActions | BoundariesA ...state, fetching: false, }; - case AuthActionTypes.LOAD_AUTH_ACTIONS: - return { - ...state, - authActionsFetching: true, - }; - case AuthActionTypes.LOAD_AUTH_ACTIONS_SUCCESS: - return { - ...state, - authActionsFetching: false, - authActionsInitialized: true, - allowChangePassword: action.payload.allowChangePassword, - allowResetPassword: action.payload.allowResetPassword, - }; - case AuthActionTypes.LOAD_AUTH_ACTIONS_FAILED: - return { - ...state, - authActionsFetching: false, - authActionsInitialized: true, - allowChangePassword: false, - allowResetPassword: false, - }; case BoundariesActionTypes.RESET_AFTER_ERROR: { return { ...defaultState }; } diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index 8cf2b2884545..c77a709a8223 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -8,6 +8,7 @@ import { Canvas, RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrap import { Webhook, MLModel, Organization, Job, Label, User, QualityReport, QualityConflict, QualitySettings, FramesMetaData, RQStatus, EventLogger, Invitation, + SerializedAPISchema, } from 'cvat-core-wrapper'; import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors'; import { KeyMap } from 'utils/mousetrap-react'; @@ -18,11 +19,7 @@ export interface AuthState { initialized: boolean; fetching: boolean; user: User | null; - authActionsFetching: boolean; - authActionsInitialized: boolean; showChangePasswordDialog: boolean; - allowChangePassword: boolean; - allowResetPassword: boolean; hasEmailVerificationBeenSent: boolean; } @@ -345,6 +342,18 @@ export interface AboutState { initialized: boolean; } +export interface ServerAPIState { + schema: SerializedAPISchema | null; + fetching: boolean; + initialized: boolean; + configuration: { + isRegistrationEnabled: boolean; + isBasicLoginEnabled: boolean; + isPasswordResetEnabled: boolean; + isPasswordChangeEnabled: boolean; + }; +} + export interface UserAgreement { name: string; urlDisplayText: string; @@ -434,7 +443,9 @@ export interface NotificationsState { changePassword: null | ErrorState; requestPasswordReset: null | ErrorState; resetPassword: null | ErrorState; - loadAuthActions: null | ErrorState; + }; + serverAPI: { + fetching: null | ErrorState; }; projects: { fetching: null | ErrorState; @@ -967,6 +978,7 @@ export interface CombinedState { invitations: InvitationsState; webhooks: WebhooksState; analytics: AnalyticsState; + serverAPI: ServerAPIState; } export interface Indexable { diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index 81049aca60a5..16a6353bebea 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -24,6 +24,7 @@ import { OrganizationActionsTypes } from 'actions/organization-actions'; import { JobsActionTypes } from 'actions/jobs-actions'; import { WebhooksActionsTypes } from 'actions/webhooks-actions'; import { InvitationsActionTypes } from 'actions/invitations-actions'; +import { ServerAPIActionTypes } from 'actions/server-actions'; import { AnalyticsActionsTypes } from 'actions/analytics-actions'; import { NotificationsState } from '.'; @@ -38,7 +39,9 @@ const defaultState: NotificationsState = { changePassword: null, requestPasswordReset: null, resetPassword: null, - loadAuthActions: null, + }, + serverAPI: { + fetching: null, }, projects: { fetching: null, @@ -381,15 +384,15 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } - case AuthActionTypes.LOAD_AUTH_ACTIONS_FAILED: { + case ServerAPIActionTypes.GET_SERVER_API_SCHEMA_FAILED: { return { ...state, errors: { ...state.errors, - auth: { - ...state.errors.auth, - loadAuthActions: { - message: 'Could not check available auth actions', + serverAPI: { + ...state.errors.serverAPI, + fetching: { + message: 'Could not receive server schema', reason: action.payload.error, shouldLog: !(action.payload.error instanceof ServerError), }, diff --git a/cvat-ui/src/reducers/root-reducer.ts b/cvat-ui/src/reducers/root-reducer.ts index 323eb016153c..2aa6958a8009 100644 --- a/cvat-ui/src/reducers/root-reducer.ts +++ b/cvat-ui/src/reducers/root-reducer.ts @@ -25,6 +25,7 @@ import organizationsReducer from './organizations-reducer'; import webhooksReducer from './webhooks-reducer'; import analyticsReducer from './analytics-reducer'; import invitationsReducer from './invitations-reducer'; +import serverAPIReducer from './server-api-reducer'; export default function createRootReducer(): Reducer { return combineReducers({ @@ -49,5 +50,6 @@ export default function createRootReducer(): Reducer { webhooks: webhooksReducer, analytics: analyticsReducer, invitations: invitationsReducer, + serverAPI: serverAPIReducer, }); } diff --git a/cvat-ui/src/reducers/server-api-reducer.ts b/cvat-ui/src/reducers/server-api-reducer.ts new file mode 100644 index 000000000000..c57604fdb109 --- /dev/null +++ b/cvat-ui/src/reducers/server-api-reducer.ts @@ -0,0 +1,68 @@ +// Copyright (C) 2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import { BoundariesActions, BoundariesActionTypes } from 'actions/boundaries-actions'; +import { ServerAPIActions, ServerAPIActionTypes } from 'actions/server-actions'; +import { ServerAPIState } from '.'; + +const defaultState: ServerAPIState = { + schema: null, + fetching: false, + initialized: false, + configuration: { + isRegistrationEnabled: true, + isBasicLoginEnabled: true, + isPasswordResetEnabled: true, + isPasswordChangeEnabled: true, + }, +}; + +export default function ( + state: ServerAPIState = defaultState, + action: ServerAPIActions | BoundariesActions, +): ServerAPIState { + switch (action.type) { + case ServerAPIActionTypes.GET_SERVER_API_SCHEMA: { + return { + ...state, + fetching: true, + initialized: false, + }; + } + case ServerAPIActionTypes.GET_SERVER_API_SCHEMA_SUCCESS: { + const { schema } = action.payload; + const isRegistrationEnabled = Object.keys(schema.paths).includes('/api/auth/register'); + const isBasicLoginEnabled = Object.keys(schema.paths).includes('/api/auth/login'); + const isPasswordResetEnabled = Object.keys(schema.paths).includes('/api/auth/password/reset'); + const isPasswordChangeEnabled = Object.keys(schema.paths).includes('/api/auth/password/change'); + + return { + ...state, + fetching: false, + initialized: true, + schema, + configuration: { + isRegistrationEnabled, + isBasicLoginEnabled, + isPasswordResetEnabled, + isPasswordChangeEnabled, + }, + }; + } + case ServerAPIActionTypes.GET_SERVER_API_SCHEMA_FAILED: { + return { + ...state, + fetching: false, + initialized: true, + }; + } + case BoundariesActionTypes.RESET_AFTER_ERROR: { + return { + ...defaultState, + }; + } + default: + return state; + } +} diff --git a/cvat/apps/iam/urls.py b/cvat/apps/iam/urls.py index d8d74c3e7794..8b8135fc2d9a 100644 --- a/cvat/apps/iam/urls.py +++ b/cvat/apps/iam/urls.py @@ -16,8 +16,11 @@ ConfirmEmailViewEx, LoginViewEx ) +BASIC_LOGIN_PATH_NAME = 'rest_login' +BASIC_REGISTER_PATH_NAME = 'rest_register' + urlpatterns = [ - path('login', LoginViewEx.as_view(), name='rest_login'), + path('login', LoginViewEx.as_view(), name=BASIC_LOGIN_PATH_NAME), path('logout', LogoutView.as_view(), name='rest_logout'), path('signing', SigningView.as_view(), name='signing'), path('rules', RulesView.as_view(), name='rules'), @@ -25,7 +28,7 @@ if settings.IAM_TYPE == 'BASIC': urlpatterns += [ - path('register', RegisterViewEx.as_view(), name='rest_register'), + path('register', RegisterViewEx.as_view(), name=BASIC_REGISTER_PATH_NAME), # password path('password/reset', PasswordResetView.as_view(), name='rest_password_reset'),