Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TASK-857] Add Access Log section to security route #5109

Merged
merged 13 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Libraries
import React from 'react';

// Partial components
import Button from 'js/components/common/button';
import PaginatedQueryUniversalTable from 'js/universalTable/paginatedQueryUniversalTable.component';

// Utilities
import useAccessLogsQuery, {type AccessLog} from 'js/query/queries/accessLogs.query';
import {formatTime} from 'js/utils';
import sessionStore from 'js/stores/session';

// Styles
import securityStyles from 'js/account/security/securityRoute.module.scss';

export default function AccessLogsSection() {
function logOutAllSessions() {
sessionStore.logOutAll();
}

return (
<>
<header className={securityStyles.securityHeader}>
<h2 className={securityStyles.securityHeaderText}>
{t('Recent account activity')}
</h2>

{/* TODO: we comment this out until we know how to handle exsiting
sessions for the moment of release of the feature. */}
{/*<div className={securityStyles.securityHeaderActions}>
<Button
type='text'
size='m'
onClick={logOutAllSessions}
label={t('Log out of all devices')}
startIcon='logout'
/>
</div>*/}
</header>

<PaginatedQueryUniversalTable<AccessLog>
queryHook={useAccessLogsQuery}
columns={[
// The `key`s of these columns are matching the `AccessLog` interface
// properties (from `accessLogs.query.ts` file) using dot notation.
{key: 'metadata.source', label: t('Source')},
{
key: 'date_created',
label: t('Last activity'),
cellFormatter: (date: string) => formatTime(date),
},
{key: 'metadata.ip_address', label: t('IP Address')},
]}
/>
</>
);
}
53 changes: 31 additions & 22 deletions jsapp/js/account/security/apiToken/apiTokenSection.component.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
// Libraries
import React, {
useState,
useEffect,
} from 'react';
import {dataInterface} from 'js/dataInterface';
import cx from 'classnames';

// Partial components
import TextBox from 'js/components/common/textBox';
import Button from 'js/components/common/button';

// Utils
import {dataInterface} from 'js/dataInterface';
import {notify} from 'js/utils';

// Styles
import styles from './apiTokenSection.module.scss';
import securityStyles from 'js/account/security/securityRoute.module.scss';

const HIDDEN_TOKEN_VALUE = '*'.repeat(40);

Expand Down Expand Up @@ -42,28 +51,28 @@ export default function ApiTokenDisplay() {
}, [isVisible]);

return (
<div className={styles.root}>
<div className={styles.titleSection}>
<h2 className={styles.title}>{t('API Key')}</h2>
</div>

<div className={styles.bodySection}>
<TextBox
type={isVisible && !isFetching && token !== null ? 'text' : 'password'}
value={token !== null ? token : HIDDEN_TOKEN_VALUE}
readOnly
/>
</div>
<section className={securityStyles.securitySection}>
<div className={securityStyles.securitySectionTitle}>
<h2 className={securityStyles.securitySectionTitleText}>{t('API Key')}</h2>
</div>

<div className={styles.optionsSection}>
<Button
label='Display'
size='m'
type='secondary'
onClick={toggleTokenVisibility}
/>
</div>
</div>
<div className={cx(securityStyles.securitySectionBody, styles.body)}>
<TextBox
type={isVisible && !isFetching && token !== null ? 'text' : 'password'}
value={token !== null ? token : HIDDEN_TOKEN_VALUE}
readOnly
className={styles.token}
/>
</div>

<div className={styles.options}>
<Button
label='Display'
size='m'
type='primary'
onClick={toggleTokenVisibility}
/>
</div>
</section>
);
}
60 changes: 10 additions & 50 deletions jsapp/js/account/security/apiToken/apiTokenSection.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,19 @@
@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;
.body {
flex: 5;
}

