* feat: (wip) authorize note access
* fix: remove multiEditorEnabled
* refactor: update SNJS + eslint
* refactor: remove privileges in favor of SNJS protections
* fix: do not close editor when editing an archived note
* chore: remove progress indicator for webpack dev server
* fix: add rel="noreferrer" to bugsnag links
* chore(deps): upgrade snjs
* chore(deps): upgrade snjs
* feat: batch manager protection + react challenge modal + eslint fix
* fix: lint errors
* fix: launch state error
* fix: challenge modal: cancel instead of dismiss when pressing escape
* feat: improve focus styles
* fix: cancel session revoking when pressing escape on confirm dialog
* fix: lint warning
* chore(deps): upgrade minor versions
* feat: make SNWebCrypto a constant
* feat: add random identifier to bugsnag reports
* fix: check onKeyUp instead of onKeyDown
* feat: implement SNJS backup file password retrieval
* chore(deps): upgrade snjs
* feat: display warning banner when using the app with no account
* fix: properly color svg button
* fix: wording
* fix: hide account warning after login + improve key storage wording
* chore(deps): upgrade stylekit
* feat: use stylekit fonts for the editor
* chore(deps): bump nokogiri from 1.10.8 to 1.11.1 (#511)
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.10.8 to 1.11.1.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.10.8...v1.11.1)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com>
* chore(deps): bump ini from 1.3.5 to 1.3.8 (#504)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com>
* fix: rename master branch to main
* fix: add missing placeholders for submodules (#516)
Co-authored-by: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com>
* chore(deps): upgrade snjs, babel, typescript, reach, mobx, preact
* feat: clear protection session
* fix: use correct close icon size
* fix: hide protections paragraph when no account or passcode exist
* chore(deps): remove unused dependencies
* fix: button casing
* feat: implement SNApplication.hasProtectionSources
* chore(version): 3.6.0
* feat: enable sessions management for every build
* feat: make "Protected" flag more subtle
* fix: only match protected note title
* fix: remove inconsistencies between protected note label and date
* feat: show warning when protecting a note with no protection source
* feat: make unprotecting a note a protected action
* chore(deps): upgrade snjs
* chore(version): 3.6.0-beta01
* fix: run docker with root to fix crashing on Linux (undoes 62da387d3a) (#525)
* feat: make encrypted backups protected (#524)
Co-authored-by: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: proletarius101 <54175165+proletarius101@users.noreply.github.com>
Co-authored-by: Darius JJ Chuck <79410894+standarius@users.noreply.github.com>
Co-authored-by: Antonella Sgarlatta <antonella@standardnotes.org>
259 lines
8.2 KiB
TypeScript
259 lines
8.2 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, useAutorun } from './utils';
|
|
import { WebApplication } from '@/ui_models/application';
|
|
|
|
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) {
|
|
if (response.error?.message) {
|
|
setErrorMessage(response.error.message);
|
|
} else {
|
|
setErrorMessage('An unknown error occured while loading sessions.');
|
|
}
|
|
} else {
|
|
const sessions = response as Session[];
|
|
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="sk-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="sk-panel-row">
|
|
<div className="sk-button-group">
|
|
<button
|
|
className="sk-button neutral sk-label"
|
|
ref={cancelRevokeRef}
|
|
onClick={closeRevokeSessionAlert}
|
|
>
|
|
<span>{SessionStrings.RevokeCancelButton}</span>
|
|
</button>
|
|
<button
|
|
className="sk-button danger sk-label"
|
|
onClick={() => {
|
|
closeRevokeSessionAlert();
|
|
revokeSession(confirmRevokingSessionUuid);
|
|
}}
|
|
>
|
|
<span>{SessionStrings.RevokeConfirmButton}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AlertDialog>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const Sessions: FunctionComponent<{
|
|
appState: AppState;
|
|
application: WebApplication;
|
|
}> = ({ appState, application }) => {
|
|
const [showModal, setShowModal] = useState(false);
|
|
useAutorun(() => setShowModal(appState.isSessionsModalVisible));
|
|
|
|
if (showModal) {
|
|
return <SessionsModal application={application} appState={appState} />;
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
export const SessionsModalDirective = toDirective(Sessions);
|