{!this.state.isMfaActive && this.state.isMfaAvailable && this.state.dateDisabled && (
{
).replace('##date##', formatDate(this.state.dateDisabled))}
/>
)}
-
- {this.state.isPlansMessageVisible && (
-
- {t('This feature is not available on your current plan. Please visit the ')}
- {t('Plans page')}
- {t(' to upgrade your account.')}
- >
- }
+
+ {this.state.isPlansMessageVisible && (
+
+ {t('This feature is not available on your current plan. Please visit the ')}
+ {t('Plans page')}
+ {t(' to upgrade your account.')}
+ >
+ }
+ />
+ )}
+
- {t(
- "Connect your KoboToolbox account with your organization's identity provider for single-sign on (SSO). Afterwards, you will only " +
- 'be able to sign in via SSO unless you disable this setting here. This will also update your email address in case your current ' +
- 'address is different.'
- )}
-
+
+ {t(
+ "Connect your KoboToolbox account with your organization's identity provider for single-sign on (SSO). Afterwards, you will only " +
+ 'be able to sign in via SSO unless you disable this setting here. This will also update your email address in case your current ' +
+ 'address is different.'
+ )}
) : (
-
{t('Already connected')}
+
+ {t('Already connected')}
+
)}
{socialAccounts.length === 0 ? (
-
+
{socialApps.map((socialApp) => (
) : (
-
+
)}
-
+
);
});
diff --git a/jsapp/js/account/security/sso/ssoSection.module.scss b/jsapp/js/account/security/sso/ssoSection.module.scss
index f90a601ad9..356bab7ab9 100644
--- a/jsapp/js/account/security/sso/ssoSection.module.scss
+++ b/jsapp/js/account/security/sso/ssoSection.module.scss
@@ -1,43 +1,11 @@
@use 'scss/colors';
@use 'scss/sizes';
-@use 'scss/libs/_mdl';
-.root {
- margin-top: sizes.$x20;
- margin-bottom: sizes.$x60;
- padding-top: sizes.$x14;
- display: flex;
- align-items: baseline;
- column-gap: sizes.$x16;
- border-top: sizes.$x1 solid;
- border-color: colors.$kobo-gray-300;
-}
-
-.titleSection {
- flex: 2;
- display: flex;
- flex-direction: row;
- align-items: center;
- flex-wrap: wrap;
-}
-
-.title {
- margin: 0;
- color: colors.$kobo-gray-700;
- font-weight: 600;
- line-height: 1.6;
-}
-
-.bodySection {
+.body {
flex: 6;
- display: flex;
- flex-direction: row;
- align-items: center;
- flex-wrap: wrap;
- padding-left: sizes.$x16;
}
-.optionsSection {
+.options {
text-align: right;
flex: 2;
diff --git a/jsapp/js/actions/mfaActions.ts b/jsapp/js/actions/mfaActions.ts
index 098c7afa1a..8128867a67 100644
--- a/jsapp/js/actions/mfaActions.ts
+++ b/jsapp/js/actions/mfaActions.ts
@@ -102,7 +102,7 @@ mfaActions.getMfaAvailability.listen(() => {
mfaActions.getMfaAvailability.failed({isMfaAvailable: false, isPlansMessageVisible: false});
});
}
- })
+ });
});
mfaActions.confirmCode.listen((mfaCode: string) => {
diff --git a/jsapp/js/api.endpoints.ts b/jsapp/js/api.endpoints.ts
index 9d0ade0300..5a4c6e899d 100644
--- a/jsapp/js/api.endpoints.ts
+++ b/jsapp/js/api.endpoints.ts
@@ -10,4 +10,6 @@ export const endpoints = {
PORTAL_URL: '/api/v2/stripe/customer-portal',
/** Expected parameters: price_id and subscription_id **/
CHANGE_PLAN_URL: '/api/v2/stripe/change-plan',
+ ACCESS_LOGS_URL: '/api/v2/access-logs/me',
+ LOGOUT_ALL: '/logout-all/',
};
diff --git a/jsapp/js/query/queries/accessLogs.query.ts b/jsapp/js/query/queries/accessLogs.query.ts
new file mode 100644
index 0000000000..ea2bf8df1d
--- /dev/null
+++ b/jsapp/js/query/queries/accessLogs.query.ts
@@ -0,0 +1,53 @@
+import {keepPreviousData, useQuery} from '@tanstack/react-query';
+import {endpoints} from 'js/api.endpoints';
+import type {PaginatedResponse} from 'js/dataInterface';
+import {fetchGet} from 'js/api';
+
+export interface AccessLog {
+ app_label: 'kobo_auth' | string;
+ model_name: 'User' | string;
+ object_id: number;
+ /** User URL */
+ user: string;
+ user_uid: string;
+ username: string;
+ action: 'auth' | string;
+ metadata: {
+ /** E.g. "Firefox (Ubuntu)" */
+ source: string;
+ auth_type: 'Digest' | string;
+ ip_address: string;
+ };
+ /** Date string */
+ date_created: string;
+ log_type: 'access' | string;
+}
+
+async function getAccessLogs(limit: number, offset: number) {
+ const params = new URLSearchParams({
+ limit: limit.toString(),
+ offset: offset.toString(),
+ });
+ return fetchGet>(
+ endpoints.ACCESS_LOGS_URL + '?' + params,
+ {
+ errorMessageDisplay: t('There was an error getting the list.'),
+ }
+ );
+}
+
+export default function useAccessLogsQuery(
+ itemLimit: number,
+ pageOffset: number
+) {
+ return useQuery({
+ queryKey: ['accessLogs', itemLimit, pageOffset],
+ queryFn: () => getAccessLogs(itemLimit, pageOffset),
+ placeholderData: keepPreviousData,
+ // We might want to improve this in future, for now let's not retry
+ retry: false,
+ // The `refetchOnWindowFocus` option is `true` by default, I'm setting it
+ // here so we don't forget about it.
+ refetchOnWindowFocus: true,
+ });
+}
diff --git a/jsapp/js/stores/session.ts b/jsapp/js/stores/session.ts
index a862740c4e..b9dcf5c2a4 100644
--- a/jsapp/js/stores/session.ts
+++ b/jsapp/js/stores/session.ts
@@ -5,6 +5,8 @@ import type {AccountResponse, FailResponse} from 'js/dataInterface';
import {log, currentLang} from 'js/utils';
import type {Json} from 'js/components/common/common.interfaces';
import type {ProjectViewsSettings} from 'js/projects/customViewStore';
+import {fetchPost, handleApiFail} from 'js/api';
+import {endpoints} from 'js/api.endpoints';
class SessionStore {
currentAccount: AccountResponse | {username: string; date_joined: string} = {
@@ -97,6 +99,20 @@ class SessionStore {
);
}
+ /**
+ * Useful if you need to log out all sessions.
+ */
+ public async logOutAll() {
+ try {
+ // Logging out is simply POSTing to this endpoint
+ await fetchPost(endpoints.LOGOUT_ALL, {});
+ // After that we force reload
+ window.location.replace('');
+ } catch (err) {
+ // `fetchPost` is handling the error
+ }
+ }
+
private saveUiLanguage() {
// We want to save the language if it differs from the one we saved or if
// none is saved yet.
diff --git a/jsapp/js/universalTable/universalTable.component.tsx b/jsapp/js/universalTable/universalTable.component.tsx
index 490cb1a770..eafe0c980d 100644
--- a/jsapp/js/universalTable/universalTable.component.tsx
+++ b/jsapp/js/universalTable/universalTable.component.tsx
@@ -162,7 +162,7 @@ export default function UniversalTable(
.map((col) => col.key);
options.state.columnPinning = {left: pinnedColumns || []};
- const hasPagination = (
+ const hasPagination = Boolean(
props.pageIndex !== undefined &&
props.pageCount &&
props.pageSize &&