.bodySection {
flex: 6;
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
padding-left: sizes.$x4;

label {
display: inline-block;
vertical-align: top;
width: 100%;

input {
// API token display
font-family: mdl.$font_mono;
// 40 characters + padding + border
max-width: calc(40ch + 22px);
}
.token {
input {
// API token display
font-family: mdl.$font_mono;
}
}

.optionsSection {
flex: 2;
text-align: right;

button {
display: inline-block;
}
.options {
flex: 3;
display: flex;
justify-content: flex-end;
}
60 changes: 36 additions & 24 deletions jsapp/js/account/security/email/emailSection.component.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
// Libraries
import React, {useEffect, useState} from 'react';
import cx from 'classnames';

// Stores and email related
import sessionStore from 'js/stores/session';
import {
getUserEmails,
setUserEmail,
deleteUnverifiedUserEmails,
} from './emailSection.api';
import type {EmailResponse} from './emailSection.api';
import style from './emailSection.module.scss';

// Partial components
import Button from 'jsapp/js/components/common/button';
import TextBox from 'jsapp/js/components/common/textBox';
import Icon from 'jsapp/js/components/common/icon';
import {formatTime} from 'jsapp/js/utils';
import {notify} from 'js/utils';

// Utils
import {formatTime, notify} from 'js/utils';

// Styles
import styles from './emailSection.module.scss';
import securityStyles from 'js/account/security/securityRoute.module.scss';

interface EmailState {
emails: EmailResponse[];
Expand All @@ -23,9 +33,14 @@ interface EmailState {
export default function EmailSection() {
const [session] = useState(() => sessionStore);

let initialEmail = '';
if ('email' in session.currentAccount) {
initialEmail = session.currentAccount.email;
}

const [email, setEmail] = useState<EmailState>({
emails: [],
newEmail: '',
newEmail: initialEmail,
refreshedEmail: false,
refreshedEmailDate: '',
});
Expand All @@ -48,7 +63,7 @@ export default function EmailSection() {
newEmail: '',
});
});
});
}, () => {/* Avoid crashing app when 500 error happens */});
}

function deleteNewUserEmail() {
Expand Down Expand Up @@ -103,26 +118,31 @@ export default function EmailSection() {
);

return (
<div className={style.root}>
<div className={style.titleSection}>
<h2 className={style.title}>{t('Email address')}</h2>
<section className={securityStyles.securitySection}>
<div className={securityStyles.securitySectionTitle}>
<h2 className={securityStyles.securitySectionTitleText}>{t('Email address')}</h2>
</div>

<div className={style.bodySection}>
<div className={cx(securityStyles.securitySectionBody, styles.body)}>
{!session.isPending &&
session.isInitialLoadComplete &&
'email' in currentAccount && (
<p className={style.currentEmail}>{currentAccount.email}</p>
<TextBox
value={email.newEmail}
placeholder={t('Type new email address')}
onChange={onTextFieldChange.bind(onTextFieldChange)}
type='email'
/>
)}

{unverifiedEmail?.email &&
!session.isPending &&
session.isInitialLoadComplete &&
'email' in currentAccount && (
<>
<div className={style.unverifiedEmail}>
<div className={styles.unverifiedEmail}>
<Icon name='alert' />
<p className={style['blurb']}>
<p className={styles.blurb}>
<strong>
{t('Check your email ##UNVERIFIED_EMAIL##. ').replace(
'##UNVERIFIED_EMAIL##',
Expand All @@ -136,7 +156,7 @@ export default function EmailSection() {
</p>
</div>

<div className={style.editEmail}>
<div className={styles.unverifiedEmailButtons}>
<Button
label='Resend'
size='m'
Expand Down Expand Up @@ -167,27 +187,19 @@ export default function EmailSection() {
</div>

<form
className={style.optionsSection}
className={styles.options}
onSubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
>
{/*TODO: Move TextBox into a modal--it messes up the flow of the row right now*/}
<TextBox
value={email.newEmail}
placeholder={t('Type new email address')}
onChange={onTextFieldChange.bind(onTextFieldChange)}
type='email'
/>

<Button
label='Change'
size='m'
type='secondary'
type='primary'
onClick={handleSubmit}
/>
</form>
</div>
</section>
);
}
Loading
Loading