diff --git a/app/soapbox/features/auth_token_list/index.tsx b/app/soapbox/features/auth_token_list/index.tsx new file mode 100644 index 000000000..8014def9f --- /dev/null +++ b/app/soapbox/features/auth_token_list/index.tsx @@ -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 = ({ token }) => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + + const handleRevoke = () => { + dispatch(revokeOAuthTokenById(token.id)); + }; + + return ( +
+ + + {token.app_name} + + + + + +
+ +
+
+
+ ); +}; + +const AuthTokenList: React.FC = () =>{ + const dispatch = useAppDispatch(); + const intl = useIntl(); + const tokens = useAppSelector(state => state.security.get('tokens')); + + useEffect(() => { + dispatch(fetchOAuthTokens()); + }, []); + + const body = tokens ? ( +
+ {tokens.map((token) => ( + + ))} +
+ ) : ; + + return ( + + + + + + + + {body} + + + + ); +}; + +export default AuthTokenList; diff --git a/app/soapbox/features/settings/index.tsx b/app/soapbox/features/settings/index.tsx index e019aee83..c43f4fc3d 100644 --- a/app/soapbox/features/settings/index.tsx +++ b/app/soapbox/features/settings/index.tsx @@ -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' })} + {features.sessionsAPI && ( + + )} diff --git a/app/soapbox/features/ui/components/compare_history_modal.tsx b/app/soapbox/features/ui/components/compare_history_modal.tsx index 92bfe8b23..d0ab73d55 100644 --- a/app/soapbox/features/ui/components/compare_history_modal.tsx +++ b/app/soapbox/features/ui/components/compare_history_modal.tsx @@ -43,8 +43,6 @@ const CompareHistoryModal: React.FC = ({ onClose, statusId const poll = typeof version.poll !== 'string' && version.poll; - console.log(version.toJS()); - return (
{version.spoiler_text?.length > 0 && ( diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 14e9e5974..6cfab8d89 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -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 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 }) => { + {/* */} diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index 1e6b03706..aab5bf76d 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -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'); +} diff --git a/app/soapbox/reducers/security.js b/app/soapbox/reducers/security.tsx similarity index 51% rename from app/soapbox/reducers/security.js rename to app/soapbox/reducers/security.tsx index b29fa6de3..143b912c0 100644 --- a/app/soapbox/reducers/security.js +++ b/app/soapbox/reducers/security.tsx @@ -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(), mfa: ImmutableMap({ settings: ImmutableMap({ totp: false, @@ -19,28 +26,32 @@ const initialState = ImmutableMap({ }), }); -const deleteToken = (state, tokenId) => { +type State = ReturnType; + +export type Token = ReturnType; + +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: diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index b75256fab..be3660f79 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -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 diff --git a/app/styles/forms.scss b/app/styles/forms.scss index d93ba0b03..fe03ddf01 100644 --- a/app/styles/forms.scss +++ b/app/styles/forms.scss @@ -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;