From 44de4474c3bcdd92a46a691a9c0b7b24781f80c5 Mon Sep 17 00:00:00 2001 From: forehalo Date: Fri, 17 Jan 2025 07:06:11 +0000 Subject: [PATCH] feat(server): use user model (#9710) --- .../src/__tests__/auth/controller.spec.ts | 4 +- .../server/src/__tests__/auth/service.spec.ts | 11 +- .../src/__tests__/oauth/controller.spec.ts | 10 +- .../src/__tests__/payment/service.spec.ts | 10 +- .../server/src/__tests__/utils/user.ts | 5 +- .../server/src/__tests__/utils/utils.ts | 4 +- .../src/__tests__/workspace-invite.e2e.ts | 10 +- packages/backend/server/src/app.module.ts | 4 +- .../server/src/core/auth/controller.ts | 13 +- .../backend/server/src/core/auth/resolver.ts | 4 +- .../backend/server/src/core/auth/service.ts | 28 +- .../server/src/core/features/management.ts | 6 +- .../server/src/core/selfhost/controller.ts | 33 +- .../backend/server/src/core/user/event.ts | 61 ++++ .../backend/server/src/core/user/index.ts | 6 +- .../backend/server/src/core/user/resolver.ts | 42 +-- .../backend/server/src/core/user/service.ts | 339 ------------------ .../src/core/workspaces/resolvers/service.ts | 14 +- .../src/core/workspaces/resolvers/team.ts | 10 +- .../core/workspaces/resolvers/workspace.ts | 11 +- packages/backend/server/src/models/index.ts | 2 +- packages/backend/server/src/models/user.ts | 53 --- .../server/src/plugins/oauth/controller.ts | 8 +- .../server/src/plugins/payment/service.ts | 10 +- tsconfig.json | 2 +- 25 files changed, 179 insertions(+), 521 deletions(-) create mode 100644 packages/backend/server/src/core/user/event.ts delete mode 100644 packages/backend/server/src/core/user/service.ts diff --git a/packages/backend/server/src/__tests__/auth/controller.spec.ts b/packages/backend/server/src/__tests__/auth/controller.spec.ts index f4dc56d339..e9c4779ae2 100644 --- a/packages/backend/server/src/__tests__/auth/controller.spec.ts +++ b/packages/backend/server/src/__tests__/auth/controller.spec.ts @@ -8,12 +8,11 @@ import { MailService } from '../../base'; import { AuthModule, CurrentUser } from '../../core/auth'; import { AuthService } from '../../core/auth/service'; import { FeatureModule } from '../../core/features'; -import { UserModule, UserService } from '../../core/user'; +import { UserModule } from '../../core/user'; import { createTestingApp, getSession, sessionCookie } from '../utils'; const test = ava as TestFn<{ auth: AuthService; - user: UserService; u1: CurrentUser; db: PrismaClient; mailer: Sinon.SinonStubbedInstance; @@ -31,7 +30,6 @@ test.before(async t => { }); t.context.auth = app.get(AuthService); - t.context.user = app.get(UserService); t.context.db = app.get(PrismaClient); t.context.mailer = app.get(MailService); t.context.app = app; diff --git a/packages/backend/server/src/__tests__/auth/service.spec.ts b/packages/backend/server/src/__tests__/auth/service.spec.ts index cb6f903ff5..2f1a7c21c4 100644 --- a/packages/backend/server/src/__tests__/auth/service.spec.ts +++ b/packages/backend/server/src/__tests__/auth/service.spec.ts @@ -6,12 +6,13 @@ import { CurrentUser } from '../../core/auth'; import { AuthService } from '../../core/auth/service'; import { FeatureModule } from '../../core/features'; import { QuotaModule } from '../../core/quota'; -import { UserModule, UserService } from '../../core/user'; +import { UserModule } from '../../core/user'; +import { Models } from '../../models'; import { createTestingModule, initTestingDB } from '../utils'; const test = ava as TestFn<{ auth: AuthService; - user: UserService; + models: Models; u1: CurrentUser; db: PrismaClient; m: TestingModule; @@ -24,7 +25,7 @@ test.before(async t => { }); t.context.auth = m.get(AuthService); - t.context.user = m.get(UserService); + t.context.models = m.get(Models); t.context.db = m.get(PrismaClient); t.context.m = m; }); @@ -55,9 +56,9 @@ test('should throw if user not found', async t => { }); test('should throw if password not set', async t => { - const { user, auth } = t.context; + const { models, auth } = t.context; - await user.createUser({ + await models.user.create({ email: 'u2@affine.pro', name: 'u2', }); diff --git a/packages/backend/server/src/__tests__/oauth/controller.spec.ts b/packages/backend/server/src/__tests__/oauth/controller.spec.ts index 54a931ab91..b7c363d039 100644 --- a/packages/backend/server/src/__tests__/oauth/controller.spec.ts +++ b/packages/backend/server/src/__tests__/oauth/controller.spec.ts @@ -11,7 +11,7 @@ import { URLHelper } from '../../base'; import { ConfigModule } from '../../base/config'; import { CurrentUser } from '../../core/auth'; import { AuthService } from '../../core/auth/service'; -import { UserService } from '../../core/user'; +import { Models } from '../../models'; import { OAuthProviderName } from '../../plugins/oauth/config'; import { GoogleOAuthProvider } from '../../plugins/oauth/providers/google'; import { OAuthService } from '../../plugins/oauth/service'; @@ -20,7 +20,7 @@ import { createTestingApp, getSession, initTestingDB } from '../utils'; const test = ava as TestFn<{ auth: AuthService; oauth: OAuthService; - user: UserService; + models: Models; u1: CurrentUser; db: PrismaClient; app: INestApplication; @@ -47,7 +47,7 @@ test.before(async t => { t.context.auth = app.get(AuthService); t.context.oauth = app.get(OAuthService); - t.context.user = app.get(UserService); + t.context.models = app.get(Models); t.context.db = app.get(PrismaClient); t.context.app = app; }); @@ -309,9 +309,9 @@ test('should not throw if account registered', async t => { }); test('should be able to fullfil user with oauth sign in', async t => { - const { app, user, db } = t.context; + const { app, models, db } = t.context; - const u3 = await user.createUser({ + const u3 = await models.user.create({ name: 'u3', email: 'u3@affine.pro', registered: false, diff --git a/packages/backend/server/src/__tests__/payment/service.spec.ts b/packages/backend/server/src/__tests__/payment/service.spec.ts index 5f94f246fd..383f524c50 100644 --- a/packages/backend/server/src/__tests__/payment/service.spec.ts +++ b/packages/backend/server/src/__tests__/payment/service.spec.ts @@ -234,11 +234,7 @@ test.before(async t => { test.beforeEach(async t => { const { db, app, stripe } = t.context; - Sinon.reset(); await initTestingDB(db); - t.context.runtime.fetch - .withArgs('plugins.payment/showLifetimePrice') - .resolves(true); t.context.u1 = await app.get(AuthService).signUp('u1@affine.pro', '1'); await db.workspace.create({ @@ -254,7 +250,13 @@ test.beforeEach(async t => { }, }); + Sinon.reset(); + // default stubs + t.context.runtime.fetch + .withArgs('plugins.payment/showLifetimePrice') + .resolves(true); + // @ts-expect-error stub stripe.prices.list.callsFake((params: Stripe.PriceListParams) => { if (params.lookup_keys) { diff --git a/packages/backend/server/src/__tests__/utils/user.ts b/packages/backend/server/src/__tests__/utils/user.ts index 5d1140c524..0925a0658f 100644 --- a/packages/backend/server/src/__tests__/utils/user.ts +++ b/packages/backend/server/src/__tests__/utils/user.ts @@ -7,7 +7,8 @@ import { type CurrentUser, } from '../../core/auth'; import { sessionUser } from '../../core/auth/service'; -import { UserService, type UserType } from '../../core/user'; +import { UserType } from '../../core/user'; +import { Models } from '../../models'; import { gql } from './common'; export type UserAuthedType = UserType & { token: ClientTokenType }; @@ -52,7 +53,7 @@ export async function signUp( password: string, autoVerifyEmail = true ): Promise { - const user = await app.get(UserService).createUser({ + const user = await app.get(Models).user.create({ name, email, password, diff --git a/packages/backend/server/src/__tests__/utils/utils.ts b/packages/backend/server/src/__tests__/utils/utils.ts index 1884bfe7cf..bb8918f0c4 100644 --- a/packages/backend/server/src/__tests__/utils/utils.ts +++ b/packages/backend/server/src/__tests__/utils/utils.ts @@ -13,7 +13,7 @@ import { GlobalExceptionFilter, Runtime } from '../../base'; import { GqlModule } from '../../base/graphql'; import { AuthGuard, AuthModule } from '../../core/auth'; import { UserFeaturesInit1698652531198 } from '../../data/migrations/1698652531198-user-features-init'; -import { ModelModules } from '../../models'; +import { ModelsModule } from '../../models'; export type PermissionEnum = 'Owner' | 'Admin' | 'Write' | 'Read'; @@ -80,7 +80,7 @@ export async function createTestingModule( ? [AppModule] : dedupeModules([ ...FunctionalityModules, - ModelModules, + ModelsModule, AuthModule, GqlModule, ...imports, diff --git a/packages/backend/server/src/__tests__/workspace-invite.e2e.ts b/packages/backend/server/src/__tests__/workspace-invite.e2e.ts index 0e21adc6ad..933df20b3e 100644 --- a/packages/backend/server/src/__tests__/workspace-invite.e2e.ts +++ b/packages/backend/server/src/__tests__/workspace-invite.e2e.ts @@ -10,7 +10,7 @@ import ava from 'ava'; import { AppModule } from '../app.module'; import { MailService } from '../base/mailer'; import { AuthService } from '../core/auth/service'; -import { UserService } from '../core/user'; +import { Models } from '../models'; import { acceptInviteById, createTestingApp, @@ -27,7 +27,7 @@ const test = ava as TestFn<{ client: PrismaClient; auth: AuthService; mail: MailService; - user: UserService; + models: Models; }>; test.beforeEach(async t => { @@ -38,7 +38,7 @@ test.beforeEach(async t => { t.context.client = app.get(PrismaClient); t.context.auth = app.get(AuthService); t.context.mail = app.get(MailService); - t.context.user = app.get(UserService); + t.context.models = app.get(Models); }); test.afterEach.always(async t => { @@ -87,14 +87,14 @@ test('should revoke a user', async t => { }); test('should create user if not exist', async t => { - const { app, user } = t.context; + const { app, models } = t.context; const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); const workspace = await createWorkspace(app, u1.token.token); await inviteUser(app, u1.token.token, workspace.id, 'u2@affine.pro'); - const u2 = await user.findUserByEmail('u2@affine.pro'); + const u2 = await models.user.getUserByEmail('u2@affine.pro'); t.not(u2, undefined, 'failed to create user'); t.is(u2?.name, 'u2', 'failed to create user'); }); diff --git a/packages/backend/server/src/app.module.ts b/packages/backend/server/src/app.module.ts index a6ce6da9c6..5237684edf 100644 --- a/packages/backend/server/src/app.module.ts +++ b/packages/backend/server/src/app.module.ts @@ -37,7 +37,7 @@ import { StorageModule } from './core/storage'; import { SyncModule } from './core/sync'; import { UserModule } from './core/user'; import { WorkspaceModule } from './core/workspaces'; -import { ModelModules } from './models'; +import { ModelsModule } from './models'; import { REGISTERED_PLUGINS } from './plugins'; import { ENABLED_PLUGINS } from './plugins/registry'; @@ -155,7 +155,7 @@ export function buildAppModule() { factor // basic .use(...FunctionalityModules) - .use(ModelModules) + .use(ModelsModule) .useIf(config => config.flavor.sync, WebSocketModule) // auth diff --git a/packages/backend/server/src/core/auth/controller.ts b/packages/backend/server/src/core/auth/controller.ts index 218e2037ed..4ba205609d 100644 --- a/packages/backend/server/src/core/auth/controller.ts +++ b/packages/backend/server/src/core/auth/controller.ts @@ -27,7 +27,6 @@ import { UseNamedGuard, } from '../../base'; import { Models, TokenType } from '../../models'; -import { UserService } from '../user'; import { validators } from '../utils/validators'; import { Public } from './guard'; import { AuthService } from './service'; @@ -56,7 +55,6 @@ export class AuthController { constructor( private readonly url: URLHelper, private readonly auth: AuthService, - private readonly user: UserService, private readonly models: Models, private readonly config: Config, private readonly runtime: Runtime @@ -81,9 +79,7 @@ export class AuthController { } validators.assertValidEmail(params.email); - const user = await this.user.findUserWithHashedPasswordByEmail( - params.email - ); + const user = await this.models.user.getUserByEmail(params.email); const magicLinkAvailable = !!this.config.mailer.host; @@ -159,7 +155,7 @@ export class AuthController { redirectUrl?: string ) { // send email magic link - const user = await this.user.findUserByEmail(email); + const user = await this.models.user.getUserByEmail(email); if (!user) { const allowSignup = await this.runtime.fetch('auth/allowSignup'); if (!allowSignup) { @@ -263,10 +259,7 @@ export class AuthController { throw new InvalidEmailToken(); } - const user = await this.user.fulfillUser(email, { - emailVerifiedAt: new Date(), - registered: true, - }); + const user = await this.models.user.fulfill(email); await this.auth.setCookies(req, res, user.id); res.send({ id: user.id }); diff --git a/packages/backend/server/src/core/auth/resolver.ts b/packages/backend/server/src/core/auth/resolver.ts index 2ddcadb888..c7c60767a9 100644 --- a/packages/backend/server/src/core/auth/resolver.ts +++ b/packages/backend/server/src/core/auth/resolver.ts @@ -23,7 +23,6 @@ import { } from '../../base'; import { Models, TokenType } from '../../models'; import { Admin } from '../common'; -import { UserService } from '../user'; import { UserType } from '../user/types'; import { validators } from '../utils/validators'; import { Public } from './guard'; @@ -48,7 +47,6 @@ export class AuthResolver { constructor( private readonly url: URLHelper, private readonly auth: AuthService, - private readonly user: UserService, private readonly models: Models ) {} @@ -234,7 +232,7 @@ export class AuthResolver { throw new InvalidEmailToken(); } - const hasRegistered = await this.user.findUserByEmail(email); + const hasRegistered = await this.models.user.getUserByEmail(email); if (hasRegistered) { if (hasRegistered.id !== user.id) { diff --git a/packages/backend/server/src/core/auth/service.ts b/packages/backend/server/src/core/auth/service.ts index de53a620d8..3b71931049 100644 --- a/packages/backend/server/src/core/auth/service.ts +++ b/packages/backend/server/src/core/auth/service.ts @@ -7,7 +7,6 @@ import { Models, type User, type UserSession } from '../../models'; import { FeatureManagementService } from '../features/management'; import { QuotaService } from '../quota/service'; import { QuotaType } from '../quota/types'; -import { UserService } from '../user/service'; import type { CurrentUser } from './session'; export function sessionUser( @@ -47,17 +46,16 @@ export class AuthService implements OnApplicationBootstrap { private readonly models: Models, private readonly mailer: MailService, private readonly feature: FeatureManagementService, - private readonly quota: QuotaService, - private readonly user: UserService + private readonly quota: QuotaService ) {} async onApplicationBootstrap() { if (this.config.node.dev) { try { const [email, name, password] = ['dev@affine.pro', 'Dev User', 'dev']; - let devUser = await this.user.findUserByEmail(email); + let devUser = await this.models.user.getUserByEmail(email); if (!devUser) { - devUser = await this.user.createUser_without_verification({ + devUser = await this.models.user.create({ email, name, password, @@ -86,8 +84,8 @@ export class AuthService implements OnApplicationBootstrap { ); } - return this.user - .createUser_without_verification({ + return this.models.user + .create({ email, password, }) @@ -95,7 +93,7 @@ export class AuthService implements OnApplicationBootstrap { } async signIn(email: string, password: string): Promise { - return this.user.signIn(email, password).then(sessionUser); + return this.models.user.signIn(email, password).then(sessionUser); } async signOut(sessionId: string, userId?: string) { @@ -131,7 +129,7 @@ export class AuthService implements OnApplicationBootstrap { userSession = sessions.at(-1)!; } - const user = await this.user.findUserById(userSession.userId); + const user = await this.models.user.get(userSession.userId); if (!user) { return null; @@ -284,25 +282,23 @@ export class AuthService implements OnApplicationBootstrap { id: string, newPassword: string ): Promise> { - return this.user.updateUser(id, { password: newPassword }); + return this.models.user.update(id, { password: newPassword }); } async changeEmail( id: string, newEmail: string ): Promise> { - return this.user.updateUser(id, { + return this.models.user.update(id, { email: newEmail, emailVerifiedAt: new Date(), }); } async setEmailVerified(id: string) { - return await this.user.updateUser( - id, - { emailVerifiedAt: new Date() }, - { emailVerifiedAt: true } - ); + return await this.models.user.update(id, { + emailVerifiedAt: new Date(), + }); } async sendChangePasswordEmail(email: string, callbackUrl: string) { diff --git a/packages/backend/server/src/core/features/management.ts b/packages/backend/server/src/core/features/management.ts index 5c90b7f692..c8cf4ceea4 100644 --- a/packages/backend/server/src/core/features/management.ts +++ b/packages/backend/server/src/core/features/management.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { type EventPayload, OnEvent, Runtime } from '../../base'; -import { UserService } from '../user/service'; +import { Models } from '../../models'; import { FeatureService } from './service'; import { FeatureType } from './types'; @@ -18,7 +18,7 @@ export class FeatureManagementService { constructor( private readonly feature: FeatureService, - private readonly user: UserService, + private readonly models: Models, private readonly runtime: Runtime ) {} @@ -100,7 +100,7 @@ export class FeatureManagementService { ); if (earlyAccessControlEnabled && !this.isStaff(email)) { - const user = await this.user.findUserByEmail(email); + const user = await this.models.user.getUserByEmail(email); if (!user) { return false; } diff --git a/packages/backend/server/src/core/selfhost/controller.ts b/packages/backend/server/src/core/selfhost/controller.ts index 369a6a6d0f..e1d80abbd9 100644 --- a/packages/backend/server/src/core/selfhost/controller.ts +++ b/packages/backend/server/src/core/selfhost/controller.ts @@ -7,10 +7,12 @@ import { InternalServerError, Mutex, PasswordRequired, + Runtime, } from '../../base'; +import { Models } from '../../models'; import { AuthService, Public } from '../auth'; import { ServerService } from '../config'; -import { UserService } from '../user/service'; +import { validators } from '../utils/validators'; interface CreateUserInput { email: string; @@ -20,11 +22,12 @@ interface CreateUserInput { @Controller('/api/setup') export class CustomSetupController { constructor( - private readonly user: UserService, + private readonly models: Models, private readonly auth: AuthService, private readonly event: EventEmitter, private readonly mutex: Mutex, - private readonly server: ServerService + private readonly server: ServerService, + private readonly runtime: Runtime ) {} @Public() @@ -34,21 +37,33 @@ export class CustomSetupController { @Res() res: Response, @Body() input: CreateUserInput ) { + if (await this.server.initialized()) { + throw new ActionForbidden('First user already created'); + } + + validators.assertValidEmail(input.email); + if (!input.password) { throw new PasswordRequired(); } + const config = await this.runtime.fetchAll({ + 'auth/password.max': true, + 'auth/password.min': true, + }); + + validators.assertValidPassword(input.password, { + max: config['auth/password.max'], + min: config['auth/password.min'], + }); + await using lock = await this.mutex.acquire('createFirstAdmin'); if (!lock) { throw new InternalServerError(); } - if (await this.server.initialized()) { - throw new ActionForbidden('First user already created'); - } - - const user = await this.user.createUser({ + const user = await this.models.user.create({ email: input.email, password: input.password, registered: true, @@ -59,7 +74,7 @@ export class CustomSetupController { await this.auth.setCookies(req, res, user.id); res.send({ id: user.id, email: user.email, name: user.name }); } catch (e) { - await this.user.deleteUser(user.id); + await this.models.user.delete(user.id); throw e; } } diff --git a/packages/backend/server/src/core/user/event.ts b/packages/backend/server/src/core/user/event.ts new file mode 100644 index 0000000000..89ec485221 --- /dev/null +++ b/packages/backend/server/src/core/user/event.ts @@ -0,0 +1,61 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { Config, type EventPayload, OnEvent } from '../../base'; + +@Injectable() +export class UserEventsListener { + private readonly logger = new Logger(UserEventsListener.name); + + constructor(private readonly config: Config) {} + + @OnEvent('user.updated') + async onUserUpdated(user: EventPayload<'user.updated'>) { + const { enabled, customerIo } = this.config.metrics; + if (enabled && customerIo?.token) { + const payload = { + name: user.name, + email: user.email, + created_at: Number(user.createdAt) / 1000, + }; + try { + await fetch(`https://track.customer.io/api/v1/customers/${user.id}`, { + method: 'PUT', + headers: { + Authorization: `Basic ${customerIo.token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + } catch (e) { + this.logger.error('Failed to publish user update event:', e); + } + } + } + + @OnEvent('user.deleted') + async onUserDeleted(user: EventPayload<'user.deleted'>) { + const { enabled, customerIo } = this.config.metrics; + if (enabled && customerIo?.token) { + try { + if (user.emailVerifiedAt) { + // suppress email if email is verified + await fetch( + `https://track.customer.io/api/v1/customers/${user.email}/suppress`, + { + method: 'POST', + headers: { + Authorization: `Basic ${customerIo.token}`, + }, + } + ); + } + await fetch(`https://track.customer.io/api/v1/customers/${user.id}`, { + method: 'DELETE', + headers: { Authorization: `Basic ${customerIo.token}` }, + }); + } catch (e) { + this.logger.error('Failed to publish user delete event:', e); + } + } + } +} diff --git a/packages/backend/server/src/core/user/index.ts b/packages/backend/server/src/core/user/index.ts index fc0361e991..aeb8f25c1e 100644 --- a/packages/backend/server/src/core/user/index.ts +++ b/packages/backend/server/src/core/user/index.ts @@ -3,16 +3,14 @@ import { Module } from '@nestjs/common'; import { PermissionModule } from '../permission'; import { StorageModule } from '../storage'; import { UserAvatarController } from './controller'; +import { UserEventsListener } from './event'; import { UserManagementResolver, UserResolver } from './resolver'; -import { UserService } from './service'; @Module({ imports: [StorageModule, PermissionModule], - providers: [UserResolver, UserService, UserManagementResolver], + providers: [UserResolver, UserManagementResolver, UserEventsListener], controllers: [UserAvatarController], - exports: [UserService], }) export class UserModule {} -export { UserService } from './service'; export { UserType } from './types'; diff --git a/packages/backend/server/src/core/user/resolver.ts b/packages/backend/server/src/core/user/resolver.ts index 23e861b37e..22e46648c0 100644 --- a/packages/backend/server/src/core/user/resolver.ts +++ b/packages/backend/server/src/core/user/resolver.ts @@ -17,13 +17,13 @@ import { Throttle, UserNotFound, } from '../../base'; +import { Models } from '../../models'; import { Public } from '../auth/guard'; import { sessionUser } from '../auth/service'; import { CurrentUser } from '../auth/session'; import { Admin } from '../common'; import { AvatarStorage } from '../storage'; import { validators } from '../utils/validators'; -import { UserService } from './service'; import { DeleteAccount, ManageUserInput, @@ -37,7 +37,7 @@ import { export class UserResolver { constructor( private readonly storage: AvatarStorage, - private readonly users: UserService + private readonly models: Models ) {} @Throttle('strict') @@ -54,7 +54,7 @@ export class UserResolver { validators.assertValidEmail(email); // TODO(@forehalo): need to limit a user can only get another user witch is in the same workspace - const user = await this.users.findUserWithHashedPasswordByEmail(email); + const user = await this.models.user.getUserByEmail(email); // return empty response when user not exists if (!user) return null; @@ -99,7 +99,7 @@ export class UserResolver { await this.storage.delete(user.avatarUrl); } - return this.users.updateUser(user.id, { avatarUrl }); + return this.models.user.update(user.id, { avatarUrl }); } @Mutation(() => UserType, { @@ -115,7 +115,7 @@ export class UserResolver { return user; } - return sessionUser(await this.users.updateUser(user.id, input)); + return sessionUser(await this.models.user.update(user.id, input)); } @Mutation(() => RemoveAvatar, { @@ -126,7 +126,7 @@ export class UserResolver { if (!user) { throw new UserNotFound(); } - await this.users.updateUser(user.id, { avatarUrl: null }); + await this.models.user.update(user.id, { avatarUrl: null }); return { success: true }; } @@ -134,7 +134,7 @@ export class UserResolver { async deleteAccount( @CurrentUser() user: CurrentUser ): Promise { - await this.users.deleteUser(user.id); + await this.models.user.delete(user.id); return { success: true }; } } @@ -162,7 +162,7 @@ class CreateUserInput { export class UserManagementResolver { constructor( private readonly db: PrismaClient, - private readonly user: UserService + private readonly models: Models ) {} @Query(() => Int, { @@ -178,11 +178,7 @@ export class UserManagementResolver { async users( @Args({ name: 'filter', type: () => ListUserInput }) input: ListUserInput ): Promise { - const users = await this.db.user.findMany({ - select: { ...this.user.defaultUserSelect, password: true }, - skip: input.skip, - take: input.first, - }); + const users = await this.models.user.pagination(input.skip, input.first); return users.map(sessionUser); } @@ -192,12 +188,7 @@ export class UserManagementResolver { description: 'Get user by id', }) async getUser(@Args('id') id: string) { - const user = await this.db.user.findUnique({ - select: { ...this.user.defaultUserSelect, password: true }, - where: { - id, - }, - }); + const user = await this.models.user.get(id); if (!user) { return null; @@ -212,12 +203,7 @@ export class UserManagementResolver { nullable: true, }) async getUserByEmail(@Args('email') email: string) { - const user = await this.db.user.findUnique({ - select: { ...this.user.defaultUserSelect, password: true }, - where: { - email, - }, - }); + const user = await this.models.user.getUserByEmail(email); if (!user) { return null; @@ -232,7 +218,7 @@ export class UserManagementResolver { async createUser( @Args({ name: 'input', type: () => CreateUserInput }) input: CreateUserInput ) { - const { id } = await this.user.createUser({ + const { id } = await this.models.user.create({ email: input.email, registered: true, }); @@ -251,7 +237,7 @@ export class UserManagementResolver { if (user.id === id) { throw new CannotDeleteOwnAccount(); } - await this.user.deleteUser(id); + await this.models.user.delete(id); return { success: true }; } @@ -276,7 +262,7 @@ export class UserManagementResolver { } return sessionUser( - await this.user.updateUser(user.id, { + await this.models.user.update(user.id, { email: input.email, name: input.name, }) diff --git a/packages/backend/server/src/core/user/service.ts b/packages/backend/server/src/core/user/service.ts deleted file mode 100644 index 08095e74b1..0000000000 --- a/packages/backend/server/src/core/user/service.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { Prisma, PrismaClient, User } from '@prisma/client'; - -import { - Config, - CryptoHelper, - EmailAlreadyUsed, - EventEmitter, - type EventPayload, - OnEvent, - Runtime, - WrongSignInCredentials, - WrongSignInMethod, -} from '../../base'; -import { PermissionService } from '../permission'; -import { Quota_FreePlanV1_1 } from '../quota/schema'; -import { validators } from '../utils/validators'; - -type CreateUserInput = Omit & { name?: string }; - -@Injectable() -export class UserService { - private readonly logger = new Logger(UserService.name); - - defaultUserSelect = { - id: true, - name: true, - email: true, - emailVerifiedAt: true, - avatarUrl: true, - registered: true, - createdAt: true, - } satisfies Prisma.UserSelect; - - constructor( - private readonly config: Config, - private readonly runtime: Runtime, - private readonly crypto: CryptoHelper, - private readonly prisma: PrismaClient, - private readonly emitter: EventEmitter, - private readonly permission: PermissionService - ) {} - - get userCreatingData() { - return { - name: 'Unnamed', - features: { - create: { - reason: 'sign up', - activated: true, - feature: { - connect: { - feature_version: Quota_FreePlanV1_1, - }, - }, - }, - }, - }; - } - - async createUser(data: CreateUserInput) { - validators.assertValidEmail(data.email); - - if (data.password) { - const config = await this.runtime.fetchAll({ - 'auth/password.max': true, - 'auth/password.min': true, - }); - validators.assertValidPassword(data.password, { - max: config['auth/password.max'], - min: config['auth/password.min'], - }); - } - - return this.createUser_without_verification(data); - } - - async createUser_without_verification(data: CreateUserInput) { - const user = await this.findUserByEmail(data.email); - - if (user) { - throw new EmailAlreadyUsed(); - } - - if (data.password) { - data.password = await this.crypto.encryptPassword(data.password); - } - - if (!data.name) { - data.name = data.email.split('@')[0]; - } - - return this.prisma.user.create({ - select: this.defaultUserSelect, - data: { - ...this.userCreatingData, - ...data, - }, - }); - } - - async findUserById(id: string) { - return this.prisma.user - .findUnique({ - where: { id }, - select: this.defaultUserSelect, - }) - .catch(() => { - return null; - }); - } - - async findUserByEmail( - email: string - ): Promise | null> { - validators.assertValidEmail(email); - const rows = await this.prisma.$queryRaw< - // see [this.defaultUserSelect] - { - id: string; - name: string; - email: string; - email_verified: Date | null; - avatar_url: string | null; - registered: boolean; - created_at: Date; - }[] - >` - SELECT "id", "name", "email", "email_verified", "avatar_url", "registered", "created_at" - FROM "users" - WHERE lower("email") = lower(${email}) - `; - - const user = rows[0]; - - if (!user) { - return null; - } - - return { - ...user, - emailVerifiedAt: user.email_verified, - avatarUrl: user.avatar_url, - createdAt: user.created_at, - }; - } - - /** - * supposed to be used only for `Credential SignIn` - */ - async findUserWithHashedPasswordByEmail(email: string): Promise { - validators.assertValidEmail(email); - - // see https://www.prisma.io/docs/orm/prisma-client/using-raw-sql/raw-queries#typing-queryraw-results - const rows = await this.prisma.$queryRaw< - { - id: string; - name: string; - email: string; - password: string | null; - email_verified: Date | null; - avatar_url: string | null; - registered: boolean; - created_at: Date; - }[] - >` - SELECT * - FROM "users" - WHERE lower("email") = lower(${email}) - `; - - const user = rows[0]; - if (!user) { - return null; - } - - return { - ...user, - emailVerifiedAt: user.email_verified, - avatarUrl: user.avatar_url, - createdAt: user.created_at, - }; - } - - async signIn(email: string, password: string) { - const user = await this.findUserWithHashedPasswordByEmail(email); - - if (!user) { - throw new WrongSignInCredentials({ email }); - } - - if (!user.password) { - throw new WrongSignInMethod(); - } - - const passwordMatches = await this.crypto.verifyPassword( - password, - user.password - ); - - if (!passwordMatches) { - throw new WrongSignInCredentials({ email }); - } - - return user; - } - - async fulfillUser( - email: string, - data: Omit, 'id'> - ) { - const user = await this.findUserByEmail(email); - if (!user) { - return this.createUser({ - ...this.userCreatingData, - email, - name: email.split('@')[0], - ...data, - }); - } else { - if (user.registered) { - delete data.registered; - } - if (user.emailVerifiedAt) { - delete data.emailVerifiedAt; - } - - if (Object.keys(data).length) { - return await this.prisma.user.update({ - where: { id: user.id }, - data, - }); - } - } - - // @ts-expect-error will be removed - this.emitter.emit('user.updated', user); - - return user; - } - - async updateUser( - id: string, - data: Omit, 'id'>, - select: Prisma.UserSelect = this.defaultUserSelect - ) { - if (data.password) { - const config = await this.runtime.fetchAll({ - 'auth/password.max': true, - 'auth/password.min': true, - }); - validators.assertValidPassword(data.password, { - max: config['auth/password.max'], - min: config['auth/password.min'], - }); - - data.password = await this.crypto.encryptPassword(data.password); - } - - if (data.email) { - validators.assertValidEmail(data.email); - const emailTaken = await this.prisma.user.count({ - where: { - email: data.email, - id: { - not: id, - }, - }, - }); - - if (emailTaken) { - throw new EmailAlreadyUsed(); - } - } - - const user = await this.prisma.user.update({ where: { id }, data, select }); - - this.emitter.emit('user.updated', user); - - return user; - } - - async deleteUser(id: string) { - const ownedWorkspaces = await this.permission.getOwnedWorkspaces(id); - const user = await this.prisma.user.delete({ where: { id } }); - this.emitter.emit('user.deleted', { ...user, ownedWorkspaces }); - } - - @OnEvent('user.updated') - async onUserUpdated(user: EventPayload<'user.updated'>) { - const { enabled, customerIo } = this.config.metrics; - if (enabled && customerIo?.token) { - const payload = { - name: user.name, - email: user.email, - created_at: Number(user.createdAt) / 1000, - }; - try { - await fetch(`https://track.customer.io/api/v1/customers/${user.id}`, { - method: 'PUT', - headers: { - Authorization: `Basic ${customerIo.token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }); - } catch (e) { - this.logger.error('Failed to publish user update event:', e); - } - } - } - - @OnEvent('user.deleted') - async onUserDeleted(user: EventPayload<'user.deleted'>) { - const { enabled, customerIo } = this.config.metrics; - if (enabled && customerIo?.token) { - try { - if (user.emailVerifiedAt) { - // suppress email if email is verified - await fetch( - `https://track.customer.io/api/v1/customers/${user.email}/suppress`, - { - method: 'POST', - headers: { - Authorization: `Basic ${customerIo.token}`, - }, - } - ); - } - await fetch(`https://track.customer.io/api/v1/customers/${user.id}`, { - method: 'DELETE', - headers: { Authorization: `Basic ${customerIo.token}` }, - }); - } catch (e) { - this.logger.error('Failed to publish user delete event:', e); - } - } - } -} diff --git a/packages/backend/server/src/core/workspaces/resolvers/service.ts b/packages/backend/server/src/core/workspaces/resolvers/service.ts index 14cf1bb64a..a852c7767d 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/service.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/service.ts @@ -3,10 +3,10 @@ import { PrismaClient } from '@prisma/client'; import { getStreamAsBuffer } from 'get-stream'; import { Cache, MailService, UserNotFound } from '../../../base'; +import { Models } from '../../../models'; import { DocContentService } from '../../doc-renderer'; import { Permission, PermissionService } from '../../permission'; import { WorkspaceBlobStorage } from '../../storage'; -import { UserService } from '../../user'; export const defaultWorkspaceAvatar = 'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQtSURBVHgBfVa9jhxFEK6q7rkf+4T2AgdIIC0ZoXkBuNQJtngBuIzs1hIRye1FhL438D0CRgKRGUeE6wwkhHYlkE2AtGdkbN/MdJe/qu7Z27PWnnG5Znq7v/rqd47pHddkNh/918tR1/FBamXc9zxOPVFKfJ4yP86qD1LD3/986/3F2zB40+LXv83HrHq/6+gAoNS1kF4odUz2nhJRTkI5E6mD6Bk1crLJkLy5cHc+P4ohzxLng8RKLqKUq6hkUtBSe8Zvdmfir7TT2a0fnkzeaeCbv/44ztSfZskjP2ygVRM0mbYTpgHMMMS8CsIIj/c+//Hp8UYD3z758whQUwdeEwPjAZQLqJhI0VxB2MVco+kXP/0zuZKD6dP5uM397ELzqEtMba/UJ4t7iXeq8U94z52Q+js09qjlIXMxAEsRDJpI59dVPzlDTooHko7BdlR2FcYmAtbGMmAt2mFI4yDQkIjtEQkxUAMKAPD9SiOK4b578N0S7Nt+fqFKbTbmRD1YGXurEmdtnjjz4kFuIV0gtWewV62hMHBY2gpEOw3Rnmztx9jnO72xzTV/YkzgNmgkiypeYJdCLjonqyAAg7VCshVpjTbD08HbxrySdhKxcDvoJTA5gLvpeXVQ+K340WKea9UkNeZVqGSba/IbF6athj+LUeRmRCyiAVnlAKhJJQfmugGZ28ZWna24RGzwNUNUqpWGf6HkajvAgNA4NsSjHgcb9obx+k5c3DUttcwd3NcHxpVurXQ2d4MZACGw9TwEHsdtbEwytL1xywAGcxavjoH1quLVywuGi+aBhFWexRilFSwK0QzgdUdkkVMeKw4wijrgxjzz2CefCRZn+21ViOWW4Ym9nNnyFLMbMS8ivNhGP8RdlgUojBkuBLDpEPi+5LpWiDURgFkKOIIckJTgN/sZ84KtKkKpDnsOZiTQ47jD4ZGwHghbw6AXIL3lo5Zg6Tp2AwIAyYJ8BRzGfmfPl6kI7HOLUdN2LIg+4IfL5SiFdvkK4blI6h50qda7jQI0CUMLdEhFIkqtQciMvXsgpaZ1pWtVUfrIa+TX5/8+RBcftAhTa91r8ycXA5ZxBqhAh2zgVagUAddxMkxfF/JxfvbpB+8d2jhBtsPhtuqsE0HJlhxYeHKdkCU8xUCos8dmkDdnGaOlJ1yy9dM52J2spqldvz9fTgB4z+aQd2kqjUY2KU2s4dTT7ezD0AqDAbvZiKF/VO9+fGPv9IoBu+b/P5ti6djDY+JlSg4ug1jc6fJbMAx9/3b4CNGTD/evT698D9avv188m4gKvko8MiMeJC3jmOvU9MSuHXZohAVpOrmxd+10HW/jR3/58uU45TRFt35ZR2XpY61DzW+tH3z/7xdM8sP93d3Fm1gbDawbEtU7CMtt/JVxEw01Kh7RAmoBE4+u7eycYv38bRivAZbdHBtPrwOHAAAAAElFTkSuQmCC'; @@ -35,7 +35,7 @@ export class WorkspaceService { private readonly mailer: MailService, private readonly permission: PermissionService, private readonly prisma: PrismaClient, - private readonly user: UserService + private readonly models: Models ) {} async getInviteInfo(inviteId: string): Promise { @@ -92,7 +92,7 @@ export class WorkspaceService { return; } const workspace = await this.getWorkspaceInfo(workspaceId); - const invitee = await this.user.findUserById(inviteeUserId); + const invitee = await this.models.user.getPublicUser(inviteeUserId); if (!invitee) { this.logger.error( `Invitee user not found in workspace: ${workspaceId}, userId: ${inviteeUserId}` @@ -111,10 +111,10 @@ export class WorkspaceService { await this.getInviteInfo(inviteId); const workspace = await this.getWorkspaceInfo(workspaceId); const invitee = inviteeUserId - ? await this.user.findUserById(inviteeUserId) + ? await this.models.user.getPublicUser(inviteeUserId) : null; const inviter = inviterUserId - ? await this.user.findUserById(inviterUserId) + ? await this.models.user.getPublicUser(inviterUserId) : await this.permission.getWorkspaceOwner(workspaceId); if (!inviter || !invitee) { @@ -138,7 +138,7 @@ export class WorkspaceService { return; } - const invitee = await this.user.findUserById(inviteeUserId); + const invitee = await this.models.user.getPublicUser(inviteeUserId); if (!invitee) { this.logger.error( `Invitee user not found for inviteId: ${inviteId}, userId: ${inviteeUserId}` @@ -199,7 +199,7 @@ export class WorkspaceService { userId: string, ws: { id: string; role: Permission } ) { - const user = await this.user.findUserById(userId); + const user = await this.models.user.getPublicUser(userId); if (!user) throw new UserNotFound(); const workspace = await this.getWorkspaceInfo(ws.id); await this.mailer.sendRoleChangedEmail(user?.email, { diff --git a/packages/backend/server/src/core/workspaces/resolvers/team.ts b/packages/backend/server/src/core/workspaces/resolvers/team.ts index 17b1605b93..5bfcc04242 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/team.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/team.ts @@ -20,10 +20,10 @@ import { URLHelper, UserFriendlyError, } from '../../../base'; +import { Models } from '../../../models'; import { CurrentUser } from '../../auth'; import { Permission, PermissionService } from '../../permission'; import { QuotaManagementService } from '../../quota'; -import { UserService } from '../../user'; import { InviteLink, InviteResult, @@ -47,7 +47,7 @@ export class TeamWorkspaceResolver { private readonly url: URLHelper, private readonly prisma: PrismaClient, private readonly permissions: PermissionService, - private readonly users: UserService, + private readonly models: Models, private readonly quota: QuotaManagementService, private readonly mutex: RequestMutex, private readonly workspaceService: WorkspaceService @@ -92,7 +92,7 @@ export class TeamWorkspaceResolver { for (const [idx, email] of emails.entries()) { const ret: InviteResult = { email, sentSuccess: false, inviteId: null }; try { - let target = await this.users.findUserByEmail(email); + let target = await this.models.user.getUserByEmail(email); if (target) { const originRecord = await this.prisma.workspaceUserPermission.findFirst({ @@ -104,7 +104,7 @@ export class TeamWorkspaceResolver { // only invite if the user is not already in the workspace if (originRecord) continue; } else { - target = await this.users.createUser({ + target = await this.models.user.create({ email, registered: false, }); @@ -352,7 +352,7 @@ export class TeamWorkspaceResolver { userId, workspaceId, }: EventPayload<'workspace.members.requestDeclined'>) { - const user = await this.users.findUserById(userId); + const user = await this.models.user.getPublicUser(userId); const workspace = await this.workspaceService.getWorkspaceInfo(workspaceId); // send decline mail await this.workspaceService.sendReviewDeclinedEmail( diff --git a/packages/backend/server/src/core/workspaces/resolvers/workspace.ts b/packages/backend/server/src/core/workspaces/resolvers/workspace.ts index 8036ae24e7..73ecf9170b 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/workspace.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/workspace.ts @@ -30,11 +30,12 @@ import { UserFriendlyError, UserNotFound, } from '../../../base'; +import { Models } from '../../../models'; import { CurrentUser, Public } from '../../auth'; import { type Editor, PgWorkspaceDocStorageAdapter } from '../../doc'; import { Permission, PermissionService } from '../../permission'; import { QuotaManagementService, QuotaQueryType } from '../../quota'; -import { UserService, UserType } from '../../user'; +import { UserType } from '../../user'; import { InvitationType, InviteUserType, @@ -82,7 +83,7 @@ export class WorkspaceResolver { private readonly prisma: PrismaClient, private readonly permissions: PermissionService, private readonly quota: QuotaManagementService, - private readonly users: UserService, + private readonly models: Models, private readonly event: EventEmitter, private readonly mutex: RequestMutex, private readonly workspaceService: WorkspaceService, @@ -407,7 +408,7 @@ export class WorkspaceResolver { // member limit check await this.quota.checkWorkspaceSeat(workspaceId); - let target = await this.users.findUserByEmail(email); + let target = await this.models.user.getUserByEmail(email); if (target) { const originRecord = await this.prisma.workspaceUserPermission.findFirst({ @@ -419,7 +420,7 @@ export class WorkspaceResolver { // only invite if the user is not already in the workspace if (originRecord) return originRecord.id; } else { - target = await this.users.createUser({ + target = await this.models.user.create({ email, registered: false, }); @@ -480,7 +481,7 @@ export class WorkspaceResolver { const inviteeId = inviteeUserId || user?.id; if (!inviteeId) throw new UserNotFound(); - const invitee = await this.users.findUserById(inviteeId); + const invitee = await this.models.user.getPublicUser(inviteeId); return { workspace, user: owner, invitee }; } diff --git a/packages/backend/server/src/models/index.ts b/packages/backend/server/src/models/index.ts index c52b01d8d7..bc0d917343 100644 --- a/packages/backend/server/src/models/index.ts +++ b/packages/backend/server/src/models/index.ts @@ -71,7 +71,7 @@ const ModelsSymbolProvider: ExistingProvider = { providers: [...Object.values(MODELS), ModelsProvider, ModelsSymbolProvider], exports: [ModelsProvider], }) -export class ModelModules {} +export class ModelsModule {} export * from './feature'; export * from './page'; diff --git a/packages/backend/server/src/models/user.ts b/packages/backend/server/src/models/user.ts index 4c97a44a0f..5b5bded615 100644 --- a/packages/backend/server/src/models/user.ts +++ b/packages/backend/server/src/models/user.ts @@ -6,8 +6,6 @@ import { CryptoHelper, EmailAlreadyUsed, EventEmitter, - type EventPayload, - OnEvent, WrongSignInCredentials, WrongSignInMethod, } from '../base'; @@ -248,55 +246,4 @@ export class UserModel extends BaseModel { async count() { return this.db.user.count(); } - - @OnEvent('user.updated') - async onUserUpdated(user: EventPayload<'user.updated'>) { - const { enabled, customerIo } = this.config.metrics; - if (enabled && customerIo?.token) { - const payload = { - name: user.name, - email: user.email, - created_at: Number(user.createdAt) / 1000, - }; - try { - await fetch(`https://track.customer.io/api/v1/customers/${user.id}`, { - method: 'PUT', - headers: { - Authorization: `Basic ${customerIo.token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }); - } catch (e) { - this.logger.error('Failed to publish user update event:', e); - } - } - } - - @OnEvent('user.deleted') - async onUserDeleted(user: EventPayload<'user.deleted'>) { - const { enabled, customerIo } = this.config.metrics; - if (enabled && customerIo?.token) { - try { - if (user.emailVerifiedAt) { - // suppress email if email is verified - await fetch( - `https://track.customer.io/api/v1/customers/${user.email}/suppress`, - { - method: 'POST', - headers: { - Authorization: `Basic ${customerIo.token}`, - }, - } - ); - } - await fetch(`https://track.customer.io/api/v1/customers/${user.id}`, { - method: 'DELETE', - headers: { Authorization: `Basic ${customerIo.token}` }, - }); - } catch (e) { - this.logger.error('Failed to publish user delete event:', e); - } - } - } } diff --git a/packages/backend/server/src/plugins/oauth/controller.ts b/packages/backend/server/src/plugins/oauth/controller.ts index f38eaa2eb0..9e8871045e 100644 --- a/packages/backend/server/src/plugins/oauth/controller.ts +++ b/packages/backend/server/src/plugins/oauth/controller.ts @@ -18,7 +18,7 @@ import { UnknownOauthProvider, } from '../../base'; import { AuthService, Public } from '../../core/auth'; -import { UserService } from '../../core/user'; +import { Models } from '../../models'; import { OAuthProviderName } from './config'; import { OAuthAccount, Tokens } from './providers/def'; import { OAuthProviderFactory } from './register'; @@ -29,7 +29,7 @@ export class OAuthController { constructor( private readonly auth: AuthService, private readonly oauth: OAuthService, - private readonly user: UserService, + private readonly models: Models, private readonly providerFactory: OAuthProviderFactory, private readonly db: PrismaClient ) {} @@ -137,9 +137,7 @@ export class OAuthController { return connectedUser.user; } - const user = await this.user.fulfillUser(externalAccount.email, { - emailVerifiedAt: new Date(), - registered: true, + const user = await this.models.user.fulfill(externalAccount.email, { avatarUrl: externalAccount.avatarUrl, }); diff --git a/packages/backend/server/src/plugins/payment/service.ts b/packages/backend/server/src/plugins/payment/service.ts index 90cceaa800..f711fc3ee5 100644 --- a/packages/backend/server/src/plugins/payment/service.ts +++ b/packages/backend/server/src/plugins/payment/service.ts @@ -25,7 +25,7 @@ import { } from '../../base'; import { CurrentUser } from '../../core/auth'; import { FeatureManagementService } from '../../core/features'; -import { UserService } from '../../core/user'; +import { Models } from '../../models'; import { CheckoutParams, Invoice, @@ -75,7 +75,7 @@ export class SubscriptionService implements OnApplicationBootstrap { private readonly stripe: Stripe, private readonly db: PrismaClient, private readonly feature: FeatureManagementService, - private readonly user: UserService, + private readonly models: Models, private readonly userManager: UserSubscriptionManager, private readonly workspaceManager: WorkspaceSubscriptionManager, private readonly mutex: Mutex @@ -435,7 +435,7 @@ export class SubscriptionService implements OnApplicationBootstrap { return null; } - const user = await this.user.findUserByEmail(customer.email); + const user = await this.models.user.getPublicUserByEmail(customer.email); if (!user) { return null; @@ -485,7 +485,9 @@ export class SubscriptionService implements OnApplicationBootstrap { return null; } - const user = await this.user.findUserByEmail(invoice.customer_email); + const user = await this.models.user.getPublicUserByEmail( + invoice.customer_email + ); // TODO(@forehalo): the email may actually not appear to be AFFiNE user // There is coming feature that allow anonymous user with only email provided to buy selfhost licenses diff --git a/tsconfig.json b/tsconfig.json index 3d9a96ba42..e8b6b2477b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,7 @@ "typeRoots": ["./tools/@types", "./node_modules/@types"], // Emit - "lib": ["ESNext"], + "lib": ["ES2024"], "target": "ES2024", "useDefineForClassFields": false, "declaration": true,