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 { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { fetchMfa } from 'soapbox/actions/mfa';
|
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 { 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';
|
import Preferences from '../preferences';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -19,6 +20,7 @@ const messages = defineMessages({
|
||||||
changeEmail: { id: 'settings.change_email', defaultMessage: 'Change Email' },
|
changeEmail: { id: 'settings.change_email', defaultMessage: 'Change Email' },
|
||||||
changePassword: { id: 'settings.change_password', defaultMessage: 'Change Password' },
|
changePassword: { id: 'settings.change_password', defaultMessage: 'Change Password' },
|
||||||
configureMfa: { id: 'settings.configure_mfa', defaultMessage: 'Configure MFA' },
|
configureMfa: { id: 'settings.configure_mfa', defaultMessage: 'Configure MFA' },
|
||||||
|
sessions: { id: 'settings.sessions', defaultMessage: 'Active sessions' },
|
||||||
deleteAccount: { id: 'settings.delete_account', defaultMessage: 'Delete Account' },
|
deleteAccount: { id: 'settings.delete_account', defaultMessage: 'Delete Account' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -29,11 +31,13 @@ const Settings = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const mfa = useAppSelector((state) => state.security.get('mfa'));
|
const mfa = useAppSelector((state) => state.security.get('mfa'));
|
||||||
|
const features = useAppSelector((state) => getFeatures(state.instance));
|
||||||
const account = useOwnAccount();
|
const account = useOwnAccount();
|
||||||
|
|
||||||
const navigateToChangeEmail = React.useCallback(() => history.push('/settings/email'), [history]);
|
const navigateToChangeEmail = React.useCallback(() => history.push('/settings/email'), [history]);
|
||||||
const navigateToChangePassword = React.useCallback(() => history.push('/settings/password'), [history]);
|
const navigateToChangePassword = React.useCallback(() => history.push('/settings/password'), [history]);
|
||||||
const navigateToMfa = React.useCallback(() => history.push('/settings/mfa'), [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 navigateToEditProfile = React.useCallback(() => history.push('/settings/profile'), [history]);
|
||||||
|
|
||||||
const isMfaEnabled = mfa.getIn(['settings', 'totp']);
|
const isMfaEnabled = mfa.getIn(['settings', 'totp']);
|
||||||
|
@ -74,6 +78,9 @@ const Settings = () => {
|
||||||
intl.formatMessage({ id: 'mfa.enabled', defaultMessage: 'Enabled' }) :
|
intl.formatMessage({ id: 'mfa.enabled', defaultMessage: 'Enabled' }) :
|
||||||
intl.formatMessage({ id: 'mfa.disabled', defaultMessage: 'Disabled' })}
|
intl.formatMessage({ id: 'mfa.disabled', defaultMessage: 'Disabled' })}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
{features.sessionsAPI && (
|
||||||
|
<ListItem label={intl.formatMessage(messages.sessions)} onClick={navigateToSessions} />
|
||||||
|
)}
|
||||||
</List>
|
</List>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|
||||||
|
|
|
@ -43,8 +43,6 @@ const CompareHistoryModal: React.FC<ICompareHistoryModal> = ({ onClose, statusId
|
||||||
|
|
||||||
const poll = typeof version.poll !== 'string' && version.poll;
|
const poll = typeof version.poll !== 'string' && version.poll;
|
||||||
|
|
||||||
console.log(version.toJS());
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col py-2 first:pt-0 last:pb-0'>
|
<div className='flex flex-col py-2 first:pt-0 last:pb-0'>
|
||||||
{version.spoiler_text?.length > 0 && (
|
{version.spoiler_text?.length > 0 && (
|
||||||
|
|
|
@ -8,10 +8,19 @@ import { useDispatch } from 'react-redux';
|
||||||
import { Switch, useHistory } from 'react-router-dom';
|
import { Switch, useHistory } from 'react-router-dom';
|
||||||
import { Redirect } 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 { fetchChats } from 'soapbox/actions/chats';
|
||||||
|
import { uploadCompose, resetCompose } from 'soapbox/actions/compose';
|
||||||
import { fetchCustomEmojis } from 'soapbox/actions/custom_emojis';
|
import { fetchCustomEmojis } from 'soapbox/actions/custom_emojis';
|
||||||
|
import { fetchFilters } from 'soapbox/actions/filters';
|
||||||
import { fetchMarker } from 'soapbox/actions/markers';
|
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 { 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 Icon from 'soapbox/components/icon';
|
||||||
import SidebarNavigation from 'soapbox/components/sidebar-navigation';
|
import SidebarNavigation from 'soapbox/components/sidebar-navigation';
|
||||||
import ThumbNavigation from 'soapbox/components/thumb_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 { getAccessToken } from 'soapbox/utils/auth';
|
||||||
import { getVapidKey } from 'soapbox/utils/auth';
|
import { getVapidKey } from 'soapbox/utils/auth';
|
||||||
import { isStandalone } from 'soapbox/utils/state';
|
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 GroupSidebarPanel from '../groups/sidebar_panel';
|
||||||
|
|
||||||
import BackgroundShapes from './components/background_shapes';
|
import BackgroundShapes from './components/background_shapes';
|
||||||
|
@ -115,12 +114,13 @@ import {
|
||||||
SettingsStore,
|
SettingsStore,
|
||||||
TestTimeline,
|
TestTimeline,
|
||||||
LogoutPage,
|
LogoutPage,
|
||||||
|
AuthTokenList,
|
||||||
} from './util/async-components';
|
} from './util/async-components';
|
||||||
import { WrappedRoute } from './util/react_router_helpers';
|
import { WrappedRoute } from './util/react_router_helpers';
|
||||||
|
|
||||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||||
// Without this it ends up in ~8 very commonly used bundles.
|
// 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;
|
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/account' page={DefaultPage} component={DeleteAccount} content={children} />
|
||||||
<WrappedRoute path='/settings/media_display' page={DefaultPage} component={MediaDisplay} 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/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='/settings' page={DefaultPage} component={Settings} content={children} />
|
||||||
{/* <WrappedRoute path='/backups' page={DefaultPage} component={Backups} content={children} /> */}
|
{/* <WrappedRoute path='/backups' page={DefaultPage} component={Backups} content={children} /> */}
|
||||||
<WrappedRoute path='/soapbox/config' adminOnly page={DefaultPage} component={SoapboxConfig} content={children} />
|
<WrappedRoute path='/soapbox/config' adminOnly page={DefaultPage} component={SoapboxConfig} content={children} />
|
||||||
|
|
|
@ -497,3 +497,7 @@ export function DatePicker() {
|
||||||
export function CompareHistoryModal() {
|
export function CompareHistoryModal() {
|
||||||
return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/compare_history_modal');
|
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 {
|
import {
|
||||||
MFA_FETCH_SUCCESS,
|
MFA_FETCH_SUCCESS,
|
||||||
|
@ -10,8 +11,14 @@ import {
|
||||||
REVOKE_TOKEN_SUCCESS,
|
REVOKE_TOKEN_SUCCESS,
|
||||||
} from '../actions/security';
|
} from '../actions/security';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const TokenRecord = ImmutableRecord({
|
||||||
tokens: ImmutableList(),
|
id: 0,
|
||||||
|
app_name: '',
|
||||||
|
valid_until: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const ReducerRecord = ImmutableRecord({
|
||||||
|
tokens: ImmutableList<Token>(),
|
||||||
mfa: ImmutableMap({
|
mfa: ImmutableMap({
|
||||||
settings: ImmutableMap({
|
settings: ImmutableMap({
|
||||||
totp: false,
|
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 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);
|
return state.set('mfa', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const enableMfa = (state, method) => {
|
const enableMfa = (state: State, method: string) => {
|
||||||
return state.setIn(['mfa', 'settings', method], true);
|
return state.setIn(['mfa', 'settings', method], true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const disableMfa = (state, method) => {
|
const disableMfa = (state: State, method: string) => {
|
||||||
return state.setIn(['mfa', 'settings', method], false);
|
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) {
|
switch (action.type) {
|
||||||
case FETCH_TOKENS_SUCCESS:
|
case FETCH_TOKENS_SUCCESS:
|
||||||
return state.set('tokens', fromJS(action.tokens));
|
return state.set('tokens', ImmutableList(action.tokens.map(TokenRecord)));
|
||||||
case REVOKE_TOKEN_SUCCESS:
|
case REVOKE_TOKEN_SUCCESS:
|
||||||
return deleteToken(state, action.id);
|
return deleteToken(state, action.id);
|
||||||
case MFA_FETCH_SUCCESS:
|
case MFA_FETCH_SUCCESS:
|
|
@ -459,6 +459,13 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
v.software === TRUTHSOCIAL,
|
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.
|
* Can store client settings in the database.
|
||||||
* @see PATCH /api/v1/accounts/update_credentials
|
* @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 {
|
.file-picker img {
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
max-height: 100px;
|
max-height: 100px;
|
||||||
|
|
Loading…
Reference in a new issue