mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-01 09:30:01 +08:00
feat(server): passkey pre-refactor (#15060)
#### PR Dependency Tree * **PR #15060** 👈 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** * OpenApp native sign-in and native session exchange (JWT) for mobile & desktop. * Centralized short-lived auth challenge store for one-time tokens. * Encrypted per-endpoint token storage and native token handlers (Android, iOS, Electron). * **Improvements** * Richer auth-method reporting (password, magic link, OAuth, passkey) and improved sign-in flows. * Hardened magic-link, OAuth, and session issuance; JWT-backed sessions and websocket JWT support. * UX tweaks: form-based password submit, OTP autocomplete, adjusted captcha flow. * **Bug Fixes** * Expanded tests and auth-state resets to avoid cross-test leakage. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -79,6 +79,13 @@ export function configureDefaultAuthProvider(framework: Framework) {
|
||||
},
|
||||
});
|
||||
},
|
||||
async signInOpenAppSignInCode(code: string) {
|
||||
await fetchService.fetch('/api/auth/open-app/sign-in', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ code }),
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
},
|
||||
async signOut() {
|
||||
const csrfToken = getCookieValue(CSRF_COOKIE_NAME);
|
||||
await fetchService.fetch('/api/auth/sign-out', {
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface AuthProvider {
|
||||
challenge?: string;
|
||||
}): Promise<void>;
|
||||
|
||||
signInOpenAppSignInCode(code: string): Promise<void>;
|
||||
|
||||
signOut(): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
@@ -222,11 +222,7 @@ export class AuthService extends Service {
|
||||
}
|
||||
|
||||
async signInOpenAppSignInCode(code: string) {
|
||||
await this.fetchService.fetch('/api/auth/open-app/sign-in', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ code }),
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
await this.store.signInOpenAppSignInCode(code);
|
||||
|
||||
this.session.revalidate();
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export class CaptchaService extends Service {
|
||||
revalidate = effect(
|
||||
exhaustMap(() => {
|
||||
return fromPromise(async signal => {
|
||||
if (!this.needCaptcha$.value) {
|
||||
if (!this.needCaptcha$.value || !this.validatorProvider) {
|
||||
return {};
|
||||
}
|
||||
const res = await this.fetchService.fetch('/api/auth/challenge', {
|
||||
@@ -46,17 +46,14 @@ export class CaptchaService extends Service {
|
||||
if (!data || !data.challenge || !data.resource) {
|
||||
throw new Error('Invalid challenge');
|
||||
}
|
||||
if (this.validatorProvider) {
|
||||
const token = await this.validatorProvider.validate(
|
||||
data.challenge,
|
||||
data.resource
|
||||
);
|
||||
return {
|
||||
token,
|
||||
challenge: data.challenge,
|
||||
};
|
||||
}
|
||||
return { challenge: data.challenge, token: undefined };
|
||||
const token = await this.validatorProvider.validate(
|
||||
data.challenge,
|
||||
data.resource
|
||||
);
|
||||
return {
|
||||
token,
|
||||
challenge: data.challenge,
|
||||
};
|
||||
}).pipe(
|
||||
tap(({ challenge, token }) => {
|
||||
this.verifyToken$.next(token);
|
||||
|
||||
@@ -19,6 +19,11 @@ export interface AccountProfile {
|
||||
email: string;
|
||||
name: string;
|
||||
hasPassword: boolean;
|
||||
authMethods?: {
|
||||
password: { bound: boolean };
|
||||
oauth: { bound: boolean; providers: string[] };
|
||||
passkey: { bound: boolean; count: number };
|
||||
};
|
||||
avatarUrl: string | null;
|
||||
emailVerified: string | null;
|
||||
features?: string[];
|
||||
@@ -61,16 +66,20 @@ export class AuthStore extends Store {
|
||||
}
|
||||
|
||||
async fetchSession() {
|
||||
const { user } = await this.nbstoreService.realtime.request(
|
||||
'user.profile.get',
|
||||
{},
|
||||
{ timeoutMs: 10000 }
|
||||
);
|
||||
const { user } = await this.fetchService
|
||||
.fetch('/api/auth/session')
|
||||
.then(res => res.json());
|
||||
const authMethods = user
|
||||
? await this.fetchService
|
||||
.fetch('/api/auth/methods')
|
||||
.then(res => (res.ok ? res.json() : undefined))
|
||||
: undefined;
|
||||
return {
|
||||
user: user
|
||||
? {
|
||||
...user,
|
||||
hasPassword: Boolean(user.hasPassword),
|
||||
authMethods,
|
||||
emailVerified: user.emailVerified ? 'true' : null,
|
||||
}
|
||||
: null,
|
||||
@@ -103,6 +112,10 @@ export class AuthStore extends Store {
|
||||
await this.authProvider.signInPassword(credential);
|
||||
}
|
||||
|
||||
async signInOpenAppSignInCode(code: string) {
|
||||
await this.authProvider.signInOpenAppSignInCode(code);
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
await this.authProvider.signOut();
|
||||
await this.nbstoreService.realtime.configure({
|
||||
@@ -155,8 +168,12 @@ export class AuthStore extends Store {
|
||||
|
||||
const data = (await res.json()) as {
|
||||
registered: boolean;
|
||||
hasPassword: boolean;
|
||||
magicLink: boolean;
|
||||
methods: {
|
||||
password: { available: boolean };
|
||||
magicLink: { available: boolean };
|
||||
oauth: { available: boolean; providers: string[] };
|
||||
passkey: { available: boolean; discoverable: boolean };
|
||||
};
|
||||
};
|
||||
|
||||
return data;
|
||||
|
||||
@@ -30,7 +30,10 @@ export type GLOBAL_DIALOG_SCHEMA = {
|
||||
snapshotUrl: string;
|
||||
}) => void;
|
||||
'sign-in': (props: { server?: string; step?: string }) => void;
|
||||
'change-password': (props: { server?: string }) => void;
|
||||
'change-password': (props: {
|
||||
server?: string;
|
||||
hasPassword?: boolean;
|
||||
}) => void;
|
||||
'verify-email': (props: { server?: string; changeEmail?: boolean }) => void;
|
||||
'enable-cloud': (props: {
|
||||
workspaceId: string;
|
||||
|
||||
Reference in New Issue
Block a user