pl-api: support 2fa configuration in gotosocial

Signed-off-by: Nicole Mikołajczyk <git@mkljczk.pl>
This commit is contained in:
Nicole Mikołajczyk
2025-04-10 16:17:45 +02:00
parent 28933f5612
commit 3292207b5a
10 changed files with 106 additions and 42 deletions

View File

@ -1499,14 +1499,27 @@ class PlApiClient {
* Requires features{@link Features['manageMfa']}.
*/
getMfaSettings: async () => {
const response = await this.request('/api/pleroma/accounts/mfa');
let response;
switch (this.features.version.software) {
case GOTOSOCIAL:
response = await this.request('/api/v1/user').then(({ json }) => ({
settings: {
enabled: !!json?.two_factor_enabled_at,
method: 'totp',
},
}));
break;
default:
response = (await this.request('/api/pleroma/accounts/mfa')).json;
}
return v.parse(v.object({
settings: v.object({
enabled: v.boolean(),
totp: v.boolean(),
}),
}), response.json);
}), response);
},
/**
@ -1524,36 +1537,66 @@ class PlApiClient {
* Requires features{@link Features['manageMfa']}.
*/
getMfaSetup: async (method: 'totp') => {
const response = await this.request(`/api/pleroma/accounts/mfa/setup/${method}`);
let response;
switch (this.features.version.software) {
case GOTOSOCIAL:
response = await this.request('/api/v1/user/2fa/qruri').then(({ data }) => ({
provisioning_uri: data,
key: new URL(data).searchParams.get('secret'),
}));
break;
default:
response = (await this.request(`/api/pleroma/accounts/mfa/setup/${method}`)).json;
}
return v.parse(v.object({
key: v.string(),
key: v.fallback(v.string(), ''),
provisioning_uri: v.string(),
}), response.json);
}), response);
},
/**
* Requires features{@link Features['manageMfa']}.
*/
confirmMfaSetup: async (method: 'totp', code: string, password: string) => {
const response = await this.request(`/api/pleroma/accounts/mfa/confirm/${method}`, {
method: 'POST',
body: { code, password },
});
let response;
if (response.json?.error) throw response.json.error;
switch (this.features.version.software) {
case GOTOSOCIAL:
response = await this.request('/api/v1/user/2fa/enable', { method: 'POST', body: { code } });
break;
default:
response = (await this.request(`/api/pleroma/accounts/mfa/confirm/${method}`, {
method: 'POST',
body: { code, password },
})).json;
}
return response.json as {};
if (response?.error) throw response.error;
return response as {};
},
/**
* Requires features{@link Features['manageMfa']}.
*/
disableMfa: async (method: 'totp', password: string) => {
const response = await this.request(`/api/pleroma/accounts/mfa/${method}`, {
method: 'DELETE',
body: { password },
});
let response;
switch (this.features.version.software) {
case GOTOSOCIAL:
response = await this.request('/api/v1/user/2fa/disable', {
method: 'POST',
body: { password },
});
break;
default:
response = await this.request(`/api/pleroma/accounts/mfa/${method}`, {
method: 'DELETE',
body: { password },
});
}
if (response.json?.error) throw response.json.error;

View File

@ -987,7 +987,6 @@ const getFeatures = (instance: Instance) => {
/**
* @see GET /api/pleroma/accounts/mfa
* @see GET /api/pleroma/accounts/mfa/backup_codes
* @see GET /api/pleroma/accounts/mfa/setup/:method
* @see POST /api/pleroma/accounts/mfa/confirm/:method
* @see DELETE /api/pleroma/accounts/mfa/:method
@ -995,6 +994,23 @@ const getFeatures = (instance: Instance) => {
manageMfa: any([
v.software === AKKOMA,
v.software === PLEROMA,
v.software === GOTOSOCIAL && gte(v.version, '0.19.0'),
]),
/**
* @see GET /api/pleroma/accounts/mfa/backup_codes
*/
manageMfaBackupCodes: any([
v.software === AKKOMA,
v.software === PLEROMA,
]),
/**
* @see POST /api/v1/user/2fa/enable
*/
manageMfaRequiresPassword: any([
v.software === AKKOMA,
v.software === PLEROMA,
]),
/**

View File

@ -2,6 +2,7 @@ export * from './accounts';
export * from './admin';
export * from './apps';
export * from './chats';
export type { PaginationParams } from './common';
export * from './events';
export * from './filtering';
export * from './grouped-notifications';

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import Column from 'pl-fe/components/ui/column';
import Stack from 'pl-fe/components/ui/stack';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { useFeatures } from 'pl-fe/hooks/use-features';
import DisableOtpForm from './mfa/disable-otp-form';
import EnableOtpForm from './mfa/enable-otp-form';
@ -25,6 +26,7 @@ const messages = defineMessages({
const MfaForm: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const features = useFeatures();
const [displayOtpForm, setDisplayOtpForm] = useState<boolean>(false);
useEffect(() => {
@ -44,8 +46,8 @@ const MfaForm: React.FC = () => {
<DisableOtpForm />
) : (
<Stack space={4}>
<EnableOtpForm displayOtpForm={displayOtpForm} handleSetupProceedClick={handleSetupProceedClick} />
{displayOtpForm && <OtpConfirmForm />}
{features.manageMfaBackupCodes && <EnableOtpForm displayOtpForm={displayOtpForm} handleSetupProceedClick={handleSetupProceedClick} />}
{(displayOtpForm || !features.manageMfaBackupCodes) && <OtpConfirmForm />}
</Stack>
)}
</Column>

View File

@ -12,6 +12,7 @@ import Input from 'pl-fe/components/ui/input';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useFeatures } from 'pl-fe/hooks/use-features';
import toast from 'pl-fe/toast';
const messages = defineMessages({
@ -28,6 +29,7 @@ const OtpConfirmForm: React.FC = () => {
const intl = useIntl();
const history = useHistory();
const dispatch = useAppDispatch();
const features = useFeatures();
const [state, setState] = useState<{ password: string; isLoading: boolean; code: string; qrCodeURI: string; confirmKey: string }>({
password: '',
@ -101,20 +103,22 @@ const OtpConfirmForm: React.FC = () => {
/>
</FormGroup>
<FormGroup
labelText={intl.formatMessage(messages.passwordPlaceholder)}
hintText={<FormattedMessage id='mfa.mfa_setup.password_hint' defaultMessage='Enter your current password to confirm your identity.' />}
>
<Input
type='password'
name='password'
placeholder={intl.formatMessage(messages.passwordPlaceholder)}
onChange={handleInputChange}
disabled={state.isLoading}
value={state.password}
required
/>
</FormGroup>
{features.manageMfaRequiresPassword && (
<FormGroup
labelText={intl.formatMessage(messages.passwordPlaceholder)}
hintText={<FormattedMessage id='mfa.mfa_setup.password_hint' defaultMessage='Enter your current password to confirm your identity.' />}
>
<Input
type='password'
name='password'
placeholder={intl.formatMessage(messages.passwordPlaceholder)}
onChange={handleInputChange}
disabled={state.isLoading}
value={state.password}
required
/>
</FormGroup>
)}
<FormActions>
<Button

View File

@ -4,8 +4,7 @@ import { importEntities } from 'pl-fe/actions/importer';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useClient } from 'pl-fe/hooks/use-client';
import type { SearchParams } from 'pl-api';
import type { PaginationParams } from 'pl-api/dist/params/common';
import type { PaginationParams, SearchParams } from 'pl-api';
const useSearchAccounts = (
query: string,

View File

@ -6833,10 +6833,10 @@ pkg-dir@^4.1.0:
dependencies:
find-up "^4.0.0"
pl-api@^1.0.0-rc.44:
version "1.0.0-rc.44"
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-1.0.0-rc.44.tgz#e944ab2e27bd0756f5acc126972297715d2eb163"
integrity sha512-HiXHfbrbh3TOS4KcFIyITxfJ9TXS4SxOdg9/eS2ntiV+bKcuRPpEbHv14AuL6E1kxsEOOZdbSRo3NvGY774org==
pl-api@^1.0.0-rc.45:
version "1.0.0-rc.45"
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-1.0.0-rc.45.tgz#6c1986e850ab36ee1ba31248a2b90638bec06170"
integrity sha512-NM5QZ9x9sjK3sOxThqO48926Lr+Uw/P911jaLl4CcKEQV+IuLYEdOheYdpeH2Ub5LErcUwxiiriv5Xepx+FEEA==
dependencies:
blurhash "^2.0.5"
http-link-header "^1.1.3"

View File

@ -4,8 +4,7 @@ import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client';
import { usePlHooksQueryClient } from 'pl-hooks/contexts/query-client';
import { importEntities } from 'pl-hooks/importer';
import type { SearchParams, Tag } from 'pl-api';
import type { PaginationParams } from 'pl-api/dist/params/common';
import type { PaginationParams, SearchParams, Tag } from 'pl-api';
const useSearchAccounts = (
query: string,