Merge branch 'sessions-page' into 'develop'
Restore session management See merge request soapbox-pub/soapbox-fe!1360
This commit is contained in:
commit
8deb92cd44
8 changed files with 140 additions and 50 deletions
87
app/soapbox/features/auth_token_list/index.tsx
Normal file
87
app/soapbox/features/auth_token_list/index.tsx
Normal file
|
@ -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:
|
|
@ -459,6 +459,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…
Reference in a new issue