pl-api: Support GoToSocial tokens page

Signed-off-by: mkljczk <git@mkljczk.pl>
This commit is contained in:
mkljczk
2025-03-07 12:14:38 +01:00
parent ae5cd0ee49
commit a34bb7070d
10 changed files with 132 additions and 37 deletions

View File

@ -1260,11 +1260,11 @@ class PlApiClient {
* Requires features{@link Features['sessions']}.
* @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apioauth_tokens}
*/
getOauthTokens: async () => {
const response = await this.request('/api/oauth_tokens');
return v.parse(filteredArray(oauthTokenSchema), response.json);
},
getOauthTokens: () => this.#paginatedGet(
this.features.version.software === GOTOSOCIAL ? '/api/v1/tokens' : '/api/oauth_tokens',
{},
oauthTokenSchema,
),
/**
* Revoke a user session by its ID
@ -1272,8 +1272,17 @@ class PlApiClient {
* Requires features{@link Features['sessions']}.
* @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#delete-apioauth_tokensid}
*/
deleteOauthToken: async (oauthTokenId: number) => {
const response = await this.request(`/api/oauth_tokens/${oauthTokenId}`, { method: 'DELETE' });
deleteOauthToken: async (oauthTokenId: string) => {
let response;
switch (this.features.version.software) {
case GOTOSOCIAL:
response = await this.request(`/api/v1/tokens/${oauthTokenId}/invalidate`, { method: 'POST' });
break;
default:
response = await this.request(`/api/oauth_tokens/${oauthTokenId}`, { method: 'DELETE' });
break;
}
return response.json as {};
},

View File

@ -8,14 +8,29 @@ import { datetimeSchema } from './utils';
*/
const oauthTokenSchema = v.pipe(
v.any(),
v.transform((token: any) => ({
...token,
valid_until: token?.valid_until?.padEnd(27, 'Z'),
})),
v.transform((token: any) => {
if (token.application) {
return {
...token,
app_name: token.application.name,
app_website: token.application.website,
scopes: token.scope.split(' '),
};
}
return {
...token,
valid_until: token?.valid_until?.padEnd(27, 'Z'),
};
}),
v.object({
app_name: v.string(),
id: v.number(),
valid_until: datetimeSchema,
app_website: v.fallback(v.string(), ''),
id: v.pipe(v.unknown(), v.transform(String)),
created_at: v.fallback(v.nullable(datetimeSchema), null),
valid_until: v.fallback(v.nullable(datetimeSchema), null),
last_used: v.fallback(v.nullable(datetimeSchema), null),
scopes: v.fallback(v.array(v.string()), []),
}),
);

View File

@ -1267,8 +1267,14 @@ const getFeatures = (instance: Instance) => {
* Ability to manage account sessions.
* @see GET /api/oauth_tokens.json
* @see DELETE /api/oauth_tokens/:id
* @see GET /api/v1/tokens
* @see GET /api/v1/tokens/:id
* @see POST /api/v1/tokens/:id/invalidate
*/
sessions: v.software === PLEROMA,
sessions: any([
v.software === PLEROMA,
v.software === GOTOSOCIAL && gte(v.version, '0.18.2'),
]),
/**
* Can store client settings in the database.

View File

@ -1,6 +1,6 @@
{
"name": "pl-api",
"version": "1.0.0-rc.29",
"version": "1.0.0-rc.30",
"type": "module",
"homepage": "https://github.com/mkljczk/pl-fe/tree/develop/packages/pl-api",
"repository": {

View File

@ -103,7 +103,7 @@
"multiselect-react-dropdown": "^2.0.25",
"mutative": "^1.1.0",
"path-browserify": "^1.0.1",
"pl-api": "^1.0.0-rc.29",
"pl-api": "^1.0.0-rc.30",
"postcss": "^8.4.49",
"process": "^0.11.10",
"punycode": "^2.1.1",

View File

@ -21,11 +21,11 @@ const REVOKE_TOKEN_SUCCESS = 'REVOKE_TOKEN_SUCCESS' as const;
const fetchOAuthTokens = () =>
(dispatch: AppDispatch, getState: () => RootState) =>
getClient(getState).settings.getOauthTokens().then((tokens) => {
getClient(getState).settings.getOauthTokens().then(({ items: tokens }) => {
dispatch<SecurityAction>({ type: FETCH_TOKENS_SUCCESS, tokens });
});
const revokeOAuthTokenById = (tokenId: number) =>
const revokeOAuthTokenById = (tokenId: string) =>
(dispatch: AppDispatch, getState: () => RootState) =>
getClient(getState).settings.deleteOauthToken(tokenId).then(() => {
dispatch<SecurityAction>({ type: REVOKE_TOKEN_SUCCESS, tokenId });
@ -65,7 +65,7 @@ const moveAccount = (targetAccount: string, password: string) =>
type SecurityAction =
| { type: typeof FETCH_TOKENS_SUCCESS; tokens: Array<OauthToken> }
| { type: typeof REVOKE_TOKEN_SUCCESS; tokenId: number }
| { type: typeof REVOKE_TOKEN_SUCCESS; tokenId: string }
| { type: typeof AUTH_LOGGED_OUT; account: Account }
export {

View File

@ -4,7 +4,7 @@ import React, { useMemo } from 'react';
import { hexToHsl } from 'pl-fe/utils/theme';
interface IBadge {
title: React.ReactNode;
title: React.ReactNode | string;
slug: string;
color?: string;
}

View File

@ -1,11 +1,13 @@
import React, { useEffect } from 'react';
import { defineMessages, FormattedDate, useIntl } from 'react-intl';
import { defineMessages, FormattedDate, FormattedMessage, useIntl } from 'react-intl';
import { fetchOAuthTokens, revokeOAuthTokenById } from 'pl-fe/actions/security';
import Badge from 'pl-fe/components/badge';
import Button from 'pl-fe/components/ui/button';
import Card, { CardBody, CardHeader, CardTitle } from 'pl-fe/components/ui/card';
import Column from 'pl-fe/components/ui/column';
import HStack from 'pl-fe/components/ui/hstack';
import Icon from 'pl-fe/components/ui/icon';
import Spinner from 'pl-fe/components/ui/spinner';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
@ -51,19 +53,82 @@ const AuthToken: React.FC<IAuthToken> = ({ token, isCurrent }) => {
return (
<div className='rounded-lg bg-gray-100 p-4 dark:bg-primary-800'>
<Stack space={2}>
<Stack>
<Text size='md' weight='medium'>{token.app_name}</Text>
<Stack space={2} className='h-full justify-between'>
<Stack space={1}>
<Text size='md' weight='medium'>
<HStack space={1} alignItems='center'>
{token.app_name}
{token.app_website && (
<a href={token.app_website} target='_blank' rel='noopener'>
<Icon
src={require('@tabler/icons/outline/external-link.svg')}
className='inline size-4 text-inherit'
/>
</a>
)}
</HStack>
</Text>
{token.scopes?.length > 0 && (
<HStack space={2} alignItems='center' wrap>
<Text size='sm' theme='muted'>
<FormattedMessage
id='security.tokens.scopes'
defaultMessage='Scopes:'
/>
</Text>
{token.scopes.map((scope, index) => (
<Badge title={scope} slug='opaque' key={scope} />
))}
</HStack>
)}
{token.created_at && (
<Text size='sm' theme='muted'>
<FormattedMessage
id='security.tokens.created_at'
defaultMessage='Created on {date}'
values={{ date: <FormattedDate
value={token.created_at}
hour12
year='numeric'
month='short'
day='2-digit'
hour='numeric'
minute='2-digit'
/> }}
/>
</Text>
)}
{token.last_used && (
<Text size='sm' theme='muted'>
<FormattedMessage
id='security.tokens.last_used'
defaultMessage='Last used on {date}'
values={{ date: <FormattedDate
value={token.last_used}
hour12
year='numeric'
month='short'
day='2-digit'
hour='numeric'
minute='2-digit'
/> }}
/>
</Text>
)}
{token.valid_until && (
<Text size='sm' theme='muted'>
<FormattedDate
value={token.valid_until}
hour12
year='numeric'
month='short'
day='2-digit'
hour='numeric'
minute='2-digit'
<FormattedMessage
id='security.tokens.valid_until'
defaultMessage='Expires on {date}'
values={{ date: <FormattedDate
value={token.valid_until}
hour12
year='numeric'
month='short'
day='2-digit'
hour='numeric'
minute='2-digit'
/> }}
/>
</Text>
)}

View File

@ -26,7 +26,7 @@ const initialState: State = {
},
};
const deleteToken = (state: State, tokenId: number) => state.tokens = state.tokens.filter(token => token.id !== tokenId);
const deleteToken = (state: State, tokenId: string) => state.tokens = state.tokens.filter(token => token.id !== tokenId);
const importMfa = (state: State, data: any) => state.mfa = data;

View File

@ -7553,10 +7553,10 @@ pkg-dir@^4.1.0:
dependencies:
find-up "^4.0.0"
pl-api@^1.0.0-rc.29:
version "1.0.0-rc.29"
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-1.0.0-rc.29.tgz#3d49281b15b1fca70cc631bc3107ca27f68c6034"
integrity sha512-cgQUX14DyaoSpPwU1eBB+VkABCd5YwHqFisJ0erVqYJSbKG17oeyrFiu/BCer7/ExQR8GF0UyoZvcbkCsQv1eQ==
pl-api@^1.0.0-rc.30:
version "1.0.0-rc.30"
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-1.0.0-rc.30.tgz#1702557d5e723ba40c73323782c4ad3910e3f908"
integrity sha512-9rmx87EV3oqI3e2ZonHVRcYBS3J1SyfQBNMmWwPmr0aPiA/kVeDcCgRjlxc0Z0srLWGg4Bwfxdcp8zx7aSRMqg==
dependencies:
blurhash "^2.0.5"
http-link-header "^1.1.3"