mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-21 20:11:43 +08:00
feat(core): improve login flow (#15219)
#### PR Dependency Tree * **PR #15219** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added secure, automatic auth session token refresh and request replay for expired-token responses across Android, iOS, and Electron. * Updated sign-in flows to manage sessions without returning tokens to the app layer. * Added “Devices” management UI with sign out per device and sign out all. * Enabled support for both Hashcash and Turnstile captcha providers. * **Bug Fixes** * Improved refresh de-duplication, inflight cancellation/clear behavior, and recovery from corrupted/invalid sessions. * **Tests** * Expanded auth-session, refresh/revoke, and replay coverage (Electron unit tests, Android instrumentation tests, iOS auth date parser tests). * **Chores** * Removed CAPTCHA site key from build-time configuration and adjusted CI test execution. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -18,7 +18,11 @@ import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { AuthService, ServerService } from '../../../../modules/cloud';
|
||||
import {
|
||||
AuthService,
|
||||
type DeviceAuthSession,
|
||||
ServerService,
|
||||
} from '../../../../modules/cloud';
|
||||
import type { SettingState } from '../types';
|
||||
import { AIUsagePanel } from './ai-usage-panel';
|
||||
import { DeleteAccount } from './delete-account';
|
||||
@@ -170,6 +174,114 @@ const StoragePanel = ({
|
||||
);
|
||||
};
|
||||
|
||||
const DevicesPanel = () => {
|
||||
const t = useI18n();
|
||||
const auth = useService(AuthService);
|
||||
const [sessions, setSessions] = useState<DeviceAuthSession[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
setSessions(await auth.listDeviceSessions());
|
||||
} catch (error) {
|
||||
notify.error({
|
||||
title: t['com.affine.settings.devices.load-failed'](),
|
||||
message: String(error),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [auth, t]);
|
||||
|
||||
useEffect(() => {
|
||||
reload().catch(error => {
|
||||
notify.error({
|
||||
title: t['com.affine.settings.devices.load-failed'](),
|
||||
message: String(error),
|
||||
});
|
||||
});
|
||||
}, [reload, t]);
|
||||
|
||||
const revoke = useCallback(
|
||||
async (session: DeviceAuthSession) => {
|
||||
if (
|
||||
!window.confirm(
|
||||
t['com.affine.settings.devices.confirm']({
|
||||
device: session.deviceName ?? session.platform,
|
||||
})
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await auth.revokeDeviceSession(session.id, session.current);
|
||||
if (!session.current) await reload();
|
||||
} catch (error) {
|
||||
notify.error({
|
||||
title: t['com.affine.settings.devices.sign-out-failed'](),
|
||||
message: String(error),
|
||||
});
|
||||
}
|
||||
},
|
||||
[auth, reload, t]
|
||||
);
|
||||
|
||||
const revokeAll = useCallback(async () => {
|
||||
if (!window.confirm(t['com.affine.settings.devices.confirm-all']())) return;
|
||||
try {
|
||||
await auth.revokeAllDeviceSessions();
|
||||
} catch (error) {
|
||||
notify.error({
|
||||
title: t['com.affine.settings.devices.sign-out-all-failed'](),
|
||||
message: String(error),
|
||||
});
|
||||
}
|
||||
}, [auth, t]);
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.devices.title']()}
|
||||
desc={t['com.affine.settings.devices.description']()}
|
||||
spreadCol={false}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{loading ? (
|
||||
<span>{t['com.affine.settings.devices.loading']()}</span>
|
||||
) : null}
|
||||
{sessions.map(session => (
|
||||
<div
|
||||
key={session.id}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 12 }}
|
||||
>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div>
|
||||
{session.deviceName ?? session.platform}
|
||||
{session.current
|
||||
? ` (${t['com.affine.settings.devices.current']()})`
|
||||
: ''}
|
||||
</div>
|
||||
<div>
|
||||
{session.platform}
|
||||
{session.appVersion ? ` · ${session.appVersion}` : ''}
|
||||
{` · ${t['com.affine.settings.devices.last-used']({ time: new Date(session.lastSeenAt).toLocaleString() })}`}
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => void revoke(session)}>
|
||||
{t['com.affine.settings.devices.sign-out']()}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{sessions.length > 1 ? (
|
||||
<Button onClick={() => void revokeAll()}>
|
||||
{t['com.affine.settings.devices.sign-out-all']()}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const AccountSetting = ({
|
||||
onChangeSettingState,
|
||||
}: {
|
||||
@@ -244,6 +356,7 @@ export const AccountSetting = ({
|
||||
: t['com.affine.settings.password.action.set']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<DevicesPanel />
|
||||
<StoragePanel onChangeSettingState={onChangeSettingState} />
|
||||
{serverFeatures?.copilot && (
|
||||
<AIUsagePanel onChangeSettingState={onChangeSettingState} />
|
||||
|
||||
Reference in New Issue
Block a user