Restore session management

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
set-dependency-scanning-config-1
marcin mikołajczak 2 years ago
parent 38b960244a
commit c540ae2201

@ -0,0 +1,87 @@
import React from 'react';
import { useEffect } from 'react';
import { defineMessages, FormattedDate, useIntl } from 'react-intl';
import { fetchOAuthTokens, revokeOAuthTokenById } from 'soapbox/actions/security';
import { Button, Card, CardBody, CardHeader, CardTitle, Column, Spinner, Stack, Text } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { Token } from 'soapbox/reducers/security';
const messages = defineMessages({
header: { id: 'security.headers.tokens', defaultMessage: 'Sessions' },
revoke: { id: 'security.tokens.revoke', defaultMessage: 'Revoke' },
});
interface IAuthToken {
token: Token,
}
const AuthToken: React.FC<IAuthToken> = ({ token }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const handleRevoke = () => {
dispatch(revokeOAuthTokenById(token.id));
};
return (
<div className='p-4 rounded-lg bg-gray-100 dark:bg-slate-700'>
<Stack space={2}>
<Stack>
<Text size='md' weight='medium'>{token.app_name}</Text>
<Text size='sm' theme='muted'>
<FormattedDate
value={new Date(token.valid_until)}
hour12={false}
year='numeric'
month='short'
day='2-digit'
hour='2-digit'
minute='2-digit'
/>
</Text>
</Stack>
<div className='flex justify-end'>
<Button theme='primary' onClick={handleRevoke}>
{intl.formatMessage(messages.revoke)}
</Button>
</div>
</Stack>
</div>
);
};
const AuthTokenList: React.FC = () =>{
const dispatch = useAppDispatch();
const intl = useIntl();
const tokens = useAppSelector(state => state.security.get('tokens'));
useEffect(() => {
dispatch(fetchOAuthTokens());
}, []);
const body = tokens ? (
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
{tokens.map((token) => (
<AuthToken key={token.id} token={token} />
))}
</div>
) : <Spinner />;
return (
<Column label={intl.formatMessage(messages.header)} transparent withHeader={false}>
<Card variant='rounded'>
<CardHeader backHref='/settings'>
<CardTitle title={intl.formatMessage(messages.header)} />
</CardHeader>
<CardBody>
{body}
</CardBody>
</Card>
</Column>
);
};
export default AuthTokenList;

