255 lines
8.0 KiB
TypeScript
255 lines
8.0 KiB
TypeScript
import { AppState } from '@/ui_models/app_state';
|
|
import {
|
|
SNApplication,
|
|
SessionStrings,
|
|
UuidString,
|
|
isNullOrUndefined,
|
|
RemoteSession,
|
|
} from '@standardnotes/snjs';
|
|
import { FunctionComponent } from 'preact';
|
|
import { useState, useEffect, useRef, useMemo } from 'preact/hooks';
|
|
import { Dialog } from '@reach/dialog';
|
|
import { Alert } from '@reach/alert';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogDescription,
|
|
AlertDialogLabel,
|
|
} from '@reach/alert-dialog';
|
|
import { toDirective } from './utils';
|
|
import { WebApplication } from '@/ui_models/application';
|
|
import { observer } from 'mobx-react-lite';
|
|
|
|
type Session = RemoteSession & {
|
|
revoking?: true;
|
|
};
|
|
|
|
function useSessions(
|
|
application: SNApplication
|
|
): [
|
|
Session[],
|
|
() => void,
|
|
boolean,
|
|
(uuid: UuidString) => Promise<void>,
|
|
string
|
|
] {
|
|
const [sessions, setSessions] = useState<Session[]>([]);
|
|
const [lastRefreshDate, setLastRefreshDate] = useState(Date.now());
|
|
const [refreshing, setRefreshing] = useState(true);
|
|
const [errorMessage, setErrorMessage] = useState('');
|
|
|
|
useEffect(() => {
|
|
(async () => {
|
|
setRefreshing(true);
|
|
const response = await application.getSessions();
|
|
if ('error' in response || !response.data) {
|
|
if (response.error?.message) {
|
|
setErrorMessage(response.error.message);
|
|
} else {
|
|
setErrorMessage('An unknown error occured while loading sessions.');
|
|
}
|
|
} else {
|
|
const sessions = response.data;
|
|
setSessions(sessions);
|
|
setErrorMessage('');
|
|
}
|
|
setRefreshing(false);
|
|
})();
|
|
}, [application, lastRefreshDate]);
|
|
|
|
function refresh() {
|
|
setLastRefreshDate(Date.now());
|
|
}
|
|
|
|
async function revokeSession(uuid: UuidString) {
|
|
const sessionsBeforeRevoke = sessions;
|
|
|
|
const responsePromise = application.revokeSession(uuid);
|
|
|
|
const sessionsDuringRevoke = sessions.slice();
|
|
const toRemoveIndex = sessions.findIndex(
|
|
(session) => session.uuid === uuid
|
|
);
|
|
sessionsDuringRevoke[toRemoveIndex] = {
|
|
...sessionsDuringRevoke[toRemoveIndex],
|
|
revoking: true,
|
|
};
|
|
setSessions(sessionsDuringRevoke);
|
|
|
|
const response = await responsePromise;
|
|
if (isNullOrUndefined(response)) {
|
|
setSessions(sessionsBeforeRevoke);
|
|
} else if ('error' in response) {
|
|
if (response.error?.message) {
|
|
setErrorMessage(response.error?.message);
|
|
} else {
|
|
setErrorMessage('An unknown error occured while revoking the session.');
|
|
}
|
|
setSessions(sessionsBeforeRevoke);
|
|
} else {
|
|
setSessions(sessions.filter((session) => session.uuid !== uuid));
|
|
}
|
|
}
|
|
|
|
return [sessions, refresh, refreshing, revokeSession, errorMessage];
|
|
}
|
|
|
|
const SessionsModal: FunctionComponent<{
|
|
appState: AppState;
|
|
application: SNApplication;
|
|
}> = ({ appState, application }) => {
|
|
const close = () => appState.closeSessionsModal();
|
|
|
|
const [
|
|
sessions,
|
|
refresh,
|
|
refreshing,
|
|
revokeSession,
|
|
errorMessage,
|
|
] = useSessions(application);
|
|
|
|
const [confirmRevokingSessionUuid, setRevokingSessionUuid] = useState('');
|
|
const closeRevokeSessionAlert = () => setRevokingSessionUuid('');
|
|
const cancelRevokeRef = useRef<HTMLButtonElement>();
|
|
|
|
const formatter = useMemo(
|
|
() =>
|
|
new Intl.DateTimeFormat(undefined, {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
weekday: 'long',
|
|
hour: 'numeric',
|
|
minute: 'numeric',
|
|
}),
|
|
[]
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<Dialog onDismiss={close} className="sessions-modal">
|
|
<div className="sk-modal-content">
|
|
<div class="sn-component">
|
|
<div class="sk-panel">
|
|
<div class="sk-panel-header">
|
|
<div class="sk-panel-header-title">Active Sessions</div>
|
|
<div className="buttons">
|
|
<button
|
|
class="sk-a close-button info"
|
|
disabled={refreshing}
|
|
onClick={refresh}
|
|
>
|
|
Refresh
|
|
</button>
|
|
<button class="sk-a close-button info" onClick={close}>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="sk-panel-content">
|
|
{refreshing ? (
|
|
<>
|
|
<div class="sk-spinner small info"></div>
|
|
<h2 className="sk-p sessions-modal-refreshing">
|
|
Loading sessions
|
|
</h2>
|
|
</>
|
|
) : (
|
|
<>
|
|
{errorMessage && (
|
|
<Alert className="sk-p bold">{errorMessage}</Alert>
|
|
)}
|
|
{sessions.length > 0 && (
|
|
<ul>
|
|
{sessions.map((session) => (
|
|
<li>
|
|
<h2>{session.device_info}</h2>
|
|
{session.current ? (
|
|
<span className="info bold">Current session</span>
|
|
) : (
|
|
<>
|
|
<p>
|
|
Signed in on{' '}
|
|
{formatter.format(session.updated_at)}
|
|
</p>
|
|
<button
|
|
className="sn-button danger sk-label"
|
|
disabled={session.revoking}
|
|
onClick={() =>
|
|
setRevokingSessionUuid(session.uuid)
|
|
}
|
|
>
|
|
<span>Revoke</span>
|
|
</button>
|
|
</>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
{confirmRevokingSessionUuid && (
|
|
<AlertDialog
|
|
onDismiss={() => {
|
|
setRevokingSessionUuid('');
|
|
}}
|
|
leastDestructiveRef={cancelRevokeRef}
|
|
>
|
|
<div className="sk-modal-content">
|
|
<div className="sn-component">
|
|
<div className="sk-panel">
|
|
<div className="sk-panel-content">
|
|
<div className="sk-panel-section">
|
|
<AlertDialogLabel className="sk-h3 sk-panel-section-title">
|
|
{SessionStrings.RevokeTitle}
|
|
</AlertDialogLabel>
|
|
<AlertDialogDescription className="sk-panel-row">
|
|
<p>{SessionStrings.RevokeText}</p>
|
|
</AlertDialogDescription>
|
|
<div className="flex my-1 gap-2">
|
|
<button
|
|
className="sn-button neutral sk-label"
|
|
ref={cancelRevokeRef}
|
|
onClick={closeRevokeSessionAlert}
|
|
>
|
|
<span>{SessionStrings.RevokeCancelButton}</span>
|
|
</button>
|
|
<button
|
|
className="sn-button danger sk-label"
|
|
onClick={() => {
|
|
closeRevokeSessionAlert();
|
|
revokeSession(confirmRevokingSessionUuid);
|
|
}}
|
|
>
|
|
<span>{SessionStrings.RevokeConfirmButton}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AlertDialog>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const Sessions: FunctionComponent<{
|
|
appState: AppState;
|
|
application: WebApplication;
|
|
}> = observer(({ appState, application }) => {
|
|
if (appState.isSessionsModalVisible) {
|
|
return <SessionsModal application={application} appState={appState} />;
|
|
} else {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
export const SessionsModalDirective = toDirective(Sessions);
|