= {}) {
+ const url = new URL(
+ this.config.baseUrl + (path.startsWith('/') ? path : '/' + path)
+ );
+
+ for (const key in query) {
+ url.searchParams.set(key, query[key]);
+ }
+
+ return url.toString();
+ }
+
+ safeRedirect(res: Response, to: string) {
+ try {
+ const finalTo = new URL(decodeURIComponent(to), this.config.baseUrl);
+
+ for (const host of this.redirectAllowHosts) {
+ const hostURL = new URL(host);
+ if (
+ hostURL.origin === finalTo.origin &&
+ finalTo.pathname.startsWith(hostURL.pathname)
+ ) {
+ return res.redirect(finalTo.toString().replace(/\/$/, ''));
+ }
+ }
+ } catch {
+ // just ignore invalid url
+ }
+
+ // redirect to home if the url is invalid
+ return res.redirect(this.home);
+ }
+}
diff --git a/packages/backend/server/src/fundamentals/index.ts b/packages/backend/server/src/fundamentals/index.ts
index af674c34ff..02c5515e16 100644
--- a/packages/backend/server/src/fundamentals/index.ts
+++ b/packages/backend/server/src/fundamentals/index.ts
@@ -14,6 +14,7 @@ export {
} from './config';
export * from './error';
export { EventEmitter, type EventPayload, OnEvent } from './event';
+export { CryptoHelper, URLHelper } from './helpers';
export { MailService } from './mailer';
export { CallCounter, CallTimer, metrics } from './metrics';
export {
@@ -21,7 +22,6 @@ export {
GlobalExceptionFilter,
OptionalModule,
} from './nestjs';
-export { SessionService } from './session';
export * from './storage';
export { type StorageProvider, StorageProviderFactory } from './storage';
export { AuthThrottlerGuard, CloudThrottlerGuard, Throttle } from './throttler';
diff --git a/packages/backend/server/src/fundamentals/mailer/mail.service.ts b/packages/backend/server/src/fundamentals/mailer/mail.service.ts
index aab0620a4e..7bab15dc55 100644
--- a/packages/backend/server/src/fundamentals/mailer/mail.service.ts
+++ b/packages/backend/server/src/fundamentals/mailer/mail.service.ts
@@ -1,12 +1,14 @@
import { Inject, Injectable, Optional } from '@nestjs/common';
import { Config } from '../config';
+import { URLHelper } from '../helpers';
import { MAILER_SERVICE, type MailerService, type Options } from './mailer';
import { emailTemplate } from './template';
@Injectable()
export class MailService {
constructor(
private readonly config: Config,
+ private readonly url: URLHelper,
@Optional() @Inject(MAILER_SERVICE) private readonly mailer?: MailerService
) {}
@@ -41,7 +43,7 @@ export class MailService {
}
) {
// TODO: use callback url when need support desktop app
- const buttonUrl = `${this.config.origin}/invite/${inviteId}`;
+ const buttonUrl = this.url.link(`/invite/${inviteId}`);
const workspaceAvatar = invitationInfo.workspace.avatar;
const content = `${
@@ -92,7 +94,23 @@ export class MailService {
});
}
- async sendSignInEmail(url: string, options: Options) {
+ async sendSignUpMail(url: string, options: Options) {
+ const html = emailTemplate({
+ title: 'Create AFFiNE Account',
+ content:
+ 'Click the button below to complete your account creation and sign in. This magic link will expire in 30 minutes.',
+ buttonContent: ' Create account and sign in',
+ buttonUrl: url,
+ });
+
+ return this.sendMail({
+ html,
+ subject: 'Your AFFiNE account is waiting for you!',
+ ...options,
+ });
+ }
+
+ async sendSignInMail(url: string, options: Options) {
const html = emailTemplate({
title: 'Sign in to AFFiNE',
content:
@@ -164,6 +182,20 @@ export class MailService {
html,
});
}
+ async sendVerifyEmail(to: string, url: string) {
+ const html = emailTemplate({
+ title: 'Verify your email address',
+ content:
+ 'You recently requested to verify the email address associated with your AFFiNE account. To complete this process, please click on the verification link below. This magic link will expire in 30 minutes.',
+ buttonContent: 'Verify your email address',
+ buttonUrl: url,
+ });
+ return this.sendMail({
+ to,
+ subject: `Verify your email for AFFiNE`,
+ html,
+ });
+ }
async sendNotificationChangeEmail(to: string) {
const html = emailTemplate({
title: 'Email change successful',
diff --git a/packages/backend/server/src/fundamentals/nestjs/optional-module.ts b/packages/backend/server/src/fundamentals/nestjs/optional-module.ts
index e7003485b7..b53d2408f6 100644
--- a/packages/backend/server/src/fundamentals/nestjs/optional-module.ts
+++ b/packages/backend/server/src/fundamentals/nestjs/optional-module.ts
@@ -9,7 +9,7 @@ import { omit } from 'lodash-es';
import { Config, ConfigPaths } from '../config';
-interface OptionalModuleMetadata extends ModuleMetadata {
+export interface OptionalModuleMetadata extends ModuleMetadata {
/**
* Only install module if given config paths are defined in AFFiNE config.
*/
diff --git a/packages/backend/server/src/fundamentals/session/index.ts b/packages/backend/server/src/fundamentals/session/index.ts
deleted file mode 100644
index 3ee1759310..0000000000
--- a/packages/backend/server/src/fundamentals/session/index.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { Global, Injectable, Module } from '@nestjs/common';
-
-import { SessionCache } from '../cache';
-
-@Injectable()
-export class SessionService {
- private readonly prefix = 'session:';
- public readonly sessionTtl = 30 * 60 * 1000; // 30 min
-
- constructor(private readonly cache: SessionCache) {}
-
- /**
- * get session
- * @param key session key
- * @returns
- */
- async get(key: string) {
- return this.cache.get(this.prefix + key);
- }
-
- /**
- * set session
- * @param key session key
- * @param value session value
- * @param sessionTtl session ttl (ms), default 30 min
- * @returns return true if success
- */
- async set(key: string, value?: any, sessionTtl = this.sessionTtl) {
- return this.cache.set(this.prefix + key, value, {
- ttl: sessionTtl,
- });
- }
-
- async delete(key: string) {
- return this.cache.delete(this.prefix + key);
- }
-}
-
-@Global()
-@Module({
- providers: [SessionService],
- exports: [SessionService],
-})
-export class SessionModule {}
diff --git a/packages/backend/server/src/fundamentals/utils/request.ts b/packages/backend/server/src/fundamentals/utils/request.ts
index 7ee27745ce..21f4ed520f 100644
--- a/packages/backend/server/src/fundamentals/utils/request.ts
+++ b/packages/backend/server/src/fundamentals/utils/request.ts
@@ -1,53 +1,8 @@
import type { ArgumentsHost, ExecutionContext } from '@nestjs/common';
import type { GqlContextType } from '@nestjs/graphql';
-import { GqlArgumentsHost, GqlExecutionContext } from '@nestjs/graphql';
+import { GqlArgumentsHost } from '@nestjs/graphql';
import type { Request, Response } from 'express';
-
-export function getRequestResponseFromContext(context: ExecutionContext) {
- switch (context.getType()) {
- case 'graphql': {
- const gqlContext = GqlExecutionContext.create(context).getContext<{
- req: Request;
- }>();
- return {
- req: gqlContext.req,
- res: gqlContext.req.res,
- };
- }
- case 'http': {
- const http = context.switchToHttp();
- return {
- req: http.getRequest(),
- res: http.getResponse(),
- };
- }
- case 'ws': {
- const ws = context.switchToWs();
- const req = ws.getClient().handshake;
-
- const cookies = req?.headers?.cookie;
- // patch cookies to match auth guard logic
- if (typeof cookies === 'string') {
- req.cookies = cookies
- .split(';')
- .map(v => v.split('='))
- .reduce(
- (acc, v) => {
- acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(
- v[1].trim()
- );
- return acc;
- },
- {} as Record
- );
- }
-
- return { req };
- }
- default:
- throw new Error('Unknown context type for getting request and response');
- }
-}
+import type { Socket } from 'socket.io';
export function getRequestResponseFromHost(host: ArgumentsHost) {
switch (host.getType()) {
@@ -67,11 +22,47 @@ export function getRequestResponseFromHost(host: ArgumentsHost) {
res: http.getResponse(),
};
}
- default:
- throw new Error('Unknown host type for getting request and response');
+ case 'ws': {
+ const ws = host.switchToWs();
+ const req = ws.getClient().client.conn.request as Request;
+
+ const cookieStr = req?.headers?.cookie;
+ // patch cookies to match auth guard logic
+ if (typeof cookieStr === 'string') {
+ req.cookies = cookieStr.split(';').reduce(
+ (cookies, cookie) => {
+ const [key, val] = cookie.split('=');
+
+ if (key) {
+ cookies[decodeURIComponent(key.trim())] = val
+ ? decodeURIComponent(val.trim())
+ : val;
+ }
+
+ return cookies;
+ },
+ {} as Record
+ );
+ }
+
+ return { req };
+ }
+ case 'rpc': {
+ const rpc = host.switchToRpc();
+ const { req } = rpc.getContext<{ req: Request }>();
+
+ return {
+ req,
+ res: req.res,
+ };
+ }
}
}
export function getRequestFromHost(host: ArgumentsHost) {
return getRequestResponseFromHost(host).req;
}
+
+export function getRequestResponseFromContext(ctx: ExecutionContext) {
+ return getRequestResponseFromHost(ctx);
+}
diff --git a/packages/backend/server/src/global.d.ts b/packages/backend/server/src/global.d.ts
index f7674001d3..ebb0fae5f1 100644
--- a/packages/backend/server/src/global.d.ts
+++ b/packages/backend/server/src/global.d.ts
@@ -1,6 +1,6 @@
declare namespace Express {
interface Request {
- user?: import('@prisma/client').User | null;
+ user?: import('./core/auth/current-user').CurrentUser;
}
}
diff --git a/packages/backend/server/src/plugins/config.ts b/packages/backend/server/src/plugins/config.ts
index 2e150c971c..eea08c491f 100644
--- a/packages/backend/server/src/plugins/config.ts
+++ b/packages/backend/server/src/plugins/config.ts
@@ -1,4 +1,5 @@
import { GCloudConfig } from './gcloud/config';
+import { OAuthConfig } from './oauth';
import { PaymentConfig } from './payment';
import { RedisOptions } from './redis';
import { R2StorageConfig, S3StorageConfig } from './storage';
@@ -10,13 +11,14 @@ declare module '../fundamentals/config' {
readonly gcloud: GCloudConfig;
readonly 'cloudflare-r2': R2StorageConfig;
readonly 'aws-s3': S3StorageConfig;
+ readonly oauth: OAuthConfig;
}
export type AvailablePlugins = keyof PluginsConfig;
interface AFFiNEConfig {
readonly plugins: {
- enabled: AvailablePlugins[];
+ enabled: Set;
use(
plugin: Plugin,
config?: DeepPartial
diff --git a/packages/backend/server/src/plugins/gcloud/index.ts b/packages/backend/server/src/plugins/gcloud/index.ts
index ed8b238c16..16a5a4494e 100644
--- a/packages/backend/server/src/plugins/gcloud/index.ts
+++ b/packages/backend/server/src/plugins/gcloud/index.ts
@@ -1,10 +1,11 @@
import { Global } from '@nestjs/common';
-import { OptionalModule } from '../../fundamentals';
+import { Plugin } from '../registry';
import { GCloudMetrics } from './metrics';
@Global()
-@OptionalModule({
+@Plugin({
+ name: 'gcloud',
imports: [GCloudMetrics],
})
export class GCloudModule {}
diff --git a/packages/backend/server/src/plugins/index.ts b/packages/backend/server/src/plugins/index.ts
index 9780e7322a..42ea147ad3 100644
--- a/packages/backend/server/src/plugins/index.ts
+++ b/packages/backend/server/src/plugins/index.ts
@@ -1,13 +1,7 @@
-import type { AvailablePlugins } from '../fundamentals/config';
-import { GCloudModule } from './gcloud';
-import { PaymentModule } from './payment';
-import { RedisModule } from './redis';
-import { AwsS3Module, CloudflareR2Module } from './storage';
+import './gcloud';
+import './oauth';
+import './payment';
+import './redis';
+import './storage';
-export const pluginsMap = new Map([
- ['payment', PaymentModule],
- ['redis', RedisModule],
- ['gcloud', GCloudModule],
- ['cloudflare-r2', CloudflareR2Module],
- ['aws-s3', AwsS3Module],
-]);
+export { REGISTERED_PLUGINS } from './registry';
diff --git a/packages/backend/server/src/plugins/oauth/controller.ts b/packages/backend/server/src/plugins/oauth/controller.ts
new file mode 100644
index 0000000000..92f968e892
--- /dev/null
+++ b/packages/backend/server/src/plugins/oauth/controller.ts
@@ -0,0 +1,230 @@
+import {
+ BadRequestException,
+ Controller,
+ Get,
+ Query,
+ Req,
+ Res,
+} from '@nestjs/common';
+import { ConnectedAccount, PrismaClient } from '@prisma/client';
+import type { Request, Response } from 'express';
+
+import { AuthService, Public } from '../../core/auth';
+import { UserService } from '../../core/user';
+import { URLHelper } from '../../fundamentals';
+import { OAuthAccount, Tokens } from './providers/def';
+import { OAuthProviderFactory } from './register';
+import { OAuthService } from './service';
+import { OAuthProviderName } from './types';
+
+@Controller('/oauth')
+export class OAuthController {
+ constructor(
+ private readonly auth: AuthService,
+ private readonly oauth: OAuthService,
+ private readonly user: UserService,
+ private readonly providerFactory: OAuthProviderFactory,
+ private readonly url: URLHelper,
+ private readonly db: PrismaClient
+ ) {}
+
+ @Public()
+ @Get('/login')
+ async login(
+ @Res() res: Response,
+ @Query('provider') unknownProviderName: string,
+ @Query('redirect_uri') redirectUri?: string
+ ) {
+ // @ts-expect-error safe
+ const providerName = OAuthProviderName[unknownProviderName];
+ const provider = this.providerFactory.get(providerName);
+
+ if (!provider) {
+ throw new BadRequestException('Invalid provider');
+ }
+
+ const state = await this.oauth.saveOAuthState({
+ redirectUri: redirectUri ?? this.url.home,
+ provider: providerName,
+ });
+
+ return res.redirect(provider.getAuthUrl(state));
+ }
+
+ @Public()
+ @Get('/callback')
+ async callback(
+ @Req() req: Request,
+ @Res() res: Response,
+ @Query('code') code?: string,
+ @Query('state') stateStr?: string
+ ) {
+ if (!code) {
+ throw new BadRequestException('Missing query parameter `code`');
+ }
+
+ if (!stateStr) {
+ throw new BadRequestException('Invalid callback state parameter');
+ }
+
+ const state = await this.oauth.getOAuthState(stateStr);
+
+ if (!state) {
+ throw new BadRequestException('OAuth state expired, please try again.');
+ }
+
+ if (!state.provider) {
+ throw new BadRequestException(
+ 'Missing callback state parameter `provider`'
+ );
+ }
+
+ const provider = this.providerFactory.get(state.provider);
+
+ if (!provider) {
+ throw new BadRequestException('Invalid provider');
+ }
+
+ const tokens = await provider.getToken(code);
+ const externAccount = await provider.getUser(tokens.accessToken);
+ const user = req.user;
+
+ try {
+ if (!user) {
+ // if user not found, login
+ const user = await this.loginFromOauth(
+ state.provider,
+ externAccount,
+ tokens
+ );
+ const session = await this.auth.createUserSession(
+ user,
+ req.cookies[AuthService.sessionCookieName]
+ );
+ res.cookie(AuthService.sessionCookieName, session.sessionId, {
+ expires: session.expiresAt ?? void 0, // expiredAt is `string | null`
+ ...this.auth.cookieOptions,
+ });
+ } else {
+ // if user is found, connect the account to this user
+ await this.connectAccountFromOauth(
+ user,
+ state.provider,
+ externAccount,
+ tokens
+ );
+ }
+ } catch (e: any) {
+ return res.redirect(
+ this.url.link('/signIn', {
+ redirect_uri: state.redirectUri,
+ error: e.message,
+ })
+ );
+ }
+
+ this.url.safeRedirect(res, state.redirectUri);
+ }
+
+ private async loginFromOauth(
+ provider: OAuthProviderName,
+ externalAccount: OAuthAccount,
+ tokens: Tokens
+ ) {
+ const connectedUser = await this.db.connectedAccount.findFirst({
+ where: {
+ provider,
+ providerAccountId: externalAccount.id,
+ },
+ include: {
+ user: true,
+ },
+ });
+
+ if (connectedUser) {
+ // already connected
+ await this.updateConnectedAccount(connectedUser, tokens);
+
+ return connectedUser.user;
+ }
+
+ let user = await this.user.findUserByEmail(externalAccount.email);
+
+ if (user) {
+ // we can't directly connect the external account with given email in sign in scenario for safety concern.
+ // let user manually connect in account sessions instead.
+ throw new BadRequestException(
+ 'The account with provided email is not register in the same way.'
+ );
+ } else {
+ user = await this.createUserWithConnectedAccount(
+ provider,
+ externalAccount,
+ tokens
+ );
+ }
+
+ return user;
+ }
+
+ updateConnectedAccount(connectedUser: ConnectedAccount, tokens: Tokens) {
+ return this.db.connectedAccount.update({
+ where: {
+ id: connectedUser.id,
+ },
+ data: tokens,
+ });
+ }
+
+ async createUserWithConnectedAccount(
+ provider: OAuthProviderName,
+ externalAccount: OAuthAccount,
+ tokens: Tokens
+ ) {
+ return this.user.createUser({
+ email: externalAccount.email,
+ name: 'Unnamed',
+ avatarUrl: externalAccount.avatarUrl,
+ emailVerifiedAt: new Date(),
+ connectedAccounts: {
+ create: {
+ provider,
+ providerAccountId: externalAccount.id,
+ ...tokens,
+ },
+ },
+ });
+ }
+
+ private async connectAccountFromOauth(
+ user: { id: string },
+ provider: OAuthProviderName,
+ externalAccount: OAuthAccount,
+ tokens: Tokens
+ ) {
+ const connectedUser = await this.db.connectedAccount.findFirst({
+ where: {
+ provider,
+ providerAccountId: externalAccount.id,
+ },
+ });
+
+ if (connectedUser) {
+ if (connectedUser.id !== user.id) {
+ throw new BadRequestException(
+ 'The third-party account has already been connected to another user.'
+ );
+ }
+ } else {
+ await this.db.connectedAccount.create({
+ data: {
+ userId: user.id,
+ provider,
+ providerAccountId: externalAccount.id,
+ accessToken: tokens.accessToken,
+ refreshToken: tokens.refreshToken,
+ },
+ });
+ }
+ }
+}
diff --git a/packages/backend/server/src/plugins/oauth/index.ts b/packages/backend/server/src/plugins/oauth/index.ts
new file mode 100644
index 0000000000..0b14d1d984
--- /dev/null
+++ b/packages/backend/server/src/plugins/oauth/index.ts
@@ -0,0 +1,25 @@
+import { AuthModule } from '../../core/auth';
+import { ServerFeature } from '../../core/config';
+import { UserModule } from '../../core/user';
+import { Plugin } from '../registry';
+import { OAuthController } from './controller';
+import { OAuthProviders } from './providers';
+import { OAuthProviderFactory } from './register';
+import { OAuthResolver } from './resolver';
+import { OAuthService } from './service';
+
+@Plugin({
+ name: 'oauth',
+ imports: [AuthModule, UserModule],
+ providers: [
+ OAuthProviderFactory,
+ OAuthService,
+ OAuthResolver,
+ ...OAuthProviders,
+ ],
+ controllers: [OAuthController],
+ contributesTo: ServerFeature.OAuth,
+ if: config => !!config.plugins.oauth,
+})
+export class OAuthModule {}
+export type { OAuthConfig } from './types';
diff --git a/packages/backend/server/src/plugins/oauth/providers/def.ts b/packages/backend/server/src/plugins/oauth/providers/def.ts
new file mode 100644
index 0000000000..7e7913cdaf
--- /dev/null
+++ b/packages/backend/server/src/plugins/oauth/providers/def.ts
@@ -0,0 +1,21 @@
+import { OAuthProviderName } from '../types';
+
+export interface OAuthAccount {
+ id: string;
+ email: string;
+ avatarUrl?: string;
+}
+
+export interface Tokens {
+ accessToken: string;
+ scope?: string;
+ refreshToken?: string;
+ expiresAt?: Date;
+}
+
+export abstract class OAuthProvider {
+ abstract provider: OAuthProviderName;
+ abstract getAuthUrl(state?: string): string;
+ abstract getToken(code: string): Promise;
+ abstract getUser(token: string): Promise;
+}
diff --git a/packages/backend/server/src/plugins/oauth/providers/github.ts b/packages/backend/server/src/plugins/oauth/providers/github.ts
new file mode 100644
index 0000000000..50227539a7
--- /dev/null
+++ b/packages/backend/server/src/plugins/oauth/providers/github.ts
@@ -0,0 +1,113 @@
+import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
+
+import { Config, URLHelper } from '../../../fundamentals';
+import { AutoRegisteredOAuthProvider } from '../register';
+import { OAuthProviderName } from '../types';
+
+interface AuthTokenResponse {
+ access_token: string;
+ scope: string;
+ token_type: string;
+}
+
+export interface UserInfo {
+ login: string;
+ email: string;
+ avatar_url: string;
+ name: string;
+}
+
+@Injectable()
+export class GithubOAuthProvider extends AutoRegisteredOAuthProvider {
+ provider = OAuthProviderName.GitHub;
+
+ constructor(
+ protected readonly AFFiNEConfig: Config,
+ private readonly url: URLHelper
+ ) {
+ super();
+ }
+
+ getAuthUrl(state: string) {
+ return `https://github.com/login/oauth/authorize?${this.url.stringify({
+ client_id: this.config.clientId,
+ redirect_uri: this.url.link('/oauth/callback'),
+ scope: 'user',
+ ...this.config.args,
+ state,
+ })}`;
+ }
+
+ async getToken(code: string) {
+ try {
+ const response = await fetch(
+ 'https://github.com/login/oauth/access_token',
+ {
+ method: 'POST',
+ body: this.url.stringify({
+ code,
+ client_id: this.config.clientId,
+ client_secret: this.config.clientSecret,
+ redirect_uri: this.url.link('/oauth/callback'),
+ }),
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ }
+ );
+
+ if (response.ok) {
+ const ghToken = (await response.json()) as AuthTokenResponse;
+
+ return {
+ accessToken: ghToken.access_token,
+ scope: ghToken.scope,
+ };
+ } else {
+ throw new Error(
+ `Server responded with non-success code ${
+ response.status
+ }, ${JSON.stringify(await response.json())}`
+ );
+ }
+ } catch (e) {
+ throw new HttpException(
+ `Failed to get access_token, err: ${(e as Error).message}`,
+ HttpStatus.BAD_REQUEST
+ );
+ }
+ }
+
+ async getUser(token: string) {
+ try {
+ const response = await fetch('https://api.github.com/user', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (response.ok) {
+ const user = (await response.json()) as UserInfo;
+
+ return {
+ id: user.login,
+ avatarUrl: user.avatar_url,
+ email: user.email,
+ };
+ } else {
+ throw new Error(
+ `Server responded with non-success code ${
+ response.status
+ } ${await response.text()}`
+ );
+ }
+ } catch (e) {
+ throw new HttpException(
+ `Failed to get user information, err: ${(e as Error).stack}`,
+ HttpStatus.BAD_REQUEST
+ );
+ }
+ }
+}
diff --git a/packages/backend/server/src/plugins/oauth/providers/google.ts b/packages/backend/server/src/plugins/oauth/providers/google.ts
new file mode 100644
index 0000000000..fb22bd36f0
--- /dev/null
+++ b/packages/backend/server/src/plugins/oauth/providers/google.ts
@@ -0,0 +1,121 @@
+import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
+
+import { Config, URLHelper } from '../../../fundamentals';
+import { AutoRegisteredOAuthProvider } from '../register';
+import { OAuthProviderName } from '../types';
+
+interface GoogleOAuthTokenResponse {
+ access_token: string;
+ expires_in: number;
+ refresh_token: string;
+ scope: string;
+ token_type: string;
+}
+
+export interface UserInfo {
+ id: string;
+ email: string;
+ picture: string;
+ name: string;
+}
+
+@Injectable()
+export class GoogleOAuthProvider extends AutoRegisteredOAuthProvider {
+ override provider = OAuthProviderName.Google;
+
+ constructor(
+ protected readonly AFFiNEConfig: Config,
+ private readonly url: URLHelper
+ ) {
+ super();
+ }
+
+ getAuthUrl(state: string) {
+ return `https://accounts.google.com/o/oauth2/v2/auth?${this.url.stringify({
+ client_id: this.config.clientId,
+ redirect_uri: this.url.link('/oauth/callback'),
+ response_type: 'code',
+ scope: 'openid email profile',
+ promot: 'select_account',
+ access_type: 'offline',
+ ...this.config.args,
+ state,
+ })}`;
+ }
+
+ async getToken(code: string) {
+ try {
+ const response = await fetch('https://oauth2.googleapis.com/token', {
+ method: 'POST',
+ body: this.url.stringify({
+ code,
+ client_id: this.config.clientId,
+ client_secret: this.config.clientSecret,
+ redirect_uri: this.url.link('/oauth/callback'),
+ grant_type: 'authorization_code',
+ }),
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ });
+
+ if (response.ok) {
+ const ghToken = (await response.json()) as GoogleOAuthTokenResponse;
+
+ return {
+ accessToken: ghToken.access_token,
+ refreshToken: ghToken.refresh_token,
+ expiresAt: new Date(Date.now() + ghToken.expires_in * 1000),
+ scope: ghToken.scope,
+ };
+ } else {
+ throw new Error(
+ `Server responded with non-success code ${
+ response.status
+ }, ${JSON.stringify(await response.json())}`
+ );
+ }
+ } catch (e) {
+ throw new HttpException(
+ `Failed to get access_token, err: ${(e as Error).message}`,
+ HttpStatus.BAD_REQUEST
+ );
+ }
+ }
+
+ async getUser(token: string) {
+ try {
+ const response = await fetch(
+ 'https://www.googleapis.com/oauth2/v2/userinfo',
+ {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ );
+
+ if (response.ok) {
+ const user = (await response.json()) as UserInfo;
+
+ return {
+ id: user.id,
+ avatarUrl: user.picture,
+ email: user.email,
+ };
+ } else {
+ throw new Error(
+ `Server responded with non-success code ${
+ response.status
+ } ${await response.text()}`
+ );
+ }
+ } catch (e) {
+ throw new HttpException(
+ `Failed to get user information, err: ${(e as Error).stack}`,
+ HttpStatus.BAD_REQUEST
+ );
+ }
+ }
+}
diff --git a/packages/backend/server/src/plugins/oauth/providers/index.ts b/packages/backend/server/src/plugins/oauth/providers/index.ts
new file mode 100644
index 0000000000..7af95d12d8
--- /dev/null
+++ b/packages/backend/server/src/plugins/oauth/providers/index.ts
@@ -0,0 +1,4 @@
+import { GithubOAuthProvider } from './github';
+import { GoogleOAuthProvider } from './google';
+
+export const OAuthProviders = [GoogleOAuthProvider, GithubOAuthProvider];
diff --git a/packages/backend/server/src/plugins/oauth/register.ts b/packages/backend/server/src/plugins/oauth/register.ts
new file mode 100644
index 0000000000..d6c53c57d2
--- /dev/null
+++ b/packages/backend/server/src/plugins/oauth/register.ts
@@ -0,0 +1,58 @@
+import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
+
+import { Config } from '../../fundamentals';
+import { OAuthProvider } from './providers/def';
+import { OAuthProviderName } from './types';
+
+const PROVIDERS: Map = new Map();
+
+export function registerOAuthProvider(
+ name: OAuthProviderName,
+ provider: OAuthProvider
+) {
+ PROVIDERS.set(name, provider);
+}
+
+@Injectable()
+export class OAuthProviderFactory {
+ get providers() {
+ return PROVIDERS.keys();
+ }
+
+ get(name: OAuthProviderName): OAuthProvider | undefined {
+ return PROVIDERS.get(name);
+ }
+}
+
+export abstract class AutoRegisteredOAuthProvider
+ extends OAuthProvider
+ implements OnModuleInit
+{
+ protected abstract AFFiNEConfig: Config;
+
+ get optionalConfig() {
+ return this.AFFiNEConfig.plugins.oauth?.providers?.[this.provider];
+ }
+
+ get config() {
+ const config = this.optionalConfig;
+
+ if (!config) {
+ throw new Error(
+ `OAuthProvider Config should not be used before registered`
+ );
+ }
+
+ return config;
+ }
+
+ onModuleInit() {
+ const config = this.optionalConfig;
+ if (config && config.clientId && config.clientSecret) {
+ registerOAuthProvider(this.provider, this);
+ new Logger(`OAuthProvider:${this.provider}`).log(
+ 'OAuth provider registered.'
+ );
+ }
+ }
+}
diff --git a/packages/backend/server/src/plugins/oauth/resolver.ts b/packages/backend/server/src/plugins/oauth/resolver.ts
new file mode 100644
index 0000000000..467cc90360
--- /dev/null
+++ b/packages/backend/server/src/plugins/oauth/resolver.ts
@@ -0,0 +1,17 @@
+import { registerEnumType, ResolveField, Resolver } from '@nestjs/graphql';
+
+import { ServerConfigType } from '../../core/config';
+import { OAuthProviderFactory } from './register';
+import { OAuthProviderName } from './types';
+
+registerEnumType(OAuthProviderName, { name: 'OAuthProviderType' });
+
+@Resolver(() => ServerConfigType)
+export class OAuthResolver {
+ constructor(private readonly factory: OAuthProviderFactory) {}
+
+ @ResolveField(() => [OAuthProviderName])
+ oauthProviders() {
+ return this.factory.providers;
+ }
+}
diff --git a/packages/backend/server/src/plugins/oauth/service.ts b/packages/backend/server/src/plugins/oauth/service.ts
new file mode 100644
index 0000000000..d05dc623df
--- /dev/null
+++ b/packages/backend/server/src/plugins/oauth/service.ts
@@ -0,0 +1,39 @@
+import { randomUUID } from 'node:crypto';
+
+import { Injectable } from '@nestjs/common';
+
+import { SessionCache } from '../../fundamentals';
+import { OAuthProviderFactory } from './register';
+import { OAuthProviderName } from './types';
+
+const OAUTH_STATE_KEY = 'OAUTH_STATE';
+
+interface OAuthState {
+ redirectUri: string;
+ provider: OAuthProviderName;
+}
+
+@Injectable()
+export class OAuthService {
+ constructor(
+ private readonly providerFactory: OAuthProviderFactory,
+ private readonly cache: SessionCache
+ ) {}
+
+ async saveOAuthState(state: OAuthState) {
+ const token = randomUUID();
+ await this.cache.set(`${OAUTH_STATE_KEY}:${token}`, state, {
+ ttl: 3600 * 3 * 1000 /* 3 hours */,
+ });
+
+ return token;
+ }
+
+ async getOAuthState(token: string) {
+ return this.cache.get(`${OAUTH_STATE_KEY}:${token}`);
+ }
+
+ availableOAuthProviders() {
+ return this.providerFactory.providers;
+ }
+}
diff --git a/packages/backend/server/src/plugins/oauth/types.ts b/packages/backend/server/src/plugins/oauth/types.ts
new file mode 100644
index 0000000000..6d66a264c3
--- /dev/null
+++ b/packages/backend/server/src/plugins/oauth/types.ts
@@ -0,0 +1,15 @@
+export interface OAuthProviderConfig {
+ clientId: string;
+ clientSecret: string;
+ args?: Record;
+}
+
+export enum OAuthProviderName {
+ Google = 'google',
+ GitHub = 'github',
+}
+
+export interface OAuthConfig {
+ enabled: boolean;
+ providers: Partial<{ [key in OAuthProviderName]: OAuthProviderConfig }>;
+}
diff --git a/packages/backend/server/src/plugins/payment/index.ts b/packages/backend/server/src/plugins/payment/index.ts
index 9a4e33578f..975582a879 100644
--- a/packages/backend/server/src/plugins/payment/index.ts
+++ b/packages/backend/server/src/plugins/payment/index.ts
@@ -1,13 +1,14 @@
import { ServerFeature } from '../../core/config';
import { FeatureModule } from '../../core/features';
-import { OptionalModule } from '../../fundamentals';
+import { Plugin } from '../registry';
import { SubscriptionResolver, UserSubscriptionResolver } from './resolver';
import { ScheduleManager } from './schedule';
import { SubscriptionService } from './service';
import { StripeProvider } from './stripe';
import { StripeWebhook } from './webhook';
-@OptionalModule({
+@Plugin({
+ name: 'payment',
imports: [FeatureModule],
providers: [
ScheduleManager,
diff --git a/packages/backend/server/src/plugins/payment/resolver.ts b/packages/backend/server/src/plugins/payment/resolver.ts
index e171b7a998..e3f5462d99 100644
--- a/packages/backend/server/src/plugins/payment/resolver.ts
+++ b/packages/backend/server/src/plugins/payment/resolver.ts
@@ -21,8 +21,8 @@ import type { User, UserInvoice, UserSubscription } from '@prisma/client';
import { PrismaClient } from '@prisma/client';
import { groupBy } from 'lodash-es';
-import { Auth, CurrentUser, Public } from '../../core/auth';
-import { UserType } from '../../core/users';
+import { CurrentUser, Public } from '../../core/auth';
+import { UserType } from '../../core/user';
import { Config } from '../../fundamentals';
import { decodeLookupKey, SubscriptionService } from './service';
import {
@@ -155,7 +155,6 @@ class CreateCheckoutSessionInput {
idempotencyKey!: string;
}
-@Auth()
@Resolver(() => UserSubscriptionType)
export class SubscriptionResolver {
constructor(
@@ -217,7 +216,7 @@ export class SubscriptionResolver {
description: 'Create a subscription checkout link of stripe',
})
async checkout(
- @CurrentUser() user: User,
+ @CurrentUser() user: CurrentUser,
@Args({ name: 'recurring', type: () => SubscriptionRecurring })
recurring: SubscriptionRecurring,
@Args('idempotencyKey') idempotencyKey: string
@@ -241,7 +240,7 @@ export class SubscriptionResolver {
description: 'Create a subscription checkout link of stripe',
})
async createCheckoutSession(
- @CurrentUser() user: User,
+ @CurrentUser() user: CurrentUser,
@Args({ name: 'input', type: () => CreateCheckoutSessionInput })
input: CreateCheckoutSessionInput
) {
@@ -265,13 +264,13 @@ export class SubscriptionResolver {
@Mutation(() => String, {
description: 'Create a stripe customer portal to manage payment methods',
})
- async createCustomerPortal(@CurrentUser() user: User) {
+ async createCustomerPortal(@CurrentUser() user: CurrentUser) {
return this.service.createCustomerPortal(user.id);
}
@Mutation(() => UserSubscriptionType)
async cancelSubscription(
- @CurrentUser() user: User,
+ @CurrentUser() user: CurrentUser,
@Args('idempotencyKey') idempotencyKey: string
) {
return this.service.cancelSubscription(idempotencyKey, user.id);
@@ -279,7 +278,7 @@ export class SubscriptionResolver {
@Mutation(() => UserSubscriptionType)
async resumeSubscription(
- @CurrentUser() user: User,
+ @CurrentUser() user: CurrentUser,
@Args('idempotencyKey') idempotencyKey: string
) {
return this.service.resumeCanceledSubscription(idempotencyKey, user.id);
@@ -287,7 +286,7 @@ export class SubscriptionResolver {
@Mutation(() => UserSubscriptionType)
async updateSubscriptionRecurring(
- @CurrentUser() user: User,
+ @CurrentUser() user: CurrentUser,
@Args({ name: 'recurring', type: () => SubscriptionRecurring })
recurring: SubscriptionRecurring,
@Args('idempotencyKey') idempotencyKey: string
diff --git a/packages/backend/server/src/plugins/payment/service.ts b/packages/backend/server/src/plugins/payment/service.ts
index 30a1ffb3f8..a23fe51c8d 100644
--- a/packages/backend/server/src/plugins/payment/service.ts
+++ b/packages/backend/server/src/plugins/payment/service.ts
@@ -10,6 +10,7 @@ import type {
import { PrismaClient } from '@prisma/client';
import Stripe from 'stripe';
+import { CurrentUser } from '../../core/auth';
import { FeatureManagementService } from '../../core/features';
import { EventEmitter } from '../../fundamentals';
import { ScheduleManager } from './schedule';
@@ -75,7 +76,7 @@ export class SubscriptionService {
redirectUrl,
idempotencyKey,
}: {
- user: User;
+ user: CurrentUser;
recurring: SubscriptionRecurring;
plan: SubscriptionPlan;
promotionCode?: string | null;
@@ -549,7 +550,7 @@ export class SubscriptionService {
private async getOrCreateCustomer(
idempotencyKey: string,
- user: User
+ user: CurrentUser
): Promise {
const customer = await this.db.userStripeCustomer.findUnique({
where: {
@@ -649,7 +650,7 @@ export class SubscriptionService {
}
private async getAvailableCoupon(
- user: User,
+ user: CurrentUser,
couponType: CouponType
): Promise {
const earlyAccess = await this.features.isEarlyAccessUser(user.email);
diff --git a/packages/backend/server/src/plugins/redis/index.ts b/packages/backend/server/src/plugins/redis/index.ts
index 46b44fe7fd..58d82c4642 100644
--- a/packages/backend/server/src/plugins/redis/index.ts
+++ b/packages/backend/server/src/plugins/redis/index.ts
@@ -2,9 +2,10 @@ import { Global, Provider, Type } from '@nestjs/common';
import { Redis, type RedisOptions } from 'ioredis';
import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';
-import { Cache, OptionalModule, SessionCache } from '../../fundamentals';
+import { Cache, SessionCache } from '../../fundamentals';
import { ThrottlerStorage } from '../../fundamentals/throttler';
import { SocketIoAdapterImpl } from '../../fundamentals/websocket';
+import { Plugin } from '../registry';
import { RedisCache } from './cache';
import {
CacheRedis,
@@ -47,7 +48,8 @@ const socketIoRedisAdapterProvider: Provider = {
};
@Global()
-@OptionalModule({
+@Plugin({
+ name: 'redis',
providers: [CacheRedis, SessionRedis, ThrottlerRedis, SocketIoRedis],
overrides: [
cacheProvider,
diff --git a/packages/backend/server/src/plugins/registry.ts b/packages/backend/server/src/plugins/registry.ts
new file mode 100644
index 0000000000..2eea3728af
--- /dev/null
+++ b/packages/backend/server/src/plugins/registry.ts
@@ -0,0 +1,22 @@
+import { omit } from 'lodash-es';
+
+import { AvailablePlugins } from '../fundamentals/config';
+import { OptionalModule, OptionalModuleMetadata } from '../fundamentals/nestjs';
+
+export const REGISTERED_PLUGINS = new Map();
+
+function register(plugin: AvailablePlugins, module: AFFiNEModule) {
+ REGISTERED_PLUGINS.set(plugin, module);
+}
+
+interface PluginModuleMetadata extends OptionalModuleMetadata {
+ name: AvailablePlugins;
+}
+
+export const Plugin = (options: PluginModuleMetadata) => {
+ return (target: any) => {
+ register(options.name, target);
+
+ return OptionalModule(omit(options, 'name'))(target);
+ };
+};
diff --git a/packages/backend/server/src/plugins/storage/index.ts b/packages/backend/server/src/plugins/storage/index.ts
index 914b68a5b0..7128f79126 100644
--- a/packages/backend/server/src/plugins/storage/index.ts
+++ b/packages/backend/server/src/plugins/storage/index.ts
@@ -1,5 +1,5 @@
-import { OptionalModule } from '../../fundamentals';
import { registerStorageProvider } from '../../fundamentals/storage';
+import { Plugin } from '../registry';
import { R2StorageProvider } from './providers/r2';
import { S3StorageProvider } from './providers/s3';
@@ -18,7 +18,8 @@ registerStorageProvider('aws-s3', (config, bucket) => {
return new S3StorageProvider(config.plugins['aws-s3'], bucket);
});
-@OptionalModule({
+@Plugin({
+ name: 'cloudflare-r2',
requires: [
'plugins.cloudflare-r2.accountId',
'plugins.cloudflare-r2.credentials.accessKeyId',
@@ -28,7 +29,8 @@ registerStorageProvider('aws-s3', (config, bucket) => {
})
export class CloudflareR2Module {}
-@OptionalModule({
+@Plugin({
+ name: 'aws-s3',
requires: [
'plugins.aws-s3.credentials.accessKeyId',
'plugins.aws-s3.credentials.secretAccessKey',
diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql
index 14a1d27036..cfb163f47f 100644
--- a/packages/backend/server/src/schema.gql
+++ b/packages/backend/server/src/schema.gql
@@ -67,14 +67,14 @@ type InviteUserType {
"""User avatar url"""
avatarUrl: String
- """User created date"""
- createdAt: DateTime
+ """User email verified"""
+ createdAt: DateTime @deprecated(reason: "useless")
"""User email"""
email: String
"""User email verified"""
- emailVerified: DateTime
+ emailVerified: Boolean
"""User password has been set"""
hasPassword: Boolean
@@ -111,7 +111,7 @@ type Mutation {
addToEarlyAccess(email: String!): Int!
addWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Int!
cancelSubscription(idempotencyKey: String!): UserSubscription!
- changeEmail(token: String!): UserType!
+ changeEmail(email: String!, token: String!): UserType!
changePassword(newPassword: String!, token: String!): UserType!
"""Create a subscription checkout link of stripe"""
@@ -141,15 +141,17 @@ type Mutation {
revoke(userId: String!, workspaceId: String!): Boolean!
revokePage(pageId: String!, workspaceId: String!): Boolean! @deprecated(reason: "use revokePublicPage")
revokePublicPage(pageId: String!, workspaceId: String!): WorkspacePage!
- sendChangeEmail(callbackUrl: String!, email: String!): Boolean!
- sendChangePasswordEmail(callbackUrl: String!, email: String!): Boolean!
- sendSetPasswordEmail(callbackUrl: String!, email: String!): Boolean!
+ sendChangeEmail(callbackUrl: String!, email: String): Boolean!
+ sendChangePasswordEmail(callbackUrl: String!, email: String): Boolean!
+ sendSetPasswordEmail(callbackUrl: String!, email: String): Boolean!
sendVerifyChangeEmail(callbackUrl: String!, email: String!, token: String!): Boolean!
+ sendVerifyEmail(callbackUrl: String!): Boolean!
setBlob(blob: Upload!, workspaceId: String!): String!
setWorkspaceExperimentalFeature(enable: Boolean!, feature: FeatureType!, workspaceId: String!): Boolean!
sharePage(pageId: String!, workspaceId: String!): Boolean! @deprecated(reason: "renamed to publicPage")
signIn(email: String!, password: String!): UserType!
signUp(email: String!, name: String!, password: String!): UserType!
+ updateProfile(input: UpdateUserInput!): UserType!
updateSubscriptionRecurring(idempotencyKey: String!, recurring: SubscriptionRecurring!): UserSubscription!
"""Update workspace"""
@@ -157,6 +159,12 @@ type Mutation {
"""Upload user avatar"""
uploadAvatar(avatar: Upload!): UserType!
+ verifyEmail(token: String!): Boolean!
+}
+
+enum OAuthProviderType {
+ GitHub
+ Google
}
"""User permission in workspace"""
@@ -239,6 +247,7 @@ type ServerConfigType {
"""server identical name could be shown as badge on user interface"""
name: String!
+ oauthProviders: [OAuthProviderType!]!
"""server type"""
type: ServerDeploymentType!
@@ -253,6 +262,7 @@ enum ServerDeploymentType {
}
enum ServerFeature {
+ OAuth
Payment
}
@@ -288,10 +298,9 @@ enum SubscriptionStatus {
Unpaid
}
-type TokenType {
- refresh: String!
- sessionToken: String
- token: String!
+input UpdateUserInput {
+ """User name"""
+ name: String
}
input UpdateWorkspaceInput {
@@ -356,14 +365,14 @@ type UserType {
"""User avatar url"""
avatarUrl: String
- """User created date"""
- createdAt: DateTime
+ """User email verified"""
+ createdAt: DateTime @deprecated(reason: "useless")
"""User email"""
email: String!
"""User email verified"""
- emailVerified: DateTime
+ emailVerified: Boolean!
"""User password has been set"""
hasPassword: Boolean
@@ -377,7 +386,7 @@ type UserType {
name: String!
quota: UserQuota
subscription: UserSubscription
- token: TokenType!
+ token: tokenType! @deprecated(reason: "use [/api/auth/authorize]")
}
type WorkspaceBlobSizes {
@@ -432,4 +441,10 @@ type WorkspaceType {
"""Shared pages of workspace"""
sharedPages: [String!]! @deprecated(reason: "use WorkspaceType.publicPages")
+}
+
+type tokenType {
+ refresh: String!
+ sessionToken: String
+ token: String!
}
\ No newline at end of file
diff --git a/packages/backend/server/tests/app.e2e.ts b/packages/backend/server/tests/app.e2e.ts
index 6fd112927c..111d661324 100644
--- a/packages/backend/server/tests/app.e2e.ts
+++ b/packages/backend/server/tests/app.e2e.ts
@@ -1,16 +1,8 @@
-import { ok } from 'node:assert';
-import { randomUUID } from 'node:crypto';
-
-import { Transformer } from '@napi-rs/image';
import type { INestApplication } from '@nestjs/common';
-import { hashSync } from '@node-rs/argon2';
-import { PrismaClient, type User } from '@prisma/client';
import ava, { type TestFn } from 'ava';
-import type { Express } from 'express';
import request from 'supertest';
import { AppModule } from '../src/app.module';
-import { FeatureManagementService } from '../src/core/features';
import { createTestingApp } from './utils';
const gql = '/graphql';
@@ -19,43 +11,9 @@ const test = ava as TestFn<{
app: INestApplication;
}>;
-class FakePrisma {
- fakeUser: User = {
- id: randomUUID(),
- name: 'Alex Yang',
- avatarUrl: '',
- email: 'alex.yang@example.org',
- password: hashSync('123456'),
- emailVerified: new Date(),
- createdAt: new Date(),
- };
- get user() {
- // eslint-disable-next-line @typescript-eslint/no-this-alias
- const prisma = this;
- return {
- async findFirst() {
- return prisma.fakeUser;
- },
- async findUnique() {
- return this.findFirst();
- },
- async update() {
- return this.findFirst();
- },
- };
- }
-}
-
test.beforeEach(async t => {
const { app } = await createTestingApp({
imports: [AppModule],
- tapModule(builder) {
- builder
- .overrideProvider(PrismaClient)
- .useClass(FakePrisma)
- .overrideProvider(FeatureManagementService)
- .useValue({ canEarlyAccess: () => true });
- },
});
t.context.app = app;
@@ -66,7 +24,6 @@ test.afterEach.always(async t => {
});
test('should init app', async t => {
- t.is(typeof t.context.app, 'object');
await request(t.context.app.getHttpServer())
.post(gql)
.send({
@@ -78,130 +35,22 @@ test('should init app', async t => {
})
.expect(400);
- const { token } = await createToken(t.context.app);
-
- await request(t.context.app.getHttpServer())
+ const response = await request(t.context.app.getHttpServer())
.post(gql)
- .auth(token, { type: 'bearer' })
.send({
- query: `
- query {
- __typename
- }
- `,
- })
- .expect(200)
- .expect(res => {
- t.is(res.body.data.__typename, 'Query');
- });
-});
-
-test('should find default user', async t => {
- const { token } = await createToken(t.context.app);
- await request(t.context.app.getHttpServer())
- .post(gql)
- .auth(token, { type: 'bearer' })
- .send({
- query: `
- query {
- user(email: "alex.yang@example.org") {
- ... on UserType {
- email
- }
- ... on LimitedUserType {
- email
- }
+ query: `query {
+ serverConfig {
+ name
+ version
+ type
+ features
}
- }
- `,
+ }`,
})
- .expect(200)
- .expect(res => {
- t.is(res.body.data.user.email, 'alex.yang@example.org');
- });
+ .expect(200);
+
+ const config = response.body.data.serverConfig;
+
+ t.is(config.type, 'Affine');
+ t.true(Array.isArray(config.features));
});
-
-test('should be able to upload avatar and remove it', async t => {
- const { token, id } = await createToken(t.context.app);
- const png = await Transformer.fromRgbaPixels(
- Buffer.alloc(400 * 400 * 4).fill(255),
- 400,
- 400
- ).png();
-
- await request(t.context.app.getHttpServer())
- .post(gql)
- .auth(token, { type: 'bearer' })
- .field(
- 'operations',
- JSON.stringify({
- name: 'uploadAvatar',
- query: `mutation uploadAvatar($avatar: Upload!) {
- uploadAvatar(avatar: $avatar) {
- id
- name
- avatarUrl
- email
- }
- }
- `,
- variables: { id, avatar: null },
- })
- )
- .field('map', JSON.stringify({ '0': ['variables.avatar'] }))
- .attach('0', png, 'avatar.png')
- .expect(200)
- .expect(res => {
- t.is(res.body.data.uploadAvatar.id, id);
- });
-
- await request(t.context.app.getHttpServer())
- .post(gql)
- .auth(token, { type: 'bearer' })
- .set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
- .send({
- query: `
- mutation removeAvatar {
- removeAvatar {
- success
- }
- }
- `,
- })
- .expect(200)
- .expect(res => {
- t.is(res.body.data.removeAvatar.success, true);
- });
-});
-
-async function createToken(app: INestApplication): Promise<{
- id: string;
- token: string;
-}> {
- let token;
- let id;
- await request(app.getHttpServer())
- .post(gql)
- .send({
- query: `
- mutation {
- signIn(email: "alex.yang@example.org", password: "123456") {
- id
- token {
- token
- }
- }
- }
- `,
- })
- .expect(200)
- .expect(res => {
- id = res.body.data.signIn.id;
- ok(
- typeof res.body.data.signIn.token.token === 'string',
- 'res.body.data.signIn.token.token is not a string'
- );
- token = res.body.data.signIn.token.token;
- });
- return { token: token!, id: id! };
-}
diff --git a/packages/backend/server/tests/auth.e2e.ts b/packages/backend/server/tests/auth.e2e.ts
index 4300928b6a..a6b4db7c29 100644
--- a/packages/backend/server/tests/auth.e2e.ts
+++ b/packages/backend/server/tests/auth.e2e.ts
@@ -39,7 +39,7 @@ test('change email', async t => {
if (mail.hasConfigured()) {
const u1Email = 'u1@affine.pro';
const u2Email = 'u2@affine.pro';
- const tokenRegex = /token=3D([^"&\s]+)/;
+ const tokenRegex = /token=3D([^"&]+)/;
const u1 = await signUp(app, 'u1', u1Email, '1');
@@ -57,7 +57,7 @@ test('change email', async t => {
const changeTokenMatch = changeEmailContent.Content.Body.match(tokenRegex);
const changeEmailToken = changeTokenMatch
- ? decodeURIComponent(changeTokenMatch[1].replace(/=3D/g, '='))
+ ? decodeURIComponent(changeTokenMatch[1].replace(/=\r\n/, ''))
: null;
t.not(
@@ -85,7 +85,7 @@ test('change email', async t => {
const verifyTokenMatch = verifyEmailContent.Content.Body.match(tokenRegex);
const verifyEmailToken = verifyTokenMatch
- ? decodeURIComponent(verifyTokenMatch[1].replace(/=3D/g, '='))
+ ? decodeURIComponent(verifyTokenMatch[1].replace(/=\r\n/, ''))
: null;
t.not(
@@ -94,7 +94,7 @@ test('change email', async t => {
'fail to get verify change email token from email content'
);
- await changeEmail(app, u1.token.token, verifyEmailToken as string);
+ await changeEmail(app, u1.token.token, verifyEmailToken as string, u2Email);
const afterNotificationMailCount = await getCurrentMailMessageCount();
diff --git a/packages/backend/server/tests/auth.spec.ts b/packages/backend/server/tests/auth.spec.ts
deleted file mode 100644
index f5719ba5d2..0000000000
--- a/packages/backend/server/tests/auth.spec.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-///
-import { TestingModule } from '@nestjs/testing';
-import test from 'ava';
-
-import { AuthResolver } from '../src/core/auth/resolver';
-import { AuthService } from '../src/core/auth/service';
-import { ConfigModule } from '../src/fundamentals/config';
-import {
- mintChallengeResponse,
- verifyChallengeResponse,
-} from '../src/fundamentals/storage';
-import { createTestingModule } from './utils';
-
-let authService: AuthService;
-let authResolver: AuthResolver;
-let module: TestingModule;
-
-test.beforeEach(async () => {
- module = await createTestingModule({
- imports: [
- ConfigModule.forRoot({
- auth: {
- accessTokenExpiresIn: 1,
- refreshTokenExpiresIn: 1,
- leeway: 1,
- },
- host: 'example.org',
- https: true,
- }),
- ],
- });
-
- authService = module.get(AuthService);
- authResolver = module.get(AuthResolver);
-});
-
-test.afterEach.always(async () => {
- await module.close();
-});
-
-test('should be able to register and signIn', async t => {
- await authService.signUp('Alex Yang', 'alexyang@example.org', '123456');
- await authService.signIn('alexyang@example.org', '123456');
- t.pass();
-});
-
-test('should be able to verify', async t => {
- await authService.signUp('Alex Yang', 'alexyang@example.org', '123456');
- await authService.signIn('alexyang@example.org', '123456');
- const date = new Date();
-
- const user = {
- id: '1',
- name: 'Alex Yang',
- email: 'alexyang@example.org',
- emailVerified: date,
- createdAt: date,
- avatarUrl: '',
- };
- {
- const token = await authService.sign(user);
- const claim = await authService.verify(token);
- t.is(claim.id, '1');
- t.is(claim.name, 'Alex Yang');
- t.is(claim.email, 'alexyang@example.org');
- t.is(claim.emailVerified?.toISOString(), date.toISOString());
- t.is(claim.createdAt.toISOString(), date.toISOString());
- }
- {
- const token = await authService.refresh(user);
- const claim = await authService.verify(token);
- t.is(claim.id, '1');
- t.is(claim.name, 'Alex Yang');
- t.is(claim.email, 'alexyang@example.org');
- t.is(claim.emailVerified?.toISOString(), date.toISOString());
- t.is(claim.createdAt.toISOString(), date.toISOString());
- }
-});
-
-test('should not be able to return token if user is invalid', async t => {
- const date = new Date();
- const user = {
- id: '1',
- name: 'Alex Yang',
- email: 'alexyang@example.org',
- emailVerified: date,
- createdAt: date,
- avatarUrl: '',
- };
- const anotherUser = {
- id: '2',
- name: 'Alex Yang 2',
- email: 'alexyang@example.org',
- emailVerified: date,
- createdAt: date,
- avatarUrl: '',
- };
- await t.throwsAsync(
- authResolver.token(
- {
- req: {
- headers: {
- referer: 'https://example.org',
- host: 'example.org',
- },
- } as any,
- },
- user,
- anotherUser
- ),
- {
- message: 'Invalid user',
- }
- );
-});
-
-test('should not return sessionToken if request headers is invalid', async t => {
- const date = new Date();
- const user = {
- id: '1',
- name: 'Alex Yang',
- email: 'alexyang@example.org',
- emailVerified: date,
- createdAt: date,
- avatarUrl: '',
- };
- const result = await authResolver.token(
- {
- req: {
- headers: {},
- } as any,
- },
- user,
- user
- );
- t.is(result.sessionToken, undefined);
-});
-
-test('should return valid sessionToken if request headers valid', async t => {
- const date = new Date();
- const user = {
- id: '1',
- name: 'Alex Yang',
- email: 'alexyang@example.org',
- emailVerified: date,
- createdAt: date,
- avatarUrl: '',
- };
- const result = await authResolver.token(
- {
- req: {
- headers: {
- referer: 'https://example.org/open-app/test',
- host: 'example.org',
- },
- cookies: {
- 'next-auth.session-token': '123456',
- },
- } as any,
- },
- user,
- user
- );
- t.is(result.sessionToken, '123456');
-});
-
-test('verify challenge', async t => {
- const resource = 'xp8D3rcXV9bMhWrb6abxl';
- const response = await mintChallengeResponse(resource, 20);
- const success = await verifyChallengeResponse(response, 20, resource);
- t.true(success);
-});
diff --git a/packages/backend/server/tests/feature.spec.ts b/packages/backend/server/tests/feature.spec.ts
index 60be45e8bb..fc59bed01f 100644
--- a/packages/backend/server/tests/feature.spec.ts
+++ b/packages/backend/server/tests/feature.spec.ts
@@ -11,7 +11,7 @@ import {
FeatureService,
FeatureType,
} from '../src/core/features';
-import { UserType } from '../src/core/users/types';
+import { UserType } from '../src/core/user/types';
import { WorkspaceResolver } from '../src/core/workspaces/resolvers';
import { Permission } from '../src/core/workspaces/types';
import { ConfigModule } from '../src/fundamentals/config';
@@ -54,11 +54,6 @@ test.beforeEach(async t => {
const { app } = await createTestingApp({
imports: [
ConfigModule.forRoot({
- auth: {
- accessTokenExpiresIn: 1,
- refreshTokenExpiresIn: 1,
- leeway: 1,
- },
host: 'example.org',
https: true,
featureFlags: {
diff --git a/packages/backend/server/tests/mailer.e2e.ts b/packages/backend/server/tests/mailer.e2e.ts
index 8197c8befa..ecbf0c0e85 100644
--- a/packages/backend/server/tests/mailer.e2e.ts
+++ b/packages/backend/server/tests/mailer.e2e.ts
@@ -21,15 +21,7 @@ const test = ava as TestFn<{
test.beforeEach(async t => {
t.context.module = await createTestingModule({
- imports: [
- ConfigModule.forRoot({
- auth: {
- accessTokenExpiresIn: 1,
- refreshTokenExpiresIn: 1,
- leeway: 1,
- },
- }),
- ],
+ imports: [ConfigModule.forRoot({})],
});
t.context.auth = t.context.module.get(AuthService);
});
diff --git a/packages/backend/server/tests/session.spec.ts b/packages/backend/server/tests/session.spec.ts
deleted file mode 100644
index 7e668317b5..0000000000
--- a/packages/backend/server/tests/session.spec.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-///
-
-import { TestingModule } from '@nestjs/testing';
-import ava, { type TestFn } from 'ava';
-
-import { CacheModule } from '../src/fundamentals/cache';
-import { SessionModule, SessionService } from '../src/fundamentals/session';
-import { createTestingModule } from './utils';
-
-const test = ava as TestFn<{
- session: SessionService;
- module: TestingModule;
-}>;
-
-test.beforeEach(async t => {
- const module = await createTestingModule({
- imports: [CacheModule, SessionModule],
- });
- const session = module.get(SessionService);
- t.context.module = module;
- t.context.session = session;
-});
-
-test.afterEach.always(async t => {
- await t.context.module.close();
-});
-
-test('should be able to set session', async t => {
- const { session } = t.context;
- await session.set('test', 'value');
- t.is(await session.get('test'), 'value');
-});
-
-test('should be expired by ttl', async t => {
- const { session } = t.context;
- await session.set('test', 'value', 100);
- t.is(await session.get('test'), 'value');
- await new Promise(resolve => setTimeout(resolve, 500));
- t.is(await session.get('test'), undefined);
-});
diff --git a/packages/backend/server/tests/utils/user.ts b/packages/backend/server/tests/utils/user.ts
index 3ead722ffd..f57dce3e39 100644
--- a/packages/backend/server/tests/utils/user.ts
+++ b/packages/backend/server/tests/utils/user.ts
@@ -1,16 +1,18 @@
import type { INestApplication } from '@nestjs/common';
+import { PrismaClient } from '@prisma/client';
import request from 'supertest';
-import type { TokenType } from '../../src/core/auth';
-import type { UserType } from '../../src/core/users';
+import type { ClientTokenType } from '../../src/core/auth';
+import type { UserType } from '../../src/core/user';
import { gql } from './common';
export async function signUp(
app: INestApplication,
name: string,
email: string,
- password: string
-): Promise {
+ password: string,
+ autoVerifyEmail = true
+): Promise {
const res = await request(app.getHttpServer())
.post(gql)
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
@@ -24,9 +26,23 @@ export async function signUp(
`,
})
.expect(200);
+
+ if (autoVerifyEmail) {
+ await setEmailVerified(app, email);
+ }
+
return res.body.data.signUp;
}
+async function setEmailVerified(app: INestApplication, email: string) {
+ await app.get(PrismaClient).user.update({
+ where: { email },
+ data: {
+ emailVerifiedAt: new Date(),
+ },
+ });
+}
+
export async function currentUser(app: INestApplication, token: string) {
const res = await request(app.getHttpServer())
.post(gql)
@@ -36,7 +52,7 @@ export async function currentUser(app: INestApplication, token: string) {
query: `
query {
currentUser {
- id, name, email, emailVerified, avatarUrl, createdAt, hasPassword,
+ id, name, email, emailVerified, avatarUrl, hasPassword,
token { token }
}
}
@@ -94,8 +110,9 @@ export async function sendVerifyChangeEmail(
export async function changeEmail(
app: INestApplication,
userToken: string,
- token: string
-): Promise {
+ token: string,
+ email: string
+): Promise {
const res = await request(app.getHttpServer())
.post(gql)
.auth(userToken, { type: 'bearer' })
@@ -103,7 +120,7 @@ export async function changeEmail(
.send({
query: `
mutation {
- changeEmail(token: "${token}") {
+ changeEmail(token: "${token}", email: "${email}") {
id
name
avatarUrl
diff --git a/packages/backend/server/tests/utils/utils.ts b/packages/backend/server/tests/utils/utils.ts
index 00cec6b3bd..8d3c32bf55 100644
--- a/packages/backend/server/tests/utils/utils.ts
+++ b/packages/backend/server/tests/utils/utils.ts
@@ -1,11 +1,13 @@
import { INestApplication, ModuleMetadata } from '@nestjs/common';
+import { APP_GUARD } from '@nestjs/core';
import { Query, Resolver } from '@nestjs/graphql';
import { Test, TestingModuleBuilder } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
+import cookieParser from 'cookie-parser';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import { AppModule, FunctionalityModules } from '../../src/app.module';
-import { AuthModule } from '../../src/core/auth';
+import { AuthGuard, AuthModule } from '../../src/core/auth';
import { UserFeaturesInit1698652531198 } from '../../src/data/migrations/1698652531198-user-features-init';
import { GqlModule } from '../../src/fundamentals/graphql';
@@ -78,7 +80,14 @@ export async function createTestingModule(
const builder = Test.createTestingModule({
imports,
- providers: [MockResolver, ...(moduleDef.providers ?? [])],
+ providers: [
+ {
+ provide: APP_GUARD,
+ useClass: AuthGuard,
+ },
+ MockResolver,
+ ...(moduleDef.providers ?? []),
+ ],
controllers: moduleDef.controllers,
});
@@ -113,6 +122,8 @@ export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) {
})
);
+ app.use(cookieParser());
+
if (moduleDef.tapApp) {
moduleDef.tapApp(app);
}
diff --git a/packages/frontend/component/src/components/auth-components/email-verified-email.tsx b/packages/frontend/component/src/components/auth-components/email-verified-email.tsx
new file mode 100644
index 0000000000..bd49495333
--- /dev/null
+++ b/packages/frontend/component/src/components/auth-components/email-verified-email.tsx
@@ -0,0 +1,22 @@
+import { useAFFiNEI18N } from '@affine/i18n/hooks';
+import type { FC } from 'react';
+
+import { Button } from '../../ui/button';
+import { AuthPageContainer } from './auth-page-container';
+
+export const ConfirmChangeEmail: FC<{
+ onOpenAffine: () => void;
+}> = ({ onOpenAffine }) => {
+ const t = useAFFiNEI18N();
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/frontend/component/src/components/auth-components/onboarding-page.tsx b/packages/frontend/component/src/components/auth-components/onboarding-page.tsx
index 96fe6a8c29..05504f6a7c 100644
--- a/packages/frontend/component/src/components/auth-components/onboarding-page.tsx
+++ b/packages/frontend/component/src/components/auth-components/onboarding-page.tsx
@@ -37,7 +37,7 @@ function getCallbackUrl(location: Location) {
try {
const url =
location.state?.callbackURL ||
- new URLSearchParams(location.search).get('callbackUrl');
+ new URLSearchParams(location.search).get('redirect_uri');
if (typeof url === 'string' && url) {
if (!url.startsWith('http:') && !url.startsWith('https:')) {
return url;
diff --git a/packages/frontend/component/src/components/auth-components/type.ts b/packages/frontend/component/src/components/auth-components/type.ts
index d297b4bb4b..819bc607c0 100644
--- a/packages/frontend/component/src/components/auth-components/type.ts
+++ b/packages/frontend/component/src/components/auth-components/type.ts
@@ -3,4 +3,5 @@ export interface User {
name: string;
email: string;
image?: string | null;
+ avatarUrl: string | null;
}
diff --git a/packages/frontend/component/src/components/not-found-page/not-found-page.tsx b/packages/frontend/component/src/components/not-found-page/not-found-page.tsx
index aad290e815..6506b64be9 100644
--- a/packages/frontend/component/src/components/not-found-page/not-found-page.tsx
+++ b/packages/frontend/component/src/components/not-found-page/not-found-page.tsx
@@ -4,6 +4,7 @@ import { SignOutIcon } from '@blocksuite/icons';
import { Avatar } from '../../ui/avatar';
import { Button, IconButton } from '../../ui/button';
import { Tooltip } from '../../ui/tooltip';
+import type { User } from '../auth-components';
import { NotFoundPattern } from './not-found-pattern';
import {
largeButtonEffect,
@@ -12,11 +13,7 @@ import {
} from './styles.css';
export interface NotFoundPageProps {
- user: {
- name: string;
- email: string;
- avatar: string;
- } | null;
+ user?: User | null;
onBack: () => void;
onSignOut: () => void;
}
@@ -47,7 +44,7 @@ export const NotFoundPage = ({
{user ? (
-
+
{user.email}
diff --git a/packages/frontend/core/.webpack/config.ts b/packages/frontend/core/.webpack/config.ts
index af384252a6..4432eb69b5 100644
--- a/packages/frontend/core/.webpack/config.ts
+++ b/packages/frontend/core/.webpack/config.ts
@@ -384,6 +384,7 @@ export const createConfiguration: (
{ context: '/api', target: 'http://localhost:3010' },
{ context: '/socket.io', target: 'http://localhost:3010', ws: true },
{ context: '/graphql', target: 'http://localhost:3010' },
+ { context: '/oauth', target: 'http://localhost:3010' },
],
} as DevServerConfiguration,
} satisfies webpack.Configuration;
diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json
index 9ab8811791..9281dfd823 100644
--- a/packages/frontend/core/package.json
+++ b/packages/frontend/core/package.json
@@ -78,7 +78,6 @@
"lottie-web": "^5.12.2",
"mini-css-extract-plugin": "^2.8.0",
"nanoid": "^5.0.6",
- "next-auth": "^4.24.5",
"next-themes": "^0.2.1",
"postcss-loader": "^8.1.0",
"react": "18.2.0",
diff --git a/packages/frontend/core/src/atoms/cloud-user.ts b/packages/frontend/core/src/atoms/cloud-user.ts
deleted file mode 100644
index 2b43c0cf25..0000000000
--- a/packages/frontend/core/src/atoms/cloud-user.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { atom } from 'jotai';
-import type { SessionContextValue } from 'next-auth/react';
-
-export const sessionAtom = atom | null>(null);
diff --git a/packages/frontend/core/src/components/affine/auth/index.tsx b/packages/frontend/core/src/components/affine/auth/index.tsx
index 7a6660e285..fe86068d03 100644
--- a/packages/frontend/core/src/components/affine/auth/index.tsx
+++ b/packages/frontend/core/src/components/affine/auth/index.tsx
@@ -24,7 +24,7 @@ export type AuthProps = {
setAuthEmail: (state: AuthProps['email']) => void;
setEmailType: (state: AuthProps['emailType']) => void;
email: string;
- emailType: 'setPassword' | 'changePassword' | 'changeEmail';
+ emailType: 'setPassword' | 'changePassword' | 'changeEmail' | 'verifyEmail';
onSignedIn?: () => void;
};
@@ -59,8 +59,10 @@ export const AuthModal: FC = ({
emailType,
}) => {
const onSignedIn = useCallback(() => {
+ setAuthState('signIn');
+ setAuthEmail('');
setOpen(false);
- }, [setOpen]);
+ }, [setAuthState, setAuthEmail, setOpen]);
return (
diff --git a/packages/frontend/core/src/components/affine/auth/oauth.tsx b/packages/frontend/core/src/components/affine/auth/oauth.tsx
new file mode 100644
index 0000000000..2747383a9d
--- /dev/null
+++ b/packages/frontend/core/src/components/affine/auth/oauth.tsx
@@ -0,0 +1,66 @@
+import { Button } from '@affine/component/ui/button';
+import {
+ useOAuthProviders,
+ useServerFeatures,
+} from '@affine/core/hooks/affine/use-server-config';
+import { OAuthProviderType } from '@affine/graphql';
+import { GithubIcon, GoogleDuotoneIcon } from '@blocksuite/icons';
+import { type ReactElement, useCallback } from 'react';
+
+import { useAuth } from './use-auth';
+
+const OAuthProviderMap: Record<
+ OAuthProviderType,
+ {
+ icon: ReactElement;
+ }
+> = {
+ [OAuthProviderType.Google]: {
+ icon: ,
+ },
+
+ [OAuthProviderType.GitHub]: {
+ icon: ,
+ },
+};
+
+export function OAuth() {
+ const { oauth } = useServerFeatures();
+
+ if (!oauth) {
+ return null;
+ }
+
+ return ;
+}
+
+function OAuthProviders() {
+ const providers = useOAuthProviders();
+
+ return providers.map(provider => (
+
+ ));
+}
+
+function OAuthProvider({ provider }: { provider: OAuthProviderType }) {
+ const { icon } = OAuthProviderMap[provider];
+ const { oauthSignIn } = useAuth();
+
+ const onClick = useCallback(() => {
+ oauthSignIn(provider);
+ }, [provider, oauthSignIn]);
+
+ return (
+
+ );
+}
diff --git a/packages/frontend/core/src/components/affine/auth/send-email.tsx b/packages/frontend/core/src/components/affine/auth/send-email.tsx
index 91b3a6330b..8e767c22dc 100644
--- a/packages/frontend/core/src/components/affine/auth/send-email.tsx
+++ b/packages/frontend/core/src/components/affine/auth/send-email.tsx
@@ -12,6 +12,7 @@ import {
sendChangeEmailMutation,
sendChangePasswordEmailMutation,
sendSetPasswordEmailMutation,
+ sendVerifyEmailMutation,
} from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useSetAtom } from 'jotai/react';
@@ -29,7 +30,9 @@ const useEmailTitle = (emailType: AuthPanelProps['emailType']) => {
case 'changePassword':
return t['com.affine.auth.reset.password']();
case 'changeEmail':
- return t['com.affine.settings.email.action']();
+ return t['com.affine.settings.email.action.change']();
+ case 'verifyEmail':
+ return t['com.affine.settings.email.action.verify']();
}
};
const useContent = (emailType: AuthPanelProps['emailType'], email: string) => {
@@ -41,7 +44,8 @@ const useContent = (emailType: AuthPanelProps['emailType'], email: string) => {
case 'changePassword':
return t['com.affine.auth.reset.password.message']();
case 'changeEmail':
- return t['com.affine.auth.change.email.message']({
+ case 'verifyEmail':
+ return t['com.affine.auth.verify.email.message']({
email,
});
}
@@ -56,7 +60,8 @@ const useNotificationHint = (emailType: AuthPanelProps['emailType']) => {
case 'changePassword':
return t['com.affine.auth.sent.change.password.hint']();
case 'changeEmail':
- return t['com.affine.auth.sent.change.email.hint']();
+ case 'verifyEmail':
+ return t['com.affine.auth.sent.verify.email.hint']();
}
};
const useButtonContent = (emailType: AuthPanelProps['emailType']) => {
@@ -68,7 +73,8 @@ const useButtonContent = (emailType: AuthPanelProps['emailType']) => {
case 'changePassword':
return t['com.affine.auth.send.reset.password.link']();
case 'changeEmail':
- return t['com.affine.auth.send.change.email.link']();
+ case 'verifyEmail':
+ return t['com.affine.auth.send.verify.email.hint']();
}
};
@@ -87,12 +93,17 @@ const useSendEmail = (emailType: AuthPanelProps['emailType']) => {
useMutation({
mutation: sendChangeEmailMutation,
});
+ const { trigger: sendVerifyEmail, isMutating: isVerifyEmailMutation } =
+ useMutation({
+ mutation: sendVerifyEmailMutation,
+ });
return {
loading:
isChangePasswordMutating ||
isSetPasswordMutating ||
- isChangeEmailMutating,
+ isChangeEmailMutating ||
+ isVerifyEmailMutation,
sendEmail: useCallback(
(email: string) => {
let trigger: (args: {
@@ -113,6 +124,10 @@ const useSendEmail = (emailType: AuthPanelProps['emailType']) => {
trigger = sendChangeEmail;
callbackUrl = 'changeEmail';
break;
+ case 'verifyEmail':
+ trigger = sendVerifyEmail;
+ callbackUrl = 'verify-email';
+ break;
}
// TODO: add error handler
return trigger({
@@ -127,6 +142,7 @@ const useSendEmail = (emailType: AuthPanelProps['emailType']) => {
sendChangeEmail,
sendChangePasswordEmail,
sendSetPasswordEmail,
+ sendVerifyEmail,
]
),
};
diff --git a/packages/frontend/core/src/components/affine/auth/sign-in-with-password.tsx b/packages/frontend/core/src/components/affine/auth/sign-in-with-password.tsx
index bb649bd9d8..e5e6ff8515 100644
--- a/packages/frontend/core/src/components/affine/auth/sign-in-with-password.tsx
+++ b/packages/frontend/core/src/components/affine/auth/sign-in-with-password.tsx
@@ -5,10 +5,9 @@ import {
ModalHeader,
} from '@affine/component/auth-components';
import { Button } from '@affine/component/ui/button';
+import { useSession } from '@affine/core/hooks/affine/use-current-user';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
-// eslint-disable-next-line @typescript-eslint/no-restricted-imports
-import { useSession } from 'next-auth/react';
import type { FC } from 'react';
import { useCallback, useState } from 'react';
@@ -25,7 +24,7 @@ export const SignInWithPassword: FC = ({
onSignedIn,
}) => {
const t = useAFFiNEI18N();
- const { update } = useSession();
+ const { reload } = useSession();
const [password, setPassword] = useState('');
const [passwordError, setPasswordError] = useState(false);
@@ -39,7 +38,6 @@ export const SignInWithPassword: FC = ({
const onSignIn = useAsyncCallback(async () => {
const res = await signInCloud('credentials', {
- redirect: false,
email,
password,
}).catch(console.error);
@@ -48,9 +46,9 @@ export const SignInWithPassword: FC = ({
return setPasswordError(true);
}
- await update();
+ await reload();
onSignedIn?.();
- }, [email, password, onSignedIn, update]);
+ }, [email, password, onSignedIn, reload]);
const sendMagicLink = useAsyncCallback(async () => {
if (allowSendEmail && verifyToken && !sendingEmail) {
diff --git a/packages/frontend/core/src/components/affine/auth/sign-in.tsx b/packages/frontend/core/src/components/affine/auth/sign-in.tsx
index b53bcecc81..0d1a7a6a2f 100644
--- a/packages/frontend/core/src/components/affine/auth/sign-in.tsx
+++ b/packages/frontend/core/src/components/affine/auth/sign-in.tsx
@@ -12,7 +12,7 @@ import {
} from '@affine/graphql';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
-import { ArrowDownBigIcon, GoogleDuotoneIcon } from '@blocksuite/icons';
+import { ArrowDownBigIcon } from '@blocksuite/icons';
import { type FC, useState } from 'react';
import { useCallback } from 'react';
@@ -20,6 +20,7 @@ import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-s
import { useMutation } from '../../../hooks/use-mutation';
import { emailRegex } from '../../../utils/email-regex';
import type { AuthPanelProps } from './index';
+import { OAuth } from './oauth';
import * as style from './style.css';
import { INTERNAL_BETA_URL, useAuth } from './use-auth';
import { Captcha, useCaptcha } from './use-captcha';
@@ -46,7 +47,6 @@ export const SignIn: FC = ({
allowSendEmail,
signIn,
signUp,
- signInWithGoogle,
} = useAuth();
const { trigger: verifyUser, isMutating } = useMutation({
@@ -59,6 +59,10 @@ export const SignIn: FC = ({
}
const onContinue = useAsyncCallback(async () => {
+ if (!allowSendEmail) {
+ return;
+ }
+
if (!validateEmail(email)) {
setIsValidEmail(false);
return;
@@ -99,13 +103,14 @@ export const SignIn: FC = ({
const res = await signUp(email, verifyToken, challenge);
if (res?.status === 403 && res?.url === INTERNAL_BETA_URL) {
return setAuthState('noAccess');
- } else if (!res || res.status >= 400 || res.error) {
+ } else if (!res || res.status >= 400) {
return;
}
setAuthState('afterSignUpSendEmail');
}
}
}, [
+ allowSendEmail,
subscriptionData,
challenge,
email,
@@ -124,20 +129,7 @@ export const SignIn: FC = ({
subTitle={t['com.affine.brand.affineCloud']()}
/>
- }
- onClick={useCallback(() => {
- signInWithGoogle();
- }, [signInWithGoogle])}
- >
- {t['Continue with Google']()}
-
+
void
) {
- if (res?.error) {
+ if (!res?.ok) {
pushNotification({
title: 'Send email error',
message: 'Please back to home and try again',
@@ -64,8 +64,13 @@ export const useAuth = () => {
const [authStore, setAuthStore] = useAtom(authStoreAtom);
const startResendCountDown = useSetAtom(countDownAtom);
- const signIn = useCallback(
- async (email: string, verifyToken: string, challenge?: string) => {
+ const sendEmailMagicLink = useCallback(
+ async (
+ signUp: boolean,
+ email: string,
+ verifyToken: string,
+ challenge?: string
+ ) => {
setAuthStore(prev => {
return {
...prev,
@@ -76,18 +81,19 @@ export const useAuth = () => {
const res = await signInCloud(
'email',
{
- email: email,
- callbackUrl: subscriptionData
- ? subscriptionData.getRedirectUrl(false)
- : '/auth/signIn',
- redirect: false,
+ email,
},
- challenge
- ? {
- challenge,
- token: verifyToken,
- }
- : { token: verifyToken }
+ {
+ ...(challenge
+ ? {
+ challenge,
+ token: verifyToken,
+ }
+ : { token: verifyToken }),
+ callbackUrl: subscriptionData
+ ? subscriptionData.getRedirectUrl(signUp)
+ : '/auth/signIn',
+ }
).catch(console.error);
handleSendEmailError(res, pushNotification);
@@ -107,47 +113,24 @@ export const useAuth = () => {
const signUp = useCallback(
async (email: string, verifyToken: string, challenge?: string) => {
- setAuthStore(prev => {
- return {
- ...prev,
- isMutating: true,
- };
- });
-
- const res = await signInCloud(
- 'email',
- {
- email: email,
- callbackUrl: subscriptionData
- ? subscriptionData.getRedirectUrl(true)
- : '/auth/signUp',
- redirect: false,
- },
- challenge
- ? {
- challenge,
- token: verifyToken,
- }
- : { token: verifyToken }
- ).catch(console.error);
-
- handleSendEmailError(res, pushNotification);
-
- setAuthStore({
- isMutating: false,
- allowSendEmail: false,
- resendCountDown: COUNT_DOWN_TIME,
- });
-
- startResendCountDown();
-
- return res;
+ return sendEmailMagicLink(true, email, verifyToken, challenge).catch(
+ console.error
+ );
},
- [pushNotification, setAuthStore, startResendCountDown, subscriptionData]
+ [sendEmailMagicLink]
);
- const signInWithGoogle = useCallback(() => {
- signInCloud('google').catch(console.error);
+ const signIn = useCallback(
+ async (email: string, verifyToken: string, challenge?: string) => {
+ return sendEmailMagicLink(false, email, verifyToken, challenge).catch(
+ console.error
+ );
+ },
+ [sendEmailMagicLink]
+ );
+
+ const oauthSignIn = useCallback((provider: OAuthProviderType) => {
+ signInCloud(provider).catch(console.error);
}, []);
const resetCountDown = useCallback(() => {
@@ -165,6 +148,6 @@ export const useAuth = () => {
isMutating: authStore.isMutating,
signUp,
signIn,
- signInWithGoogle,
+ oauthSignIn,
};
};
diff --git a/packages/frontend/core/src/components/affine/awareness/index.tsx b/packages/frontend/core/src/components/affine/awareness/index.tsx
index f736203343..ac46150ca8 100644
--- a/packages/frontend/core/src/components/affine/awareness/index.tsx
+++ b/packages/frontend/core/src/components/affine/awareness/index.tsx
@@ -3,21 +3,21 @@ import { useLiveData } from '@toeverything/infra/livedata';
import { Suspense, useEffect } from 'react';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
-import { useCurrentUser } from '../../../hooks/affine/use-current-user';
+import { useSession } from '../../../hooks/affine/use-current-user';
import { CurrentWorkspaceService } from '../../../modules/workspace/current-workspace';
const SyncAwarenessInnerLoggedIn = () => {
- const currentUser = useCurrentUser();
+ const { user } = useSession();
const currentWorkspace = useLiveData(
useService(CurrentWorkspaceService).currentWorkspace
);
useEffect(() => {
- if (currentUser && currentWorkspace) {
+ if (user && currentWorkspace) {
currentWorkspace.blockSuiteWorkspace.awarenessStore.awareness.setLocalStateField(
'user',
{
- name: currentUser.name,
+ name: user.name,
// todo: add avatar?
}
);
@@ -30,7 +30,7 @@ const SyncAwarenessInnerLoggedIn = () => {
};
}
return;
- }, [currentUser, currentWorkspace]);
+ }, [user, currentWorkspace]);
return null;
};
diff --git a/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx
index 287042d2af..b6a23a2aaa 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx
+++ b/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx
@@ -13,6 +13,7 @@ import {
allBlobSizesQuery,
removeAvatarMutation,
SubscriptionPlan,
+ updateUserProfileMutation,
uploadAvatarMutation,
} from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
@@ -58,11 +59,10 @@ export const UserAvatar = () => {
async (file: File) => {
try {
const reducedFile = await validateAndReduceImage(file);
- await avatarTrigger({
+ const data = await avatarTrigger({
avatar: reducedFile, // Pass the reducedFile directly to the avatarTrigger
});
- // XXX: This is a hack to force the user to update, since next-auth can not only use update function without params
- await user.update({ name: user.name });
+ user.update({ avatarUrl: data.uploadAvatar.avatarUrl });
pushNotification({
title: 'Update user avatar success',
type: 'success',
@@ -82,8 +82,7 @@ export const UserAvatar = () => {
async (e: MouseEvent) => {
e.stopPropagation();
await removeAvatarTrigger();
- // XXX: This is a hack to force the user to update, since next-auth can not only use update function without params
- user.update({ name: user.name }).catch(console.error);
+ user.update({ avatarUrl: null });
},
[removeAvatarTrigger, user]
);
@@ -97,9 +96,9 @@ export const UserAvatar = () => {
}
- onRemove={user.image ? handleRemoveUserAvatar : undefined}
+ onRemove={user.avatarUrl ? handleRemoveUserAvatar : undefined}
avatarTooltipOptions={{ content: t['Click to replace photo']() }}
removeTooltipOptions={{ content: t['Remove photo']() }}
data-testid="user-setting-avatar"
@@ -115,14 +114,30 @@ export const AvatarAndName = () => {
const t = useAFFiNEI18N();
const user = useCurrentUser();
const [input, setInput] = useState(user.name);
+ const pushNotification = useSetAtom(pushNotificationAtom);
+ const { trigger: updateProfile } = useMutation({
+ mutation: updateUserProfileMutation,
+ });
const allowUpdate = !!input && input !== user.name;
- const handleUpdateUserName = useCallback(() => {
+ const handleUpdateUserName = useAsyncCallback(async () => {
if (!allowUpdate) {
return;
}
- user.update({ name: input }).catch(console.error);
- }, [allowUpdate, input, user]);
+
+ try {
+ const data = await updateProfile({
+ input: { name: input },
+ });
+ user.update({ name: data.updateProfile.name });
+ } catch (e) {
+ pushNotification({
+ title: 'Failed to update user name.',
+ message: String(e),
+ type: 'error',
+ });
+ }
+ }, [allowUpdate, input, user, updateProfile, pushNotification]);
return (
{
openModal: true,
state: 'sendEmail',
email: user.email,
- emailType: 'changeEmail',
+ emailType: user.emailVerified ? 'changeEmail' : 'verifyEmail',
});
- }, [setAuthModal, user.email]);
+ }, [setAuthModal, user.email, user.emailVerified]);
const onPasswordButtonClick = useCallback(() => {
setAuthModal({
@@ -249,7 +264,9 @@ export const AccountSetting: FC = () => {
-
+
diff --git a/packages/frontend/core/src/components/cloud/share-header-right-item/user-avatar.tsx b/packages/frontend/core/src/components/cloud/share-header-right-item/user-avatar.tsx
index b9ad390902..0b4b313b53 100644
--- a/packages/frontend/core/src/components/cloud/share-header-right-item/user-avatar.tsx
+++ b/packages/frontend/core/src/components/cloud/share-header-right-item/user-avatar.tsx
@@ -26,7 +26,7 @@ const UserInfo = () => {
@@ -51,7 +51,7 @@ export const PublishPageUserAvatar = () => {
const location = useLocation();
const handleSignOut = useAsyncCallback(async () => {
- await signOutCloud({ callbackUrl: location.pathname });
+ await signOutCloud(location.pathname);
}, [location.pathname]);
const menuItem = useMemo(() => {
@@ -84,7 +84,7 @@ export const PublishPageUserAvatar = () => {
}}
>
);
diff --git a/packages/frontend/core/src/components/pure/footer/index.tsx b/packages/frontend/core/src/components/pure/footer/index.tsx
index a8a0af8d0f..68ef7dcc72 100644
--- a/packages/frontend/core/src/components/pure/footer/index.tsx
+++ b/packages/frontend/core/src/components/pure/footer/index.tsx
@@ -25,7 +25,7 @@ const SignInButton = () => {
{
- signInCloud().catch(console.error);
+ signInCloud('email').catch(console.error);
}, [])}
>
diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx
index 289dc641a8..216335fce4 100644
--- a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx
+++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx
@@ -1,5 +1,6 @@
import { Divider } from '@affine/component/ui/divider';
import { MenuItem } from '@affine/component/ui/menu';
+import { useSession } from '@affine/core/hooks/affine/use-current-user';
import { Unreachable } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Logo1Icon } from '@blocksuite/icons';
@@ -7,9 +8,7 @@ import { WorkspaceManager } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { useSetAtom } from 'jotai';
-// eslint-disable-next-line @typescript-eslint/no-restricted-imports
-import { useSession } from 'next-auth/react';
-import { useCallback, useEffect, useMemo } from 'react';
+import { useCallback, useEffect } from 'react';
import {
authAtom,
@@ -68,9 +67,9 @@ export const UserWithWorkspaceList = ({
}: {
onEventEnd?: () => void;
}) => {
- const { data: session, status } = useSession();
+ const { user, status } = useSession();
- const isAuthenticated = useMemo(() => status === 'authenticated', [status]);
+ const isAuthenticated = status === 'authenticated';
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom);
@@ -124,7 +123,7 @@ export const UserWithWorkspaceList = ({
{isAuthenticated ? (
) : (
diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.tsx
index 88b721533b..23b3a90f12 100644
--- a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.tsx
+++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.tsx
@@ -1,6 +1,7 @@
import { ScrollableContainer } from '@affine/component';
import { Divider } from '@affine/component/ui/divider';
import { WorkspaceList } from '@affine/component/workspace-list';
+import { useSession } from '@affine/core/hooks/affine/use-current-user';
import {
useWorkspaceAvatar,
useWorkspaceName,
@@ -12,8 +13,6 @@ import { WorkspaceManager, type WorkspaceMetadata } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { useSetAtom } from 'jotai';
-// eslint-disable-next-line @typescript-eslint/no-restricted-imports
-import { useSession } from 'next-auth/react';
import { useCallback, useMemo } from 'react';
import {
@@ -119,10 +118,9 @@ export const AFFiNEWorkspaceList = ({
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
- // TODO: AFFiNE Cloud support
const { status } = useSession();
- const isAuthenticated = useMemo(() => status === 'authenticated', [status]);
+ const isAuthenticated = status === 'authenticated';
const cloudWorkspaces = useMemo(
() =>
diff --git a/packages/frontend/core/src/hooks/affine/use-current-login-status.ts b/packages/frontend/core/src/hooks/affine/use-current-login-status.ts
index 4bd5ca2e0e..14ff11afa7 100644
--- a/packages/frontend/core/src/hooks/affine/use-current-login-status.ts
+++ b/packages/frontend/core/src/hooks/affine/use-current-login-status.ts
@@ -1,10 +1,6 @@
-// eslint-disable-next-line @typescript-eslint/no-restricted-imports
-import { useSession } from 'next-auth/react';
+import { useSession } from './use-current-user';
-export function useCurrentLoginStatus():
- | 'authenticated'
- | 'unauthenticated'
- | 'loading' {
+export function useCurrentLoginStatus() {
const session = useSession();
return session.status;
}
diff --git a/packages/frontend/core/src/hooks/affine/use-current-user.ts b/packages/frontend/core/src/hooks/affine/use-current-user.ts
index 35cbe9bbc3..d4329d9ba6 100644
--- a/packages/frontend/core/src/hooks/affine/use-current-user.ts
+++ b/packages/frontend/core/src/hooks/affine/use-current-user.ts
@@ -1,42 +1,83 @@
-import { type User } from '@affine/component/auth-components';
-import type { DefaultSession, Session } from 'next-auth';
-// eslint-disable-next-line @typescript-eslint/no-restricted-imports
-import { getSession, useSession } from 'next-auth/react';
-import { useEffect, useMemo, useReducer } from 'react';
+import { DebugLogger } from '@affine/debug';
+import { getBaseUrl } from '@affine/graphql';
+import { useMemo, useReducer } from 'react';
+import useSWR from 'swr';
import { SessionFetchErrorRightAfterLoginOrSignUp } from '../../unexpected-application-state/errors';
+import { useAsyncCallback } from '../affine-async-hooks';
-export type CheckedUser = User & {
+const logger = new DebugLogger('auth');
+
+interface User {
+ id: string;
+ email: string;
+ name: string;
hasPassword: boolean;
- update: ReturnType
['update'];
+ avatarUrl: string | null;
+ emailVerified: string | null;
+}
+
+export interface Session {
+ user?: User | null;
+ status: 'authenticated' | 'unauthenticated' | 'loading';
+ reload: () => Promise;
+}
+
+export type CheckedUser = Session['user'] & {
+ update: (changes?: Partial) => void;
};
-declare module 'next-auth' {
- interface Session {
- user: {
- name: string;
- email: string;
- id: string;
- hasPassword: boolean;
- } & Omit, 'name' | 'email'>;
+export async function getSession(
+ url: string = getBaseUrl() + '/api/auth/session'
+) {
+ try {
+ const res = await fetch(url);
+
+ if (res.ok) {
+ return (await res.json()) as { user?: User | null };
+ }
+
+ logger.error('Failed to fetch session', res.statusText);
+ return { user: null };
+ } catch (e) {
+ logger.error('Failed to fetch session', e);
+ return { user: null };
}
}
+export function useSession(): Session {
+ const { data, mutate, isLoading } = useSWR('session', () => getSession());
+
+ return {
+ user: data?.user,
+ status: isLoading
+ ? 'loading'
+ : data?.user
+ ? 'authenticated'
+ : 'unauthenticated',
+ reload: async () => {
+ return mutate().then(e => {
+ console.error(e);
+ });
+ },
+ };
+}
+
type UpdateSessionAction =
| {
type: 'update';
- payload: Session;
+ payload?: Partial;
}
| {
type: 'fetchError';
payload: null;
};
-function updateSessionReducer(prevState: Session, action: UpdateSessionAction) {
+function updateSessionReducer(prevState: User, action: UpdateSessionAction) {
const { type, payload } = action;
switch (type) {
case 'update':
- return payload;
+ return { ...prevState, ...payload };
case 'fetchError':
return prevState;
}
@@ -49,11 +90,11 @@ function updateSessionReducer(prevState: Session, action: UpdateSessionAction) {
* If network error or API response error, it will use the cached value.
*/
export function useCurrentUser(): CheckedUser {
- const { data, update } = useSession();
+ const session = useSession();
- const [session, dispatcher] = useReducer(
+ const [user, dispatcher] = useReducer(
updateSessionReducer,
- data,
+ session.user,
firstSession => {
if (!firstSession) {
// barely possible.
@@ -64,10 +105,10 @@ export function useCurrentUser(): CheckedUser {
() => {
getSession()
.then(session => {
- if (session) {
+ if (session.user) {
dispatcher({
type: 'update',
- payload: session,
+ payload: session.user,
});
}
})
@@ -77,35 +118,30 @@ export function useCurrentUser(): CheckedUser {
}
);
}
+
return firstSession;
}
);
- useEffect(() => {
- if (data) {
+ const update = useAsyncCallback(
+ async (changes?: Partial) => {
dispatcher({
type: 'update',
- payload: data,
+ payload: changes,
});
- } else {
- dispatcher({
- type: 'fetchError',
- payload: null,
- });
- }
- }, [data, update]);
- const user = session.user;
+ await session.reload();
+ },
+ [dispatcher, session]
+ );
- return useMemo(() => {
- return {
- id: user.id,
- name: user.name,
- email: user.email,
- image: user.image,
- hasPassword: user?.hasPassword ?? false,
+ return useMemo(
+ () => ({
+ ...user,
update,
- };
- // spread the user object to make sure the hook will not be re-rendered when user ref changed but the properties not.
- }, [user.id, user.name, user.email, user.image, user.hasPassword, update]);
+ }),
+ // only list the things will change as deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [user.id, user.avatarUrl, user.name, update]
+ );
}
diff --git a/packages/frontend/core/src/hooks/affine/use-delete-collection-info.ts b/packages/frontend/core/src/hooks/affine/use-delete-collection-info.ts
index 2a99a4a3cf..e1b3b6764d 100644
--- a/packages/frontend/core/src/hooks/affine/use-delete-collection-info.ts
+++ b/packages/frontend/core/src/hooks/affine/use-delete-collection-info.ts
@@ -1,11 +1,12 @@
-// eslint-disable-next-line @typescript-eslint/no-restricted-imports
-import { useSession } from 'next-auth/react';
import { useMemo } from 'react';
+import { useSession } from './use-current-user';
+
export const useDeleteCollectionInfo = () => {
- const user = useSession().data?.user;
+ const { user } = useSession();
+
return useMemo(
- () => (user ? { userName: user.name ?? '', userId: user.id } : null),
+ () => (user ? { userName: user.name, userId: user.id } : null),
[user]
);
};
diff --git a/packages/frontend/core/src/hooks/affine/use-server-config.ts b/packages/frontend/core/src/hooks/affine/use-server-config.ts
index ef0878d1de..f6cf86146e 100644
--- a/packages/frontend/core/src/hooks/affine/use-server-config.ts
+++ b/packages/frontend/core/src/hooks/affine/use-server-config.ts
@@ -1,5 +1,5 @@
import type { ServerFeature } from '@affine/graphql';
-import { serverConfigQuery } from '@affine/graphql';
+import { oauthProvidersQuery, serverConfigQuery } from '@affine/graphql';
import type { BareFetcher, Middleware } from 'swr';
import { useQueryImmutable } from '../use-query';
@@ -44,6 +44,21 @@ export const useServerFeatures = (): ServerFeatureRecord => {
}, {} as ServerFeatureRecord);
};
+export const useOAuthProviders = () => {
+ const { data, error } = useQueryImmutable(
+ { query: oauthProvidersQuery },
+ {
+ use: [errorHandler],
+ }
+ );
+
+ if (error || !data) {
+ return [];
+ }
+
+ return data.serverConfig.oauthProviders;
+};
+
export const useServerBaseUrl = () => {
const config = useServerConfig();
diff --git a/packages/frontend/core/src/pages/404.tsx b/packages/frontend/core/src/pages/404.tsx
index 280a33da82..8d112e5c92 100644
--- a/packages/frontend/core/src/pages/404.tsx
+++ b/packages/frontend/core/src/pages/404.tsx
@@ -1,7 +1,6 @@
import { NotFoundPage } from '@affine/component/not-found-page';
+import { useSession } from '@affine/core/hooks/affine/use-current-user';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
-// eslint-disable-next-line @typescript-eslint/no-restricted-imports
-import { useSession } from 'next-auth/react';
import type { ReactElement } from 'react';
import { useCallback, useState } from 'react';
@@ -10,7 +9,7 @@ import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
import { signOutCloud } from '../utils/cloud-utils';
export const PageNotFound = (): ReactElement => {
- const { data: session } = useSession();
+ const { user } = useSession();
const { jumpToIndex } = useNavigateHelper();
const [open, setOpen] = useState(false);
@@ -25,22 +24,12 @@ export const PageNotFound = (): ReactElement => {
const onConfirmSignOut = useAsyncCallback(async () => {
setOpen(false);
- await signOutCloud({
- callbackUrl: '/signIn',
- });
+ await signOutCloud('/signIn');
}, [setOpen]);
return (
<>
diff --git a/packages/frontend/core/src/pages/auth.tsx b/packages/frontend/core/src/pages/auth.tsx
index c32fe72536..6f27042856 100644
--- a/packages/frontend/core/src/pages/auth.tsx
+++ b/packages/frontend/core/src/pages/auth.tsx
@@ -12,6 +12,7 @@ import {
changeEmailMutation,
changePasswordMutation,
sendVerifyChangeEmailMutation,
+ verifyEmailMutation,
} from '@affine/graphql';
import { fetcher } from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
@@ -42,6 +43,7 @@ const authTypeSchema = z.enum([
'changeEmail',
'confirm-change-email',
'subscription-redirect',
+ 'verify-email',
]);
export const AuthPage = (): ReactElement | null => {
@@ -73,12 +75,10 @@ export const AuthPage = (): ReactElement | null => {
// FIXME: There is not notification
if (res?.sendVerifyChangeEmail) {
pushNotification({
- title: t['com.affine.auth.sent.change.email.hint'](),
+ title: t['com.affine.auth.sent.verify.email.hint'](),
type: 'success',
});
- }
-
- if (!res?.sendVerifyChangeEmail) {
+ } else {
pushNotification({
title: t['com.affine.auth.sent.change.email.fail'](),
type: 'error',
@@ -156,6 +156,9 @@ export const AuthPage = (): ReactElement | null => {
case 'subscription-redirect': {
return ;
}
+ case 'verify-email': {
+ return ;
+ }
}
return null;
};
@@ -171,20 +174,37 @@ export const loader: LoaderFunction = async args => {
if (args.params.authType === 'confirm-change-email') {
const url = new URL(args.request.url);
const searchParams = url.searchParams;
- const token = searchParams.get('token');
+ const token = searchParams.get('token') ?? '';
+ const email = decodeURIComponent(searchParams.get('email') ?? '');
const res = await fetcher({
query: changeEmailMutation,
variables: {
- token: token || '',
+ token: token,
+ email: email,
},
}).catch(console.error);
// TODO: Add error handling
if (!res?.changeEmail) {
return redirect('/expired');
}
+ } else if (args.params.authType === 'verify-email') {
+ const url = new URL(args.request.url);
+ const searchParams = url.searchParams;
+ const token = searchParams.get('token') ?? '';
+ const res = await fetcher({
+ query: verifyEmailMutation,
+ variables: {
+ token: token,
+ },
+ }).catch(console.error);
+
+ if (!res?.verifyEmail) {
+ return redirect('/expired');
+ }
}
return null;
};
+
export const Component = () => {
const loginStatus = useCurrentLoginStatus();
const { jumpToExpired } = useNavigateHelper();
diff --git a/packages/frontend/core/src/pages/desktop-signin.tsx b/packages/frontend/core/src/pages/desktop-signin.tsx
index f258fc16f2..04d45b3cb0 100644
--- a/packages/frontend/core/src/pages/desktop-signin.tsx
+++ b/packages/frontend/core/src/pages/desktop-signin.tsx
@@ -1,34 +1,43 @@
-import { getSession } from 'next-auth/react';
+import { OAuthProviderType } from '@affine/graphql';
import { type LoaderFunction } from 'react-router-dom';
import { z } from 'zod';
+import { getSession } from '../hooks/affine/use-current-user';
import { signInCloud, signOutCloud } from '../utils/cloud-utils';
-const supportedProvider = z.enum(['google']);
+const supportedProvider = z.enum([
+ 'google',
+ ...Object.values(OAuthProviderType),
+]);
export const loader: LoaderFunction = async ({ request }) => {
const url = new URL(request.url);
const searchParams = url.searchParams;
const provider = searchParams.get('provider');
- const callback_url = searchParams.get('callback_url');
- if (!callback_url) {
+ const redirectUri =
+ searchParams.get('redirect_uri') ??
+ /* backward compatibility */ searchParams.get('callback_url');
+
+ if (!redirectUri) {
return null;
}
const session = await getSession();
- if (session) {
+ if (session.user) {
// already signed in, need to sign out first
- await signOutCloud({
- callbackUrl: request.url, // retry
- });
+ await signOutCloud(request.url);
}
const maybeProvider = supportedProvider.safeParse(provider);
if (maybeProvider.success) {
- const provider = maybeProvider.data;
- await signInCloud(provider, {
- callbackUrl: callback_url,
+ let provider = maybeProvider.data;
+ // BACKWARD COMPATIBILITY
+ if (provider === 'google') {
+ provider = OAuthProviderType.Google;
+ }
+ await signInCloud(provider, undefined, {
+ redirectUri,
});
}
return null;
diff --git a/packages/frontend/core/src/providers/modal-provider.tsx b/packages/frontend/core/src/providers/modal-provider.tsx
index a1922eae86..41ed373658 100644
--- a/packages/frontend/core/src/providers/modal-provider.tsx
+++ b/packages/frontend/core/src/providers/modal-provider.tsx
@@ -216,9 +216,7 @@ export const SignOutConfirmModal = () => {
const onConfirm = useAsyncCallback(async () => {
setOpen(false);
- await signOutCloud({
- redirect: false,
- });
+ await signOutCloud();
// if current workspace is affine cloud, switch to local workspace
if (currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
diff --git a/packages/frontend/core/src/providers/session-provider.tsx b/packages/frontend/core/src/providers/session-provider.tsx
index 9decb80477..4993b40568 100644
--- a/packages/frontend/core/src/providers/session-provider.tsx
+++ b/packages/frontend/core/src/providers/session-provider.tsx
@@ -1,11 +1,10 @@
import { pushNotificationAtom } from '@affine/component/notification-center';
+import { useSession } from '@affine/core/hooks/affine/use-current-user';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { affine } from '@affine/electron-api';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CLOUD_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY } from '@affine/workspace-impl';
-import { useAtom, useSetAtom } from 'jotai';
-// eslint-disable-next-line @typescript-eslint/no-restricted-imports
-import { SessionProvider, useSession } from 'next-auth/react';
+import { useSetAtom } from 'jotai';
import {
type PropsWithChildren,
startTransition,
@@ -13,13 +12,11 @@ import {
useRef,
} from 'react';
-import { sessionAtom } from '../atoms/cloud-user';
import { useOnceSignedInEvents } from '../atoms/event';
-const SessionDefence = (props: PropsWithChildren) => {
+export const CloudSessionProvider = (props: PropsWithChildren) => {
const session = useSession();
const prevSession = useRef>();
- const [sessionInAtom, setSession] = useAtom(sessionAtom);
const pushNotification = useSetAtom(pushNotificationAtom);
const onceSignedInEvents = useOnceSignedInEvents();
const t = useAFFiNEI18N();
@@ -32,10 +29,6 @@ const SessionDefence = (props: PropsWithChildren) => {
}, [onceSignedInEvents]);
useEffect(() => {
- if (sessionInAtom !== session && session.status === 'authenticated') {
- setSession(session);
- }
-
if (prevSession.current !== session && session.status !== 'loading') {
// unauthenticated -> authenticated
if (
@@ -55,22 +48,7 @@ const SessionDefence = (props: PropsWithChildren) => {
}
prevSession.current = session;
}
- }, [
- session,
- sessionInAtom,
- prevSession,
- setSession,
- pushNotification,
- refreshAfterSignedInEvents,
- t,
- ]);
+ }, [session, prevSession, pushNotification, refreshAfterSignedInEvents, t]);
+
return props.children;
};
-
-export const CloudSessionProvider = ({ children }: PropsWithChildren) => {
- return (
-
- {children}
-
- );
-};
diff --git a/packages/frontend/core/src/utils/cloud-utils.tsx b/packages/frontend/core/src/utils/cloud-utils.tsx
index 4d27e54515..b70951a8d1 100644
--- a/packages/frontend/core/src/utils/cloud-utils.tsx
+++ b/packages/frontend/core/src/utils/cloud-utils.tsx
@@ -1,12 +1,12 @@
import {
generateRandUTF16Chars,
+ getBaseUrl,
+ OAuthProviderType,
SPAN_ID_BYTES,
TRACE_ID_BYTES,
traceReporter,
} from '@affine/graphql';
import { CLOUD_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY } from '@affine/workspace-impl';
-// eslint-disable-next-line @typescript-eslint/no-restricted-imports
-import { signIn, signOut } from 'next-auth/react';
type TraceParams = {
startTime: string;
@@ -43,62 +43,95 @@ function onRejectHandleTrace(
return Promise.reject(res);
}
-export const signInCloud: typeof signIn = async (provider, ...rest) => {
+type Providers = 'credentials' | 'email' | OAuthProviderType;
+
+export const signInCloud = async (
+ provider: Providers,
+ credentials?: { email: string; password?: string },
+ searchParams: Record = {}
+): Promise => {
const traceParams = genTraceParams();
- if (environment.isDesktop) {
- if (provider === 'google') {
+
+ if (provider === 'credentials' || provider === 'email') {
+ if (!credentials) {
+ throw new Error('Invalid Credentials');
+ }
+
+ return signIn(credentials, searchParams)
+ .then(res => onResolveHandleTrace(res, traceParams))
+ .catch(err => onRejectHandleTrace(err, traceParams));
+ } else if (OAuthProviderType[provider]) {
+ if (environment.isDesktop) {
open(
`${
runtimeConfig.serverUrlPrefix
- }/desktop-signin?provider=google&callback_url=${buildCallbackUrl(
+ }/desktop-signin?provider=${provider}&redirect_uri=${buildRedirectUri(
'/open-app/signin-redirect'
)}`,
'_target'
);
- return;
} else {
- const [options, ...tail] = rest;
- const callbackUrl =
- runtimeConfig.serverUrlPrefix +
- (provider === 'email'
- ? '/open-app/signin-redirect'
- : location.pathname);
- return signIn(
- provider,
- {
- ...options,
- callbackUrl: buildCallbackUrl(callbackUrl),
- },
- ...tail
- )
- .then(res => onResolveHandleTrace(res, traceParams))
- .catch(err => onRejectHandleTrace(err, traceParams));
+ location.href = `${
+ runtimeConfig.serverUrlPrefix
+ }/oauth/login?provider=${provider}&redirect_uri=${encodeURIComponent(
+ searchParams.redirectUri ?? location.pathname
+ )}`;
}
+
+ return;
} else {
- return signIn(provider, ...rest)
- .then(res => onResolveHandleTrace(res, traceParams))
- .catch(err => onRejectHandleTrace(err, traceParams));
+ throw new Error('Invalid Provider');
}
};
-export const signOutCloud: typeof signOut = async options => {
+async function signIn(
+ credential: { email: string; password?: string },
+ searchParams: Record = {}
+) {
+ const url = new URL(getBaseUrl() + '/api/auth/sign-in');
+
+ for (const key in searchParams) {
+ url.searchParams.set(key, searchParams[key]);
+ }
+
+ const redirectUri =
+ runtimeConfig.serverUrlPrefix +
+ (environment.isDesktop
+ ? buildRedirectUri('/open-app/signin-redirect')
+ : location.pathname);
+
+ url.searchParams.set('redirect_uri', redirectUri);
+
+ return fetch(url.toString(), {
+ method: 'POST',
+ body: JSON.stringify(credential),
+ headers: {
+ 'content-type': 'application/json',
+ },
+ });
+}
+
+export const signOutCloud = async (redirectUri?: string) => {
const traceParams = genTraceParams();
- return signOut({
- callbackUrl: '/',
- ...options,
- })
+ return fetch(getBaseUrl() + '/api/auth/sign-out')
.then(result => {
- if (result) {
+ if (result.ok) {
new BroadcastChannel(
CLOUD_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY
).postMessage(1);
+
+ if (redirectUri && location.href !== redirectUri) {
+ setTimeout(() => {
+ location.href = redirectUri;
+ }, 0);
+ }
}
return onResolveHandleTrace(result, traceParams);
})
.catch(err => onRejectHandleTrace(err, traceParams));
};
-export function buildCallbackUrl(callbackUrl: string) {
+export function buildRedirectUri(callbackUrl: string) {
const params: string[][] = [];
if (environment.isDesktop && window.appInfo.schema) {
params.push(['schema', window.appInfo.schema]);
diff --git a/packages/frontend/electron/src/main/deep-link.ts b/packages/frontend/electron/src/main/deep-link.ts
index a098bfb861..b61c9cd54f 100644
--- a/packages/frontend/electron/src/main/deep-link.ts
+++ b/packages/frontend/electron/src/main/deep-link.ts
@@ -8,7 +8,6 @@ import { logger } from './logger';
import {
getMainWindow,
handleOpenUrlInHiddenWindow,
- removeCookie,
setCookie,
} from './main-window';
@@ -82,28 +81,16 @@ async function handleOauthJwt(url: string) {
return;
}
- const isSecure = CLOUD_BASE_URL.startsWith('https://');
-
// set token to cookie
await setCookie({
url: CLOUD_BASE_URL,
httpOnly: true,
value: token,
secure: true,
- name: isSecure
- ? '__Secure-next-auth.session-token'
- : 'next-auth.session-token',
+ name: 'sid',
expirationDate: Math.floor(Date.now() / 1000 + 3600 * 24 * 7),
});
- // force reset next-auth.callback-url
- // there could be incorrect callback-url in cookie that will cause auth failure
- // so we need to reset it to empty to mitigate this issue
- await removeCookie(
- CLOUD_BASE_URL,
- isSecure ? '__Secure-next-auth.callback-url' : 'next-auth.callback-url'
- );
-
let hiddenWindow: BrowserWindow | null = null;
ipcMain.once('affine:login', () => {
diff --git a/packages/frontend/graphql/src/graphql/change-email.gql b/packages/frontend/graphql/src/graphql/change-email.gql
index 77746962ce..6ebd72e91f 100644
--- a/packages/frontend/graphql/src/graphql/change-email.gql
+++ b/packages/frontend/graphql/src/graphql/change-email.gql
@@ -1,8 +1,6 @@
-mutation changeEmail($token: String!) {
- changeEmail(token: $token) {
+mutation changeEmail($token: String!, $email: String!) {
+ changeEmail(token: $token, email: $email) {
id
- name
- avatarUrl
email
}
}
diff --git a/packages/frontend/graphql/src/graphql/change-password.gql b/packages/frontend/graphql/src/graphql/change-password.gql
index e1f86b3ffd..fb60b1a952 100644
--- a/packages/frontend/graphql/src/graphql/change-password.gql
+++ b/packages/frontend/graphql/src/graphql/change-password.gql
@@ -1,8 +1,5 @@
mutation changePassword($token: String!, $newPassword: String!) {
changePassword(token: $token, newPassword: $newPassword) {
id
- name
- avatarUrl
- email
}
}
diff --git a/packages/frontend/graphql/src/graphql/early-access-list.gql b/packages/frontend/graphql/src/graphql/early-access-list.gql
index 13c92f22a6..1b87d67fa6 100644
--- a/packages/frontend/graphql/src/graphql/early-access-list.gql
+++ b/packages/frontend/graphql/src/graphql/early-access-list.gql
@@ -5,7 +5,6 @@ query earlyAccessUsers {
email
avatarUrl
emailVerified
- createdAt
subscription {
plan
recurring
diff --git a/packages/frontend/graphql/src/graphql/get-current-user.gql b/packages/frontend/graphql/src/graphql/get-current-user.gql
index 272f527e04..8c9d837921 100644
--- a/packages/frontend/graphql/src/graphql/get-current-user.gql
+++ b/packages/frontend/graphql/src/graphql/get-current-user.gql
@@ -5,7 +5,6 @@ query getCurrentUser {
email
emailVerified
avatarUrl
- createdAt
token {
sessionToken
}
diff --git a/packages/frontend/graphql/src/graphql/get-oauth-providers.gql b/packages/frontend/graphql/src/graphql/get-oauth-providers.gql
new file mode 100644
index 0000000000..afbdcc6a8e
--- /dev/null
+++ b/packages/frontend/graphql/src/graphql/get-oauth-providers.gql
@@ -0,0 +1,5 @@
+query oauthProviders {
+ serverConfig {
+ oauthProviders
+ }
+}
diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts
index 272ce02ab4..d28ef61e64 100644
--- a/packages/frontend/graphql/src/graphql/index.ts
+++ b/packages/frontend/graphql/src/graphql/index.ts
@@ -101,11 +101,9 @@ export const changeEmailMutation = {
definitionName: 'changeEmail',
containsFile: false,
query: `
-mutation changeEmail($token: String!) {
- changeEmail(token: $token) {
+mutation changeEmail($token: String!, $email: String!) {
+ changeEmail(token: $token, email: $email) {
id
- name
- avatarUrl
email
}
}`,
@@ -120,9 +118,6 @@ export const changePasswordMutation = {
mutation changePassword($token: String!, $newPassword: String!) {
changePassword(token: $token, newPassword: $newPassword) {
id
- name
- avatarUrl
- email
}
}`,
};
@@ -212,7 +207,6 @@ query earlyAccessUsers {
email
avatarUrl
emailVerified
- createdAt
subscription {
plan
recurring
@@ -248,7 +242,6 @@ query getCurrentUser {
email
emailVerified
avatarUrl
- createdAt
token {
sessionToken
}
@@ -324,6 +317,19 @@ query getMembersByWorkspaceId($workspaceId: String!, $skip: Int!, $take: Int!) {
}`,
};
+export const oauthProvidersQuery = {
+ id: 'oauthProvidersQuery' as const,
+ operationName: 'oauthProviders',
+ definitionName: 'serverConfig',
+ containsFile: false,
+ query: `
+query oauthProviders {
+ serverConfig {
+ oauthProviders
+ }
+}`,
+};
+
export const getPublicWorkspaceQuery = {
id: 'getPublicWorkspaceQuery' as const,
operationName: 'getPublicWorkspace',
@@ -627,8 +633,8 @@ export const sendChangeEmailMutation = {
definitionName: 'sendChangeEmail',
containsFile: false,
query: `
-mutation sendChangeEmail($email: String!, $callbackUrl: String!) {
- sendChangeEmail(email: $email, callbackUrl: $callbackUrl)
+mutation sendChangeEmail($callbackUrl: String!) {
+ sendChangeEmail(callbackUrl: $callbackUrl)
}`,
};
@@ -638,8 +644,8 @@ export const sendChangePasswordEmailMutation = {
definitionName: 'sendChangePasswordEmail',
containsFile: false,
query: `
-mutation sendChangePasswordEmail($email: String!, $callbackUrl: String!) {
- sendChangePasswordEmail(email: $email, callbackUrl: $callbackUrl)
+mutation sendChangePasswordEmail($callbackUrl: String!) {
+ sendChangePasswordEmail(callbackUrl: $callbackUrl)
}`,
};
@@ -649,8 +655,8 @@ export const sendSetPasswordEmailMutation = {
definitionName: 'sendSetPasswordEmail',
containsFile: false,
query: `
-mutation sendSetPasswordEmail($email: String!, $callbackUrl: String!) {
- sendSetPasswordEmail(email: $email, callbackUrl: $callbackUrl)
+mutation sendSetPasswordEmail($callbackUrl: String!) {
+ sendSetPasswordEmail(callbackUrl: $callbackUrl)
}`,
};
@@ -665,6 +671,17 @@ mutation sendVerifyChangeEmail($token: String!, $email: String!, $callbackUrl: S
}`,
};
+export const sendVerifyEmailMutation = {
+ id: 'sendVerifyEmailMutation' as const,
+ operationName: 'sendVerifyEmail',
+ definitionName: 'sendVerifyEmail',
+ containsFile: false,
+ query: `
+mutation sendVerifyEmail($callbackUrl: String!) {
+ sendVerifyEmail(callbackUrl: $callbackUrl)
+}`,
+};
+
export const serverConfigQuery = {
id: 'serverConfigQuery' as const,
operationName: 'serverConfig',
@@ -695,36 +712,6 @@ mutation setWorkspacePublicById($id: ID!, $public: Boolean!) {
}`,
};
-export const signInMutation = {
- id: 'signInMutation' as const,
- operationName: 'signIn',
- definitionName: 'signIn',
- containsFile: false,
- query: `
-mutation signIn($email: String!, $password: String!) {
- signIn(email: $email, password: $password) {
- token {
- token
- }
- }
-}`,
-};
-
-export const signUpMutation = {
- id: 'signUpMutation' as const,
- operationName: 'signUp',
- definitionName: 'signUp',
- containsFile: false,
- query: `
-mutation signUp($name: String!, $email: String!, $password: String!) {
- signUp(name: $name, email: $email, password: $password) {
- token {
- token
- }
- }
-}`,
-};
-
export const subscriptionQuery = {
id: 'subscriptionQuery' as const,
operationName: 'subscription',
@@ -766,6 +753,20 @@ mutation updateSubscription($recurring: SubscriptionRecurring!, $idempotencyKey:
}`,
};
+export const updateUserProfileMutation = {
+ id: 'updateUserProfileMutation' as const,
+ operationName: 'updateUserProfile',
+ definitionName: 'updateProfile',
+ containsFile: false,
+ query: `
+mutation updateUserProfile($input: UpdateUserInput!) {
+ updateProfile(input: $input) {
+ id
+ name
+ }
+}`,
+};
+
export const uploadAvatarMutation = {
id: 'uploadAvatarMutation' as const,
operationName: 'uploadAvatar',
@@ -782,6 +783,17 @@ mutation uploadAvatar($avatar: Upload!) {
}`,
};
+export const verifyEmailMutation = {
+ id: 'verifyEmailMutation' as const,
+ operationName: 'verifyEmail',
+ definitionName: 'verifyEmail',
+ containsFile: false,
+ query: `
+mutation verifyEmail($token: String!) {
+ verifyEmail(token: $token)
+}`,
+};
+
export const enabledFeaturesQuery = {
id: 'enabledFeaturesQuery' as const,
operationName: 'enabledFeatures',
diff --git a/packages/frontend/graphql/src/graphql/send-change-email.gql b/packages/frontend/graphql/src/graphql/send-change-email.gql
index b9421d15b5..0300a20427 100644
--- a/packages/frontend/graphql/src/graphql/send-change-email.gql
+++ b/packages/frontend/graphql/src/graphql/send-change-email.gql
@@ -1,3 +1,3 @@
-mutation sendChangeEmail($email: String!, $callbackUrl: String!) {
- sendChangeEmail(email: $email, callbackUrl: $callbackUrl)
+mutation sendChangeEmail($callbackUrl: String!) {
+ sendChangeEmail(callbackUrl: $callbackUrl)
}
diff --git a/packages/frontend/graphql/src/graphql/send-change-password-email.gql b/packages/frontend/graphql/src/graphql/send-change-password-email.gql
index ed99bab15b..3b40efba25 100644
--- a/packages/frontend/graphql/src/graphql/send-change-password-email.gql
+++ b/packages/frontend/graphql/src/graphql/send-change-password-email.gql
@@ -1,3 +1,3 @@
-mutation sendChangePasswordEmail($email: String!, $callbackUrl: String!) {
- sendChangePasswordEmail(email: $email, callbackUrl: $callbackUrl)
+mutation sendChangePasswordEmail($callbackUrl: String!) {
+ sendChangePasswordEmail(callbackUrl: $callbackUrl)
}
diff --git a/packages/frontend/graphql/src/graphql/send-set-password-email.gql b/packages/frontend/graphql/src/graphql/send-set-password-email.gql
index 8caaebd989..e8da4bbefb 100644
--- a/packages/frontend/graphql/src/graphql/send-set-password-email.gql
+++ b/packages/frontend/graphql/src/graphql/send-set-password-email.gql
@@ -1,3 +1,3 @@
-mutation sendSetPasswordEmail($email: String!, $callbackUrl: String!) {
- sendSetPasswordEmail(email: $email, callbackUrl: $callbackUrl)
+mutation sendSetPasswordEmail($callbackUrl: String!) {
+ sendSetPasswordEmail(callbackUrl: $callbackUrl)
}
diff --git a/packages/frontend/graphql/src/graphql/send-verify-email.gql b/packages/frontend/graphql/src/graphql/send-verify-email.gql
new file mode 100644
index 0000000000..300f005916
--- /dev/null
+++ b/packages/frontend/graphql/src/graphql/send-verify-email.gql
@@ -0,0 +1,3 @@
+mutation sendVerifyEmail($callbackUrl: String!) {
+ sendVerifyEmail(callbackUrl: $callbackUrl)
+}
diff --git a/packages/frontend/graphql/src/graphql/sign-in.gql b/packages/frontend/graphql/src/graphql/sign-in.gql
deleted file mode 100644
index b43e7dcbee..0000000000
--- a/packages/frontend/graphql/src/graphql/sign-in.gql
+++ /dev/null
@@ -1,7 +0,0 @@
-mutation signIn($email: String!, $password: String!) {
- signIn(email: $email, password: $password) {
- token {
- token
- }
- }
-}
diff --git a/packages/frontend/graphql/src/graphql/sign-up.gql b/packages/frontend/graphql/src/graphql/sign-up.gql
deleted file mode 100644
index a93df61981..0000000000
--- a/packages/frontend/graphql/src/graphql/sign-up.gql
+++ /dev/null
@@ -1,7 +0,0 @@
-mutation signUp($name: String!, $email: String!, $password: String!) {
- signUp(name: $name, email: $email, password: $password) {
- token {
- token
- }
- }
-}
diff --git a/packages/frontend/graphql/src/graphql/update-user-profile.gql b/packages/frontend/graphql/src/graphql/update-user-profile.gql
new file mode 100644
index 0000000000..edb8d4fcb7
--- /dev/null
+++ b/packages/frontend/graphql/src/graphql/update-user-profile.gql
@@ -0,0 +1,6 @@
+mutation updateUserProfile($input: UpdateUserInput!) {
+ updateProfile(input: $input) {
+ id
+ name
+ }
+}
diff --git a/packages/frontend/graphql/src/graphql/verify-email.gql b/packages/frontend/graphql/src/graphql/verify-email.gql
new file mode 100644
index 0000000000..493a21e5b5
--- /dev/null
+++ b/packages/frontend/graphql/src/graphql/verify-email.gql
@@ -0,0 +1,3 @@
+mutation verifyEmail($token: String!) {
+ verifyEmail(token: $token)
+}
diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts
index b92793f97f..c43b7c1ef1 100644
--- a/packages/frontend/graphql/src/schema.ts
+++ b/packages/frontend/graphql/src/schema.ts
@@ -57,6 +57,11 @@ export enum InvoiceStatus {
Void = 'Void',
}
+export enum OAuthProviderType {
+ GitHub = 'GitHub',
+ Google = 'Google',
+}
+
/** User permission in workspace */
export enum Permission {
Admin = 'Admin',
@@ -77,6 +82,7 @@ export enum ServerDeploymentType {
}
export enum ServerFeature {
+ OAuth = 'OAuth',
Payment = 'Payment',
}
@@ -104,6 +110,11 @@ export enum SubscriptionStatus {
Unpaid = 'Unpaid',
}
+export interface UpdateUserInput {
+ /** User name */
+ name: InputMaybe;
+}
+
export interface UpdateWorkspaceInput {
id: Scalars['ID']['input'];
/** is Public workspace */
@@ -176,17 +187,12 @@ export type CancelSubscriptionMutation = {
export type ChangeEmailMutationVariables = Exact<{
token: Scalars['String']['input'];
+ email: Scalars['String']['input'];
}>;
export type ChangeEmailMutation = {
__typename?: 'Mutation';
- changeEmail: {
- __typename?: 'UserType';
- id: string;
- name: string;
- avatarUrl: string | null;
- email: string;
- };
+ changeEmail: { __typename?: 'UserType'; id: string; email: string };
};
export type ChangePasswordMutationVariables = Exact<{
@@ -196,13 +202,7 @@ export type ChangePasswordMutationVariables = Exact<{
export type ChangePasswordMutation = {
__typename?: 'Mutation';
- changePassword: {
- __typename?: 'UserType';
- id: string;
- name: string;
- avatarUrl: string | null;
- email: string;
- };
+ changePassword: { __typename?: 'UserType'; id: string };
};
export type CreateCheckoutSessionMutationVariables = Exact<{
@@ -270,8 +270,7 @@ export type EarlyAccessUsersQuery = {
name: string;
email: string;
avatarUrl: string | null;
- emailVerified: string | null;
- createdAt: string | null;
+ emailVerified: boolean;
subscription: {
__typename?: 'UserSubscription';
plan: SubscriptionPlan;
@@ -301,10 +300,9 @@ export type GetCurrentUserQuery = {
id: string;
name: string;
email: string;
- emailVerified: string | null;
+ emailVerified: boolean;
avatarUrl: string | null;
- createdAt: string | null;
- token: { __typename?: 'TokenType'; sessionToken: string | null };
+ token: { __typename?: 'tokenType'; sessionToken: string | null };
} | null;
};
@@ -365,11 +363,21 @@ export type GetMembersByWorkspaceIdQuery = {
permission: Permission;
inviteId: string;
accepted: boolean;
- emailVerified: string | null;
+ emailVerified: boolean | null;
}>;
};
};
+export type OauthProvidersQueryVariables = Exact<{ [key: string]: never }>;
+
+export type OauthProvidersQuery = {
+ __typename?: 'Query';
+ serverConfig: {
+ __typename?: 'ServerConfigType';
+ oauthProviders: Array;
+ };
+};
+
export type GetPublicWorkspaceQueryVariables = Exact<{
id: Scalars['String']['input'];
}>;
@@ -386,18 +394,14 @@ export type GetUserQueryVariables = Exact<{
export type GetUserQuery = {
__typename?: 'Query';
user:
- | {
- __typename: 'LimitedUserType';
- email: string;
- hasPassword: boolean | null;
- }
+ | { __typename: 'LimitedUserType'; email: string; hasPassword: boolean }
| {
__typename: 'UserType';
id: string;
name: string;
avatarUrl: string | null;
email: string;
- hasPassword: boolean | null;
+ hasPassword: boolean;
}
| null;
};
@@ -628,7 +632,6 @@ export type RevokePublicPageMutation = {
};
export type SendChangeEmailMutationVariables = Exact<{
- email: Scalars['String']['input'];
callbackUrl: Scalars['String']['input'];
}>;
@@ -638,7 +641,6 @@ export type SendChangeEmailMutation = {
};
export type SendChangePasswordEmailMutationVariables = Exact<{
- email: Scalars['String']['input'];
callbackUrl: Scalars['String']['input'];
}>;
@@ -648,7 +650,6 @@ export type SendChangePasswordEmailMutation = {
};
export type SendSetPasswordEmailMutationVariables = Exact<{
- email: Scalars['String']['input'];
callbackUrl: Scalars['String']['input'];
}>;
@@ -668,6 +669,15 @@ export type SendVerifyChangeEmailMutation = {
sendVerifyChangeEmail: boolean;
};
+export type SendVerifyEmailMutationVariables = Exact<{
+ callbackUrl: Scalars['String']['input'];
+}>;
+
+export type SendVerifyEmailMutation = {
+ __typename?: 'Mutation';
+ sendVerifyEmail: boolean;
+};
+
export type ServerConfigQueryVariables = Exact<{ [key: string]: never }>;
export type ServerConfigQuery = {
@@ -692,33 +702,6 @@ export type SetWorkspacePublicByIdMutation = {
updateWorkspace: { __typename?: 'WorkspaceType'; id: string };
};
-export type SignInMutationVariables = Exact<{
- email: Scalars['String']['input'];
- password: Scalars['String']['input'];
-}>;
-
-export type SignInMutation = {
- __typename?: 'Mutation';
- signIn: {
- __typename?: 'UserType';
- token: { __typename?: 'TokenType'; token: string };
- };
-};
-
-export type SignUpMutationVariables = Exact<{
- name: Scalars['String']['input'];
- email: Scalars['String']['input'];
- password: Scalars['String']['input'];
-}>;
-
-export type SignUpMutation = {
- __typename?: 'Mutation';
- signUp: {
- __typename?: 'UserType';
- token: { __typename?: 'TokenType'; token: string };
- };
-};
-
export type SubscriptionQueryVariables = Exact<{ [key: string]: never }>;
export type SubscriptionQuery = {
@@ -755,6 +738,15 @@ export type UpdateSubscriptionMutation = {
};
};
+export type UpdateUserProfileMutationVariables = Exact<{
+ input: UpdateUserInput;
+}>;
+
+export type UpdateUserProfileMutation = {
+ __typename?: 'Mutation';
+ updateProfile: { __typename?: 'UserType'; id: string; name: string };
+};
+
export type UploadAvatarMutationVariables = Exact<{
avatar: Scalars['Upload']['input'];
}>;
@@ -770,6 +762,15 @@ export type UploadAvatarMutation = {
};
};
+export type VerifyEmailMutationVariables = Exact<{
+ token: Scalars['String']['input'];
+}>;
+
+export type VerifyEmailMutation = {
+ __typename?: 'Mutation';
+ verifyEmail: boolean;
+};
+
export type EnabledFeaturesQueryVariables = Exact<{
id: Scalars['String']['input'];
}>;
@@ -938,6 +939,11 @@ export type Queries =
variables: GetMembersByWorkspaceIdQueryVariables;
response: GetMembersByWorkspaceIdQuery;
}
+ | {
+ name: 'oauthProvidersQuery';
+ variables: OauthProvidersQueryVariables;
+ response: OauthProvidersQuery;
+ }
| {
name: 'getPublicWorkspaceQuery';
variables: GetPublicWorkspaceQueryVariables;
@@ -1145,31 +1151,36 @@ export type Mutations =
variables: SendVerifyChangeEmailMutationVariables;
response: SendVerifyChangeEmailMutation;
}
+ | {
+ name: 'sendVerifyEmailMutation';
+ variables: SendVerifyEmailMutationVariables;
+ response: SendVerifyEmailMutation;
+ }
| {
name: 'setWorkspacePublicByIdMutation';
variables: SetWorkspacePublicByIdMutationVariables;
response: SetWorkspacePublicByIdMutation;
}
- | {
- name: 'signInMutation';
- variables: SignInMutationVariables;
- response: SignInMutation;
- }
- | {
- name: 'signUpMutation';
- variables: SignUpMutationVariables;
- response: SignUpMutation;
- }
| {
name: 'updateSubscriptionMutation';
variables: UpdateSubscriptionMutationVariables;
response: UpdateSubscriptionMutation;
}
+ | {
+ name: 'updateUserProfileMutation';
+ variables: UpdateUserProfileMutationVariables;
+ response: UpdateUserProfileMutation;
+ }
| {
name: 'uploadAvatarMutation';
variables: UploadAvatarMutationVariables;
response: UploadAvatarMutation;
}
+ | {
+ name: 'verifyEmailMutation';
+ variables: VerifyEmailMutationVariables;
+ response: VerifyEmailMutation;
+ }
| {
name: 'setWorkspaceExperimentalFeatureMutation';
variables: SetWorkspaceExperimentalFeatureMutationVariables;
diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json
index afdab4139f..af2b9c366d 100644
--- a/packages/frontend/i18n/src/resources/en.json
+++ b/packages/frontend/i18n/src/resources/en.json
@@ -406,10 +406,12 @@
"com.affine.appearanceSettings.windowFrame.description": "Customise appearance of Windows Client.",
"com.affine.appearanceSettings.windowFrame.frameless": "Frameless",
"com.affine.appearanceSettings.windowFrame.title": "Window frame style",
- "com.affine.auth.change.email.message": "Your current email is {{email}}. We’ll send a temporary verification link to this email.",
+ "com.affine.auth.verify.email.message": "Your current email is {{email}}. We’ll send a temporary verification link to this email.",
"com.affine.auth.change.email.page.subtitle": "Please enter your new email address below. We will send a verification link to this email address to complete the process.",
"com.affine.auth.change.email.page.success.subtitle": "Congratulations! You have successfully updated the email address associated with your AFFiNE Cloud account.",
"com.affine.auth.change.email.page.success.title": "Email address updated!",
+ "com.affine.auth.verify.email.page.success.title": "Email address verified!",
+ "com.affine.auth.verify.email.page.success.subtitle": "Congratulations! You have successfully verified the email address associated with your AFFiNE Cloud account.",
"com.affine.auth.change.email.page.title": "Change email address",
"com.affine.auth.create.count": "Create Account",
"com.affine.auth.desktop.signing.in": "Signing in...",
@@ -430,11 +432,11 @@
"com.affine.auth.reset.password.message": "You will receive an email with a link to reset your password. Please check your inbox.",
"com.affine.auth.reset.password.page.success": "Password reset successful",
"com.affine.auth.reset.password.page.title": "Reset your AFFiNE Cloud password",
- "com.affine.auth.send.change.email.link": "Send verification link",
+ "com.affine.auth.send.verify.email.hint": "Send verification link",
"com.affine.auth.send.reset.password.link": "Send reset link",
"com.affine.auth.send.set.password.link": "Send set link",
"com.affine.auth.sent": "Sent",
- "com.affine.auth.sent.change.email.hint": "Verification link has been sent.",
+ "com.affine.auth.sent.verify.email.hint": "Verification link has been sent.",
"com.affine.auth.sent.change.email.fail": "The verification link failed to be sent, please try again later.",
"com.affine.auth.sent.change.password.hint": "Reset password link has been sent.",
"com.affine.auth.sent.reset.password.success.message": "Your password has upgraded! You can sign in AFFiNE Cloud with new password!",
@@ -951,7 +953,8 @@
"com.affine.settings.auto-check-description": "If enabled, it will automatically check for new versions at regular intervals.",
"com.affine.settings.auto-download-description": " If enabled, new versions will be automatically downloaded to the current device.",
"com.affine.settings.email": "Email",
- "com.affine.settings.email.action": "Change Email",
+ "com.affine.settings.email.action.change": "Change Email",
+ "com.affine.settings.email.action.verify": "Verify Email",
"com.affine.settings.member-tooltip": "Enable AFFiNE Cloud to collaborate with others",
"com.affine.settings.noise-style": "Noise background on the sidebar",
"com.affine.settings.noise-style-description": "Use background noise effect on the sidebar.",
diff --git a/packages/frontend/i18n/src/resources/fr.json b/packages/frontend/i18n/src/resources/fr.json
index 9079833774..468fd8bea1 100644
--- a/packages/frontend/i18n/src/resources/fr.json
+++ b/packages/frontend/i18n/src/resources/fr.json
@@ -406,7 +406,7 @@
"com.affine.appearanceSettings.windowFrame.description": "Personnalisez l'apparence de l'application Windows",
"com.affine.appearanceSettings.windowFrame.frameless": "Sans Bords",
"com.affine.appearanceSettings.windowFrame.title": "Style de fenêtre",
- "com.affine.auth.change.email.message": "Votre email actuel est {{email}}. Nous enverrons un lien de vérification temporaire à cette addresse.",
+ "com.affine.auth.verify.email.message": "Votre email actuel est {{email}}. Nous enverrons un lien de vérification temporaire à cette addresse.",
"com.affine.auth.change.email.page.subtitle": "Rentrez votre nouvelle adresse mail en dessous. Nous enverrons un lien de vérification à cette adresse mail pour compléter le processus",
"com.affine.auth.change.email.page.success.subtitle": "Félicitation ! Vous avez réussi à mettre à jour votre adresse mail associé avec votre compte AFFiNE cloud ",
"com.affine.auth.change.email.page.success.title": "Adresse mail mise à jour !",
@@ -430,11 +430,11 @@
"com.affine.auth.reset.password.message": "Vous allez recevoir un mail avec un lien pour réinitialiser votre mot de passe. Merci de vérifier votre boite de réception",
"com.affine.auth.reset.password.page.success": "Mot de passe réinitialisé avec succès",
"com.affine.auth.reset.password.page.title": "Réinitialiser votre mot de passe AFFiNE Cloud",
- "com.affine.auth.send.change.email.link": "Envoyer un lien de vérification",
+ "com.affine.auth.send.verify.email.hint": "Envoyer un lien de vérification",
"com.affine.auth.send.reset.password.link": "Envoyer un lien de réinitialisation",
"com.affine.auth.send.set.password.link": "Envoyer un lien pour définir votre mot de passe",
"com.affine.auth.sent": "Envoyé",
- "com.affine.auth.sent.change.email.hint": "Le lien de vérification a été envoyé",
+ "com.affine.auth.sent.verify.email.hint": "Le lien de vérification a été envoyé",
"com.affine.auth.sent.change.password.hint": "Le lien de réinitialisation de mot de passe a été envoyé",
"com.affine.auth.sent.reset.password.success.message": "Votre mot de passe a été changé ! Vous pouvez à nouveau vous connecter à AFFiNE Cloud avec votre nouveau mot de passe ! ",
"com.affine.auth.sent.set.password.hint": "Le lien pour définir votre mot de passe à été envoyé",
@@ -788,7 +788,7 @@
"com.affine.settings.auto-check-description": "Si activé, l'option cherchera automatiquement pour les nouvelles versions à intervalles réguliers",
"com.affine.settings.auto-download-description": "Si activé, les nouvelles versions seront automatiquement téléchargées sur l'appareil actuel",
"com.affine.settings.email": "Email",
- "com.affine.settings.email.action": "Changer l'Email",
+ "com.affine.settings.email.action.change": "Changer l'Email",
"com.affine.settings.member-tooltip": "Activer AFFiNE Cloud pour collaborer avec d'autres personnes",
"com.affine.settings.noise-style": "Bruit d'arrière-plan de la barre latérale",
"com.affine.settings.noise-style-description": "Utiliser l'effet de bruit d'arrière-plan sur la barre latérale",
diff --git a/packages/frontend/i18n/src/resources/ko.json b/packages/frontend/i18n/src/resources/ko.json
index 4cd2805a12..02bb7cddb9 100644
--- a/packages/frontend/i18n/src/resources/ko.json
+++ b/packages/frontend/i18n/src/resources/ko.json
@@ -406,7 +406,7 @@
"com.affine.appearanceSettings.windowFrame.description": "Windows 클라이언트의 모양을 사용자 정의합니다.",
"com.affine.appearanceSettings.windowFrame.frameless": "프레임 없이",
"com.affine.appearanceSettings.windowFrame.title": "윈도우 프레임 스타일",
- "com.affine.auth.change.email.message": "현재 이메일은 {{email}}입니다. 이 이메일 주소로 임시 인증 링크를 보내 드리겠습니다.",
+ "com.affine.auth.verify.email.message": "현재 이메일은 {{email}}입니다. 이 이메일 주소로 임시 인증 링크를 보내 드리겠습니다.",
"com.affine.auth.change.email.page.subtitle": "아래에 새 이메일 주소를 입력하세요. 절차를 완료하기 위해 이 이메일 주소로 인증 링크를 보내드립니다.",
"com.affine.auth.change.email.page.success.subtitle": "축하합니다! AFFiNE Cloud 계정과 연결된 이메일 주소를 성공적으로 업데이트했습니다.",
"com.affine.auth.change.email.page.success.title": "이메일 주소를 업데이트했습니다!",
@@ -430,11 +430,11 @@
"com.affine.auth.reset.password.message": "비밀번호를 재설정할 수 있는 링크가 포함된 이메일을 받게 됩니다. 받은 편지함을 확인해 주세요.",
"com.affine.auth.reset.password.page.success": "비밀번호 재설정 성공",
"com.affine.auth.reset.password.page.title": "AFFiNE Cloud 비밀번호 재설정",
- "com.affine.auth.send.change.email.link": "인증 링크 전송",
+ "com.affine.auth.send.verify.email.hint": "인증 링크 전송",
"com.affine.auth.send.reset.password.link": "재설정 링크 전송",
"com.affine.auth.send.set.password.link": "설정 링크 전송",
"com.affine.auth.sent": "보냄",
- "com.affine.auth.sent.change.email.hint": "인증 링크를 보냈습니다.",
+ "com.affine.auth.sent.verify.email.hint": "인증 링크를 보냈습니다.",
"com.affine.auth.sent.change.password.hint": "비밀번호 재설정 링크를 보냈습니다.",
"com.affine.auth.sent.reset.password.success.message": "비밀번호가 업그레이드했습니다! 새 비밀번호로 AFFiNE Cloud에 로그인할 수 있습니다!",
"com.affine.auth.sent.set.password.hint": "비밀번호 설정 링크를 보냈습니다.",
@@ -908,7 +908,7 @@
"com.affine.settings.auto-check-description": "이 기능을 활성화하면, 정기적으로 새 버전을 자동으로 확인합니다.",
"com.affine.settings.auto-download-description": "이 기능을 활성화하면, 새 버전이 현재 디바이스에 자동으로 다운로드됩니다.",
"com.affine.settings.email": "이메일",
- "com.affine.settings.email.action": "이메일 변경",
+ "com.affine.settings.email.action.change": "이메일 변경",
"com.affine.settings.member-tooltip": "다른 사람들과 협업할 수 있는 AFFiNE Cloud 활성화",
"com.affine.settings.noise-style": "Noise background on the sidebar",
"com.affine.settings.noise-style-description": "Use background noise effect on the sidebar.",
diff --git a/packages/frontend/i18n/src/resources/pt-BR.json b/packages/frontend/i18n/src/resources/pt-BR.json
index ce6e358b6c..8c41041054 100644
--- a/packages/frontend/i18n/src/resources/pt-BR.json
+++ b/packages/frontend/i18n/src/resources/pt-BR.json
@@ -337,11 +337,11 @@
"com.affine.auth.reset.password": "Redefinir Senha",
"com.affine.auth.reset.password.message": "Você receberá um email com um link para redefinir sua senha. Por favor verifique sua caixa de entrada.",
"com.affine.auth.reset.password.page.title": "Redefina sua senha da AFFiNE Cloud",
- "com.affine.auth.send.change.email.link": "Envie um link de verificação",
+ "com.affine.auth.send.verify.email.hint": "Envie um link de verificação",
"com.affine.auth.send.reset.password.link": "Enviar link de redefinição",
"com.affine.auth.send.set.password.link": "Enviar link de definição",
"com.affine.auth.sent": "Enviado",
- "com.affine.auth.sent.change.email.hint": "Link de verificação foi enviado.",
+ "com.affine.auth.sent.verify.email.hint": "Link de verificação foi enviado.",
"com.affine.auth.sent.change.password.hint": "Link de redefinição de senha foi enviado.",
"com.affine.auth.set.email.save": "Salvar Email",
"com.affine.auth.set.password.page.title": "Defina sua senha para AFFiNE Cloud",
@@ -422,7 +422,7 @@
"com.affine.settings.auto-check-description": "Se ativado, ele verificará automaticamente novas versões em intervalos regulares.",
"com.affine.settings.auto-download-description": "Se ativado, novas versões serão baixadas automaticamente para o dispositivo atual.",
"com.affine.settings.email": "Email",
- "com.affine.settings.email.action": "Mudar Email",
+ "com.affine.settings.email.action.change": "Mudar Email",
"com.affine.settings.password": "Senha",
"com.affine.settings.password.action.change": "Mudar senha",
"com.affine.settings.profile": "Meu Perfil",
diff --git a/packages/frontend/i18n/src/resources/ru.json b/packages/frontend/i18n/src/resources/ru.json
index 167332b6cc..1776d7e444 100644
--- a/packages/frontend/i18n/src/resources/ru.json
+++ b/packages/frontend/i18n/src/resources/ru.json
@@ -318,10 +318,10 @@
"com.affine.auth.reset.password": "Восстановить пароль",
"com.affine.auth.reset.password.message": "Вы получите письмо со ссылкой для восстановления пароля. Пожалуйста, проверьте свой почтовый ящик.",
"com.affine.auth.reset.password.page.title": "Восстановить пароль AFFiNE Cloud",
- "com.affine.auth.send.change.email.link": "Отправить ссылку для подтверждения",
+ "com.affine.auth.send.verify.email.hint": "Отправить ссылку для подтверждения",
"com.affine.auth.send.reset.password.link": "Отправить ссылку для восстановления",
"com.affine.auth.sent": "Отправлено",
- "com.affine.auth.sent.change.email.hint": "Ссылка для подтверждения отправлена.",
+ "com.affine.auth.sent.verify.email.hint": "Ссылка для подтверждения отправлена.",
"com.affine.auth.sent.change.password.hint": "Ссылка для восстановления пароля отправлена.",
"com.affine.auth.sent.set.password.hint": "Ссылка для установки пароля отправлена.",
"com.affine.auth.set.email.save": "Сохранить электронную почту",
diff --git a/packages/frontend/i18n/src/resources/zh-Hans.json b/packages/frontend/i18n/src/resources/zh-Hans.json
index 212f712dfd..b2e1f21a68 100644
--- a/packages/frontend/i18n/src/resources/zh-Hans.json
+++ b/packages/frontend/i18n/src/resources/zh-Hans.json
@@ -394,7 +394,7 @@
"com.affine.appearanceSettings.windowFrame.description": "自定义 Windows 客户端外观。",
"com.affine.appearanceSettings.windowFrame.frameless": "无边框",
"com.affine.appearanceSettings.windowFrame.title": "视窗样式",
- "com.affine.auth.change.email.message": "您当前的邮箱是 {{email}}。我们将向此邮箱发送一个临时的验证链接。",
+ "com.affine.auth.verify.email.message": "您当前的邮箱是 {{email}}。我们将向此邮箱发送一个临时的验证链接。",
"com.affine.auth.change.email.page.subtitle": "请在下方输入您的新电子邮件地址。我们将把验证链接发送至该电子邮件地址以完成此过程。",
"com.affine.auth.change.email.page.success.subtitle": "恭喜!您已更新了与 AFFiNE Cloud 账户关联的电子邮件地址。",
"com.affine.auth.change.email.page.success.title": "邮箱地址已更新!",
@@ -418,11 +418,11 @@
"com.affine.auth.reset.password.message": "您将收到一封电子邮件,以便重置密码。请在收件箱中查收。",
"com.affine.auth.reset.password.page.success": "密码重置成功",
"com.affine.auth.reset.password.page.title": "重置您的 AFFiNE Cloud 密码",
- "com.affine.auth.send.change.email.link": "发送验证链接",
+ "com.affine.auth.send.verify.email.hint": "发送验证链接",
"com.affine.auth.send.reset.password.link": "发送重置链接",
"com.affine.auth.send.set.password.link": "发送设置链接",
"com.affine.auth.sent": "已发送",
- "com.affine.auth.sent.change.email.hint": "验证链接已发送",
+ "com.affine.auth.sent.verify.email.hint": "验证链接已发送",
"com.affine.auth.sent.change.password.hint": "重置密码链接已发送。",
"com.affine.auth.sent.reset.password.success.message": "您的密码已更新!您可以使用新密码登录 AFFiNE Cloud!",
"com.affine.auth.sent.set.password.hint": "设置密码链接已发送。",
@@ -821,7 +821,8 @@
"com.affine.settings.auto-check-description": "如果启用,它将定期自动检查新版本。",
"com.affine.settings.auto-download-description": "如果启用,新版本将自动下载到当前设备。",
"com.affine.settings.email": "电子邮件",
- "com.affine.settings.email.action": "更改邮箱",
+ "com.affine.settings.email.action.change": "更改邮箱",
+ "com.affine.settings.email.action.verify": "验证邮箱",
"com.affine.settings.member-tooltip": "启用 AFFiNE Cloud 以与他人协作",
"com.affine.settings.noise-style": "侧边栏的噪点背景",
"com.affine.settings.noise-style-description": "在侧边栏使用噪点背景效果。",
diff --git a/packages/frontend/i18n/src/resources/zh-Hant.json b/packages/frontend/i18n/src/resources/zh-Hant.json
index 4e7a941f8c..45e570445a 100644
--- a/packages/frontend/i18n/src/resources/zh-Hant.json
+++ b/packages/frontend/i18n/src/resources/zh-Hant.json
@@ -336,11 +336,11 @@
"com.affine.auth.reset.password": "重設密碼",
"com.affine.auth.reset.password.message": "您將收到一封電子郵件,其中包含重設密碼的連結。請檢查您的收件箱。",
"com.affine.auth.reset.password.page.title": "重設您的 AFFiNE Cloud 密碼",
- "com.affine.auth.send.change.email.link": "發送驗證連結",
+ "com.affine.auth.send.verify.email.hint": "發送驗證連結",
"com.affine.auth.send.reset.password.link": "發送重設連結",
"com.affine.auth.send.set.password.link": "發送設定連結",
"com.affine.auth.sent": "已發送",
- "com.affine.auth.sent.change.email.hint": "驗證連結已發送。",
+ "com.affine.auth.sent.verify.email.hint": "驗證連結已發送。",
"com.affine.auth.sent.change.password.hint": "重設密碼連結已發送。",
"com.affine.auth.sent.set.password.hint": "設定密碼連結已發送。",
"com.affine.auth.set.email.save": "保存電子郵件地址",
@@ -438,7 +438,8 @@
"com.affine.settings.auto-check-description": "若啟用,將定期自動檢測新版本。",
"com.affine.settings.auto-download-description": "若啟用,將自動下載新版本。",
"com.affine.settings.email": "電子郵件地址",
- "com.affine.settings.email.action": "更改電子郵件地址",
+ "com.affine.settings.email.action.change": "更改電子郵件地址",
+ "com.affine.settings.email.action.verify": "验证電子郵件地址",
"com.affine.settings.member-tooltip": "啟用 AFFiNE Cloud 以與他人協作",
"com.affine.settings.noise-style": "側欄背景雜訊效果",
"com.affine.settings.noise-style-description": "在側欄背景使用雜訊效果。",
diff --git a/packages/frontend/native/index.js b/packages/frontend/native/index.js
index 33cc759db6..2aae7d108d 100644
--- a/packages/frontend/native/index.js
+++ b/packages/frontend/native/index.js
@@ -88,7 +88,7 @@ switch (platform) {
}
break
default:
- throw new Error(`Unsupported architecture on Android ${arch}`)
+ loadError = new Error(`Unsupported architecture on Android ${arch}`)
}
break
case 'win32':
@@ -136,7 +136,7 @@ switch (platform) {
}
break
default:
- throw new Error(`Unsupported architecture on Windows: ${arch}`)
+ loadError = new Error(`Unsupported architecture on Windows: ${arch}`)
}
break
case 'darwin':
@@ -177,22 +177,37 @@ switch (platform) {
}
break
default:
- throw new Error(`Unsupported architecture on macOS: ${arch}`)
+ loadError = new Error(`Unsupported architecture on macOS: ${arch}`)
}
break
case 'freebsd':
- if (arch !== 'x64') {
- throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
- }
- localFileExisted = existsSync(join(__dirname, 'affine.freebsd-x64.node'))
- try {
- if (localFileExisted) {
- nativeBinding = require('./affine.freebsd-x64.node')
- } else {
- nativeBinding = require('@affine/native-freebsd-x64')
- }
- } catch (e) {
- loadError = e
+ switch (arch) {
+ case 'x64':
+ localFileExisted = existsSync(join(__dirname, 'affine.freebsd-x64.node'))
+ try {
+ if (localFileExisted) {
+ nativeBinding = require('./affine.freebsd-x64.node')
+ } else {
+ nativeBinding = require('@affine/native-freebsd-x64')
+ }
+ } catch (e) {
+ loadError = e
+ }
+ break
+ case 'arm64':
+ localFileExisted = existsSync(join(__dirname, 'affine.freebsd-arm64.node'))
+ try {
+ if (localFileExisted) {
+ nativeBinding = require('./affine.freebsd-arm64.node')
+ } else {
+ nativeBinding = require('@affine/native-freebsd-arm64')
+ }
+ } catch (e) {
+ loadError = e
+ }
+ break
+ default:
+ loadError = new Error(`Unsupported architecture on FreeBSD: ${arch}`)
}
break
case 'linux':
@@ -298,25 +313,43 @@ switch (platform) {
}
}
break
+ case 's390x':
+ localFileExisted = existsSync(
+ join(__dirname, 'affine.linux-s390x-gnu.node')
+ )
+ try {
+ if (localFileExisted) {
+ nativeBinding = require('./affine.linux-s390x-gnu.node')
+ } else {
+ nativeBinding = require('@affine/native-linux-s390x-gnu')
+ }
+ } catch (e) {
+ loadError = e
+ }
+ break
default:
- throw new Error(`Unsupported architecture on Linux: ${arch}`)
+ loadError = new Error(`Unsupported architecture on Linux: ${arch}`)
}
break
default:
- throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
+ loadError = new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
}
if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) {
try {
nativeBinding = require('./affine.wasi.cjs')
- } catch {
- // ignore
+ } catch (err) {
+ if (process.env.NAPI_RS_FORCE_WASI) {
+ console.error(err)
+ }
}
if (!nativeBinding) {
try {
nativeBinding = require('@affine/native-wasm32-wasi')
} catch (err) {
- console.error(err)
+ if (process.env.NAPI_RS_FORCE_WASI) {
+ console.error(err)
+ }
}
}
}
diff --git a/packages/frontend/templates/templates.gen.ts b/packages/frontend/templates/templates.gen.ts
index d95d5c0eed..6858f0b2b3 100644
--- a/packages/frontend/templates/templates.gen.ts
+++ b/packages/frontend/templates/templates.gen.ts
@@ -1,11 +1,11 @@
/* eslint-disable simple-import-sort/imports */
// Auto generated, do not edit manually
-import json_0 from './onboarding/W-d9_llZ6rE-qoTiHKTk4.snapshot.json';
-import json_1 from './onboarding/info.json';
-import json_2 from './onboarding/blob.json';
+import json_0 from './onboarding/info.json';
+import json_1 from './onboarding/blob.json';
+import json_2 from './onboarding/W-d9_llZ6rE-qoTiHKTk4.snapshot.json';
export const onboarding = {
- 'W-d9_llZ6rE-qoTiHKTk4.snapshot.json': json_0,
- 'info.json': json_1,
- 'blob.json': json_2
+ 'info.json': json_0,
+ 'blob.json': json_1,
+ 'W-d9_llZ6rE-qoTiHKTk4.snapshot.json': json_2
}
\ No newline at end of file
diff --git a/packages/frontend/workspace-impl/package.json b/packages/frontend/workspace-impl/package.json
index f735db316b..4f6db36f6c 100644
--- a/packages/frontend/workspace-impl/package.json
+++ b/packages/frontend/workspace-impl/package.json
@@ -21,7 +21,6 @@
"is-svg": "^5.0.0",
"lodash-es": "^4.17.21",
"nanoid": "^5.0.6",
- "next-auth": "^4.24.5",
"socket.io-client": "^4.7.4",
"y-protocols": "^1.0.6",
"yjs": "^13.6.12"
diff --git a/packages/frontend/workspace-impl/src/cloud/list.ts b/packages/frontend/workspace-impl/src/cloud/list.ts
index 38965fa2b3..6c54e9bdf2 100644
--- a/packages/frontend/workspace-impl/src/cloud/list.ts
+++ b/packages/frontend/workspace-impl/src/cloud/list.ts
@@ -2,6 +2,7 @@ import { WorkspaceFlavour } from '@affine/env/workspace';
import {
createWorkspaceMutation,
deleteWorkspaceMutation,
+ findGraphQLError,
getWorkspacesQuery,
} from '@affine/graphql';
import { fetcher } from '@affine/graphql';
@@ -16,7 +17,6 @@ import {
import { globalBlockSuiteSchema } from '@toeverything/infra';
import { difference } from 'lodash-es';
import { nanoid } from 'nanoid';
-import { getSession } from 'next-auth/react';
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import { IndexedDBBlobStorage } from '../local/blob-indexeddb';
@@ -27,13 +27,11 @@ import { CLOUD_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY } from './consts';
import { AffineStaticSyncStorage } from './sync';
async function getCloudWorkspaceList() {
- const session = await getSession();
- if (!session) {
- return [];
- }
try {
const { workspaces } = await fetcher({
query: getWorkspacesQuery,
+ }).catch(() => {
+ return { workspaces: [] };
});
const ids = workspaces.map(({ id }) => id);
return ids.map(id => ({
@@ -41,10 +39,13 @@ async function getCloudWorkspaceList() {
flavour: WorkspaceFlavour.AFFINE_CLOUD,
}));
} catch (err) {
- if (err instanceof Array && err[0]?.message === 'Forbidden resource') {
+ console.log(err);
+ const e = findGraphQLError(err, e => e.extensions.code === 401);
+ if (e) {
// user not logged in
return [];
}
+
throw err;
}
}
diff --git a/tests/affine-cloud/e2e/login.spec.ts b/tests/affine-cloud/e2e/login.spec.ts
index 6bfca65460..6ede51db77 100644
--- a/tests/affine-cloud/e2e/login.spec.ts
+++ b/tests/affine-cloud/e2e/login.spec.ts
@@ -73,6 +73,7 @@ test.describe('login first', () => {
await page.getByTestId('workspace-modal-account-option').click();
await page.getByTestId('workspace-modal-sign-out-option').click();
await page.getByTestId('confirm-sign-out-button').click();
+ await page.reload();
await clickSideBarCurrentWorkspaceBanner(page);
const signInButton = page.getByTestId('cloud-signin-button');
await expect(signInButton).toBeVisible();
diff --git a/tests/kit/utils/cloud.ts b/tests/kit/utils/cloud.ts
index 7a54d1e91c..b73296a188 100644
--- a/tests/kit/utils/cloud.ts
+++ b/tests/kit/utils/cloud.ts
@@ -33,9 +33,7 @@ export async function getLatestMailMessage() {
export async function getLoginCookie(
context: BrowserContext
): Promise {
- return (await context.cookies()).find(
- c => c.name === 'next-auth.session-token'
- );
+ return (await context.cookies()).find(c => c.name === 'sid');
}
const cloudUserSchema = z.object({
@@ -106,7 +104,7 @@ export async function createRandomUser(): Promise<{
await client.user.create({
data: {
...user,
- emailVerified: new Date(),
+ emailVerifiedAt: new Date(),
password: await hash(user.password),
features: {
create: {
diff --git a/tests/storybook/.storybook/preview.tsx b/tests/storybook/.storybook/preview.tsx
index 9c5b0ade89..8d863c68dc 100644
--- a/tests/storybook/.storybook/preview.tsx
+++ b/tests/storybook/.storybook/preview.tsx
@@ -1,10 +1,6 @@
import '@affine/component/theme/global.css';
import '@affine/component/theme/theme.css';
import { createI18n } from '@affine/i18n';
-import MockSessionContext, {
- mockAuthStates,
- // @ts-ignore
-} from '@tomfreudenberg/next-auth-mock';
import { ThemeProvider, useTheme } from 'next-themes';
import { useDarkMode } from 'storybook-dark-mode';
import { AffineContext } from '@affine/component/context';
@@ -40,51 +36,6 @@ export const parameters = {
},
};
-const SB_PARAMETER_KEY = 'nextAuthMock';
-export const mockAuthPreviewToolbarItem = ({
- name = 'mockAuthState',
- description = 'Set authentication state',
- defaultValue = null,
- icon = 'user',
- items = mockAuthStates,
-} = {}) => {
- return {
- mockAuthState: {
- name,
- description,
- defaultValue,
- toolbar: {
- icon,
- items: Object.keys(items).map(e => ({
- value: e,
- title: items[e].title,
- })),
- },
- },
- };
-};
-
-export const withMockAuth: Decorator = (Story, context) => {
- // Set a session value for mocking
- const session = (() => {
- // Allow overwrite of session value by parameter in story
- const paramValue = context?.parameters[SB_PARAMETER_KEY];
- if (typeof paramValue?.session === 'string') {
- return mockAuthStates[paramValue.session]?.session;
- } else {
- return paramValue?.session
- ? paramValue.session
- : mockAuthStates[context.globals.mockAuthState]?.session;
- }
- })();
-
- return (
-
-
-
- );
-};
-
const i18n = createI18n();
const withI18n: Decorator = (Story, context) => {
const locale = context.globals.locale;
@@ -198,7 +149,6 @@ const withPlatformSelectionDecorator: Decorator = (Story, context) => {
const decorators = [
withContextDecorator,
withI18n,
- withMockAuth,
withPlatformSelectionDecorator,
];
diff --git a/tests/storybook/package.json b/tests/storybook/package.json
index 984bb28bab..b26a1f0f82 100644
--- a/tests/storybook/package.json
+++ b/tests/storybook/package.json
@@ -40,7 +40,6 @@
"@storybook/react": "^7.6.17",
"@storybook/react-vite": "^7.6.17",
"@storybook/test-runner": "^0.16.0",
- "@tomfreudenberg/next-auth-mock": "^0.5.6",
"@vanilla-extract/esbuild-plugin": "^2.3.5",
"@vitejs/plugin-react": "^4.2.1",
"chromatic": "^11.0.0",
diff --git a/yarn.lock b/yarn.lock
index 2f9d5ef3bf..5334f74a27 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -374,7 +374,6 @@ __metadata:
mime-types: "npm:^2.1.35"
mini-css-extract-plugin: "npm:^2.8.0"
nanoid: "npm:^5.0.6"
- next-auth: "npm:^4.24.5"
next-themes: "npm:^0.2.1"
postcss-loader: "npm:^8.1.0"
raw-loader: "npm:^4.0.2"
@@ -696,7 +695,6 @@ __metadata:
nanoid: "npm:^5.0.6"
nest-commander: "npm:^3.12.5"
nestjs-throttler-storage-redis: "npm:^0.4.1"
- next-auth: "npm:^4.24.5"
nodemailer: "npm:^6.9.10"
nodemon: "npm:^3.1.0"
on-headers: "npm:^1.0.2"
@@ -761,7 +759,6 @@ __metadata:
"@storybook/react-vite": "npm:^7.6.17"
"@storybook/test-runner": "npm:^0.16.0"
"@storybook/testing-library": "npm:^0.2.2"
- "@tomfreudenberg/next-auth-mock": "npm:^0.5.6"
"@vanilla-extract/esbuild-plugin": "npm:^2.3.5"
"@vitejs/plugin-react": "npm:^4.2.1"
chromatic: "npm:^11.0.0"
@@ -820,7 +817,6 @@ __metadata:
is-svg: "npm:^5.0.0"
lodash-es: "npm:^4.17.21"
nanoid: "npm:^5.0.6"
- next-auth: "npm:^4.24.5"
socket.io-client: "npm:^4.7.4"
vitest: "npm:1.3.1"
ws: "npm:^8.16.0"
@@ -3358,7 +3354,7 @@ __metadata:
languageName: node
linkType: hard
-"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.6, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2":
+"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.6, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2":
version: 7.23.9
resolution: "@babel/runtime@npm:7.23.9"
dependencies:
@@ -9346,7 +9342,7 @@ __metadata:
languageName: node
linkType: hard
-"@panva/hkdf@npm:^1.0.2, @panva/hkdf@npm:^1.1.1":
+"@panva/hkdf@npm:^1.1.1":
version: 1.1.1
resolution: "@panva/hkdf@npm:1.1.1"
checksum: 10/f0dd12903751d8792420353f809ed3c7de860cf506399759fff5f59f7acfef8a77e2b64012898cee7e5b047708fa0bd91dff5ef55a502bf8ea11aad9842160da
@@ -13334,16 +13330,6 @@ __metadata:
languageName: node
linkType: hard
-"@tomfreudenberg/next-auth-mock@npm:^0.5.6":
- version: 0.5.6
- resolution: "@tomfreudenberg/next-auth-mock@npm:0.5.6"
- peerDependencies:
- next-auth: ^4.12.3
- react: ^18
- checksum: 10/50396706be6f3e806d130df3945dce4233504782f0f16fd6d255d54ef21ae713b9eedf3a93155de29f92f59d3592bd540a60a15edfffa4b6306c7a3c786aaae2
- languageName: node
- linkType: hard
-
"@tootallnate/once@npm:2":
version: 2.0.0
resolution: "@tootallnate/once@npm:2.0.0"
@@ -24702,13 +24688,6 @@ __metadata:
languageName: node
linkType: hard
-"jose@npm:^4.11.4, jose@npm:^4.15.1":
- version: 4.15.4
- resolution: "jose@npm:4.15.4"
- checksum: 10/20fa941597150dffc7af3f41d994500cc3e71cd650b755243dbd80d91cf26c1053f95b78af588f05cfc4371e492a67c5c7a48f689b8605145a8fe28b484d725b
- languageName: node
- linkType: hard
-
"jose@npm:^5.0.0, jose@npm:^5.1.3":
version: 5.2.2
resolution: "jose@npm:5.2.2"
@@ -27654,56 +27633,6 @@ __metadata:
languageName: node
linkType: hard
-"next-auth@npm:4.24.5":
- version: 4.24.5
- resolution: "next-auth@npm:4.24.5"
- dependencies:
- "@babel/runtime": "npm:^7.20.13"
- "@panva/hkdf": "npm:^1.0.2"
- cookie: "npm:^0.5.0"
- jose: "npm:^4.11.4"
- oauth: "npm:^0.9.15"
- openid-client: "npm:^5.4.0"
- preact: "npm:^10.6.3"
- preact-render-to-string: "npm:^5.1.19"
- uuid: "npm:^8.3.2"
- peerDependencies:
- next: ^12.2.5 || ^13 || ^14
- nodemailer: ^6.6.5
- react: ^17.0.2 || ^18
- react-dom: ^17.0.2 || ^18
- peerDependenciesMeta:
- nodemailer:
- optional: true
- checksum: 10/c9256deaa7a77741be2c8829c290c43c63fd8fa86ace3196910d3fa4389c101d6a610f3c5f4b55000e766a51dd89eafc9b5cd876e373884db3bf90122fdfa6a1
- languageName: node
- linkType: hard
-
-"next-auth@patch:next-auth@npm%3A4.24.5#~/.yarn/patches/next-auth-npm-4.24.5-8428e11927.patch":
- version: 4.24.5
- resolution: "next-auth@patch:next-auth@npm%3A4.24.5#~/.yarn/patches/next-auth-npm-4.24.5-8428e11927.patch::version=4.24.5&hash=9af7e1"
- dependencies:
- "@babel/runtime": "npm:^7.20.13"
- "@panva/hkdf": "npm:^1.0.2"
- cookie: "npm:^0.5.0"
- jose: "npm:^4.11.4"
- oauth: "npm:^0.9.15"
- openid-client: "npm:^5.4.0"
- preact: "npm:^10.6.3"
- preact-render-to-string: "npm:^5.1.19"
- uuid: "npm:^8.3.2"
- peerDependencies:
- next: ^12.2.5 || ^13 || ^14
- nodemailer: ^6.6.5
- react: ^17.0.2 || ^18
- react-dom: ^17.0.2 || ^18
- peerDependenciesMeta:
- nodemailer:
- optional: true
- checksum: 10/15f251a6e31c79459bce7a2d638c6069c34b5e92effdae8d7b2c366bbe2d1e1916da6ed5bc7995c1926dd35442552deb33959ee4bd45bbab0347455c13448d4b
- languageName: node
- linkType: hard
-
"next-themes@npm:^0.2.1":
version: 0.2.1
resolution: "next-themes@npm:0.2.1"
@@ -28257,13 +28186,6 @@ __metadata:
languageName: node
linkType: hard
-"oauth@npm:^0.9.15":
- version: 0.9.15
- resolution: "oauth@npm:0.9.15"
- checksum: 10/6b0b10be19a461da417a37ea2821a773ef74dd667563291e1e83b2024b88e6571b0323a0a6887f2390fbaf28cc6ce5bfe0484fc22162b975305b1e19b76f5597
- languageName: node
- linkType: hard
-
"object-assign@npm:^4, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
@@ -28278,13 +28200,6 @@ __metadata:
languageName: node
linkType: hard
-"object-hash@npm:^2.2.0":
- version: 2.2.0
- resolution: "object-hash@npm:2.2.0"
- checksum: 10/dee06b6271bf5769ae5f1a7386fdd52c1f18aae9fcb0b8d4bb1232f2d743d06cb5b662be42378b60a1c11829f96f3f86834a16bbaa57a085763295fff8b93e27
- languageName: node
- linkType: hard
-
"object-is@npm:@nolyfill/object-is@latest":
version: 1.0.24
resolution: "@nolyfill/object-is@npm:1.0.24"
@@ -28362,13 +28277,6 @@ __metadata:
languageName: node
linkType: hard
-"oidc-token-hash@npm:^5.0.3":
- version: 5.0.3
- resolution: "oidc-token-hash@npm:5.0.3"
- checksum: 10/35fa19aea9ff2c509029ec569d74b778c8a215b92bd5e6e9bc4ebbd7ab035f44304ff02430a6397c3fb7c1d15ebfa467807ca0bcd31d06ba610b47798287d303
- languageName: node
- linkType: hard
-
"on-finished@npm:2.4.1":
version: 2.4.1
resolution: "on-finished@npm:2.4.1"
@@ -28454,18 +28362,6 @@ __metadata:
languageName: node
linkType: hard
-"openid-client@npm:^5.4.0":
- version: 5.6.1
- resolution: "openid-client@npm:5.6.1"
- dependencies:
- jose: "npm:^4.15.1"
- lru-cache: "npm:^6.0.0"
- object-hash: "npm:^2.2.0"
- oidc-token-hash: "npm:^5.0.3"
- checksum: 10/8f2485438048def1bab680a634fd4ebb85bfb0d6a12d6490ef7a0f8189688db1920fff831ed23e70f59bc15d51ba6a33fca1313f0fba28b162c61e81c7e0649c
- languageName: node
- linkType: hard
-
"optionator@npm:^0.9.3":
version: 0.9.3
resolution: "optionator@npm:0.9.3"
@@ -29710,17 +29606,6 @@ __metadata:
languageName: node
linkType: hard
-"preact-render-to-string@npm:^5.1.19":
- version: 5.2.6
- resolution: "preact-render-to-string@npm:5.2.6"
- dependencies:
- pretty-format: "npm:^3.8.0"
- peerDependencies:
- preact: ">=10"
- checksum: 10/356519f7640d1c49e11b4837b41a83b307f3f237f93de153b9dde833a701e3ce5cf1d45cb18e37a3ec9c568555e2f5373c128d8b5f6ef79de7658f3c400d3e70
- languageName: node
- linkType: hard
-
"preact@npm:10.11.3":
version: 10.11.3
resolution: "preact@npm:10.11.3"
@@ -29728,13 +29613,6 @@ __metadata:
languageName: node
linkType: hard
-"preact@npm:^10.6.3":
- version: 10.19.2
- resolution: "preact@npm:10.19.2"
- checksum: 10/1519050e79f0dec61aa85daa5dcba4a5294e89fb09ab53d5e1a215ef8526dd5ccdbe82a02842cc4875fa3ea076eee9697a7421c32ffcc6159007d27b13a60a8f
- languageName: node
- linkType: hard
-
"prelude-ls@npm:^1.2.1":
version: 1.2.1
resolution: "prelude-ls@npm:1.2.1"
From e333b4d348c96df6d295eca6034678796e7b606a Mon Sep 17 00:00:00 2001
From: Peng Xiao
Date: Tue, 12 Mar 2024 14:56:45 +0000
Subject: [PATCH 28/51] fix(core): make sidebar switch transition smooth
(#6085)
---
.../sidebar-header/sidebar-switch.css.ts | 11 ++++++++---
.../sidebar-header/sidebar-switch.tsx | 4 ++--
.../workbench/view/route-container.css.ts | 15 ++++++++++++++-
.../workbench/view/route-container.tsx | 19 +++++++++++++++----
4 files changed, 39 insertions(+), 10 deletions(-)
diff --git a/packages/frontend/core/src/components/app-sidebar/sidebar-header/sidebar-switch.css.ts b/packages/frontend/core/src/components/app-sidebar/sidebar-header/sidebar-switch.css.ts
index ee6d809f59..45c075d008 100644
--- a/packages/frontend/core/src/components/app-sidebar/sidebar-header/sidebar-switch.css.ts
+++ b/packages/frontend/core/src/components/app-sidebar/sidebar-header/sidebar-switch.css.ts
@@ -1,18 +1,23 @@
import { style } from '@vanilla-extract/css';
+
export const sidebarSwitch = style({
opacity: 0,
- display: 'none !important',
+ display: 'inline-flex',
overflow: 'hidden',
pointerEvents: 'none',
- transition: 'all .3s ease-in-out',
+ transition: 'max-width 0.2s ease-in-out, margin 0.3s ease-in-out',
selectors: {
'&[data-show=true]': {
+ maxWidth: '32px',
opacity: 1,
- display: 'inline-flex !important',
width: '32px',
flexShrink: 0,
fontSize: '24px',
pointerEvents: 'auto',
},
+ '&[data-show=false]': {
+ maxWidth: 0,
+ margin: '0 !important',
+ },
},
});
diff --git a/packages/frontend/core/src/components/app-sidebar/sidebar-header/sidebar-switch.tsx b/packages/frontend/core/src/components/app-sidebar/sidebar-header/sidebar-switch.tsx
index 818c9419f3..70e584d9ed 100644
--- a/packages/frontend/core/src/components/app-sidebar/sidebar-header/sidebar-switch.tsx
+++ b/packages/frontend/core/src/components/app-sidebar/sidebar-header/sidebar-switch.tsx
@@ -8,10 +8,10 @@ import { appSidebarOpenAtom } from '../index.jotai';
import * as styles from './sidebar-switch.css';
export const SidebarSwitch = ({
- show = true,
+ show,
className,
}: {
- show?: boolean;
+ show: boolean;
className?: string;
}) => {
const [open, setOpen] = useAtom(appSidebarOpenAtom);
diff --git a/packages/frontend/core/src/modules/workbench/view/route-container.css.ts b/packages/frontend/core/src/modules/workbench/view/route-container.css.ts
index 2f3e423992..ceb85ee034 100644
--- a/packages/frontend/core/src/modules/workbench/view/route-container.css.ts
+++ b/packages/frontend/core/src/modules/workbench/view/route-container.css.ts
@@ -39,7 +39,20 @@ export const leftSidebarButton = style({
});
export const rightSidebarButton = style({
- margin: '0 0 0 16px',
+ transition: 'all 0.2s ease-in-out',
+ selectors: {
+ '&[data-show=true]': {
+ opacity: 1,
+ width: 32,
+ maxWidth: 32,
+ marginLeft: 16,
+ },
+ '&[data-show=false]': {
+ opacity: 0,
+ maxWidth: 0,
+ marginLeft: 0,
+ },
+ },
});
export const viewHeaderContainer = style({
diff --git a/packages/frontend/core/src/modules/workbench/view/route-container.tsx b/packages/frontend/core/src/modules/workbench/view/route-container.tsx
index d746c9a9e7..8d0a3d2bdd 100644
--- a/packages/frontend/core/src/modules/workbench/view/route-container.tsx
+++ b/packages/frontend/core/src/modules/workbench/view/route-container.tsx
@@ -25,12 +25,19 @@ export interface Props {
const ToggleButton = ({
onToggle,
className,
+ show,
}: {
onToggle?: () => void;
className: string;
+ show: boolean;
}) => {
return (
-
+
);
@@ -50,14 +57,18 @@ export const RouteContainer = ({ route }: Props) => {
return (
- {viewPosition.isFirst && !leftSidebarOpen && (
-
+ {viewPosition.isFirst && (
+
)}
- {viewPosition.isLast && !rightSidebarOpen && (
+ {viewPosition.isLast && (
<>
{rightSidebarHasViews && (
From 9f74cb32ea46eda3fe4b1708b376ae151c3887f4 Mon Sep 17 00:00:00 2001
From: Peng Xiao
Date: Tue, 12 Mar 2024 15:07:48 +0000
Subject: [PATCH 29/51] fix(component): input focus implementation (#6086)
---
.../frontend/component/src/ui/input/input.tsx | 22 +------------------
.../component/src/ui/input/style.css.ts | 3 ++-
2 files changed, 3 insertions(+), 22 deletions(-)
diff --git a/packages/frontend/component/src/ui/input/input.tsx b/packages/frontend/component/src/ui/input/input.tsx
index 2bc95b4910..bb8ec5b0ec 100644
--- a/packages/frontend/component/src/ui/input/input.tsx
+++ b/packages/frontend/component/src/ui/input/input.tsx
@@ -2,7 +2,6 @@ import clsx from 'clsx';
import type {
ChangeEvent,
CSSProperties,
- FocusEvent,
FocusEventHandler,
ForwardedRef,
InputHTMLAttributes,
@@ -10,7 +9,7 @@ import type {
KeyboardEventHandler,
ReactNode,
} from 'react';
-import { forwardRef, useCallback, useState } from 'react';
+import { forwardRef, useCallback } from 'react';
import { input, inputWrapper } from './style.css';
@@ -39,8 +38,6 @@ export const Input = forwardRef(function Input(
style = {},
inputStyle = {},
size = 'default',
- onFocus,
- onBlur,
preFix,
endFix,
onEnter,
@@ -50,8 +47,6 @@ export const Input = forwardRef(function Input(
}: InputProps,
ref: ForwardedRef
) {
- const [isFocus, setIsFocus] = useState(false);
-
const handleAutoFocus = useCallback((ref: HTMLInputElement | null) => {
if (ref) {
window.setTimeout(() => ref.focus(), 0);
@@ -64,7 +59,6 @@ export const Input = forwardRef(function Input(
// status
disabled: disabled,
'no-border': noBorder,
- focus: isFocus,
// color
error: status === 'error',
success: status === 'success',
@@ -87,20 +81,6 @@ export const Input = forwardRef(function Input(
ref={autoFocus ? handleAutoFocus : ref}
disabled={disabled}
style={inputStyle}
- onFocus={useCallback(
- (e: FocusEvent) => {
- setIsFocus(true);
- onFocus?.(e);
- },
- [onFocus]
- )}
- onBlur={useCallback(
- (e: FocusEvent) => {
- setIsFocus(false);
- onBlur?.(e);
- },
- [onBlur]
- )}
onChange={useCallback(
(e: ChangeEvent) => {
propsOnChange?.(e.target.value);
diff --git a/packages/frontend/component/src/ui/input/style.css.ts b/packages/frontend/component/src/ui/input/style.css.ts
index 254e993ef8..f0e6dc37d7 100644
--- a/packages/frontend/component/src/ui/input/style.css.ts
+++ b/packages/frontend/component/src/ui/input/style.css.ts
@@ -42,8 +42,9 @@ export const inputWrapper = style({
'&.default': {
borderColor: cssVar('borderColor'),
},
- '&.default.focus': {
+ '&.default:is(:focus-within, :focus, :focus-visible)': {
borderColor: cssVar('primaryColor'),
+ outline: 'none',
boxShadow: '0px 0px 0px 2px rgba(30, 150, 235, 0.30);',
},
},
From 3f27b7e5f76e4e1dae265de6afdd9e943d359d70 Mon Sep 17 00:00:00 2001
From: Peng Xiao
Date: Tue, 12 Mar 2024 15:18:43 +0000
Subject: [PATCH 30/51] fix(core): adjust suspense loading for some components
(#6088)
Adjust setting & user info popover suspense to make them a little bit more responsive
---
.../components/affine/setting-modal/index.tsx | 62 ++++++++++++++-----
.../affine/setting-modal/style.css.ts | 8 +++
.../user-with-workspace-list/index.css.ts | 7 +++
.../user-with-workspace-list/index.tsx | 29 +++++++--
4 files changed, 84 insertions(+), 22 deletions(-)
diff --git a/packages/frontend/core/src/components/affine/setting-modal/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/index.tsx
index 68e1371202..bad3cc6713 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/index.tsx
+++ b/packages/frontend/core/src/components/affine/setting-modal/index.tsx
@@ -1,3 +1,4 @@
+import { Loading } from '@affine/component';
import { WorkspaceDetailSkeleton } from '@affine/component/setting-components';
import { Modal, type ModalProps } from '@affine/component/ui/modal';
import {
@@ -36,7 +37,15 @@ export interface SettingProps extends ModalProps {
const isGeneralSetting = (key: string): key is GeneralSettingKey =>
GeneralSettingKeys.includes(key as GeneralSettingKey);
-export const SettingModal = ({
+const CenteredLoading = () => {
+ return (
+
+
+
+ );
+};
+
+const SettingModalInner = ({
activeTab = 'appearance',
workspaceMetadata = null,
onSettingClick,
@@ -95,27 +104,12 @@ export const SettingModal = ({
}, [setOpenStarAFFiNEModal]);
return (
-
+ <>
-
+ >
+ );
+};
+
+export const SettingModal = ({
+ activeTab = 'appearance',
+ workspaceMetadata = null,
+ onSettingClick,
+ ...modalProps
+}: SettingProps) => {
+ return (
+
+ }>
+
+
);
};
diff --git a/packages/frontend/core/src/components/affine/setting-modal/style.css.ts b/packages/frontend/core/src/components/affine/setting-modal/style.css.ts
index 57f98c750c..8b95a4e82a 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/style.css.ts
+++ b/packages/frontend/core/src/components/affine/setting-modal/style.css.ts
@@ -51,3 +51,11 @@ export const link = style({
color: cssVar('linkColor'),
cursor: 'pointer',
});
+
+export const centeredLoading = style({
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: '100%',
+ width: '100%',
+});
diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.css.ts b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.css.ts
index 26f78c7fe3..86650bbe09 100644
--- a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.css.ts
+++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.css.ts
@@ -49,3 +49,10 @@ export const signInTextSecondary = style({
export const menuItem = style({
borderRadius: '8px',
});
+export const loadingWrapper = style({
+ height: 42,
+ width: '100%',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+});
diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx
index 216335fce4..303db9826c 100644
--- a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx
+++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx
@@ -1,3 +1,4 @@
+import { Loading } from '@affine/component';
import { Divider } from '@affine/component/ui/divider';
import { MenuItem } from '@affine/component/ui/menu';
import { useSession } from '@affine/core/hooks/affine/use-current-user';
@@ -8,7 +9,7 @@ import { WorkspaceManager } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { useSetAtom } from 'jotai';
-import { useCallback, useEffect } from 'react';
+import { Suspense, useCallback, useEffect } from 'react';
import {
authAtom,
@@ -62,11 +63,21 @@ const SignInItem = () => {
);
};
-export const UserWithWorkspaceList = ({
- onEventEnd,
-}: {
+const UserWithWorkspaceListLoading = () => {
+ return (
+
+
+
+ );
+};
+
+interface UserWithWorkspaceListProps {
onEventEnd?: () => void;
-}) => {
+}
+
+const UserWithWorkspaceListInner = ({
+ onEventEnd,
+}: UserWithWorkspaceListProps) => {
const { user, status } = useSession();
const isAuthenticated = status === 'authenticated';
@@ -139,3 +150,11 @@ export const UserWithWorkspaceList = ({
);
};
+
+export const UserWithWorkspaceList = (props: UserWithWorkspaceListProps) => {
+ return (
+ }>
+
+
+ );
+};
From 20bce481325ebbdbed16a2188a79caaa8e046262 Mon Sep 17 00:00:00 2001
From: Peng Xiao
Date: Wed, 13 Mar 2024 02:07:50 +0000
Subject: [PATCH 31/51] fix: experimental settings sometimes not show (#6090)
---
.../components/affine/setting-modal/setting-sidebar/index.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/frontend/core/src/components/affine/setting-modal/setting-sidebar/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/setting-sidebar/index.tsx
index 47cb5f60a8..d3467a3339 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/setting-sidebar/index.tsx
+++ b/packages/frontend/core/src/components/affine/setting-modal/setting-sidebar/index.tsx
@@ -265,7 +265,7 @@ const WorkspaceListItem = ({
if (key === 'experimental-features') {
return (
isOwner &&
- currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD &&
+ meta.flavour === WorkspaceFlavour.AFFINE_CLOUD &&
availableFeatures.length > 0
);
}
@@ -290,8 +290,8 @@ const WorkspaceListItem = ({
}, [
activeSubTab,
availableFeatures.length,
- currentWorkspace.flavour,
isOwner,
+ meta.flavour,
onClick,
t,
]);
From d8f9e357d0b81535ff6bc9aff2a3729d1a5e177e Mon Sep 17 00:00:00 2001
From: liuyi
Date: Wed, 13 Mar 2024 02:33:58 +0000
Subject: [PATCH 32/51] ci: proxy /oauth to server (#6095)
---
.github/helm/affine/templates/ingress.yaml | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/.github/helm/affine/templates/ingress.yaml b/.github/helm/affine/templates/ingress.yaml
index 5a391029d8..3c74cfdb06 100644
--- a/.github/helm/affine/templates/ingress.yaml
+++ b/.github/helm/affine/templates/ingress.yaml
@@ -60,6 +60,13 @@ spec:
name: affine-graphql
port:
number: {{ .Values.graphql.service.port }}
+ - path: /oauth
+ pathType: Prefix
+ backend:
+ service:
+ name: affine-graphql
+ port:
+ number: {{ .Values.graphql.service.port }}
- path: /
pathType: Prefix
backend:
From 05b79aae89da2b8bd6ec6cb89c2d5c884f41e479 Mon Sep 17 00:00:00 2001
From: EYHN
Date: Wed, 13 Mar 2024 02:43:28 +0000
Subject: [PATCH 33/51] fix(core): fix tags includes missing error (#6096)
fix https://toeverything.sentry.io/issues/5059796018/events/0899e77a3f6842088568b4cc42b814d7/
---
.../frontend/core/src/components/page-list/use-tag-metas.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/packages/frontend/core/src/components/page-list/use-tag-metas.ts b/packages/frontend/core/src/components/page-list/use-tag-metas.ts
index 4e28c36165..2e6f9d5e05 100644
--- a/packages/frontend/core/src/components/page-list/use-tag-metas.ts
+++ b/packages/frontend/core/src/components/page-list/use-tag-metas.ts
@@ -43,6 +43,9 @@ export function useTagMetas(pageMetas: DocMeta[]) {
const filterPageMetaByTag = useCallback(
(tagId: string) => {
return pageMetas.filter(page => {
+ if (!page.tags) {
+ return false;
+ }
return page.tags.includes(tagId);
});
},
From 495855cc0739eae8da1e059a72e50faeff9d3084 Mon Sep 17 00:00:00 2001
From: liuyi
Date: Wed, 13 Mar 2024 03:59:25 +0000
Subject: [PATCH 34/51] fix(server): server info api should be public (#6098)
---
packages/backend/server/src/app.controller.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/packages/backend/server/src/app.controller.ts b/packages/backend/server/src/app.controller.ts
index 6a217ef3eb..1f483096f7 100644
--- a/packages/backend/server/src/app.controller.ts
+++ b/packages/backend/server/src/app.controller.ts
@@ -1,11 +1,13 @@
import { Controller, Get } from '@nestjs/common';
+import { Public } from './core/auth';
import { Config } from './fundamentals/config';
@Controller('/')
export class AppController {
constructor(private readonly config: Config) {}
+ @Public()
@Get()
info() {
return {
From 73801ce8641d2e7aeec8b0fb13bd139c507322ab Mon Sep 17 00:00:00 2001
From: liuyi
Date: Wed, 13 Mar 2024 04:11:44 +0000
Subject: [PATCH 35/51] fix(server): gql schema is outdated (#6097)
---
packages/frontend/graphql/src/schema.ts | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts
index c43b7c1ef1..903dcf52a6 100644
--- a/packages/frontend/graphql/src/schema.ts
+++ b/packages/frontend/graphql/src/schema.ts
@@ -394,14 +394,18 @@ export type GetUserQueryVariables = Exact<{
export type GetUserQuery = {
__typename?: 'Query';
user:
- | { __typename: 'LimitedUserType'; email: string; hasPassword: boolean }
+ | {
+ __typename: 'LimitedUserType';
+ email: string;
+ hasPassword: boolean | null;
+ }
| {
__typename: 'UserType';
id: string;
name: string;
avatarUrl: string | null;
email: string;
- hasPassword: boolean;
+ hasPassword: boolean | null;
}
| null;
};
From 9e1adfed81b25925179b452197f53b4e889e0f4c Mon Sep 17 00:00:00 2001
From: LongYinan
Date: Wed, 13 Mar 2024 04:22:42 +0000
Subject: [PATCH 36/51] chore: bump up all non-major dependencies (#6069)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[](https://renovatebot.com)
This PR contains the following updates:
| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@storybook/test-runner](https://togithub.com/storybookjs/test-runner) | [`^0.16.0` -> `^0.17.0`](https://renovatebot.com/diffs/npm/@storybook%2ftest-runner/0.16.0/0.17.0) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) |
| [next-themes](https://togithub.com/pacocoursey/next-themes) | [`^0.2.1` -> `^0.3.0`](https://renovatebot.com/diffs/npm/next-themes/0.2.1/0.3.0) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) |
---
### Release Notes
storybookjs/test-runner (@storybook/test-runner)
### [`v0.17.0`](https://togithub.com/storybookjs/test-runner/releases/tag/v0.17.0)
[Compare Source](https://togithub.com/storybookjs/test-runner/compare/v0.16.0...v0.17.0)
##### 🚀 Enhancement
- Release 0.17.0 [#438](https://togithub.com/storybookjs/test-runner/pull/438) ([@JReinhold](https://togithub.com/JReinhold) [@shilman](https://togithub.com/shilman) [@valentinpalkovic](https://togithub.com/valentinpalkovic) [@yannbf](https://togithub.com/yannbf) [@ndelangen](https://togithub.com/ndelangen))
- Support Storybook 8 [#429](https://togithub.com/storybookjs/test-runner/pull/429) ([@yannbf](https://togithub.com/yannbf))
- Support unhandled rendering errors [#421](https://togithub.com/storybookjs/test-runner/pull/421) ([@yannbf](https://togithub.com/yannbf))
##### 🐛 Bug Fix
- Prebundle dependencies [#431](https://togithub.com/storybookjs/test-runner/pull/431) ([@yannbf](https://togithub.com/yannbf))
- Update internal example to Storybook 8 [#430](https://togithub.com/storybookjs/test-runner/pull/430) ([@yannbf](https://togithub.com/yannbf))
- swap storybook/jest to storybook/test [#427](https://togithub.com/storybookjs/test-runner/pull/427) ([@ndelangen](https://togithub.com/ndelangen))
- Add PR template [#428](https://togithub.com/storybookjs/test-runner/pull/428) ([@yannbf](https://togithub.com/yannbf))
- Fix build step [#425](https://togithub.com/storybookjs/test-runner/pull/425) ([@valentinpalkovic](https://togithub.com/valentinpalkovic))
- Remove --prerelease from sb upgrade CI [#423](https://togithub.com/storybookjs/test-runner/pull/423) ([@JReinhold](https://togithub.com/JReinhold))
- Support stories with meta id for permalinking [#419](https://togithub.com/storybookjs/test-runner/pull/419) ([@yannbf](https://togithub.com/yannbf))
##### 📝 Documentation
- Docs: Add remark regarding pnp support [#432](https://togithub.com/storybookjs/test-runner/pull/432) ([@yannbf](https://togithub.com/yannbf))
- docs: replace postRender(deprecated) with postVisit [#418](https://togithub.com/storybookjs/test-runner/pull/418) ([@junkisai](https://togithub.com/junkisai))
##### Authors: 6
- Jeppe Reinhold ([@JReinhold](https://togithub.com/JReinhold))
- Junki Saito ([@junkisai](https://togithub.com/junkisai))
- Michael Shilman ([@shilman](https://togithub.com/shilman))
- Norbert de Langen ([@ndelangen](https://togithub.com/ndelangen))
- Valentin Palkovic ([@valentinpalkovic](https://togithub.com/valentinpalkovic))
- Yann Braga ([@yannbf](https://togithub.com/yannbf))
pacocoursey/next-themes (next-themes)
### [`v0.3.0`](https://togithub.com/pacocoursey/next-themes/releases/tag/v0.3.0)
#### What's
- `"use client"` was added to the library output so that you can use it as a React Client component without creating a wrapper.
#### New Contributors
- [@MaxLeiter](https://togithub.com/MaxLeiter) made their first contribution in [https://github.com/pacocoursey/next-themes/pull/120](https://togithub.com/pacocoursey/next-themes/pull/120)
- [@gnoff](https://togithub.com/gnoff) made their first contribution in [https://github.com/pacocoursey/next-themes/pull/135](https://togithub.com/pacocoursey/next-themes/pull/135)
- [@WITS](https://togithub.com/WITS) made their first contribution in [https://github.com/pacocoursey/next-themes/pull/168](https://togithub.com/pacocoursey/next-themes/pull/168)
- [@dimaMachina](https://togithub.com/dimaMachina) made their first contribution in [https://github.com/pacocoursey/next-themes/pull/186](https://togithub.com/pacocoursey/next-themes/pull/186)
- [@amrhassab](https://togithub.com/amrhassab) made their first contribution in [https://github.com/pacocoursey/next-themes/pull/192](https://togithub.com/pacocoursey/next-themes/pull/192)
- [@BekzodIsakov](https://togithub.com/BekzodIsakov) made their first contribution in [https://github.com/pacocoursey/next-themes/pull/241](https://togithub.com/pacocoursey/next-themes/pull/241)
- [@BlankParticle](https://togithub.com/BlankParticle) made their first contribution in [https://github.com/pacocoursey/next-themes/pull/253](https://togithub.com/pacocoursey/next-themes/pull/253)
**Full Changelog**: https://github.com/pacocoursey/next-themes/compare/v0.2.0...v0.3.0
---
### Configuration
📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://togithub.com/renovatebot/renovate/discussions) if that's undesired.
---
- [ ] If you want to rebase/retry this PR, check this box
---
This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/toeverything/AFFiNE).
---
packages/frontend/component/package.json | 4 +-
packages/frontend/core/package.json | 2 +-
tests/storybook/package.json | 2 +-
yarn.lock | 369 +++++++++++++++--------
4 files changed, 242 insertions(+), 135 deletions(-)
diff --git a/packages/frontend/component/package.json b/packages/frontend/component/package.json
index 238bd61c01..ec72822790 100644
--- a/packages/frontend/component/package.json
+++ b/packages/frontend/component/package.json
@@ -58,7 +58,7 @@
"lottie-react": "^2.4.0",
"lottie-web": "^5.12.2",
"nanoid": "^5.0.6",
- "next-themes": "^0.2.1",
+ "next-themes": "^0.3.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "^4.0.12",
@@ -89,7 +89,7 @@
"@storybook/jest": "^0.2.3",
"@storybook/react": "^7.6.17",
"@storybook/react-vite": "^7.6.17",
- "@storybook/test-runner": "^0.16.0",
+ "@storybook/test-runner": "^0.17.0",
"@storybook/testing-library": "^0.2.2",
"@testing-library/react": "^14.2.1",
"@types/bytes": "^3.1.4",
diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json
index 9281dfd823..b716899184 100644
--- a/packages/frontend/core/package.json
+++ b/packages/frontend/core/package.json
@@ -78,7 +78,7 @@
"lottie-web": "^5.12.2",
"mini-css-extract-plugin": "^2.8.0",
"nanoid": "^5.0.6",
- "next-themes": "^0.2.1",
+ "next-themes": "^0.3.0",
"postcss-loader": "^8.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
diff --git a/tests/storybook/package.json b/tests/storybook/package.json
index b26a1f0f82..04ee89b7fa 100644
--- a/tests/storybook/package.json
+++ b/tests/storybook/package.json
@@ -39,7 +39,7 @@
"@storybook/builder-vite": "^7.6.17",
"@storybook/react": "^7.6.17",
"@storybook/react-vite": "^7.6.17",
- "@storybook/test-runner": "^0.16.0",
+ "@storybook/test-runner": "^0.17.0",
"@vanilla-extract/esbuild-plugin": "^2.3.5",
"@vitejs/plugin-react": "^4.2.1",
"chromatic": "^11.0.0",
diff --git a/yarn.lock b/yarn.lock
index 5334f74a27..d042b787bc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -238,7 +238,7 @@ __metadata:
"@storybook/jest": "npm:^0.2.3"
"@storybook/react": "npm:^7.6.17"
"@storybook/react-vite": "npm:^7.6.17"
- "@storybook/test-runner": "npm:^0.16.0"
+ "@storybook/test-runner": "npm:^0.17.0"
"@storybook/testing-library": "npm:^0.2.2"
"@testing-library/react": "npm:^14.2.1"
"@toeverything/theme": "npm:^0.7.29"
@@ -262,7 +262,7 @@ __metadata:
lottie-react: "npm:^2.4.0"
lottie-web: "npm:^5.12.2"
nanoid: "npm:^5.0.6"
- next-themes: "npm:^0.2.1"
+ next-themes: "npm:^0.3.0"
react: "npm:18.2.0"
react-dom: "npm:18.2.0"
react-error-boundary: "npm:^4.0.12"
@@ -374,7 +374,7 @@ __metadata:
mime-types: "npm:^2.1.35"
mini-css-extract-plugin: "npm:^2.8.0"
nanoid: "npm:^5.0.6"
- next-themes: "npm:^0.2.1"
+ next-themes: "npm:^0.3.0"
postcss-loader: "npm:^8.1.0"
raw-loader: "npm:^4.0.2"
react: "npm:18.2.0"
@@ -757,7 +757,7 @@ __metadata:
"@storybook/jest": "npm:^0.2.3"
"@storybook/react": "npm:^7.6.17"
"@storybook/react-vite": "npm:^7.6.17"
- "@storybook/test-runner": "npm:^0.16.0"
+ "@storybook/test-runner": "npm:^0.17.0"
"@storybook/testing-library": "npm:^0.2.2"
"@vanilla-extract/esbuild-plugin": "npm:^2.3.5"
"@vitejs/plugin-react": "npm:^4.2.1"
@@ -12351,6 +12351,19 @@ __metadata:
languageName: node
linkType: hard
+"@storybook/channels@npm:8.0.0":
+ version: 8.0.0
+ resolution: "@storybook/channels@npm:8.0.0"
+ dependencies:
+ "@storybook/client-logger": "npm:8.0.0"
+ "@storybook/core-events": "npm:8.0.0"
+ "@storybook/global": "npm:^5.0.0"
+ telejson: "npm:^7.2.0"
+ tiny-invariant: "npm:^1.3.1"
+ checksum: 10/8dee7c2fb193af18da52c0b66b75b8ec72290a8001166070453b37072f7ea430a090ee8a02929decb23b7a0df3f28b236ff4711f6b5e933ba1adab8f0d2b7c44
+ languageName: node
+ linkType: hard
+
"@storybook/cli@npm:7.6.17":
version: 7.6.17
resolution: "@storybook/cli@npm:7.6.17"
@@ -12420,6 +12433,15 @@ __metadata:
languageName: node
linkType: hard
+"@storybook/client-logger@npm:8.0.0":
+ version: 8.0.0
+ resolution: "@storybook/client-logger@npm:8.0.0"
+ dependencies:
+ "@storybook/global": "npm:^5.0.0"
+ checksum: 10/74b10807f806e3f0d25eb6059a7acff76c8712105dbfa5ad071a7ccd8d2b1824e0dfbe990ae446baf69a9745e12a3ff82b36682c515da84edc6f1d93bab4bb70
+ languageName: node
+ linkType: hard
+
"@storybook/codemod@npm:7.6.17":
version: 7.6.17
resolution: "@storybook/codemod@npm:7.6.17"
@@ -12473,7 +12495,7 @@ __metadata:
languageName: node
linkType: hard
-"@storybook/core-common@npm:7.6.17, @storybook/core-common@npm:^7.0.0-beta.0 || ^7.0.0-rc.0 || ^7.0.0":
+"@storybook/core-common@npm:7.6.17":
version: 7.6.17
resolution: "@storybook/core-common@npm:7.6.17"
dependencies:
@@ -12504,6 +12526,42 @@ __metadata:
languageName: node
linkType: hard
+"@storybook/core-common@npm:^8.0.0":
+ version: 8.0.0
+ resolution: "@storybook/core-common@npm:8.0.0"
+ dependencies:
+ "@storybook/core-events": "npm:8.0.0"
+ "@storybook/csf-tools": "npm:8.0.0"
+ "@storybook/node-logger": "npm:8.0.0"
+ "@storybook/types": "npm:8.0.0"
+ "@yarnpkg/fslib": "npm:2.10.3"
+ "@yarnpkg/libzip": "npm:2.3.0"
+ chalk: "npm:^4.1.0"
+ cross-spawn: "npm:^7.0.3"
+ esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0"
+ esbuild-register: "npm:^3.5.0"
+ execa: "npm:^5.0.0"
+ file-system-cache: "npm:2.3.0"
+ find-cache-dir: "npm:^3.0.0"
+ find-up: "npm:^5.0.0"
+ fs-extra: "npm:^11.1.0"
+ glob: "npm:^10.0.0"
+ handlebars: "npm:^4.7.7"
+ lazy-universal-dotenv: "npm:^4.0.0"
+ node-fetch: "npm:^2.0.0"
+ picomatch: "npm:^2.3.0"
+ pkg-dir: "npm:^5.0.0"
+ pretty-hrtime: "npm:^1.0.3"
+ resolve-from: "npm:^5.0.0"
+ semver: "npm:^7.3.7"
+ tempy: "npm:^1.0.1"
+ tiny-invariant: "npm:^1.3.1"
+ ts-dedent: "npm:^2.0.0"
+ util: "npm:^0.12.4"
+ checksum: 10/4a1e4def30fd85f7cad93562843512c9cdf2c8e9a7bceb874ab72b2a2d3c1e567b53bc4a8656566707ae26fd718db5ac933b8bfcaff63e2dffbacb545d3832c5
+ languageName: node
+ linkType: hard
+
"@storybook/core-events@npm:7.5.3":
version: 7.5.3
resolution: "@storybook/core-events@npm:7.5.3"
@@ -12522,6 +12580,15 @@ __metadata:
languageName: node
linkType: hard
+"@storybook/core-events@npm:8.0.0":
+ version: 8.0.0
+ resolution: "@storybook/core-events@npm:8.0.0"
+ dependencies:
+ ts-dedent: "npm:^2.0.0"
+ checksum: 10/61287f661e7042b2e6b5baad4fc513c889ddfecc6cd14e3ad5e9f953d19728db81435fba0db12d31b4853031cf9a439102893ff03faf72173ebcfb612d8713ab
+ languageName: node
+ linkType: hard
+
"@storybook/core-server@npm:7.6.17":
version: 7.6.17
resolution: "@storybook/core-server@npm:7.6.17"
@@ -12581,7 +12648,7 @@ __metadata:
languageName: node
linkType: hard
-"@storybook/csf-tools@npm:7.6.17, @storybook/csf-tools@npm:^7.0.0-beta.0 || ^7.0.0-rc.0 || ^7.0.0":
+"@storybook/csf-tools@npm:7.6.17":
version: 7.6.17
resolution: "@storybook/csf-tools@npm:7.6.17"
dependencies:
@@ -12598,7 +12665,24 @@ __metadata:
languageName: node
linkType: hard
-"@storybook/csf@npm:^0.1.0, @storybook/csf@npm:^0.1.1, @storybook/csf@npm:^0.1.2":
+"@storybook/csf-tools@npm:8.0.0, @storybook/csf-tools@npm:^8.0.0":
+ version: 8.0.0
+ resolution: "@storybook/csf-tools@npm:8.0.0"
+ dependencies:
+ "@babel/generator": "npm:^7.23.0"
+ "@babel/parser": "npm:^7.23.0"
+ "@babel/traverse": "npm:^7.23.2"
+ "@babel/types": "npm:^7.23.0"
+ "@storybook/csf": "npm:^0.1.2"
+ "@storybook/types": "npm:8.0.0"
+ fs-extra: "npm:^11.1.0"
+ recast: "npm:^0.23.5"
+ ts-dedent: "npm:^2.0.0"
+ checksum: 10/785cbfc16af44ff3ab35d7effceeb61c7e971d5527fc0e1a573a96ec622726a8ebc0279de6a4691292356610c48913d9acf6a66734603b03e6a25192a6349e24
+ languageName: node
+ linkType: hard
+
+"@storybook/csf@npm:^0.1.0, @storybook/csf@npm:^0.1.2":
version: 0.1.2
resolution: "@storybook/csf@npm:0.1.2"
dependencies:
@@ -12726,6 +12810,13 @@ __metadata:
languageName: node
linkType: hard
+"@storybook/node-logger@npm:8.0.0":
+ version: 8.0.0
+ resolution: "@storybook/node-logger@npm:8.0.0"
+ checksum: 10/39cdc20133f39a354c1801a4705186618f0f7601dedd2dacc3d5a27c0e434f26f7f09213b1dbb914f543f02cb16287b2cc99e56faa136e3e629404ff76645d4f
+ languageName: node
+ linkType: hard
+
"@storybook/postinstall@npm:7.6.17":
version: 7.6.17
resolution: "@storybook/postinstall@npm:7.6.17"
@@ -12755,7 +12846,7 @@ __metadata:
languageName: node
linkType: hard
-"@storybook/preview-api@npm:7.6.17, @storybook/preview-api@npm:^7.0.0-beta.0 || ^7.0.0-rc.0 || ^7.0.0":
+"@storybook/preview-api@npm:7.6.17":
version: 7.6.17
resolution: "@storybook/preview-api@npm:7.6.17"
dependencies:
@@ -12777,6 +12868,28 @@ __metadata:
languageName: node
linkType: hard
+"@storybook/preview-api@npm:^8.0.0":
+ version: 8.0.0
+ resolution: "@storybook/preview-api@npm:8.0.0"
+ dependencies:
+ "@storybook/channels": "npm:8.0.0"
+ "@storybook/client-logger": "npm:8.0.0"
+ "@storybook/core-events": "npm:8.0.0"
+ "@storybook/csf": "npm:^0.1.2"
+ "@storybook/global": "npm:^5.0.0"
+ "@storybook/types": "npm:8.0.0"
+ "@types/qs": "npm:^6.9.5"
+ dequal: "npm:^2.0.2"
+ lodash: "npm:^4.17.21"
+ memoizerific: "npm:^1.11.3"
+ qs: "npm:^6.10.0"
+ tiny-invariant: "npm:^1.3.1"
+ ts-dedent: "npm:^2.0.0"
+ util-deprecate: "npm:^1.0.2"
+ checksum: 10/9cc83e485d11a90778174d94f71dccc6652ed2b2a1053dbaac2effa814d596a8467f94d21181ca135ec416da9720b8d25f3fbe57f5b4a17dc38818d268577f5d
+ languageName: node
+ linkType: hard
+
"@storybook/preview@npm:7.6.17":
version: 7.6.17
resolution: "@storybook/preview@npm:7.6.17"
@@ -12903,25 +13016,22 @@ __metadata:
languageName: node
linkType: hard
-"@storybook/test-runner@npm:^0.16.0":
- version: 0.16.0
- resolution: "@storybook/test-runner@npm:0.16.0"
+"@storybook/test-runner@npm:^0.17.0":
+ version: 0.17.0
+ resolution: "@storybook/test-runner@npm:0.17.0"
dependencies:
"@babel/core": "npm:^7.22.5"
"@babel/generator": "npm:^7.22.5"
"@babel/template": "npm:^7.22.5"
"@babel/types": "npm:^7.22.5"
"@jest/types": "npm:^29.6.3"
- "@storybook/core-common": "npm:^7.0.0-beta.0 || ^7.0.0-rc.0 || ^7.0.0"
- "@storybook/csf": "npm:^0.1.1"
- "@storybook/csf-tools": "npm:^7.0.0-beta.0 || ^7.0.0-rc.0 || ^7.0.0"
- "@storybook/preview-api": "npm:^7.0.0-beta.0 || ^7.0.0-rc.0 || ^7.0.0"
+ "@storybook/core-common": "npm:^8.0.0"
+ "@storybook/csf": "npm:^0.1.2"
+ "@storybook/csf-tools": "npm:^8.0.0"
+ "@storybook/preview-api": "npm:^8.0.0"
"@swc/core": "npm:^1.3.18"
"@swc/jest": "npm:^0.2.23"
- can-bind-to-host: "npm:^1.1.1"
- commander: "npm:^9.0.0"
expect-playwright: "npm:^0.8.0"
- glob: "npm:^10.2.2"
jest: "npm:^29.6.4"
jest-circus: "npm:^29.6.4"
jest-environment-node: "npm:^29.6.4"
@@ -12930,14 +13040,10 @@ __metadata:
jest-runner: "npm:^29.6.4"
jest-serializer-html: "npm:^7.1.0"
jest-watch-typeahead: "npm:^2.0.0"
- node-fetch: "npm:^2"
playwright: "npm:^1.14.0"
- read-pkg-up: "npm:^7.0.1"
- tempy: "npm:^1.0.1"
- ts-dedent: "npm:^2.0.0"
bin:
test-storybook: dist/test-storybook.js
- checksum: 10/8e73882d3484899e4b964f4cd48afeb5aadd28b8f67f03c0c494294c84ae55dd1f7c0bc164a7ad3478b88e78eff74d4ac4183e603fdba6ddc1e9df2215887b6f
+ checksum: 10/c3344041e94f26f7fc2061d527fc77def63fbb0b151e0249ec6af3010d87a483f269178f1eab41525d18ab0f599a024530824a99d4439ca493e09d7ef60ef1e2
languageName: node
linkType: hard
@@ -13006,6 +13112,17 @@ __metadata:
languageName: node
linkType: hard
+"@storybook/types@npm:8.0.0":
+ version: 8.0.0
+ resolution: "@storybook/types@npm:8.0.0"
+ dependencies:
+ "@storybook/channels": "npm:8.0.0"
+ "@types/express": "npm:^4.7.0"
+ file-system-cache: "npm:2.3.0"
+ checksum: 10/ef3f01cffba88c1fd4ef8dafe228de76c746002bb17f2566a80481acce9ca129b45d72fa05c9fc987356e2dfbd945a404fedfeca383f52bffaff455853f3cea8
+ languageName: node
+ linkType: hard
+
"@swc/core-darwin-arm64@npm:1.4.2":
version: 1.4.2
resolution: "@swc/core-darwin-arm64@npm:1.4.2"
@@ -15913,7 +16030,7 @@ __metadata:
languageName: node
linkType: hard
-"assert@npm:^2.0.0, assert@npm:^2.1.0":
+"assert@npm:^2.1.0":
version: 2.1.0
resolution: "assert@npm:2.1.0"
dependencies:
@@ -16960,15 +17077,6 @@ __metadata:
languageName: node
linkType: hard
-"can-bind-to-host@npm:^1.1.1":
- version: 1.1.2
- resolution: "can-bind-to-host@npm:1.1.2"
- bin:
- can-bind-to-host: dist/bin/can-bind-to-host.js
- checksum: 10/d2ad6a0719d1d0013df9b70e362da2f3187c86bcbb2987d1d821c381bb9dfe5483ac63ce7f6dd99489c06854c12c5877a279687a09e9909985a802e77ee9c7ab
- languageName: node
- linkType: hard
-
"caniuse-api@npm:^3.0.0":
version: 3.0.0
resolution: "caniuse-api@npm:3.0.0"
@@ -17790,7 +17898,7 @@ __metadata:
languageName: node
linkType: hard
-"commander@npm:^9.0.0, commander@npm:^9.4.0, commander@npm:^9.4.1":
+"commander@npm:^9.4.0, commander@npm:^9.4.1":
version: 9.5.0
resolution: "commander@npm:9.5.0"
checksum: 10/41c49b3d0f94a1fbeb0463c85b13f15aa15a9e0b4d5e10a49c0a1d58d4489b549d62262b052ae0aa6cfda53299bee487bfe337825df15e342114dde543f82906
@@ -20006,6 +20114,86 @@ __metadata:
languageName: node
linkType: hard
+"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0, esbuild@npm:^0.20.1":
+ version: 0.20.1
+ resolution: "esbuild@npm:0.20.1"
+ dependencies:
+ "@esbuild/aix-ppc64": "npm:0.20.1"
+ "@esbuild/android-arm": "npm:0.20.1"
+ "@esbuild/android-arm64": "npm:0.20.1"
+ "@esbuild/android-x64": "npm:0.20.1"
+ "@esbuild/darwin-arm64": "npm:0.20.1"
+ "@esbuild/darwin-x64": "npm:0.20.1"
+ "@esbuild/freebsd-arm64": "npm:0.20.1"
+ "@esbuild/freebsd-x64": "npm:0.20.1"
+ "@esbuild/linux-arm": "npm:0.20.1"
+ "@esbuild/linux-arm64": "npm:0.20.1"
+ "@esbuild/linux-ia32": "npm:0.20.1"
+ "@esbuild/linux-loong64": "npm:0.20.1"
+ "@esbuild/linux-mips64el": "npm:0.20.1"
+ "@esbuild/linux-ppc64": "npm:0.20.1"
+ "@esbuild/linux-riscv64": "npm:0.20.1"
+ "@esbuild/linux-s390x": "npm:0.20.1"
+ "@esbuild/linux-x64": "npm:0.20.1"
+ "@esbuild/netbsd-x64": "npm:0.20.1"
+ "@esbuild/openbsd-x64": "npm:0.20.1"
+ "@esbuild/sunos-x64": "npm:0.20.1"
+ "@esbuild/win32-arm64": "npm:0.20.1"
+ "@esbuild/win32-ia32": "npm:0.20.1"
+ "@esbuild/win32-x64": "npm:0.20.1"
+ dependenciesMeta:
+ "@esbuild/aix-ppc64":
+ optional: true
+ "@esbuild/android-arm":
+ optional: true
+ "@esbuild/android-arm64":
+ optional: true
+ "@esbuild/android-x64":
+ optional: true
+ "@esbuild/darwin-arm64":
+ optional: true
+ "@esbuild/darwin-x64":
+ optional: true
+ "@esbuild/freebsd-arm64":
+ optional: true
+ "@esbuild/freebsd-x64":
+ optional: true
+ "@esbuild/linux-arm":
+ optional: true
+ "@esbuild/linux-arm64":
+ optional: true
+ "@esbuild/linux-ia32":
+ optional: true
+ "@esbuild/linux-loong64":
+ optional: true
+ "@esbuild/linux-mips64el":
+ optional: true
+ "@esbuild/linux-ppc64":
+ optional: true
+ "@esbuild/linux-riscv64":
+ optional: true
+ "@esbuild/linux-s390x":
+ optional: true
+ "@esbuild/linux-x64":
+ optional: true
+ "@esbuild/netbsd-x64":
+ optional: true
+ "@esbuild/openbsd-x64":
+ optional: true
+ "@esbuild/sunos-x64":
+ optional: true
+ "@esbuild/win32-arm64":
+ optional: true
+ "@esbuild/win32-ia32":
+ optional: true
+ "@esbuild/win32-x64":
+ optional: true
+ bin:
+ esbuild: bin/esbuild
+ checksum: 10/b672fd5df28ae917e2b16e77edbbf6b3099c390ab0a9d4cd331f78b4a4567cf33f506a055e1aa272ac90f7f522835b2173abea9bac6c38906acfda68e60a7ab7
+ languageName: node
+ linkType: hard
+
"esbuild@npm:^0.19.3, esbuild@npm:esbuild@~0.17.6 || ~0.18.0 || ~0.19.0":
version: 0.19.12
resolution: "esbuild@npm:0.19.12"
@@ -20086,86 +20274,6 @@ __metadata:
languageName: node
linkType: hard
-"esbuild@npm:^0.20.1":
- version: 0.20.1
- resolution: "esbuild@npm:0.20.1"
- dependencies:
- "@esbuild/aix-ppc64": "npm:0.20.1"
- "@esbuild/android-arm": "npm:0.20.1"
- "@esbuild/android-arm64": "npm:0.20.1"
- "@esbuild/android-x64": "npm:0.20.1"
- "@esbuild/darwin-arm64": "npm:0.20.1"
- "@esbuild/darwin-x64": "npm:0.20.1"
- "@esbuild/freebsd-arm64": "npm:0.20.1"
- "@esbuild/freebsd-x64": "npm:0.20.1"
- "@esbuild/linux-arm": "npm:0.20.1"
- "@esbuild/linux-arm64": "npm:0.20.1"
- "@esbuild/linux-ia32": "npm:0.20.1"
- "@esbuild/linux-loong64": "npm:0.20.1"
- "@esbuild/linux-mips64el": "npm:0.20.1"
- "@esbuild/linux-ppc64": "npm:0.20.1"
- "@esbuild/linux-riscv64": "npm:0.20.1"
- "@esbuild/linux-s390x": "npm:0.20.1"
- "@esbuild/linux-x64": "npm:0.20.1"
- "@esbuild/netbsd-x64": "npm:0.20.1"
- "@esbuild/openbsd-x64": "npm:0.20.1"
- "@esbuild/sunos-x64": "npm:0.20.1"
- "@esbuild/win32-arm64": "npm:0.20.1"
- "@esbuild/win32-ia32": "npm:0.20.1"
- "@esbuild/win32-x64": "npm:0.20.1"
- dependenciesMeta:
- "@esbuild/aix-ppc64":
- optional: true
- "@esbuild/android-arm":
- optional: true
- "@esbuild/android-arm64":
- optional: true
- "@esbuild/android-x64":
- optional: true
- "@esbuild/darwin-arm64":
- optional: true
- "@esbuild/darwin-x64":
- optional: true
- "@esbuild/freebsd-arm64":
- optional: true
- "@esbuild/freebsd-x64":
- optional: true
- "@esbuild/linux-arm":
- optional: true
- "@esbuild/linux-arm64":
- optional: true
- "@esbuild/linux-ia32":
- optional: true
- "@esbuild/linux-loong64":
- optional: true
- "@esbuild/linux-mips64el":
- optional: true
- "@esbuild/linux-ppc64":
- optional: true
- "@esbuild/linux-riscv64":
- optional: true
- "@esbuild/linux-s390x":
- optional: true
- "@esbuild/linux-x64":
- optional: true
- "@esbuild/netbsd-x64":
- optional: true
- "@esbuild/openbsd-x64":
- optional: true
- "@esbuild/sunos-x64":
- optional: true
- "@esbuild/win32-arm64":
- optional: true
- "@esbuild/win32-ia32":
- optional: true
- "@esbuild/win32-x64":
- optional: true
- bin:
- esbuild: bin/esbuild
- checksum: 10/b672fd5df28ae917e2b16e77edbbf6b3099c390ab0a9d4cd331f78b4a4567cf33f506a055e1aa272ac90f7f522835b2173abea9bac6c38906acfda68e60a7ab7
- languageName: node
- linkType: hard
-
"escalade@npm:^3.1.1":
version: 3.1.1
resolution: "escalade@npm:3.1.1"
@@ -27633,14 +27741,13 @@ __metadata:
languageName: node
linkType: hard
-"next-themes@npm:^0.2.1":
- version: 0.2.1
- resolution: "next-themes@npm:0.2.1"
+"next-themes@npm:^0.3.0":
+ version: 0.3.0
+ resolution: "next-themes@npm:0.3.0"
peerDependencies:
- next: "*"
- react: "*"
- react-dom: "*"
- checksum: 10/6c955c114b7aa920fc14edd3832d0ea95be245ad33f79f397f613696a7c16c7f4112d6e61893d4977255b270f920811741eafdf8be49bd99cecafabf09e8a499
+ react: ^16.8 || ^17 || ^18
+ react-dom: ^16.8 || ^17 || ^18
+ checksum: 10/4285c4969eac517ad7addd773bcb71e7d14bc6c6e3b24eb97b80a6e06ac03fb6cb345e75dfb448156d14430d06289948eb8cfdeb52402ca7ce786093d01d2878
languageName: node
linkType: hard
@@ -27736,7 +27843,7 @@ __metadata:
languageName: node
linkType: hard
-"node-fetch@npm:^2, node-fetch@npm:^2.0.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.2, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9":
+"node-fetch@npm:^2.0.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.2, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
@@ -30596,16 +30703,16 @@ __metadata:
languageName: node
linkType: hard
-"recast@npm:^0.23.1, recast@npm:^0.23.3":
- version: 0.23.4
- resolution: "recast@npm:0.23.4"
+"recast@npm:^0.23.1, recast@npm:^0.23.3, recast@npm:^0.23.5":
+ version: 0.23.6
+ resolution: "recast@npm:0.23.6"
dependencies:
- assert: "npm:^2.0.0"
ast-types: "npm:^0.16.1"
esprima: "npm:~4.0.0"
source-map: "npm:~0.6.1"
+ tiny-invariant: "npm:^1.3.3"
tslib: "npm:^2.0.1"
- checksum: 10/a82e388ded2154697ea54e6d65d060143c9cf4b521f770232a7483e253d45bdd9080b44dc5874d36fe720ba1a10cb20b95375896bd89f5cab631a751e93979f5
+ checksum: 10/3b7bfac05a4ec427738f3a9dc3c955a863eb5bdf42817310a2f521da127613f833c648acee95fd11b4c906186a0b283d873b787d72e3d323a0f42abfcaf4b1f9
languageName: node
linkType: hard
@@ -33207,10 +33314,10 @@ __metadata:
languageName: node
linkType: hard
-"tiny-invariant@npm:^1.3.1":
- version: 1.3.1
- resolution: "tiny-invariant@npm:1.3.1"
- checksum: 10/872dbd1ff20a21303a2fd20ce3a15602cfa7fcf9b228bd694a52e2938224313b5385a1078cb667ed7375d1612194feaca81c4ecbe93121ca1baebe344de4f84c
+"tiny-invariant@npm:^1.3.1, tiny-invariant@npm:^1.3.3":
+ version: 1.3.3
+ resolution: "tiny-invariant@npm:1.3.3"
+ checksum: 10/5e185c8cc2266967984ce3b352a4e57cb89dad5a8abb0dea21468a6ecaa67cd5bb47a3b7a85d08041008644af4f667fb8b6575ba38ba5fb00b3b5068306e59fe
languageName: node
linkType: hard
From e8f83a237d725690c6d8d5343e288d24a003d74c Mon Sep 17 00:00:00 2001
From: Peng Xiao
Date: Wed, 13 Mar 2024 04:32:39 +0000
Subject: [PATCH 37/51] fix(core): vitest on windows (#6100)
---
vitest.config.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/vitest.config.ts b/vitest.config.ts
index fbfe90d02a..a2757c587b 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import react from '@vitejs/plugin-react-swc';
+import * as fg from 'fast-glob';
import { defineConfig } from 'vitest/config';
const rootDir = fileURLToPath(new URL('.', import.meta.url));
@@ -26,10 +27,9 @@ export default defineConfig({
resolve(rootDir, './scripts/setup/global.ts'),
],
include: [
- resolve(rootDir, 'packages/common/**/*.spec.ts'),
- resolve(rootDir, 'packages/common/**/*.spec.tsx'),
- resolve(rootDir, 'packages/frontend/**/*.spec.ts'),
- resolve(rootDir, 'packages/frontend/**/*.spec.tsx'),
+ // rootDir cannot be used as a pattern on windows
+ fg.convertPathToPattern(rootDir) +
+ 'packages/{common,frontend}/**/*.spec.{ts,tsx}',
],
exclude: [
'**/node_modules',
From cacc2d311e04bf6cd0e32aefed24fca5caa00269 Mon Sep 17 00:00:00 2001
From: EYHN
Date: Wed, 13 Mar 2024 06:06:18 +0000
Subject: [PATCH 38/51] feat(infra): livedata flatten (#6083)
---
.../src/livedata/__tests__/livedata.spec.ts | 38 +++++++++++++++
packages/common/infra/src/livedata/index.ts | 48 ++++++++++++++++++-
2 files changed, 85 insertions(+), 1 deletion(-)
diff --git a/packages/common/infra/src/livedata/__tests__/livedata.spec.ts b/packages/common/infra/src/livedata/__tests__/livedata.spec.ts
index a48545a9a2..4eccf553de 100644
--- a/packages/common/infra/src/livedata/__tests__/livedata.spec.ts
+++ b/packages/common/infra/src/livedata/__tests__/livedata.spec.ts
@@ -185,4 +185,42 @@ describe('livedata', () => {
});
expect(value).toBe(1);
});
+
+ test('flat', () => {
+ {
+ const wrapped = new LiveData(new LiveData(0));
+ const flatten = wrapped.flat();
+ expect(flatten.value).toBe(0);
+
+ wrapped.next(new LiveData(1));
+ expect(flatten.value).toBe(1);
+
+ wrapped.next(LiveData.from(of(2, 3), 0));
+ expect(flatten.value).toBe(3);
+ }
+
+ {
+ const wrapped = new LiveData(
+ new LiveData([
+ new LiveData(new LiveData(1)),
+ new LiveData(new LiveData(2)),
+ ])
+ );
+ const flatten = wrapped.flat();
+ expect(flatten.value).toStrictEqual([1, 2]);
+ }
+
+ {
+ const wrapped = new LiveData([new LiveData(0), new LiveData(1)]);
+ const flatten = wrapped.flat();
+
+ expect(flatten.value).toEqual([0, 1]);
+
+ const inner = new LiveData(2);
+ wrapped.next([inner, new LiveData(3)]);
+ expect(flatten.value).toEqual([2, 3]);
+ inner.next(4);
+ expect(flatten.value).toEqual([4, 3]);
+ }
+ });
});
diff --git a/packages/common/infra/src/livedata/index.ts b/packages/common/infra/src/livedata/index.ts
index cc9259a9b1..53d737d235 100644
--- a/packages/common/infra/src/livedata/index.ts
+++ b/packages/common/infra/src/livedata/index.ts
@@ -1,5 +1,6 @@
import { DebugLogger } from '@affine/debug';
import {
+ combineLatest,
distinctUntilChanged,
EMPTY,
filter,
@@ -192,7 +193,7 @@ export class LiveData implements InteropObservable {
return subscription;
}
- map(mapper: (v: T) => R): LiveData {
+ map(mapper: (v: T) => R) {
const sub = LiveData.from(
new Observable(subscriber =>
this.subscribe({
@@ -268,6 +269,42 @@ export class LiveData implements InteropObservable {
this.upstreamSubscription?.unsubscribe();
}
+ /**
+ * flatten the livedata
+ *
+ * ```
+ * new LiveData(new LiveData(0)).flat() // LiveData
+ * ```
+ *
+ * ```
+ * new LiveData([new LiveData(0)]).flat() // LiveData
+ * ```
+ */
+ flat(): Flat {
+ return LiveData.from(
+ this.pipe(
+ switchMap(v => {
+ if (v instanceof LiveData) {
+ return (v as LiveData).flat();
+ } else if (Array.isArray(v)) {
+ return combineLatest(
+ v.map(v => {
+ if (v instanceof LiveData) {
+ return v.flat();
+ } else {
+ return of(v);
+ }
+ })
+ );
+ } else {
+ return of(v);
+ }
+ })
+ ),
+ null as any
+ ) as any;
+ }
+
reactSubscribe = (cb: () => void) => {
this.ops.next('watch');
const subscription = this.raw
@@ -297,3 +334,12 @@ export class LiveData implements InteropObservable {
}
export type LiveDataOperation = 'set' | 'get' | 'watch' | 'unwatch';
+
+export type Unwrap =
+ T extends LiveData
+ ? Unwrap
+ : T extends LiveData[]
+ ? Unwrap[]
+ : T;
+
+export type Flat = T extends LiveData ? LiveData> : T;
From fd9084ea6a3dab64b0fa8bed18b79965526bf470 Mon Sep 17 00:00:00 2001
From: EYHN
Date: Wed, 13 Mar 2024 06:26:05 +0000
Subject: [PATCH 39/51] feat(infra): computed livedata (#6091)
---
.../src/livedata/__tests__/livedata.spec.ts | 106 +++++++++++++++-
packages/common/infra/src/livedata/index.ts | 119 +++++++++++++++++-
2 files changed, 223 insertions(+), 2 deletions(-)
diff --git a/packages/common/infra/src/livedata/__tests__/livedata.spec.ts b/packages/common/infra/src/livedata/__tests__/livedata.spec.ts
index 4eccf553de..dec3f98972 100644
--- a/packages/common/infra/src/livedata/__tests__/livedata.spec.ts
+++ b/packages/common/infra/src/livedata/__tests__/livedata.spec.ts
@@ -2,7 +2,7 @@ import type { Subscriber } from 'rxjs';
import { combineLatest, Observable, of } from 'rxjs';
import { describe, expect, test, vitest } from 'vitest';
-import { LiveData } from '..';
+import { LiveData, PoisonedError } from '..';
describe('livedata', () => {
test('LiveData', async () => {
@@ -133,6 +133,47 @@ describe('livedata', () => {
}
});
+ test('poisoned', () => {
+ {
+ let subscriber: Subscriber = null!;
+ const livedata = LiveData.from(
+ new Observable(sub => {
+ subscriber = sub;
+ }),
+ 1
+ );
+
+ let value: number = 0;
+ let error: any = null;
+ livedata.subscribe({
+ next: v => {
+ value = v;
+ },
+ error: e => {
+ error = e;
+ },
+ });
+ expect(value).toBe(1);
+ subscriber.next(2);
+ expect(value).toBe(2);
+
+ expect(error).toBe(null);
+ subscriber.error('error');
+ expect(error).toBeInstanceOf(PoisonedError);
+
+ expect(() => livedata.next(3)).toThrowError(PoisonedError);
+ expect(() => livedata.value).toThrowError(PoisonedError);
+
+ let error2: any = null;
+ livedata.subscribe({
+ error: e => {
+ error2 = e;
+ },
+ });
+ expect(error2).toBeInstanceOf(PoisonedError);
+ }
+ });
+
test('map', () => {
{
const livedata = new LiveData(0);
@@ -223,4 +264,67 @@ describe('livedata', () => {
expect(flatten.value).toEqual([4, 3]);
}
});
+
+ test('computed', () => {
+ {
+ const a = new LiveData(1);
+ const b = LiveData.computed(get => get(a) + 1);
+ expect(b.value).toBe(2);
+ }
+
+ {
+ const a = new LiveData('v1');
+ const v1 = new LiveData(100);
+ const v2 = new LiveData(200);
+
+ const v = LiveData.computed(get => {
+ return get(a) === 'v1' ? get(v1) : get(v2);
+ });
+
+ expect(v.value).toBe(100);
+
+ a.next('v2');
+ expect(v.value).toBe(200);
+ }
+
+ {
+ let watched = false;
+ let count = 0;
+ let subscriber: Subscriber = null!;
+ const a = LiveData.from(
+ new Observable(sub => {
+ count++;
+ watched = true;
+ subscriber = sub;
+ sub.next(1);
+ return () => {
+ watched = false;
+ };
+ }),
+ 0
+ );
+ const b = LiveData.computed(get => get(a) + 1);
+
+ expect(watched).toBe(false);
+ expect(count).toBe(0);
+
+ const subscription = b.subscribe(_ => {});
+ expect(watched).toBe(true);
+ expect(count).toBe(1);
+ subscriber.next(2);
+ expect(b.value).toBe(3);
+
+ subscription.unsubscribe();
+ expect(watched).toBe(false);
+ expect(count).toBe(1);
+ }
+
+ {
+ let c = null! as LiveData;
+ const b = LiveData.computed(get => get(c) + 1);
+ c = LiveData.computed(get => get(b) + 1);
+
+ expect(() => b.value).toThrowError(PoisonedError);
+ }
+ });
});
diff --git a/packages/common/infra/src/livedata/index.ts b/packages/common/infra/src/livedata/index.ts
index 53d737d235..06524dc4cf 100644
--- a/packages/common/infra/src/livedata/index.ts
+++ b/packages/common/infra/src/livedata/index.ts
@@ -132,10 +132,100 @@ export class LiveData implements InteropObservable {
return data;
}
+ private static GLOBAL_COMPUTED_RECURSIVE_COUNT = 0;
+
+ /**
+ * @example
+ * ```ts
+ * const a = new LiveData('v1');
+ * const v1 = new LiveData(100);
+ * const v2 = new LiveData(200);
+ *
+ * const v = LiveData.computed(get => {
+ * return get(a) === 'v1' ? get(v1) : get(v2);
+ * });
+ *
+ * expect(v.value).toBe(100);
+ * ```
+ */
+ static computed(
+ compute: (get: (data: LiveData) => L) => T
+ ): LiveData {
+ return LiveData.from(
+ new Observable(subscribe => {
+ const execute = (next: () => void) => {
+ const subscriptions: Subscription[] = [];
+ const getfn = (data: LiveData) => {
+ let value = null as L;
+ let first = true;
+ subscriptions.push(
+ data.subscribe({
+ error(err) {
+ subscribe.error(err);
+ },
+ next(v) {
+ value = v;
+ if (!first) {
+ next();
+ }
+ first = false;
+ },
+ })
+ );
+ return value;
+ };
+
+ LiveData.GLOBAL_COMPUTED_RECURSIVE_COUNT++;
+
+ try {
+ if (LiveData.GLOBAL_COMPUTED_RECURSIVE_COUNT > 10) {
+ subscribe.error(new Error('computed recursive limit exceeded'));
+ } else {
+ subscribe.next(compute(getfn));
+ }
+ } catch (err) {
+ subscribe.error(err);
+ } finally {
+ LiveData.GLOBAL_COMPUTED_RECURSIVE_COUNT--;
+ }
+
+ return () => {
+ subscriptions.forEach(s => s.unsubscribe());
+ };
+ };
+
+ let prev = () => {};
+
+ const looper = () => {
+ const dispose = execute(looper);
+ prev();
+ prev = dispose;
+ };
+
+ looper();
+
+ return () => {
+ prev();
+ };
+ }),
+ null as any
+ );
+ }
+
private readonly raw: BehaviorSubject;
private readonly ops = new Subject();
private readonly upstreamSubscription: Subscription | undefined;
+ /**
+ * When the upstream Observable of livedata throws an error, livedata will enter poisoned state. This is an
+ * unrecoverable abnormal state. Any operation on livedata will throw a PoisonedError.
+ *
+ * Since the development specification for livedata is not to throw any error, entering the poisoned state usually
+ * means a programming error.
+ */
+ private isPoisoned = false;
+ private poisonedError: PoisonedError | null = null;
+
constructor(
initialValue: T,
upstream:
@@ -155,17 +245,26 @@ export class LiveData implements InteropObservable {
},
error: err => {
logger.error('uncatched error in livedata', err);
+ this.isPoisoned = true;
+ this.poisonedError = new PoisonedError(err);
+ this.raw.error(this.poisonedError);
},
});
}
}
getValue(): T {
+ if (this.isPoisoned) {
+ throw this.poisonedError;
+ }
this.ops.next('get');
return this.raw.value;
}
setValue(v: T) {
+ if (this.isPoisoned) {
+ throw this.poisonedError;
+ }
this.raw.next(v);
this.ops.next('set');
}
@@ -175,10 +274,13 @@ export class LiveData implements InteropObservable {
}
set value(v: T) {
- this.setValue(v);
+ this.next(v);
}
next(v: T) {
+ if (this.isPoisoned) {
+ throw this.poisonedError;
+ }
this.setValue(v);
}
@@ -306,6 +408,9 @@ export class LiveData implements InteropObservable {
}
reactSubscribe = (cb: () => void) => {
+ if (this.isPoisoned) {
+ throw this.poisonedError;
+ }
this.ops.next('watch');
const subscription = this.raw
.pipe(distinctUntilChanged(), skip(1))
@@ -317,6 +422,9 @@ export class LiveData implements InteropObservable {
};
reactGetSnapshot = () => {
+ if (this.isPoisoned) {
+ throw this.poisonedError;
+ }
this.ops.next('watch');
setImmediate(() => {
this.ops.next('unwatch');
@@ -343,3 +451,12 @@ export type Unwrap =
: T;
export type Flat = T extends LiveData ? LiveData> : T;
+
+export class PoisonedError extends Error {
+ constructor(originalError: any) {
+ super(
+ 'The livedata is poisoned, original error: ' +
+ (originalError instanceof Error ? originalError.stack : originalError)
+ );
+ }
+}
From 573528be41ce22c34609dcd99b28cfd2f66b34d7 Mon Sep 17 00:00:00 2001
From: liuyi
Date: Wed, 13 Mar 2024 07:50:10 +0000
Subject: [PATCH 40/51] fix(server): user can not signup through oauth if ever
invited (#6101)
---
.../migration.sql | 2 ++
packages/backend/server/schema.prisma | 3 +++
.../server/src/core/auth/controller.ts | 3 ++-
.../server/src/core/auth/current-user.ts | 2 +-
.../backend/server/src/core/auth/service.ts | 16 +++++++++----
.../server/src/core/user/management.ts | 4 +++-
.../backend/server/src/core/user/service.ts | 23 ++++++++++++++++++-
.../core/workspaces/resolvers/workspace.ts | 4 +++-
.../server/src/plugins/oauth/controller.ts | 14 ++++++++---
9 files changed, 58 insertions(+), 13 deletions(-)
create mode 100644 packages/backend/server/migrations/20240313033631_user_registered_flag/migration.sql
diff --git a/packages/backend/server/migrations/20240313033631_user_registered_flag/migration.sql b/packages/backend/server/migrations/20240313033631_user_registered_flag/migration.sql
new file mode 100644
index 0000000000..aba95ca449
--- /dev/null
+++ b/packages/backend/server/migrations/20240313033631_user_registered_flag/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "users" ADD COLUMN "registered" BOOLEAN NOT NULL DEFAULT true;
diff --git a/packages/backend/server/schema.prisma b/packages/backend/server/schema.prisma
index df67e2dfc6..508f4052c5 100644
--- a/packages/backend/server/schema.prisma
+++ b/packages/backend/server/schema.prisma
@@ -18,6 +18,9 @@ model User {
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
/// Not available if user signed up through OAuth providers
password String? @db.VarChar
+ /// Indicate whether the user finished the signup progress.
+ /// for example, the value will be false if user never registered and invited into a workspace by others.
+ registered Boolean @default(true)
features UserFeatures[]
customer UserStripeCustomer?
diff --git a/packages/backend/server/src/core/auth/controller.ts b/packages/backend/server/src/core/auth/controller.ts
index ccad6b1008..e2ffed2525 100644
--- a/packages/backend/server/src/core/auth/controller.ts
+++ b/packages/backend/server/src/core/auth/controller.ts
@@ -152,8 +152,9 @@ export class AuthController {
throw new BadRequestException('Invalid Sign-in mail Token');
}
- const user = await this.user.findOrCreateUser(email, {
+ const user = await this.user.fulfillUser(email, {
emailVerifiedAt: new Date(),
+ registered: true,
});
await this.auth.setCookie(req, res, user);
diff --git a/packages/backend/server/src/core/auth/current-user.ts b/packages/backend/server/src/core/auth/current-user.ts
index 1aaa5b59cd..b6757314f1 100644
--- a/packages/backend/server/src/core/auth/current-user.ts
+++ b/packages/backend/server/src/core/auth/current-user.ts
@@ -49,7 +49,7 @@ export const CurrentUser = createParamDecorator(
);
export interface CurrentUser
- extends Omit {
+ extends Pick {
hasPassword: boolean | null;
emailVerified: boolean;
}
diff --git a/packages/backend/server/src/core/auth/service.ts b/packages/backend/server/src/core/auth/service.ts
index e568937b56..187f3b28cd 100644
--- a/packages/backend/server/src/core/auth/service.ts
+++ b/packages/backend/server/src/core/auth/service.ts
@@ -36,12 +36,18 @@ export function parseAuthUserSeqNum(value: any) {
}
export function sessionUser(
- user: Omit & { password?: string | null }
+ user: Pick<
+ User,
+ 'id' | 'email' | 'avatarUrl' | 'name' | 'emailVerifiedAt'
+ > & { password?: string | null }
): CurrentUser {
- return assign(omit(user, 'password', 'emailVerifiedAt', 'createdAt'), {
- hasPassword: user.password !== null,
- emailVerified: user.emailVerifiedAt !== null,
- });
+ return assign(
+ omit(user, 'password', 'registered', 'emailVerifiedAt', 'createdAt'),
+ {
+ hasPassword: user.password !== null,
+ emailVerified: user.emailVerifiedAt !== null,
+ }
+ );
}
@Injectable()
diff --git a/packages/backend/server/src/core/user/management.ts b/packages/backend/server/src/core/user/management.ts
index 8cc158b6e3..af6f740f29 100644
--- a/packages/backend/server/src/core/user/management.ts
+++ b/packages/backend/server/src/core/user/management.ts
@@ -42,7 +42,9 @@ export class UserManagementResolver {
if (user) {
return this.feature.addEarlyAccess(user.id);
} else {
- const user = await this.users.createAnonymousUser(email);
+ const user = await this.users.createAnonymousUser(email, {
+ registered: false,
+ });
return this.feature.addEarlyAccess(user.id);
}
}
diff --git a/packages/backend/server/src/core/user/service.ts b/packages/backend/server/src/core/user/service.ts
index aaf91ef79f..4c60f00975 100644
--- a/packages/backend/server/src/core/user/service.ts
+++ b/packages/backend/server/src/core/user/service.ts
@@ -11,11 +11,12 @@ export class UserService {
email: true,
emailVerifiedAt: true,
avatarUrl: true,
+ registered: true,
} satisfies Prisma.UserSelect;
constructor(private readonly prisma: PrismaClient) {}
- get userCreatingData(): Partial {
+ get userCreatingData() {
return {
name: 'Unnamed',
features: {
@@ -106,6 +107,26 @@ export class UserService {
return this.createAnonymousUser(email, data);
}
+ async fulfillUser(
+ email: string,
+ data: Partial<
+ Pick
+ >
+ ) {
+ return this.prisma.user.upsert({
+ select: this.defaultUserSelect,
+ where: {
+ email,
+ },
+ update: data,
+ create: {
+ email,
+ ...this.userCreatingData,
+ ...data,
+ },
+ });
+ }
+
async deleteUser(id: string) {
return this.prisma.user.delete({ where: { id } });
}
diff --git a/packages/backend/server/src/core/workspaces/resolvers/workspace.ts b/packages/backend/server/src/core/workspaces/resolvers/workspace.ts
index 206a7598da..d322a69c1b 100644
--- a/packages/backend/server/src/core/workspaces/resolvers/workspace.ts
+++ b/packages/backend/server/src/core/workspaces/resolvers/workspace.ts
@@ -358,7 +358,9 @@ export class WorkspaceResolver {
// only invite if the user is not already in the workspace
if (originRecord) return originRecord.id;
} else {
- target = await this.users.createAnonymousUser(email);
+ target = await this.users.createAnonymousUser(email, {
+ registered: false,
+ });
}
const inviteId = await this.permissions.grant(
diff --git a/packages/backend/server/src/plugins/oauth/controller.ts b/packages/backend/server/src/plugins/oauth/controller.ts
index 92f968e892..29c17c78a1 100644
--- a/packages/backend/server/src/plugins/oauth/controller.ts
+++ b/packages/backend/server/src/plugins/oauth/controller.ts
@@ -153,9 +153,17 @@ export class OAuthController {
if (user) {
// we can't directly connect the external account with given email in sign in scenario for safety concern.
// let user manually connect in account sessions instead.
- throw new BadRequestException(
- 'The account with provided email is not register in the same way.'
- );
+ if (user.registered) {
+ throw new BadRequestException(
+ 'The account with provided email is not register in the same way.'
+ );
+ }
+
+ await this.user.fulfillUser(externalAccount.email, {
+ registered: true,
+ });
+
+ return user;
} else {
user = await this.createUserWithConnectedAccount(
provider,
From fddbb426a61f1c2a9731938cf6dcc39984d9eac6 Mon Sep 17 00:00:00 2001
From: regischen
Date: Wed, 13 Mar 2024 17:04:21 +0800
Subject: [PATCH 41/51] feat: bump blocksuite (#6078)
---
packages/common/env/package.json | 4 +-
packages/common/env/src/constant.ts | 8 +-
packages/common/env/src/filter.ts | 4 +-
packages/common/env/src/global.ts | 6 +-
packages/common/infra/package.json | 10 +-
.../src/blocksuite/migration/blocksuite.ts | 2 +-
.../src/blocksuite/migration/workspace.ts | 24 +-
.../common/infra/src/initialization/index.ts | 20 +-
packages/common/infra/src/page/manager.ts | 2 +-
packages/common/infra/src/page/record-list.ts | 4 +-
packages/common/infra/src/page/record.ts | 6 +-
.../src/workspace/__tests__/workspace.spec.ts | 4 +-
.../common/infra/src/workspace/context.ts | 6 +-
.../engine/sync/__tests__/engine.spec.ts | 16 +-
.../engine/sync/__tests__/peer.spec.ts | 22 +-
.../common/infra/src/workspace/list/index.ts | 6 +-
.../infra/src/workspace/list/information.ts | 10 +-
.../common/infra/src/workspace/manager.ts | 12 +-
.../common/infra/src/workspace/testing.ts | 14 +-
.../common/infra/src/workspace/upgrade.ts | 31 +-
.../common/infra/src/workspace/workspace.ts | 6 +-
packages/common/y-indexeddb/package.json | 6 +-
.../y-indexeddb/src/__tests__/index.spec.ts | 76 ++---
packages/common/y-provider/package.json | 2 +-
packages/frontend/component/package.json | 10 +-
packages/frontend/core/package.json | 14 +-
.../core/src/commands/affine-navigation.tsx | 17 +-
.../src/components/affine/awareness/index.tsx | 4 +-
.../affine/page-history-modal/data.ts | 47 +--
.../page-history-modal/history-modal.tsx | 28 +-
.../page-properties-manager.ts | 4 +-
.../property-row-value-renderer.tsx | 4 +-
.../affine/page-properties/table.tsx | 6 +-
.../affine/reference-link/index.tsx | 10 +-
.../new-workspace-setting-detail/profile.tsx | 24 +-
.../block-suite-editor/blocksuite-editor.tsx | 4 +-
.../block-suite-editor/journal-doc-title.tsx | 2 +-
.../block-suite-editor/lit-adaper.tsx | 2 +-
.../block-suite-header/favorite/index.tsx | 8 +-
.../journal/date-picker.tsx | 10 +-
.../journal/today-button.tsx | 10 +-
.../block-suite-header/menu/index.tsx | 14 +-
.../block-suite-header/title/index.tsx | 17 +-
.../block-suite-mode-switch/index.tsx | 8 +-
.../block-suite-page-list/utils.tsx | 26 +-
.../src/components/image-preview/index.tsx | 32 +-
.../src/components/page-detail-editor.tsx | 12 +-
.../use-block-suite-page-preview.spec.ts | 10 +-
.../virtualized-collection-list.tsx | 2 +-
.../docs/page-list-new-page-button.tsx | 2 +-
.../page-list/docs/virtualized-page-list.tsx | 18 +-
.../page-list/page-content-preview.tsx | 19 +-
.../src/components/page-list/page-group.tsx | 20 +-
.../page-list/tags/virtualized-tag-list.tsx | 2 +-
.../core/src/components/page-list/types.ts | 4 +-
.../use-block-suite-workspace-page.ts | 20 +-
.../view/edit-collection/edit-collection.tsx | 4 +-
.../view/edit-collection/pages-mode.tsx | 8 +-
.../view/edit-collection/rules-mode.tsx | 6 +-
.../view/edit-collection/select-page.tsx | 8 +-
.../src/components/pure/cmdk/data-hooks.tsx | 22 +-
.../pure/trash-page-footer/index.tsx | 10 +-
.../collections/collections-list.tsx | 12 +-
.../workspace-slider-bar/collections/page.tsx | 16 +-
.../components/operation-menu-button.tsx | 12 +-
.../components/postfix-item.tsx | 8 +-
.../components/reference-page.tsx | 14 +-
.../favorite/add-favourite-button.tsx | 10 +-
.../favorite/favorite-list.tsx | 6 +-
.../favorite/favourite-page.tsx | 6 +-
.../pure/workspace-slider-bar/index.tsx | 6 +-
.../root-app-sidebar/import-page.tsx | 10 +-
.../src/components/root-app-sidebar/index.tsx | 18 +-
.../root-app-sidebar/journal-button.tsx | 10 +-
.../use-block-suite-workspace-helper.spec.tsx | 36 +--
...-block-suite-workspace-page-title.spec.tsx | 11 +-
.../hooks/affine/use-all-page-list-config.tsx | 8 +-
.../affine/use-block-suite-meta-helper.ts | 24 +-
.../hooks/affine/use-reference-link-helper.ts | 10 +-
...se-register-blocksuite-editor-commands.tsx | 10 +-
.../core/src/hooks/affine/use-sidebar-drag.ts | 2 +-
.../hooks/affine/use-trash-modal-helper.ts | 6 +-
.../core/src/hooks/use-affine-adapter.ts | 3 +-
.../hooks/use-all-block-suite-page-meta.ts | 20 +-
.../hooks/use-block-suite-page-backlinks.ts | 16 +-
.../src/hooks/use-block-suite-page-meta.ts | 22 +-
.../hooks/use-block-suite-page-references.ts | 12 +-
.../hooks/use-block-suite-workspace-helper.ts | 8 +-
.../use-block-suite-workspace-page-title.ts | 28 +-
.../hooks/use-block-suite-workspace-page.ts | 16 +-
.../frontend/core/src/hooks/use-journal.ts | 26 +-
.../hooks/use-register-workspace-commands.ts | 6 +-
.../core/src/layouts/workspace-layout.tsx | 6 +-
.../core/src/modules/collection/service.ts | 6 +-
.../entities/tabs/journal.tsx | 10 +-
.../view/header-switcher.tsx | 5 +-
.../modules/workspace/properties/adapter.ts | 10 +-
.../workspace/properties/legacy-properties.ts | 13 +-
.../src/pages/share/share-detail-page.tsx | 6 +-
.../core/src/pages/share/share-header.tsx | 12 +-
.../workspace/all-page/all-page-filter.tsx | 2 +-
.../workspace/all-page/all-page-header.tsx | 2 +-
.../src/pages/workspace/all-page/all-page.tsx | 21 +-
.../src/pages/workspace/all-tag/index.tsx | 2 +-
.../src/pages/workspace/collection/index.tsx | 2 +-
.../detail-page/detail-page-header.tsx | 12 +-
.../workspace/detail-page/detail-page.tsx | 15 +-
.../src/pages/workspace/page-list-empty.tsx | 8 +-
.../core/src/pages/workspace/tag/index.tsx | 4 +-
.../core/src/pages/workspace/trash-page.tsx | 14 +-
packages/frontend/core/src/shared/index.ts | 4 +-
.../frontend/core/src/utils/user-setting.ts | 13 +-
packages/frontend/electron/package.json | 8 +-
.../frontend/workspace-impl/src/cloud/list.ts | 14 +-
.../src/local/__tests__/engine.spec.ts | 40 +--
.../src/local/__tests__/peer.spec.ts | 28 +-
.../frontend/workspace-impl/src/local/list.ts | 8 +-
tests/affine-cloud/e2e/page-history.spec.ts | 2 +-
tests/kit/playwright.ts | 8 +-
tests/storybook/package.json | 14 +-
.../src/stories/blocksuite-editor.stories.tsx | 22 +-
.../stories/image-preview-modal.stories.tsx | 6 +-
.../stories/page-info-properties.stories.tsx | 16 +-
.../src/stories/page-list.stories.tsx | 32 +-
.../src/stories/share-menu.stories.tsx | 4 +-
yarn.lock | 279 +++++++++---------
126 files changed, 891 insertions(+), 918 deletions(-)
diff --git a/packages/common/env/package.json b/packages/common/env/package.json
index 729f8050d9..a08f917787 100644
--- a/packages/common/env/package.json
+++ b/packages/common/env/package.json
@@ -3,8 +3,8 @@
"private": true,
"type": "module",
"devDependencies": {
- "@blocksuite/global": "0.13.0-canary-202403050653-934469c",
- "@blocksuite/store": "0.13.0-canary-202403050653-934469c",
+ "@blocksuite/global": "0.13.0-canary-202403120738-e15d583",
+ "@blocksuite/store": "0.13.0-canary-202403120738-e15d583",
"react": "18.2.0",
"react-dom": "18.2.0",
"vitest": "1.3.1"
diff --git a/packages/common/env/src/constant.ts b/packages/common/env/src/constant.ts
index 179ec41abb..4de96957c4 100644
--- a/packages/common/env/src/constant.ts
+++ b/packages/common/env/src/constant.ts
@@ -1,5 +1,5 @@
// This file should has not side effect
-import type { Workspace } from '@blocksuite/store';
+import type { DocCollection } from '@blocksuite/store';
declare global {
interface Window {
@@ -95,12 +95,12 @@ export const Messages = {
};
export class PageNotFoundError extends TypeError {
- readonly workspace: Workspace;
+ readonly docCollection: DocCollection;
readonly pageId: string;
- constructor(workspace: Workspace, pageId: string) {
+ constructor(docCollection: DocCollection, pageId: string) {
super();
- this.workspace = workspace;
+ this.docCollection = docCollection;
this.pageId = pageId;
}
}
diff --git a/packages/common/env/src/filter.ts b/packages/common/env/src/filter.ts
index 948d9beda7..c0c1deb454 100644
--- a/packages/common/env/src/filter.ts
+++ b/packages/common/env/src/filter.ts
@@ -1,4 +1,4 @@
-import type { Workspace } from '@blocksuite/store';
+import type { DocCollection } from '@blocksuite/store';
import { z } from 'zod';
export const literalValueSchema: z.ZodType =
@@ -81,4 +81,4 @@ export const tagSchema = z.object({
});
export type Tag = z.input;
-export type PropertiesMeta = Workspace['meta']['properties'];
+export type PropertiesMeta = DocCollection['meta']['properties'];
diff --git a/packages/common/env/src/global.ts b/packages/common/env/src/global.ts
index 68282751b2..c3d8b88290 100644
--- a/packages/common/env/src/global.ts
+++ b/packages/common/env/src/global.ts
@@ -1,6 +1,6 @@
///
import { assertEquals } from '@blocksuite/global/utils';
-import type { Workspace } from '@blocksuite/store';
+import type { DocCollection } from '@blocksuite/store';
import { z } from 'zod';
import { isDesktop, isServer } from './constant.js';
@@ -154,9 +154,9 @@ export function setupGlobal() {
globalThis.$AFFINE_SETUP = true;
}
-export function setupEditorFlags(workspace: Workspace) {
+export function setupEditorFlags(docCollection: DocCollection) {
Object.entries(runtimeConfig.editorFlags).forEach(([key, value]) => {
- workspace.awarenessStore.setFlag(
+ docCollection.awarenessStore.setFlag(
key as keyof BlockSuiteFeatureFlags,
value
);
diff --git a/packages/common/infra/package.json b/packages/common/infra/package.json
index f1d82e141b..f18e3e89f0 100644
--- a/packages/common/infra/package.json
+++ b/packages/common/infra/package.json
@@ -17,9 +17,9 @@
"@affine/debug": "workspace:*",
"@affine/env": "workspace:*",
"@affine/templates": "workspace:*",
- "@blocksuite/blocks": "0.13.0-canary-202403050653-934469c",
- "@blocksuite/global": "0.13.0-canary-202403050653-934469c",
- "@blocksuite/store": "0.13.0-canary-202403050653-934469c",
+ "@blocksuite/blocks": "0.13.0-canary-202403120738-e15d583",
+ "@blocksuite/global": "0.13.0-canary-202403120738-e15d583",
+ "@blocksuite/store": "0.13.0-canary-202403120738-e15d583",
"foxact": "^0.2.31",
"jotai": "^2.6.5",
"jotai-effect": "^0.6.0",
@@ -33,8 +33,8 @@
"devDependencies": {
"@affine-test/fixtures": "workspace:*",
"@affine/templates": "workspace:*",
- "@blocksuite/lit": "0.13.0-canary-202403050653-934469c",
- "@blocksuite/presets": "0.13.0-canary-202403050653-934469c",
+ "@blocksuite/lit": "0.13.0-canary-202403120738-e15d583",
+ "@blocksuite/presets": "0.13.0-canary-202403120738-e15d583",
"@testing-library/react": "^14.2.1",
"async-call-rpc": "^6.4.0",
"react": "^18.2.0",
diff --git a/packages/common/infra/src/blocksuite/migration/blocksuite.ts b/packages/common/infra/src/blocksuite/migration/blocksuite.ts
index 2fc17d9ac1..f07a55cd7d 100644
--- a/packages/common/infra/src/blocksuite/migration/blocksuite.ts
+++ b/packages/common/infra/src/blocksuite/migration/blocksuite.ts
@@ -35,7 +35,7 @@ export async function migratePages(
console.error(e);
}
});
- schema.upgradeWorkspace(rootDoc);
+ schema.upgradeCollection(rootDoc);
// Hard code to upgrade page version to 2.
// Let e2e to ensure the data version is correct.
diff --git a/packages/common/infra/src/blocksuite/migration/workspace.ts b/packages/common/infra/src/blocksuite/migration/workspace.ts
index b19a7f7122..f39819c8b5 100644
--- a/packages/common/infra/src/blocksuite/migration/workspace.ts
+++ b/packages/common/infra/src/blocksuite/migration/workspace.ts
@@ -1,4 +1,4 @@
-import type { Workspace } from '@blocksuite/store';
+import type { DocCollection } from '@blocksuite/store';
import type { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs';
/**
@@ -11,14 +11,14 @@ export enum MigrationPoint {
}
export function checkWorkspaceCompatibility(
- workspace: Workspace,
+ docCollection: DocCollection,
isCloud: boolean
): MigrationPoint | null {
// check if there is any key starts with 'space:' on root doc
- const spaceMetaObj = workspace.doc.share.get('space:meta') as
+ const spaceMetaObj = docCollection.doc.share.get('space:meta') as
| YMap
| undefined;
- const docKeys = Array.from(workspace.doc.share.keys());
+ const docKeys = Array.from(docCollection.doc.share.keys());
const haveSpaceMeta = !!spaceMetaObj && spaceMetaObj.size > 0;
const haveLegacySpace = docKeys.some(key => key.startsWith('space:'));
@@ -28,12 +28,12 @@ export function checkWorkspaceCompatibility(
}
// exit if no pages
- if (!workspace.meta.docs?.length) {
+ if (!docCollection.meta.docs?.length) {
return null;
}
// check guid compatibility
- const meta = workspace.doc.getMap('meta') as YMap;
+ const meta = docCollection.doc.getMap('meta') as YMap;
const pages = meta.get('pages') as YArray>;
for (const page of pages) {
const pageId = page.get('id') as string | undefined;
@@ -41,23 +41,23 @@ export function checkWorkspaceCompatibility(
return MigrationPoint.GuidFix;
}
}
- const spaces = workspace.doc.getMap('spaces') as YMap;
+ const spaces = docCollection.doc.getMap('spaces') as YMap;
for (const [pageId, _] of spaces) {
if (pageId.includes(':')) {
return MigrationPoint.GuidFix;
}
}
- const hasVersion = workspace.meta.hasVersion;
+ const hasVersion = docCollection.meta.hasVersion;
if (!hasVersion) {
return MigrationPoint.BlockVersion;
}
// TODO: Catch compatibility error from blocksuite to show upgrade page.
// Temporarily follow the check logic of blocksuite.
- if ((workspace.meta.docs?.length ?? 0) <= 1) {
+ if ((docCollection.meta.docs?.length ?? 0) <= 1) {
try {
- workspace.meta.validateVersion(workspace);
+ docCollection.meta.validateVersion(docCollection);
} catch (e) {
console.info('validateVersion error', e);
return MigrationPoint.BlockVersion;
@@ -65,9 +65,9 @@ export function checkWorkspaceCompatibility(
}
// From v2, we depend on blocksuite to check and migrate data.
- const blockVersions = workspace.meta.blockVersions;
+ const blockVersions = docCollection.meta.blockVersions;
for (const [flavour, version] of Object.entries(blockVersions ?? {})) {
- const schema = workspace.schema.flavourSchemaMap.get(flavour);
+ const schema = docCollection.schema.flavourSchemaMap.get(flavour);
if (schema?.version !== version) {
return MigrationPoint.BlockVersion;
}
diff --git a/packages/common/infra/src/initialization/index.ts b/packages/common/infra/src/initialization/index.ts
index 032e99a524..9390cc1780 100644
--- a/packages/common/infra/src/initialization/index.ts
+++ b/packages/common/infra/src/initialization/index.ts
@@ -1,9 +1,9 @@
import type { WorkspaceFlavour } from '@affine/env/workspace';
import type {
+ CollectionInfoSnapshot,
Doc,
DocSnapshot,
JobMiddleware,
- WorkspaceInfoSnapshot,
} from '@blocksuite/store';
import { Job } from '@blocksuite/store';
import { Map as YMap } from 'yjs';
@@ -49,17 +49,17 @@ export async function buildShowcaseWorkspace(
) {
const meta = await workspaceManager.createWorkspace(
flavour,
- async (blockSuiteWorkspace, blobStorage) => {
- blockSuiteWorkspace.meta.setName(workspaceName);
+ async (docCollection, blobStorage) => {
+ docCollection.meta.setName(workspaceName);
const { onboarding } = await import('@affine/templates');
- const info = onboarding['info.json'] as WorkspaceInfoSnapshot;
+ const info = onboarding['info.json'] as CollectionInfoSnapshot;
const blob = onboarding['blob.json'] as { [key: string]: string };
- const migrationMiddleware: JobMiddleware = ({ slots, workspace }) => {
+ const migrationMiddleware: JobMiddleware = ({ slots, collection }) => {
slots.afterImport.on(payload => {
if (payload.type === 'page') {
- workspace.schema.upgradeDoc(
+ collection.schema.upgradeDoc(
info?.pageVersion ?? 0,
{},
payload.page.spaceDoc
@@ -69,11 +69,11 @@ export async function buildShowcaseWorkspace(
};
const job = new Job({
- workspace: blockSuiteWorkspace,
+ collection: docCollection,
middlewares: [replaceIdMiddleware, migrationMiddleware],
});
- job.snapshotToWorkspaceInfo(info);
+ job.snapshotToCollectionInfo(info);
// for now all onboarding assets are considered served via CDN
// hack assets so that every blob exists
@@ -92,8 +92,8 @@ export async function buildShowcaseWorkspace(
})
);
- const newVersions = getLatestVersions(blockSuiteWorkspace.schema);
- blockSuiteWorkspace.doc
+ const newVersions = getLatestVersions(docCollection.schema);
+ docCollection.doc
.getMap('meta')
.set('blockVersions', new YMap(Object.entries(newVersions)));
diff --git a/packages/common/infra/src/page/manager.ts b/packages/common/infra/src/page/manager.ts
index 9725e7e9bc..c5ff670549 100644
--- a/packages/common/infra/src/page/manager.ts
+++ b/packages/common/infra/src/page/manager.ts
@@ -20,7 +20,7 @@ export class PageManager {
if (!pageRecord) {
throw new Error('Page record not found');
}
- const blockSuitePage = this.workspace.blockSuiteWorkspace.getDoc(pageId);
+ const blockSuitePage = this.workspace.docCollection.getDoc(pageId);
if (!blockSuitePage) {
throw new Error('Page not found');
}
diff --git a/packages/common/infra/src/page/record-list.ts b/packages/common/infra/src/page/record-list.ts
index c27b359265..5153548953 100644
--- a/packages/common/infra/src/page/record-list.ts
+++ b/packages/common/infra/src/page/record-list.ts
@@ -18,7 +18,7 @@ export class PageRecordList {
new Observable(subscriber => {
const emit = () => {
subscriber.next(
- this.workspace.blockSuiteWorkspace.meta.docMetas.map(
+ this.workspace.docCollection.meta.docMetas.map(
v => new PageRecord(v.id, this.workspace, this.localState)
)
);
@@ -27,7 +27,7 @@ export class PageRecordList {
emit();
const dispose =
- this.workspace.blockSuiteWorkspace.meta.docMetaUpdated.on(emit).dispose;
+ this.workspace.docCollection.meta.docMetaUpdated.on(emit).dispose;
return () => {
dispose();
};
diff --git a/packages/common/infra/src/page/record.ts b/packages/common/infra/src/page/record.ts
index e6f109ae64..b3276b5b76 100644
--- a/packages/common/infra/src/page/record.ts
+++ b/packages/common/infra/src/page/record.ts
@@ -16,7 +16,7 @@ export class PageRecord {
meta = LiveData.from(
new Observable(subscriber => {
const emit = () => {
- const meta = this.workspace.blockSuiteWorkspace.meta.docMetas.find(
+ const meta = this.workspace.docCollection.meta.docMetas.find(
page => page.id === this.id
);
if (meta === undefined) {
@@ -28,7 +28,7 @@ export class PageRecord {
emit();
const dispose =
- this.workspace.blockSuiteWorkspace.meta.docMetaUpdated.on(emit).dispose;
+ this.workspace.docCollection.meta.docMetaUpdated.on(emit).dispose;
return () => {
dispose();
};
@@ -42,7 +42,7 @@ export class PageRecord {
);
setMeta(meta: Partial): void {
- this.workspace.blockSuiteWorkspace.setDocMeta(this.id, meta);
+ this.workspace.docCollection.setDocMeta(this.id, meta);
}
mode: LiveData = LiveData.from(
diff --git a/packages/common/infra/src/workspace/__tests__/workspace.spec.ts b/packages/common/infra/src/workspace/__tests__/workspace.spec.ts
index a239341a65..1a1182ba1f 100644
--- a/packages/common/infra/src/workspace/__tests__/workspace.spec.ts
+++ b/packages/common/infra/src/workspace/__tests__/workspace.spec.ts
@@ -22,7 +22,7 @@ describe('Workspace System', () => {
expect(workspaceListService.workspaceList.value.length).toBe(1);
- const page = workspace.blockSuiteWorkspace.createDoc({
+ const page = workspace.docCollection.createDoc({
id: 'page0',
});
page.load();
@@ -30,7 +30,7 @@ describe('Workspace System', () => {
title: new page.Text('test-page'),
});
- expect(workspace.blockSuiteWorkspace.docs.size).toBe(1);
+ expect(workspace.docCollection.docs.size).toBe(1);
expect(
(page!.getBlockByFlavour('affine:page')[0] as any).title.toString()
).toBe('test-page');
diff --git a/packages/common/infra/src/workspace/context.ts b/packages/common/infra/src/workspace/context.ts
index 41e2f8d248..ca7db30229 100644
--- a/packages/common/infra/src/workspace/context.ts
+++ b/packages/common/infra/src/workspace/context.ts
@@ -18,7 +18,7 @@
* })
*/
-import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
+import { DocCollection } from '@blocksuite/store';
import { nanoid } from 'nanoid';
import type { Awareness } from 'y-protocols/awareness.js';
import type { Doc as YDoc } from 'yjs';
@@ -29,7 +29,7 @@ import { globalBlockSuiteSchema } from './global-schema';
import type { WorkspaceMetadata } from './metadata';
import { WorkspaceScope } from './service-scope';
-export const BlockSuiteWorkspaceContext = createIdentifier(
+export const BlockSuiteWorkspaceContext = createIdentifier(
'BlockSuiteWorkspaceContext'
);
@@ -53,7 +53,7 @@ export function configureWorkspaceContext(
.addImpl(WorkspaceMetadataContext, workspaceMetadata)
.addImpl(WorkspaceIdContext, workspaceMetadata.id)
.addImpl(BlockSuiteWorkspaceContext, provider => {
- return new BlockSuiteWorkspace({
+ return new DocCollection({
id: workspaceMetadata.id,
blobStorages: [
() => ({
diff --git a/packages/common/infra/src/workspace/engine/sync/__tests__/engine.spec.ts b/packages/common/infra/src/workspace/engine/sync/__tests__/engine.spec.ts
index 6bf9bd4b72..7e2580f94c 100644
--- a/packages/common/infra/src/workspace/engine/sync/__tests__/engine.spec.ts
+++ b/packages/common/infra/src/workspace/engine/sync/__tests__/engine.spec.ts
@@ -1,5 +1,5 @@
import { WorkspaceFlavour } from '@affine/env/workspace';
-import { Workspace } from '@blocksuite/store';
+import { DocCollection } from '@blocksuite/store';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { Doc } from 'yjs';
@@ -26,14 +26,14 @@ describe('SyncEngine', () => {
const storage2 = new MemoryMemento();
let prev: any;
{
- const workspace = new Workspace({
+ const docCollection = new DocCollection({
id: 'test',
schema: globalBlockSuiteSchema,
});
const syncEngine = new SyncEngine(
- workspace.doc,
+ docCollection.doc,
new TestingSyncStorage(testMeta, storage),
[
new TestingSyncStorage(testMeta, storage1),
@@ -42,7 +42,7 @@ describe('SyncEngine', () => {
);
syncEngine.start();
- const page = workspace.createDoc({
+ const page = docCollection.createDoc({
id: 'page0',
});
page.load();
@@ -69,23 +69,23 @@ describe('SyncEngine', () => {
);
await syncEngine.waitForSynced();
syncEngine.forceStop();
- prev = workspace.doc.toJSON();
+ prev = docCollection.doc.toJSON();
}
for (const current of [storage, storage1, storage2]) {
- const workspace = new Workspace({
+ const docCollection = new DocCollection({
id: 'test',
schema: globalBlockSuiteSchema,
});
const syncEngine = new SyncEngine(
- workspace.doc,
+ docCollection.doc,
new TestingSyncStorage(testMeta, current),
[]
);
syncEngine.start();
await syncEngine.waitForSynced();
- expect(workspace.doc.toJSON()).toEqual({
+ expect(docCollection.doc.toJSON()).toEqual({
...prev,
});
syncEngine.forceStop();
diff --git a/packages/common/infra/src/workspace/engine/sync/__tests__/peer.spec.ts b/packages/common/infra/src/workspace/engine/sync/__tests__/peer.spec.ts
index 4bc033a09b..f0cf51d633 100644
--- a/packages/common/infra/src/workspace/engine/sync/__tests__/peer.spec.ts
+++ b/packages/common/infra/src/workspace/engine/sync/__tests__/peer.spec.ts
@@ -1,5 +1,5 @@
import { WorkspaceFlavour } from '@affine/env/workspace';
-import { Workspace } from '@blocksuite/store';
+import { DocCollection } from '@blocksuite/store';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { MemoryMemento } from '../../../../storage';
@@ -23,19 +23,19 @@ describe('SyncPeer', () => {
let prev: any;
{
- const workspace = new Workspace({
+ const docCollection = new DocCollection({
id: 'test',
schema: globalBlockSuiteSchema,
});
const syncPeer = new SyncPeer(
- workspace.doc,
+ docCollection.doc,
new TestingSyncStorage(testMeta, storage)
);
await syncPeer.waitForLoaded();
- const page = workspace.createDoc({
+ const page = docCollection.createDoc({
id: 'page0',
});
page.load();
@@ -62,21 +62,21 @@ describe('SyncPeer', () => {
);
await syncPeer.waitForSynced();
syncPeer.stop();
- prev = workspace.doc.toJSON();
+ prev = docCollection.doc.toJSON();
}
{
- const workspace = new Workspace({
+ const docCollection = new DocCollection({
id: 'test',
schema: globalBlockSuiteSchema,
});
const syncPeer = new SyncPeer(
- workspace.doc,
+ docCollection.doc,
new TestingSyncStorage(testMeta, storage)
);
await syncPeer.waitForSynced();
- expect(workspace.doc.toJSON()).toEqual({
+ expect(docCollection.doc.toJSON()).toEqual({
...prev,
});
syncPeer.stop();
@@ -86,21 +86,21 @@ describe('SyncPeer', () => {
test('status', async () => {
const storage = new MemoryMemento();
- const workspace = new Workspace({
+ const docCollection = new DocCollection({
id: 'test',
schema: globalBlockSuiteSchema,
});
const syncPeer = new SyncPeer(
- workspace.doc,
+ docCollection.doc,
new TestingSyncStorage(testMeta, storage)
);
expect(syncPeer.status.step).toBe(SyncPeerStep.LoadingRootDoc);
await syncPeer.waitForSynced();
expect(syncPeer.status.step).toBe(SyncPeerStep.Synced);
- const page = workspace.createDoc({
+ const page = docCollection.createDoc({
id: 'page0',
});
expect(syncPeer.status.step).toBe(SyncPeerStep.LoadingSubDoc);
diff --git a/packages/common/infra/src/workspace/list/index.ts b/packages/common/infra/src/workspace/list/index.ts
index ff414bc1d6..3c90a7fa50 100644
--- a/packages/common/infra/src/workspace/list/index.ts
+++ b/packages/common/infra/src/workspace/list/index.ts
@@ -1,6 +1,6 @@
import { DebugLogger } from '@affine/debug';
import type { WorkspaceFlavour } from '@affine/env/workspace';
-import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
+import type { DocCollection } from '@blocksuite/store';
import { differenceWith } from 'lodash-es';
import { createIdentifier } from '../../di';
@@ -34,7 +34,7 @@ export interface WorkspaceListProvider {
*/
create(
initial: (
- workspace: BlockSuiteWorkspace,
+ docCollection: DocCollection,
blobStorage: BlobStorage
) => Promise
): Promise;
@@ -124,7 +124,7 @@ export class WorkspaceListService {
async create(
flavour: WorkspaceFlavour,
initial: (
- workspace: BlockSuiteWorkspace,
+ docCollection: DocCollection,
blobStorage: BlobStorage
) => Promise = () => Promise.resolve()
) {
diff --git a/packages/common/infra/src/workspace/list/information.ts b/packages/common/infra/src/workspace/list/information.ts
index e143d72e50..a9ff2363c5 100644
--- a/packages/common/infra/src/workspace/list/information.ts
+++ b/packages/common/infra/src/workspace/list/information.ts
@@ -57,13 +57,13 @@ export class WorkspaceInformation {
*/
syncWithWorkspace(workspace: Workspace) {
this.info = {
- avatar: workspace.blockSuiteWorkspace.meta.avatar ?? this.info.avatar,
- name: workspace.blockSuiteWorkspace.meta.name ?? this.info.name,
+ avatar: workspace.docCollection.meta.avatar ?? this.info.avatar,
+ name: workspace.docCollection.meta.name ?? this.info.name,
};
- workspace.blockSuiteWorkspace.meta.commonFieldsUpdated.on(() => {
+ workspace.docCollection.meta.commonFieldsUpdated.on(() => {
this.info = {
- avatar: workspace.blockSuiteWorkspace.meta.avatar ?? this.info.avatar,
- name: workspace.blockSuiteWorkspace.meta.name ?? this.info.name,
+ avatar: workspace.docCollection.meta.avatar ?? this.info.avatar,
+ name: workspace.docCollection.meta.name ?? this.info.name,
};
});
}
diff --git a/packages/common/infra/src/workspace/manager.ts b/packages/common/infra/src/workspace/manager.ts
index c4c8fd756d..49d2451cb1 100644
--- a/packages/common/infra/src/workspace/manager.ts
+++ b/packages/common/infra/src/workspace/manager.ts
@@ -2,7 +2,7 @@ import { DebugLogger } from '@affine/debug';
import { setupEditorFlags } from '@affine/env/global';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { assertEquals } from '@blocksuite/global/utils';
-import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
+import type { DocCollection } from '@blocksuite/store';
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import { fixWorkspaceVersion } from '../blocksuite';
@@ -105,7 +105,7 @@ export class WorkspaceManager {
createWorkspace(
flavour: WorkspaceFlavour,
initial?: (
- workspace: BlockSuiteWorkspace,
+ docCollection: DocCollection,
blobStorage: BlobStorage
) => Promise
): Promise {
@@ -131,9 +131,9 @@ export class WorkspaceManager {
const newId = await this.list.create(
WorkspaceFlavour.AFFINE_CLOUD,
async (ws, bs) => {
- applyUpdate(ws.doc, encodeStateAsUpdate(local.blockSuiteWorkspace.doc));
+ applyUpdate(ws.doc, encodeStateAsUpdate(local.docCollection.doc));
- for (const subdoc of local.blockSuiteWorkspace.doc.getSubdocs()) {
+ for (const subdoc of local.docCollection.doc.getSubdocs()) {
for (const newSubdoc of ws.doc.getSubdocs()) {
if (newSubdoc.guid === subdoc.guid) {
applyUpdate(newSubdoc, encodeStateAsUpdate(subdoc));
@@ -191,9 +191,9 @@ export class WorkspaceManager {
const workspace = provider.get(Workspace);
// apply compatibility fix
- fixWorkspaceVersion(workspace.blockSuiteWorkspace.doc);
+ fixWorkspaceVersion(workspace.docCollection.doc);
- setupEditorFlags(workspace.blockSuiteWorkspace);
+ setupEditorFlags(workspace.docCollection);
return workspace;
}
diff --git a/packages/common/infra/src/workspace/testing.ts b/packages/common/infra/src/workspace/testing.ts
index 6f2b364b0f..8c17e78526 100644
--- a/packages/common/infra/src/workspace/testing.ts
+++ b/packages/common/infra/src/workspace/testing.ts
@@ -1,5 +1,5 @@
import { WorkspaceFlavour } from '@affine/env/workspace';
-import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
+import { DocCollection } from '@blocksuite/store';
import { differenceBy } from 'lodash-es';
import { nanoid } from 'nanoid';
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
@@ -43,7 +43,7 @@ export class TestingLocalWorkspaceListProvider
}
async create(
initial: (
- workspace: BlockSuiteWorkspace,
+ docCollection: DocCollection,
blobStorage: BlobStorage
) => Promise
): Promise {
@@ -53,18 +53,18 @@ export class TestingLocalWorkspaceListProvider
const blobStorage = new TestingBlobStorage(meta, this.state);
const syncStorage = new TestingSyncStorage(meta, this.state);
- const workspace = new BlockSuiteWorkspace({
+ const docCollection = new DocCollection({
id: id,
idGenerator: () => nanoid(),
schema: globalBlockSuiteSchema,
});
// apply initial state
- await initial(workspace, blobStorage);
+ await initial(docCollection, blobStorage);
// save workspace to storage
- await syncStorage.push(id, encodeStateAsUpdate(workspace.doc));
- for (const subdocs of workspace.doc.getSubdocs()) {
+ await syncStorage.push(id, encodeStateAsUpdate(docCollection.doc));
+ for (const subdocs of docCollection.doc.getSubdocs()) {
await syncStorage.push(subdocs.guid, encodeStateAsUpdate(subdocs));
}
@@ -117,7 +117,7 @@ export class TestingLocalWorkspaceListProvider
return;
}
- const bs = new BlockSuiteWorkspace({
+ const bs = new DocCollection({
id,
schema: globalBlockSuiteSchema,
});
diff --git a/packages/common/infra/src/workspace/upgrade.ts b/packages/common/infra/src/workspace/upgrade.ts
index 2b57dafce4..ef7dcd6c6c 100644
--- a/packages/common/infra/src/workspace/upgrade.ts
+++ b/packages/common/infra/src/workspace/upgrade.ts
@@ -1,7 +1,7 @@
import { Unreachable } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { Slot } from '@blocksuite/global/utils';
-import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
+import type { DocCollection } from '@blocksuite/store';
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
import { checkWorkspaceCompatibility, MigrationPoint } from '../blocksuite';
@@ -38,18 +38,18 @@ export class WorkspaceUpgradeController {
}
constructor(
- private readonly blockSuiteWorkspace: BlockSuiteWorkspace,
+ private readonly docCollection: DocCollection,
private readonly sync: SyncEngine,
private readonly workspaceMetadata: WorkspaceMetadata
) {
- blockSuiteWorkspace.doc.on('update', () => {
+ docCollection.doc.on('update', () => {
this.checkIfNeedUpgrade();
});
}
checkIfNeedUpgrade() {
const needUpgrade = !!checkWorkspaceCompatibility(
- this.blockSuiteWorkspace,
+ this.docCollection,
this.workspaceMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD
);
this.status = {
@@ -72,7 +72,7 @@ export class WorkspaceUpgradeController {
await this.sync.waitForSynced();
const step = checkWorkspaceCompatibility(
- this.blockSuiteWorkspace,
+ this.docCollection,
this.workspaceMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD
);
@@ -82,9 +82,9 @@ export class WorkspaceUpgradeController {
// Clone a new doc to prevent change events.
const clonedDoc = new YDoc({
- guid: this.blockSuiteWorkspace.doc.guid,
+ guid: this.docCollection.doc.guid,
});
- applyDoc(clonedDoc, this.blockSuiteWorkspace.doc);
+ applyDoc(clonedDoc, this.docCollection.doc);
if (step === MigrationPoint.SubDoc) {
const newWorkspace = await workspaceManager.createWorkspace(
@@ -92,14 +92,11 @@ export class WorkspaceUpgradeController {
async (workspace, blobStorage) => {
await upgradeV1ToV2(clonedDoc, workspace.doc);
migrateGuidCompatibility(clonedDoc);
- await forceUpgradePages(
- workspace.doc,
- this.blockSuiteWorkspace.schema
- );
- const blobList = await this.blockSuiteWorkspace.blob.list();
+ await forceUpgradePages(workspace.doc, this.docCollection.schema);
+ const blobList = await this.docCollection.blob.list();
for (const blobKey of blobList) {
- const blob = await this.blockSuiteWorkspace.blob.get(blobKey);
+ const blob = await this.docCollection.blob.get(blobKey);
if (blob) {
await blobStorage.set(blobKey, blob);
}
@@ -110,13 +107,13 @@ export class WorkspaceUpgradeController {
return newWorkspace;
} else if (step === MigrationPoint.GuidFix) {
migrateGuidCompatibility(clonedDoc);
- await forceUpgradePages(clonedDoc, this.blockSuiteWorkspace.schema);
- applyDoc(this.blockSuiteWorkspace.doc, clonedDoc);
+ await forceUpgradePages(clonedDoc, this.docCollection.schema);
+ applyDoc(this.docCollection.doc, clonedDoc);
await this.sync.waitForSynced();
return null;
} else if (step === MigrationPoint.BlockVersion) {
- await forceUpgradePages(clonedDoc, this.blockSuiteWorkspace.schema);
- applyDoc(this.blockSuiteWorkspace.doc, clonedDoc);
+ await forceUpgradePages(clonedDoc, this.docCollection.schema);
+ applyDoc(this.docCollection.doc, clonedDoc);
await this.sync.waitForSynced();
return null;
} else {
diff --git a/packages/common/infra/src/workspace/workspace.ts b/packages/common/infra/src/workspace/workspace.ts
index 2fa2c75303..ff43b94ce2 100644
--- a/packages/common/infra/src/workspace/workspace.ts
+++ b/packages/common/infra/src/workspace/workspace.ts
@@ -1,6 +1,6 @@
import { DebugLogger } from '@affine/debug';
import { Slot } from '@blocksuite/global/utils';
-import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
+import type { DocCollection } from '@blocksuite/store';
import type { ServiceProvider } from '../di';
import { CleanupService } from '../lifecycle';
@@ -10,7 +10,7 @@ import { type WorkspaceMetadata } from './metadata';
import type { WorkspaceUpgradeController } from './upgrade';
import { type WorkspaceUpgradeStatus } from './upgrade';
-export type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
+export type { DocCollection } from '@blocksuite/store';
const logger = new DebugLogger('affine:workspace');
@@ -67,7 +67,7 @@ export class Workspace {
constructor(
public meta: WorkspaceMetadata,
public engine: WorkspaceEngine,
- public blockSuiteWorkspace: BlockSuiteWorkspace,
+ public docCollection: DocCollection,
public upgrade: WorkspaceUpgradeController,
public services: ServiceProvider
) {
diff --git a/packages/common/y-indexeddb/package.json b/packages/common/y-indexeddb/package.json
index 9f188f00ba..f9f9a9b3df 100644
--- a/packages/common/y-indexeddb/package.json
+++ b/packages/common/y-indexeddb/package.json
@@ -32,14 +32,14 @@
}
},
"dependencies": {
- "@blocksuite/global": "0.13.0-canary-202403050653-934469c",
+ "@blocksuite/global": "0.13.0-canary-202403120738-e15d583",
"idb": "^8.0.0",
"nanoid": "^5.0.6",
"y-provider": "workspace:*"
},
"devDependencies": {
- "@blocksuite/blocks": "0.13.0-canary-202403050653-934469c",
- "@blocksuite/store": "0.13.0-canary-202403050653-934469c",
+ "@blocksuite/blocks": "0.13.0-canary-202403120738-e15d583",
+ "@blocksuite/store": "0.13.0-canary-202403120738-e15d583",
"fake-indexeddb": "^5.0.2",
"vite": "^5.1.4",
"vite-plugin-dts": "3.7.3",
diff --git a/packages/common/y-indexeddb/src/__tests__/index.spec.ts b/packages/common/y-indexeddb/src/__tests__/index.spec.ts
index d514f2f419..a9dd400954 100644
--- a/packages/common/y-indexeddb/src/__tests__/index.spec.ts
+++ b/packages/common/y-indexeddb/src/__tests__/index.spec.ts
@@ -8,7 +8,7 @@ import { setTimeout } from 'node:timers/promises';
import { AffineSchemas } from '@blocksuite/blocks/schemas';
import { assertExists } from '@blocksuite/global/utils';
import type { Doc } from '@blocksuite/store';
-import { Schema, Workspace } from '@blocksuite/store';
+import { DocCollection, Schema } from '@blocksuite/store';
import { openDB } from 'idb';
import { nanoid } from 'nanoid';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
@@ -69,7 +69,7 @@ async function getUpdates(id: string): Promise {
}
let id: string;
-let workspace: Workspace;
+let docCollection: DocCollection;
const rootDBName = DEFAULT_DB_NAME;
const schema = new Schema();
@@ -78,7 +78,7 @@ schema.register(AffineSchemas);
beforeEach(() => {
id = nanoid();
- workspace = new Workspace({
+ docCollection = new DocCollection({
id,
schema,
@@ -93,7 +93,7 @@ afterEach(() => {
describe('indexeddb provider', () => {
test('connect', async () => {
- const provider = createIndexedDBProvider(workspace.doc);
+ const provider = createIndexedDBProvider(docCollection.doc);
provider.connect();
// todo: has a better way to know when data is synced
@@ -110,11 +110,11 @@ describe('indexeddb provider', () => {
updates: [
{
timestamp: expect.any(Number),
- update: encodeStateAsUpdate(workspace.doc),
+ update: encodeStateAsUpdate(docCollection.doc),
},
],
});
- const page = workspace.createDoc({ id: 'page0' });
+ const page = docCollection.createDoc({ id: 'page0' });
page.load();
const pageBlockId = page.addBlock(
'affine:page' as keyof BlockSuite.BlockModels,
@@ -139,13 +139,13 @@ describe('indexeddb provider', () => {
const data = (await store.get(id)) as WorkspacePersist | undefined;
assertExists(data);
expect(data.id).toBe(id);
- const testWorkspace = new Workspace({
+ const testWorkspace = new DocCollection({
id: 'test',
schema,
});
// data should only contain updates for the root doc
data.updates.forEach(({ update }) => {
- Workspace.Y.applyUpdate(testWorkspace.doc, update);
+ DocCollection.Y.applyUpdate(testWorkspace.doc, update);
});
const subPage = testWorkspace.doc.spaces.get('page0');
{
@@ -157,23 +157,23 @@ describe('indexeddb provider', () => {
assertExists(data);
testWorkspace.getDoc('page0')?.load();
data.updates.forEach(({ update }) => {
- Workspace.Y.applyUpdate(subPage, update);
+ DocCollection.Y.applyUpdate(subPage, update);
});
}
- expect(workspace.doc.toJSON()).toEqual(testWorkspace.doc.toJSON());
+ expect(docCollection.doc.toJSON()).toEqual(testWorkspace.doc.toJSON());
}
});
test('connect and disconnect', async () => {
- const provider = createIndexedDBProvider(workspace.doc, rootDBName);
+ const provider = createIndexedDBProvider(docCollection.doc, rootDBName);
provider.connect();
expect(provider.connected).toBe(true);
await setTimeout(200);
- const snapshot = encodeStateAsUpdate(workspace.doc);
+ const snapshot = encodeStateAsUpdate(docCollection.doc);
provider.disconnect();
expect(provider.connected).toBe(false);
{
- const page = workspace.createDoc({ id: 'page0' });
+ const page = docCollection.createDoc({ id: 'page0' });
page.load();
const pageBlockId = page.addBlock(
'affine:page' as keyof BlockSuite.BlockModels
@@ -190,7 +190,7 @@ describe('indexeddb provider', () => {
);
}
{
- const updates = await getUpdates(workspace.id);
+ const updates = await getUpdates(docCollection.id);
expect(updates.length).toBe(1);
expect(updates[0]).toEqual(snapshot);
}
@@ -199,7 +199,7 @@ describe('indexeddb provider', () => {
expect(provider.connected).toBe(true);
await setTimeout(200);
{
- const updates = await getUpdates(workspace.id);
+ const updates = await getUpdates(docCollection.id);
expect(updates).not.toEqual([]);
}
expect(provider.connected).toBe(true);
@@ -208,7 +208,7 @@ describe('indexeddb provider', () => {
});
test('cleanup', async () => {
- const provider = createIndexedDBProvider(workspace.doc);
+ const provider = createIndexedDBProvider(docCollection.doc);
provider.connect();
await setTimeout(200);
const db = await openDB(rootDBName, dbVersion);
@@ -218,7 +218,7 @@ describe('indexeddb provider', () => {
.transaction('workspace', 'readonly')
.objectStore('workspace');
const keys = await store.getAllKeys();
- expect(keys).contain(workspace.id);
+ expect(keys).contain(docCollection.id);
}
await provider.cleanup();
@@ -229,16 +229,16 @@ describe('indexeddb provider', () => {
.transaction('workspace', 'readonly')
.objectStore('workspace');
const keys = await store.getAllKeys();
- expect(keys).not.contain(workspace.id);
+ expect(keys).not.contain(docCollection.id);
}
});
test('merge', async () => {
setMergeCount(5);
- const provider = createIndexedDBProvider(workspace.doc, rootDBName);
+ const provider = createIndexedDBProvider(docCollection.doc, rootDBName);
provider.connect();
{
- const page = workspace.createDoc({ id: 'page0' });
+ const page = docCollection.createDoc({ id: 'page0' });
page.load();
const pageBlockId = page.addBlock(
'affine:page' as keyof BlockSuite.BlockModels
@@ -264,7 +264,7 @@ describe('indexeddb provider', () => {
});
test("data won't be lost", async () => {
- const doc = new Workspace.Y.Doc();
+ const doc = new DocCollection.Y.Doc();
const map = doc.getMap('map');
for (let i = 0; i < 100; i++) {
map.set(`${i}`, i);
@@ -275,7 +275,7 @@ describe('indexeddb provider', () => {
provider.disconnect();
}
{
- const newDoc = new Workspace.Y.Doc();
+ const newDoc = new DocCollection.Y.Doc();
const provider = createIndexedDBProvider(newDoc, rootDBName);
provider.connect();
provider.disconnect();
@@ -412,14 +412,14 @@ describe('subDoc', () => {
});
test('blocksuite', async () => {
- const page0 = workspace.createDoc({
+ const page0 = docCollection.createDoc({
id: 'page0',
});
page0.load();
const { paragraphBlockId: paragraphBlockIdPage1 } = initEmptyPage(page0);
- const provider = createIndexedDBProvider(workspace.doc, rootDBName);
+ const provider = createIndexedDBProvider(docCollection.doc, rootDBName);
provider.connect();
- const page1 = workspace.createDoc({
+ const page1 = docCollection.createDoc({
id: 'page1',
});
page1.load();
@@ -427,22 +427,22 @@ describe('subDoc', () => {
await setTimeout(200);
provider.disconnect();
{
- const newWorkspace = new Workspace({
+ const docCollection = new DocCollection({
id,
schema,
});
- const provider = createIndexedDBProvider(newWorkspace.doc, rootDBName);
+ const provider = createIndexedDBProvider(docCollection.doc, rootDBName);
provider.connect();
await setTimeout(200);
- const page0 = newWorkspace.getDoc('page0') as Doc;
+ const page0 = docCollection.getDoc('page0') as Doc;
page0.load();
await setTimeout(200);
{
const block = page0.getBlockById(paragraphBlockIdPage1);
assertExists(block);
}
- const page1 = newWorkspace.getDoc('page1') as Doc;
+ const page1 = docCollection.getDoc('page1') as Doc;
page1.load();
await setTimeout(200);
{
@@ -455,30 +455,30 @@ describe('subDoc', () => {
describe('utils', () => {
test('download binary', async () => {
- const page = workspace.createDoc({ id: 'page0' });
+ const page = docCollection.createDoc({ id: 'page0' });
page.load();
initEmptyPage(page);
- const provider = createIndexedDBProvider(workspace.doc, rootDBName);
+ const provider = createIndexedDBProvider(docCollection.doc, rootDBName);
provider.connect();
await setTimeout(200);
provider.disconnect();
const update = (await downloadBinary(
- workspace.id,
+ docCollection.id,
rootDBName
)) as Uint8Array;
expect(update).toBeInstanceOf(Uint8Array);
- const newWorkspace = new Workspace({
+ const newDocCollection = new DocCollection({
id,
schema,
});
- applyUpdate(newWorkspace.doc, update);
+ applyUpdate(newDocCollection.doc, update);
await setTimeout();
- expect(workspace.doc.toJSON()['meta']).toEqual(
- newWorkspace.doc.toJSON()['meta']
+ expect(docCollection.doc.toJSON()['meta']).toEqual(
+ newDocCollection.doc.toJSON()['meta']
);
- expect(Object.keys(workspace.doc.toJSON()['spaces'])).toEqual(
- Object.keys(newWorkspace.doc.toJSON()['spaces'])
+ expect(Object.keys(docCollection.doc.toJSON()['spaces'])).toEqual(
+ Object.keys(newDocCollection.doc.toJSON()['spaces'])
);
});
diff --git a/packages/common/y-provider/package.json b/packages/common/y-provider/package.json
index 4152e56843..8457069e84 100644
--- a/packages/common/y-provider/package.json
+++ b/packages/common/y-provider/package.json
@@ -24,7 +24,7 @@
"build": "vite build"
},
"devDependencies": {
- "@blocksuite/store": "0.13.0-canary-202403050653-934469c",
+ "@blocksuite/store": "0.13.0-canary-202403120738-e15d583",
"vite": "^5.1.4",
"vite-plugin-dts": "3.7.3",
"vitest": "1.3.1",
diff --git a/packages/frontend/component/package.json b/packages/frontend/component/package.json
index ec72822790..42bdf20dd1 100644
--- a/packages/frontend/component/package.json
+++ b/packages/frontend/component/package.json
@@ -72,12 +72,12 @@
"uuid": "^9.0.1"
},
"devDependencies": {
- "@blocksuite/blocks": "0.13.0-canary-202403050653-934469c",
- "@blocksuite/global": "0.13.0-canary-202403050653-934469c",
+ "@blocksuite/blocks": "0.13.0-canary-202403120738-e15d583",
+ "@blocksuite/global": "0.13.0-canary-202403120738-e15d583",
"@blocksuite/icons": "2.1.45",
- "@blocksuite/lit": "0.13.0-canary-202403050653-934469c",
- "@blocksuite/presets": "0.13.0-canary-202403050653-934469c",
- "@blocksuite/store": "0.13.0-canary-202403050653-934469c",
+ "@blocksuite/lit": "0.13.0-canary-202403120738-e15d583",
+ "@blocksuite/presets": "0.13.0-canary-202403120738-e15d583",
+ "@blocksuite/store": "0.13.0-canary-202403120738-e15d583",
"@storybook/addon-actions": "^7.6.17",
"@storybook/addon-essentials": "^7.6.17",
"@storybook/addon-interactions": "^7.6.17",
diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json
index b716899184..edc265d67c 100644
--- a/packages/frontend/core/package.json
+++ b/packages/frontend/core/package.json
@@ -25,14 +25,14 @@
"@affine/i18n": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace-impl": "workspace:*",
- "@blocksuite/block-std": "0.13.0-canary-202403050653-934469c",
- "@blocksuite/blocks": "0.13.0-canary-202403050653-934469c",
- "@blocksuite/global": "0.13.0-canary-202403050653-934469c",
+ "@blocksuite/block-std": "0.13.0-canary-202403120738-e15d583",
+ "@blocksuite/blocks": "0.13.0-canary-202403120738-e15d583",
+ "@blocksuite/global": "0.13.0-canary-202403120738-e15d583",
"@blocksuite/icons": "2.1.45",
- "@blocksuite/inline": "0.13.0-canary-202403050653-934469c",
- "@blocksuite/lit": "0.13.0-canary-202403050653-934469c",
- "@blocksuite/presets": "0.13.0-canary-202403050653-934469c",
- "@blocksuite/store": "0.13.0-canary-202403050653-934469c",
+ "@blocksuite/inline": "0.13.0-canary-202403120738-e15d583",
+ "@blocksuite/lit": "0.13.0-canary-202403120738-e15d583",
+ "@blocksuite/presets": "0.13.0-canary-202403120738-e15d583",
+ "@blocksuite/store": "0.13.0-canary-202403120738-e15d583",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",
diff --git a/packages/frontend/core/src/commands/affine-navigation.tsx b/packages/frontend/core/src/commands/affine-navigation.tsx
index 5fabb25297..5b8d114c0e 100644
--- a/packages/frontend/core/src/commands/affine-navigation.tsx
+++ b/packages/frontend/core/src/commands/affine-navigation.tsx
@@ -1,7 +1,7 @@
import { WorkspaceSubPath } from '@affine/core/shared';
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightBigIcon } from '@blocksuite/icons';
-import type { Workspace } from '@blocksuite/store';
+import type { DocCollection } from '@blocksuite/store';
import { registerAffineCommand } from '@toeverything/infra/command';
import type { createStore } from 'jotai';
@@ -11,13 +11,13 @@ import type { useNavigateHelper } from '../hooks/use-navigate-helper';
export function registerAffineNavigationCommands({
t,
store,
- workspace,
+ docCollection,
navigationHelper,
}: {
t: ReturnType;
store: ReturnType;
navigationHelper: ReturnType;
- workspace: Workspace;
+ docCollection: DocCollection;
}) {
const unsubs: Array<() => void> = [];
unsubs.push(
@@ -27,7 +27,7 @@ export function registerAffineNavigationCommands({
icon: ,
label: t['com.affine.cmdk.affine.navigation.goto-all-pages'](),
run() {
- navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
+ navigationHelper.jumpToSubPath(docCollection.id, WorkspaceSubPath.ALL);
},
})
);
@@ -39,7 +39,7 @@ export function registerAffineNavigationCommands({
icon: ,
label: 'Go to Collection List',
run() {
- navigationHelper.jumpToCollections(workspace.id);
+ navigationHelper.jumpToCollections(docCollection.id);
},
})
);
@@ -51,7 +51,7 @@ export function registerAffineNavigationCommands({
icon: ,
label: 'Go to Tag List',
run() {
- navigationHelper.jumpToTags(workspace.id);
+ navigationHelper.jumpToTags(docCollection.id);
},
})
);
@@ -91,7 +91,10 @@ export function registerAffineNavigationCommands({
icon: ,
label: t['com.affine.cmdk.affine.navigation.goto-trash'](),
run() {
- navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.TRASH);
+ navigationHelper.jumpToSubPath(
+ docCollection.id,
+ WorkspaceSubPath.TRASH
+ );
},
})
);
diff --git a/packages/frontend/core/src/components/affine/awareness/index.tsx b/packages/frontend/core/src/components/affine/awareness/index.tsx
index ac46150ca8..4c9d8df2f9 100644
--- a/packages/frontend/core/src/components/affine/awareness/index.tsx
+++ b/packages/frontend/core/src/components/affine/awareness/index.tsx
@@ -14,7 +14,7 @@ const SyncAwarenessInnerLoggedIn = () => {
useEffect(() => {
if (user && currentWorkspace) {
- currentWorkspace.blockSuiteWorkspace.awarenessStore.awareness.setLocalStateField(
+ currentWorkspace.docCollection.awarenessStore.awareness.setLocalStateField(
'user',
{
name: user.name,
@@ -23,7 +23,7 @@ const SyncAwarenessInnerLoggedIn = () => {
);
return () => {
- currentWorkspace.blockSuiteWorkspace.awarenessStore.awareness.setLocalStateField(
+ currentWorkspace.docCollection.awarenessStore.awareness.setLocalStateField(
'user',
null
);
diff --git a/packages/frontend/core/src/components/affine/page-history-modal/data.ts b/packages/frontend/core/src/components/affine/page-history-modal/data.ts
index 3debe46c18..49935e1898 100644
--- a/packages/frontend/core/src/components/affine/page-history-modal/data.ts
+++ b/packages/frontend/core/src/components/affine/page-history-modal/data.ts
@@ -1,5 +1,5 @@
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
-import { useBlockSuiteWorkspacePage } from '@affine/core/hooks/use-block-suite-workspace-page';
+import { useDocCollectionPage } from '@affine/core/hooks/use-block-suite-workspace-page';
import { timestampToLocalDate } from '@affine/core/utils';
import { DebugLogger } from '@affine/debug';
import {
@@ -10,7 +10,7 @@ import {
} from '@affine/graphql';
import { AffineCloudBlobStorage } from '@affine/workspace-impl';
import { assertEquals } from '@blocksuite/global/utils';
-import { Workspace } from '@blocksuite/store';
+import { DocCollection } from '@blocksuite/store';
import { globalBlockSuiteSchema } from '@toeverything/infra';
import { revertUpdate } from '@toeverything/y-indexeddb';
import { useEffect, useMemo } from 'react';
@@ -98,14 +98,14 @@ const snapshotFetcher = async (
// so that we do not need to worry about providers etc
// todo: fix references to the page (the referenced page will shown as deleted)
// if we simply clone the current workspace, it maybe time consuming right?
-const workspaceMap = new Map();
+const docCollectionMap = new Map();
// assume the workspace is a cloud workspace since the history feature is only enabled for cloud workspace
const getOrCreateShellWorkspace = (workspaceId: string) => {
- let workspace = workspaceMap.get(workspaceId);
- if (!workspace) {
+ let docCollection = docCollectionMap.get(workspaceId);
+ if (!docCollection) {
const blobStorage = new AffineCloudBlobStorage(workspaceId);
- workspace = new Workspace({
+ docCollection = new DocCollection({
id: workspaceId,
blobStorages: [
() => ({
@@ -114,10 +114,10 @@ const getOrCreateShellWorkspace = (workspaceId: string) => {
],
schema: globalBlockSuiteSchema,
});
- workspaceMap.set(workspaceId, workspace);
- workspace.doc.emit('sync', []);
+ docCollectionMap.set(workspaceId, docCollection);
+ docCollection.doc.emit('sync', []);
}
- return workspace;
+ return docCollection;
};
// workspace id + page id + timestamp -> snapshot (update binary)
@@ -139,17 +139,17 @@ export const usePageHistory = (
// workspace id + page id + timestamp + snapshot -> Page (to be used for rendering in blocksuite editor)
export const useSnapshotPage = (
- workspace: Workspace,
+ docCollection: DocCollection,
pageDocId: string,
ts?: string
) => {
- const snapshot = usePageHistory(workspace.id, pageDocId, ts);
+ const snapshot = usePageHistory(docCollection.id, pageDocId, ts);
const page = useMemo(() => {
if (!ts) {
return;
}
const pageId = pageDocId + '-' + ts;
- const historyShellWorkspace = getOrCreateShellWorkspace(workspace.id);
+ const historyShellWorkspace = getOrCreateShellWorkspace(docCollection.id);
let page = historyShellWorkspace.getDoc(pageId);
if (!page && snapshot) {
page = historyShellWorkspace.createDoc({
@@ -163,15 +163,15 @@ export const useSnapshotPage = (
}); // must load before applyUpdate
}
return page ?? undefined;
- }, [pageDocId, snapshot, ts, workspace]);
+ }, [pageDocId, snapshot, ts, docCollection]);
useEffect(() => {
- const historyShellWorkspace = getOrCreateShellWorkspace(workspace.id);
+ const historyShellWorkspace = getOrCreateShellWorkspace(docCollection.id);
// apply the rootdoc's update to the current workspace
// this makes sure the page reference links are not deleted ones in the preview
- const update = encodeStateAsUpdate(workspace.doc);
+ const update = encodeStateAsUpdate(docCollection.doc);
applyUpdate(historyShellWorkspace.doc, update);
- }, [workspace]);
+ }, [docCollection]);
return page;
};
@@ -187,13 +187,16 @@ export const historyListGroupByDay = (histories: DocHistory[]) => {
return [...map.entries()];
};
-export const useRestorePage = (workspace: Workspace, pageId: string) => {
- const page = useBlockSuiteWorkspacePage(workspace, pageId);
+export const useRestorePage = (
+ docCollection: DocCollection,
+ pageId: string
+) => {
+ const page = useDocCollectionPage(docCollection, pageId);
const mutateQueryResource = useMutateQueryResource();
const { trigger: recover, isMutating } = useMutation({
mutation: recoverDocMutation,
});
- const { getDocMeta, setDocTitle } = useDocMetaHelper(workspace);
+ const { getDocMeta, setDocTitle } = useDocMetaHelper(docCollection);
const onRestore = useMemo(() => {
return async (version: string, update: Uint8Array) => {
@@ -216,12 +219,12 @@ export const useRestorePage = (workspace: Workspace, pageId: string) => {
await recover({
docId: pageDocId,
timestamp: version,
- workspaceId: workspace.id,
+ workspaceId: docCollection.id,
});
await mutateQueryResource(listHistoryQuery, vars => {
return (
- vars.pageDocId === pageDocId && vars.workspaceId === workspace.id
+ vars.pageDocId === pageDocId && vars.workspaceId === docCollection.id
);
});
@@ -234,7 +237,7 @@ export const useRestorePage = (workspace: Workspace, pageId: string) => {
pageId,
recover,
setDocTitle,
- workspace.id,
+ docCollection.id,
]);
return {
diff --git a/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx b/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx
index a620d88e43..87febfdaa4 100644
--- a/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx
+++ b/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx
@@ -5,13 +5,13 @@ import { ConfirmModal, Modal } from '@affine/component/ui/modal';
import { openSettingModalAtom } from '@affine/core/atoms';
import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
-import { useBlockSuiteWorkspacePageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
+import { useDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
import { useWorkspaceQuota } from '@affine/core/hooks/use-workspace-quota';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon, ToggleCollapseIcon } from '@blocksuite/icons';
import type { Doc as BlockSuiteDoc } from '@blocksuite/store';
-import { type Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
+import { type DocCollection } from '@blocksuite/store';
import * as Collapsible from '@radix-ui/react-collapsible';
import type { DialogContentProps } from '@radix-ui/react-dialog';
import { Doc, type PageMode, Workspace } from '@toeverything/infra';
@@ -49,7 +49,7 @@ import * as styles from './styles.css';
export interface PageHistoryModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
- workspace: BlockSuiteWorkspace;
+ docCollection: DocCollection;
pageId: string;
}
@@ -444,26 +444,26 @@ const EmptyHistoryPrompt = () => {
};
const PageHistoryManager = ({
- workspace,
+ docCollection,
pageId,
onClose,
}: {
- workspace: BlockSuiteWorkspace;
+ docCollection: DocCollection;
pageId: string;
onClose: () => void;
}) => {
- const workspaceId = workspace.id;
+ const workspaceId = docCollection.id;
const [activeVersion, setActiveVersion] = useState