@ -4,10 +4,11 @@ import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { fetchMfa } from 'soapbox/actions/mfa';
import List, { ListItem } from 'soapbox/components/list';
import { Button, Card, CardBody, CardHeader, CardTitle, Column } from 'soapbox/components/ui';
import { useAppSelector, useOwnAccount } from 'soapbox/hooks';
import { getFeatures } from 'soapbox/utils/features';
import List, { ListItem } from '../../components/list';
import { Button, Card, CardBody, CardHeader, CardTitle, Column } from '../../components/ui';
import Preferences from '../preferences';
const messages = defineMessages({
@ -19,6 +20,7 @@ const messages = defineMessages({
changeEmail: { id: 'settings.change_email', defaultMessage: 'Change Email' },
changePassword: { id: 'settings.change_password', defaultMessage: 'Change Password' },
configureMfa: { id: 'settings.configure_mfa', defaultMessage: 'Configure MFA' },
sessions: { id: 'settings.sessions', defaultMessage: 'Active sessions' },
deleteAccount: { id: 'settings.delete_account', defaultMessage: 'Delete Account' },
});
@ -29,11 +31,13 @@ const Settings = () => {
const intl = useIntl();
const mfa = useAppSelector((state) => state.security.get('mfa'));
const features = useAppSelector((state) => getFeatures(state.instance));
const account = useOwnAccount();
const navigateToChangeEmail = React.useCallback(() => history.push('/settings/email'), [history]);
const navigateToChangePassword = React.useCallback(() => history.push('/settings/password'), [history]);
const navigateToMfa = React.useCallback(() => history.push('/settings/mfa'), [history]);
const navigateToSessions = React.useCallback(() => history.push('/settings/tokens'), [history]);
const navigateToEditProfile = React.useCallback(() => history.push('/settings/profile'), [history]);
const isMfaEnabled = mfa.getIn(['settings', 'totp']);
@ -74,6 +78,9 @@ const Settings = () => {
intl.formatMessage({ id: 'mfa.enabled', defaultMessage: 'Enabled' }) :
intl.formatMessage({ id: 'mfa.disabled', defaultMessage: 'Disabled' })}
</ListItem>
{features.sessionsAPI && (
<ListItem label={intl.formatMessage(messages.sessions)} onClick={navigateToSessions} />
)}
</List>
</CardBody>

@ -43,8 +43,6 @@ const CompareHistoryModal: React.FC<ICompareHistoryModal> = ({ onClose, statusId
const poll = typeof version.poll !== 'string' && version.poll;
console.log(version.toJS());
return (
<div className='flex flex-col py-2 first:pt-0 last:pb-0'>
{version.spoiler_text?.length > 0 && (

@ -8,10 +8,19 @@ import { useDispatch } from 'react-redux';
import { Switch, useHistory } from 'react-router-dom';
import { Redirect } from 'react-router-dom';
import { fetchFollowRequests } from 'soapbox/actions/accounts';
import { fetchReports, fetchUsers, fetchConfig } from 'soapbox/actions/admin';
import { fetchChats } from 'soapbox/actions/chats';
import { uploadCompose, resetCompose } from 'soapbox/actions/compose';
import { fetchCustomEmojis } from 'soapbox/actions/custom_emojis';
import { fetchFilters } from 'soapbox/actions/filters';
import { fetchMarker } from 'soapbox/actions/markers';
import { openModal } from 'soapbox/actions/modals';
import { expandNotifications } from 'soapbox/actions/notifications';
import { register as registerPushNotifications } from 'soapbox/actions/push_notifications';
import { fetchScheduledStatuses } from 'soapbox/actions/scheduled_statuses';
import { connectUserStream } from 'soapbox/actions/streaming';
import { expandHomeTimeline } from 'soapbox/actions/timelines';
import Icon from 'soapbox/components/icon';
import SidebarNavigation from 'soapbox/components/sidebar-navigation';
import ThumbNavigation from 'soapbox/components/thumb_navigation';
@ -29,16 +38,6 @@ import StatusPage from 'soapbox/pages/status_page';
import { getAccessToken } from 'soapbox/utils/auth';
import { getVapidKey } from 'soapbox/utils/auth';
import { isStandalone } from 'soapbox/utils/state';
import { fetchFollowRequests } from '../../actions/accounts';
import { fetchReports, fetchUsers, fetchConfig } from '../../actions/admin';
import { uploadCompose, resetCompose } from '../../actions/compose';
import { fetchFilters } from '../../actions/filters';
import { openModal } from '../../actions/modals';
import { expandNotifications } from '../../actions/notifications';
import { fetchScheduledStatuses } from '../../actions/scheduled_statuses';
import { connectUserStream } from '../../actions/streaming';
import { expandHomeTimeline } from '../../actions/timelines';
// import GroupSidebarPanel from '../groups/sidebar_panel';
import BackgroundShapes from './components/background_shapes';
@ -115,12 +114,13 @@ import {
SettingsStore,
TestTimeline,
LogoutPage,
AuthTokenList,
} from './util/async-components';
import { WrappedRoute } from './util/react_router_helpers';
// Dummy import, to make sure that <Status /> ends up in the application bundle.
// Without this it ends up in ~8 very commonly used bundles.
import '../../components/status';
import 'soapbox/components/status';
const isMobile = (width: number): boolean => width <= 1190;
@ -297,6 +297,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
<WrappedRoute path='/settings/account' page={DefaultPage} component={DeleteAccount} content={children} />
<WrappedRoute path='/settings/media_display' page={DefaultPage} component={MediaDisplay} content={children} />
<WrappedRoute path='/settings/mfa' page={DefaultPage} component={MfaForm} exact />
<WrappedRoute path='/settings/tokens' page={DefaultPage} component={AuthTokenList} content={children} />
<WrappedRoute path='/settings' page={DefaultPage} component={Settings} content={children} />
{/* <WrappedRoute path='/backups' page={DefaultPage} component={Backups} content={children} /> */}
<WrappedRoute path='/soapbox/config' adminOnly page={DefaultPage} component={SoapboxConfig} content={children} />

@ -497,3 +497,7 @@ export function DatePicker() {
export function CompareHistoryModal() {
return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/compare_history_modal');
}
export function AuthTokenList() {
return import(/* webpackChunkName: "features/auth_token_list" */'../../auth_token_list');
}

@ -1,4 +1,5 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { Map as ImmutableMap, List as ImmutableList, Record as ImmutableRecord, fromJS } from 'immutable';
import { AnyAction } from 'redux';
import {
MFA_FETCH_SUCCESS,
@ -10,8 +11,14 @@ import {
REVOKE_TOKEN_SUCCESS,
} from '../actions/security';
const initialState = ImmutableMap({
tokens: ImmutableList(),
const TokenRecord = ImmutableRecord({
id: 0,
app_name: '',
valid_until: '',
});
const ReducerRecord = ImmutableRecord({
tokens: ImmutableList<Token>(),
mfa: ImmutableMap({
settings: ImmutableMap({
totp: false,
@ -19,28 +26,32 @@ const initialState = ImmutableMap({
}),
});
const deleteToken = (state, tokenId) => {
type State = ReturnType<typeof ReducerRecord>;
export type Token = ReturnType<typeof TokenRecord>;
const deleteToken = (state: State, tokenId: number) => {
return state.update('tokens', tokens => {
return tokens.filterNot(token => token.get('id') === tokenId);
return tokens.filterNot(token => token.id === tokenId);
});
};
const importMfa = (state, data) => {
const importMfa = (state: State, data: any) => {
return state.set('mfa', data);
};
const enableMfa = (state, method) => {
const enableMfa = (state: State, method: string) => {
return state.setIn(['mfa', 'settings', method], true);
};
const disableMfa = (state, method) => {
const disableMfa = (state: State, method: string) => {
return state.setIn(['mfa', 'settings', method], false);
};
export default function security(state = initialState, action) {
export default function security(state = ReducerRecord(), action: AnyAction) {
switch (action.type) {
case FETCH_TOKENS_SUCCESS:
return state.set('tokens', fromJS(action.tokens));
return state.set('tokens', ImmutableList(action.tokens.map(TokenRecord)));
case REVOKE_TOKEN_SUCCESS:
return deleteToken(state, action.id);
case MFA_FETCH_SUCCESS:

@ -450,6 +450,13 @@ const getInstanceFeatures = (instance: Instance) => {
v.software === TRUTHSOCIAL,
]),
/**
* Ability to manage account sessions.
* @see GET /api/oauth_tokens.json
* @see DELETE /api/oauth_tokens/:id
*/
sessionsAPI: v.software === PLEROMA,
/**
* Can store client settings in the database.
* @see PATCH /api/v1/accounts/update_credentials

@ -704,31 +704,6 @@ code {
}
}
.authtokens {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-gap: 20px;
}
.authtoken {
&__app-name {
font-size: 16px;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
}
&__valid-until {
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
}
&__revoke {
margin-top: 10px;
}
}
.file-picker img {
max-width: 100px;
max-height: 100px;

Loading…
Cancel
Save