feat(server): use user model (#9710)

This commit is contained in:
forehalo
2025-01-17 07:06:11 +00:00
parent a2d16f4b78
commit 44de4474c3
25 changed files with 179 additions and 521 deletions

View File

@@ -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 });

View File

@@ -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) {

View File

@@ -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<CurrentUser> {
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<Omit<User, 'password'>> {
return this.user.updateUser(id, { password: newPassword });
return this.models.user.update(id, { password: newPassword });
}
async changeEmail(
id: string,
newEmail: string
): Promise<Omit<User, 'password'>> {
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) {

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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';

View File

@@ -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<DeleteAccount> {
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<UserType[]> {
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,
})

View File

@@ -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<Prisma.UserCreateInput, 'name'> & { 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<Pick<User, keyof typeof this.defaultUserSelect> | 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<User | null> {
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<Partial<Prisma.UserCreateInput>, '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<Partial<Prisma.UserCreateInput>, '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);
}
}
}
}

View File

@@ -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<InviteInfo> {
@@ -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, {

View File

@@ -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(

View File

@@ -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 };
}