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']}. * Requires features{@link Features['manageMfa']}.
*/ */
getMfaSettings: async () => { 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({ return v.parse(v.object({
settings: v.object({ settings: v.object({
enabled: v.boolean(), enabled: v.boolean(),
totp: v.boolean(), totp: v.boolean(),
}), }),
}), response.json); }), response);
}, },
/** /**
@ -1524,36 +1537,66 @@ class PlApiClient {
* Requires features{@link Features['manageMfa']}. * Requires features{@link Features['manageMfa']}.
*/ */
getMfaSetup: async (method: 'totp') => { 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({ return v.parse(v.object({
key: v.string(), key: v.fallback(v.string(), ''),
provisioning_uri: v.string(), provisioning_uri: v.string(),
}), response.json); }), response);
}, },
/** /**
* Requires features{@link Features['manageMfa']}. * Requires features{@link Features['manageMfa']}.
*/ */
confirmMfaSetup: async (method: 'totp', code: string, password: string) => { confirmMfaSetup: async (method: 'totp', code: string, password: string) => {
const response = await this.request(`/api/pleroma/accounts/mfa/confirm/${method}`, { let response;
method: 'POST',
body: { code, password },
});
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']}. * Requires features{@link Features['manageMfa']}.
*/ */
disableMfa: async (method: 'totp', password: string) => { disableMfa: async (method: 'totp', password: string) => {
const response = await this.request(`/api/pleroma/accounts/mfa/${method}`, { let response;
method: 'DELETE',
body: { password }, 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; 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
* @see GET /api/pleroma/accounts/mfa/backup_codes
* @see GET /api/pleroma/accounts/mfa/setup/:method * @see GET /api/pleroma/accounts/mfa/setup/:method
* @see POST /api/pleroma/accounts/mfa/confirm/:method * @see POST /api/pleroma/accounts/mfa/confirm/:method
* @see DELETE /api/pleroma/accounts/mfa/:method * @see DELETE /api/pleroma/accounts/mfa/:method
@ -995,6 +994,23 @@ const getFeatures = (instance: Instance) => {
manageMfa: any([ manageMfa: any([
v.software === AKKOMA, v.software === AKKOMA,
v.software === PLEROMA, 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 './admin';
export * from './apps'; export * from './apps';
export * from './chats'; export * from './chats';
export type { PaginationParams } from './common';
export * from './events'; export * from './events';
export * from './filtering'; export * from './filtering';
export * from './grouped-notifications'; export * from './grouped-notifications';

View File

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

View File

@ -104,7 +104,7 @@
"multiselect-react-dropdown": "^2.0.25", "multiselect-react-dropdown": "^2.0.25",
"mutative": "^1.1.0", "mutative": "^1.1.0",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"pl-api": "^1.0.0-rc.44", "pl-api": "^1.0.0-rc.45",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"process": "^0.11.10", "process": "^0.11.10",
"punycode": "^2.1.1", "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 Stack from 'pl-fe/components/ui/stack';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; 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 DisableOtpForm from './mfa/disable-otp-form';
import EnableOtpForm from './mfa/enable-otp-form'; import EnableOtpForm from './mfa/enable-otp-form';
@ -25,6 +26,7 @@ const messages = defineMessages({
const MfaForm: React.FC = () => { const MfaForm: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const features = useFeatures();
const [displayOtpForm, setDisplayOtpForm] = useState<boolean>(false); const [displayOtpForm, setDisplayOtpForm] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
@ -44,8 +46,8 @@ const MfaForm: React.FC = () => {
<DisableOtpForm /> <DisableOtpForm />
) : ( ) : (
<Stack space={4}> <Stack space={4}>
<EnableOtpForm displayOtpForm={displayOtpForm} handleSetupProceedClick={handleSetupProceedClick} /> {features.manageMfaBackupCodes && <EnableOtpForm displayOtpForm={displayOtpForm} handleSetupProceedClick={handleSetupProceedClick} />}
{displayOtpForm && <OtpConfirmForm />} {(displayOtpForm || !features.manageMfaBackupCodes) && <OtpConfirmForm />}
</Stack> </Stack>
)} )}
</Column> </Column>

View File

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

View File

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

View File

@ -6833,10 +6833,10 @@ pkg-dir@^4.1.0:
dependencies: dependencies:
find-up "^4.0.0" find-up "^4.0.0"
pl-api@^1.0.0-rc.44: pl-api@^1.0.0-rc.45:
version "1.0.0-rc.44" version "1.0.0-rc.45"
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-1.0.0-rc.44.tgz#e944ab2e27bd0756f5acc126972297715d2eb163" resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-1.0.0-rc.45.tgz#6c1986e850ab36ee1ba31248a2b90638bec06170"
integrity sha512-HiXHfbrbh3TOS4KcFIyITxfJ9TXS4SxOdg9/eS2ntiV+bKcuRPpEbHv14AuL6E1kxsEOOZdbSRo3NvGY774org== integrity sha512-NM5QZ9x9sjK3sOxThqO48926Lr+Uw/P911jaLl4CcKEQV+IuLYEdOheYdpeH2Ub5LErcUwxiiriv5Xepx+FEEA==
dependencies: dependencies:
blurhash "^2.0.5" blurhash "^2.0.5"
http-link-header "^1.1.3" 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 { usePlHooksQueryClient } from 'pl-hooks/contexts/query-client';
import { importEntities } from 'pl-hooks/importer'; import { importEntities } from 'pl-hooks/importer';
import type { SearchParams, Tag } from 'pl-api'; import type { PaginationParams, SearchParams, Tag } from 'pl-api';
import type { PaginationParams } from 'pl-api/dist/params/common';
const useSearchAccounts = ( const useSearchAccounts = (
query: string, query: string,