feat(server): use user model (#9710)
This commit is contained in:
@@ -8,12 +8,11 @@ import { MailService } from '../../base';
|
|||||||
import { AuthModule, CurrentUser } from '../../core/auth';
|
import { AuthModule, CurrentUser } from '../../core/auth';
|
||||||
import { AuthService } from '../../core/auth/service';
|
import { AuthService } from '../../core/auth/service';
|
||||||
import { FeatureModule } from '../../core/features';
|
import { FeatureModule } from '../../core/features';
|
||||||
import { UserModule, UserService } from '../../core/user';
|
import { UserModule } from '../../core/user';
|
||||||
import { createTestingApp, getSession, sessionCookie } from '../utils';
|
import { createTestingApp, getSession, sessionCookie } from '../utils';
|
||||||
|
|
||||||
const test = ava as TestFn<{
|
const test = ava as TestFn<{
|
||||||
auth: AuthService;
|
auth: AuthService;
|
||||||
user: UserService;
|
|
||||||
u1: CurrentUser;
|
u1: CurrentUser;
|
||||||
db: PrismaClient;
|
db: PrismaClient;
|
||||||
mailer: Sinon.SinonStubbedInstance<MailService>;
|
mailer: Sinon.SinonStubbedInstance<MailService>;
|
||||||
@@ -31,7 +30,6 @@ test.before(async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
t.context.auth = app.get(AuthService);
|
t.context.auth = app.get(AuthService);
|
||||||
t.context.user = app.get(UserService);
|
|
||||||
t.context.db = app.get(PrismaClient);
|
t.context.db = app.get(PrismaClient);
|
||||||
t.context.mailer = app.get(MailService);
|
t.context.mailer = app.get(MailService);
|
||||||
t.context.app = app;
|
t.context.app = app;
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import { CurrentUser } from '../../core/auth';
|
|||||||
import { AuthService } from '../../core/auth/service';
|
import { AuthService } from '../../core/auth/service';
|
||||||
import { FeatureModule } from '../../core/features';
|
import { FeatureModule } from '../../core/features';
|
||||||
import { QuotaModule } from '../../core/quota';
|
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';
|
import { createTestingModule, initTestingDB } from '../utils';
|
||||||
|
|
||||||
const test = ava as TestFn<{
|
const test = ava as TestFn<{
|
||||||
auth: AuthService;
|
auth: AuthService;
|
||||||
user: UserService;
|
models: Models;
|
||||||
u1: CurrentUser;
|
u1: CurrentUser;
|
||||||
db: PrismaClient;
|
db: PrismaClient;
|
||||||
m: TestingModule;
|
m: TestingModule;
|
||||||
@@ -24,7 +25,7 @@ test.before(async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
t.context.auth = m.get(AuthService);
|
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.db = m.get(PrismaClient);
|
||||||
t.context.m = m;
|
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 => {
|
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',
|
email: 'u2@affine.pro',
|
||||||
name: 'u2',
|
name: 'u2',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { URLHelper } from '../../base';
|
|||||||
import { ConfigModule } from '../../base/config';
|
import { ConfigModule } from '../../base/config';
|
||||||
import { CurrentUser } from '../../core/auth';
|
import { CurrentUser } from '../../core/auth';
|
||||||
import { AuthService } from '../../core/auth/service';
|
import { AuthService } from '../../core/auth/service';
|
||||||
import { UserService } from '../../core/user';
|
import { Models } from '../../models';
|
||||||
import { OAuthProviderName } from '../../plugins/oauth/config';
|
import { OAuthProviderName } from '../../plugins/oauth/config';
|
||||||
import { GoogleOAuthProvider } from '../../plugins/oauth/providers/google';
|
import { GoogleOAuthProvider } from '../../plugins/oauth/providers/google';
|
||||||
import { OAuthService } from '../../plugins/oauth/service';
|
import { OAuthService } from '../../plugins/oauth/service';
|
||||||
@@ -20,7 +20,7 @@ import { createTestingApp, getSession, initTestingDB } from '../utils';
|
|||||||
const test = ava as TestFn<{
|
const test = ava as TestFn<{
|
||||||
auth: AuthService;
|
auth: AuthService;
|
||||||
oauth: OAuthService;
|
oauth: OAuthService;
|
||||||
user: UserService;
|
models: Models;
|
||||||
u1: CurrentUser;
|
u1: CurrentUser;
|
||||||
db: PrismaClient;
|
db: PrismaClient;
|
||||||
app: INestApplication;
|
app: INestApplication;
|
||||||
@@ -47,7 +47,7 @@ test.before(async t => {
|
|||||||
|
|
||||||
t.context.auth = app.get(AuthService);
|
t.context.auth = app.get(AuthService);
|
||||||
t.context.oauth = app.get(OAuthService);
|
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.db = app.get(PrismaClient);
|
||||||
t.context.app = app;
|
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 => {
|
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',
|
name: 'u3',
|
||||||
email: 'u3@affine.pro',
|
email: 'u3@affine.pro',
|
||||||
registered: false,
|
registered: false,
|
||||||
|
|||||||
@@ -234,11 +234,7 @@ test.before(async t => {
|
|||||||
|
|
||||||
test.beforeEach(async t => {
|
test.beforeEach(async t => {
|
||||||
const { db, app, stripe } = t.context;
|
const { db, app, stripe } = t.context;
|
||||||
Sinon.reset();
|
|
||||||
await initTestingDB(db);
|
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');
|
t.context.u1 = await app.get(AuthService).signUp('u1@affine.pro', '1');
|
||||||
|
|
||||||
await db.workspace.create({
|
await db.workspace.create({
|
||||||
@@ -254,7 +250,13 @@ test.beforeEach(async t => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Sinon.reset();
|
||||||
|
|
||||||
// default stubs
|
// default stubs
|
||||||
|
t.context.runtime.fetch
|
||||||
|
.withArgs('plugins.payment/showLifetimePrice')
|
||||||
|
.resolves(true);
|
||||||
|
|
||||||
// @ts-expect-error stub
|
// @ts-expect-error stub
|
||||||
stripe.prices.list.callsFake((params: Stripe.PriceListParams) => {
|
stripe.prices.list.callsFake((params: Stripe.PriceListParams) => {
|
||||||
if (params.lookup_keys) {
|
if (params.lookup_keys) {
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
type CurrentUser,
|
type CurrentUser,
|
||||||
} from '../../core/auth';
|
} from '../../core/auth';
|
||||||
import { sessionUser } from '../../core/auth/service';
|
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';
|
import { gql } from './common';
|
||||||
|
|
||||||
export type UserAuthedType = UserType & { token: ClientTokenType };
|
export type UserAuthedType = UserType & { token: ClientTokenType };
|
||||||
@@ -52,7 +53,7 @@ export async function signUp(
|
|||||||
password: string,
|
password: string,
|
||||||
autoVerifyEmail = true
|
autoVerifyEmail = true
|
||||||
): Promise<UserAuthedType> {
|
): Promise<UserAuthedType> {
|
||||||
const user = await app.get(UserService).createUser({
|
const user = await app.get(Models).user.create({
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { GlobalExceptionFilter, Runtime } from '../../base';
|
|||||||
import { GqlModule } from '../../base/graphql';
|
import { GqlModule } from '../../base/graphql';
|
||||||
import { AuthGuard, AuthModule } from '../../core/auth';
|
import { AuthGuard, AuthModule } from '../../core/auth';
|
||||||
import { UserFeaturesInit1698652531198 } from '../../data/migrations/1698652531198-user-features-init';
|
import { UserFeaturesInit1698652531198 } from '../../data/migrations/1698652531198-user-features-init';
|
||||||
import { ModelModules } from '../../models';
|
import { ModelsModule } from '../../models';
|
||||||
|
|
||||||
export type PermissionEnum = 'Owner' | 'Admin' | 'Write' | 'Read';
|
export type PermissionEnum = 'Owner' | 'Admin' | 'Write' | 'Read';
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ export async function createTestingModule(
|
|||||||
? [AppModule]
|
? [AppModule]
|
||||||
: dedupeModules([
|
: dedupeModules([
|
||||||
...FunctionalityModules,
|
...FunctionalityModules,
|
||||||
ModelModules,
|
ModelsModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
GqlModule,
|
GqlModule,
|
||||||
...imports,
|
...imports,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import ava from 'ava';
|
|||||||
import { AppModule } from '../app.module';
|
import { AppModule } from '../app.module';
|
||||||
import { MailService } from '../base/mailer';
|
import { MailService } from '../base/mailer';
|
||||||
import { AuthService } from '../core/auth/service';
|
import { AuthService } from '../core/auth/service';
|
||||||
import { UserService } from '../core/user';
|
import { Models } from '../models';
|
||||||
import {
|
import {
|
||||||
acceptInviteById,
|
acceptInviteById,
|
||||||
createTestingApp,
|
createTestingApp,
|
||||||
@@ -27,7 +27,7 @@ const test = ava as TestFn<{
|
|||||||
client: PrismaClient;
|
client: PrismaClient;
|
||||||
auth: AuthService;
|
auth: AuthService;
|
||||||
mail: MailService;
|
mail: MailService;
|
||||||
user: UserService;
|
models: Models;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
test.beforeEach(async t => {
|
test.beforeEach(async t => {
|
||||||
@@ -38,7 +38,7 @@ test.beforeEach(async t => {
|
|||||||
t.context.client = app.get(PrismaClient);
|
t.context.client = app.get(PrismaClient);
|
||||||
t.context.auth = app.get(AuthService);
|
t.context.auth = app.get(AuthService);
|
||||||
t.context.mail = app.get(MailService);
|
t.context.mail = app.get(MailService);
|
||||||
t.context.user = app.get(UserService);
|
t.context.models = app.get(Models);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterEach.always(async t => {
|
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 => {
|
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 u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
|
||||||
|
|
||||||
const workspace = await createWorkspace(app, u1.token.token);
|
const workspace = await createWorkspace(app, u1.token.token);
|
||||||
|
|
||||||
await inviteUser(app, u1.token.token, workspace.id, 'u2@affine.pro');
|
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.not(u2, undefined, 'failed to create user');
|
||||||
t.is(u2?.name, 'u2', 'failed to create user');
|
t.is(u2?.name, 'u2', 'failed to create user');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import { StorageModule } from './core/storage';
|
|||||||
import { SyncModule } from './core/sync';
|
import { SyncModule } from './core/sync';
|
||||||
import { UserModule } from './core/user';
|
import { UserModule } from './core/user';
|
||||||
import { WorkspaceModule } from './core/workspaces';
|
import { WorkspaceModule } from './core/workspaces';
|
||||||
import { ModelModules } from './models';
|
import { ModelsModule } from './models';
|
||||||
import { REGISTERED_PLUGINS } from './plugins';
|
import { REGISTERED_PLUGINS } from './plugins';
|
||||||
import { ENABLED_PLUGINS } from './plugins/registry';
|
import { ENABLED_PLUGINS } from './plugins/registry';
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ export function buildAppModule() {
|
|||||||
factor
|
factor
|
||||||
// basic
|
// basic
|
||||||
.use(...FunctionalityModules)
|
.use(...FunctionalityModules)
|
||||||
.use(ModelModules)
|
.use(ModelsModule)
|
||||||
.useIf(config => config.flavor.sync, WebSocketModule)
|
.useIf(config => config.flavor.sync, WebSocketModule)
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import {
|
|||||||
UseNamedGuard,
|
UseNamedGuard,
|
||||||
} from '../../base';
|
} from '../../base';
|
||||||
import { Models, TokenType } from '../../models';
|
import { Models, TokenType } from '../../models';
|
||||||
import { UserService } from '../user';
|
|
||||||
import { validators } from '../utils/validators';
|
import { validators } from '../utils/validators';
|
||||||
import { Public } from './guard';
|
import { Public } from './guard';
|
||||||
import { AuthService } from './service';
|
import { AuthService } from './service';
|
||||||
@@ -56,7 +55,6 @@ export class AuthController {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly url: URLHelper,
|
private readonly url: URLHelper,
|
||||||
private readonly auth: AuthService,
|
private readonly auth: AuthService,
|
||||||
private readonly user: UserService,
|
|
||||||
private readonly models: Models,
|
private readonly models: Models,
|
||||||
private readonly config: Config,
|
private readonly config: Config,
|
||||||
private readonly runtime: Runtime
|
private readonly runtime: Runtime
|
||||||
@@ -81,9 +79,7 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
validators.assertValidEmail(params.email);
|
validators.assertValidEmail(params.email);
|
||||||
|
|
||||||
const user = await this.user.findUserWithHashedPasswordByEmail(
|
const user = await this.models.user.getUserByEmail(params.email);
|
||||||
params.email
|
|
||||||
);
|
|
||||||
|
|
||||||
const magicLinkAvailable = !!this.config.mailer.host;
|
const magicLinkAvailable = !!this.config.mailer.host;
|
||||||
|
|
||||||
@@ -159,7 +155,7 @@ export class AuthController {
|
|||||||
redirectUrl?: string
|
redirectUrl?: string
|
||||||
) {
|
) {
|
||||||
// send email magic link
|
// send email magic link
|
||||||
const user = await this.user.findUserByEmail(email);
|
const user = await this.models.user.getUserByEmail(email);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
const allowSignup = await this.runtime.fetch('auth/allowSignup');
|
const allowSignup = await this.runtime.fetch('auth/allowSignup');
|
||||||
if (!allowSignup) {
|
if (!allowSignup) {
|
||||||
@@ -263,10 +259,7 @@ export class AuthController {
|
|||||||
throw new InvalidEmailToken();
|
throw new InvalidEmailToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.user.fulfillUser(email, {
|
const user = await this.models.user.fulfill(email);
|
||||||
emailVerifiedAt: new Date(),
|
|
||||||
registered: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.auth.setCookies(req, res, user.id);
|
await this.auth.setCookies(req, res, user.id);
|
||||||
res.send({ id: user.id });
|
res.send({ id: user.id });
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
} from '../../base';
|
} from '../../base';
|
||||||
import { Models, TokenType } from '../../models';
|
import { Models, TokenType } from '../../models';
|
||||||
import { Admin } from '../common';
|
import { Admin } from '../common';
|
||||||
import { UserService } from '../user';
|
|
||||||
import { UserType } from '../user/types';
|
import { UserType } from '../user/types';
|
||||||
import { validators } from '../utils/validators';
|
import { validators } from '../utils/validators';
|
||||||
import { Public } from './guard';
|
import { Public } from './guard';
|
||||||
@@ -48,7 +47,6 @@ export class AuthResolver {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly url: URLHelper,
|
private readonly url: URLHelper,
|
||||||
private readonly auth: AuthService,
|
private readonly auth: AuthService,
|
||||||
private readonly user: UserService,
|
|
||||||
private readonly models: Models
|
private readonly models: Models
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -234,7 +232,7 @@ export class AuthResolver {
|
|||||||
throw new InvalidEmailToken();
|
throw new InvalidEmailToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasRegistered = await this.user.findUserByEmail(email);
|
const hasRegistered = await this.models.user.getUserByEmail(email);
|
||||||
|
|
||||||
if (hasRegistered) {
|
if (hasRegistered) {
|
||||||
if (hasRegistered.id !== user.id) {
|
if (hasRegistered.id !== user.id) {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { Models, type User, type UserSession } from '../../models';
|
|||||||
import { FeatureManagementService } from '../features/management';
|
import { FeatureManagementService } from '../features/management';
|
||||||
import { QuotaService } from '../quota/service';
|
import { QuotaService } from '../quota/service';
|
||||||
import { QuotaType } from '../quota/types';
|
import { QuotaType } from '../quota/types';
|
||||||
import { UserService } from '../user/service';
|
|
||||||
import type { CurrentUser } from './session';
|
import type { CurrentUser } from './session';
|
||||||
|
|
||||||
export function sessionUser(
|
export function sessionUser(
|
||||||
@@ -47,17 +46,16 @@ export class AuthService implements OnApplicationBootstrap {
|
|||||||
private readonly models: Models,
|
private readonly models: Models,
|
||||||
private readonly mailer: MailService,
|
private readonly mailer: MailService,
|
||||||
private readonly feature: FeatureManagementService,
|
private readonly feature: FeatureManagementService,
|
||||||
private readonly quota: QuotaService,
|
private readonly quota: QuotaService
|
||||||
private readonly user: UserService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onApplicationBootstrap() {
|
async onApplicationBootstrap() {
|
||||||
if (this.config.node.dev) {
|
if (this.config.node.dev) {
|
||||||
try {
|
try {
|
||||||
const [email, name, password] = ['dev@affine.pro', 'Dev User', 'dev'];
|
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) {
|
if (!devUser) {
|
||||||
devUser = await this.user.createUser_without_verification({
|
devUser = await this.models.user.create({
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
password,
|
password,
|
||||||
@@ -86,8 +84,8 @@ export class AuthService implements OnApplicationBootstrap {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.user
|
return this.models.user
|
||||||
.createUser_without_verification({
|
.create({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
})
|
})
|
||||||
@@ -95,7 +93,7 @@ export class AuthService implements OnApplicationBootstrap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async signIn(email: string, password: string): Promise<CurrentUser> {
|
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) {
|
async signOut(sessionId: string, userId?: string) {
|
||||||
@@ -131,7 +129,7 @@ export class AuthService implements OnApplicationBootstrap {
|
|||||||
userSession = sessions.at(-1)!;
|
userSession = sessions.at(-1)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.user.findUserById(userSession.userId);
|
const user = await this.models.user.get(userSession.userId);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return null;
|
return null;
|
||||||
@@ -284,25 +282,23 @@ export class AuthService implements OnApplicationBootstrap {
|
|||||||
id: string,
|
id: string,
|
||||||
newPassword: string
|
newPassword: string
|
||||||
): Promise<Omit<User, 'password'>> {
|
): Promise<Omit<User, 'password'>> {
|
||||||
return this.user.updateUser(id, { password: newPassword });
|
return this.models.user.update(id, { password: newPassword });
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeEmail(
|
async changeEmail(
|
||||||
id: string,
|
id: string,
|
||||||
newEmail: string
|
newEmail: string
|
||||||
): Promise<Omit<User, 'password'>> {
|
): Promise<Omit<User, 'password'>> {
|
||||||
return this.user.updateUser(id, {
|
return this.models.user.update(id, {
|
||||||
email: newEmail,
|
email: newEmail,
|
||||||
emailVerifiedAt: new Date(),
|
emailVerifiedAt: new Date(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setEmailVerified(id: string) {
|
async setEmailVerified(id: string) {
|
||||||
return await this.user.updateUser(
|
return await this.models.user.update(id, {
|
||||||
id,
|
emailVerifiedAt: new Date(),
|
||||||
{ emailVerifiedAt: new Date() },
|
});
|
||||||
{ emailVerifiedAt: true }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendChangePasswordEmail(email: string, callbackUrl: string) {
|
async sendChangePasswordEmail(email: string, callbackUrl: string) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
import { type EventPayload, OnEvent, Runtime } from '../../base';
|
import { type EventPayload, OnEvent, Runtime } from '../../base';
|
||||||
import { UserService } from '../user/service';
|
import { Models } from '../../models';
|
||||||
import { FeatureService } from './service';
|
import { FeatureService } from './service';
|
||||||
import { FeatureType } from './types';
|
import { FeatureType } from './types';
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ export class FeatureManagementService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly feature: FeatureService,
|
private readonly feature: FeatureService,
|
||||||
private readonly user: UserService,
|
private readonly models: Models,
|
||||||
private readonly runtime: Runtime
|
private readonly runtime: Runtime
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ export class FeatureManagementService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (earlyAccessControlEnabled && !this.isStaff(email)) {
|
if (earlyAccessControlEnabled && !this.isStaff(email)) {
|
||||||
const user = await this.user.findUserByEmail(email);
|
const user = await this.models.user.getUserByEmail(email);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import {
|
|||||||
InternalServerError,
|
InternalServerError,
|
||||||
Mutex,
|
Mutex,
|
||||||
PasswordRequired,
|
PasswordRequired,
|
||||||
|
Runtime,
|
||||||
} from '../../base';
|
} from '../../base';
|
||||||
|
import { Models } from '../../models';
|
||||||
import { AuthService, Public } from '../auth';
|
import { AuthService, Public } from '../auth';
|
||||||
import { ServerService } from '../config';
|
import { ServerService } from '../config';
|
||||||
import { UserService } from '../user/service';
|
import { validators } from '../utils/validators';
|
||||||
|
|
||||||
interface CreateUserInput {
|
interface CreateUserInput {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -20,11 +22,12 @@ interface CreateUserInput {
|
|||||||
@Controller('/api/setup')
|
@Controller('/api/setup')
|
||||||
export class CustomSetupController {
|
export class CustomSetupController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly user: UserService,
|
private readonly models: Models,
|
||||||
private readonly auth: AuthService,
|
private readonly auth: AuthService,
|
||||||
private readonly event: EventEmitter,
|
private readonly event: EventEmitter,
|
||||||
private readonly mutex: Mutex,
|
private readonly mutex: Mutex,
|
||||||
private readonly server: ServerService
|
private readonly server: ServerService,
|
||||||
|
private readonly runtime: Runtime
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@@ -34,21 +37,33 @@ export class CustomSetupController {
|
|||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
@Body() input: CreateUserInput
|
@Body() input: CreateUserInput
|
||||||
) {
|
) {
|
||||||
|
if (await this.server.initialized()) {
|
||||||
|
throw new ActionForbidden('First user already created');
|
||||||
|
}
|
||||||
|
|
||||||
|
validators.assertValidEmail(input.email);
|
||||||
|
|
||||||
if (!input.password) {
|
if (!input.password) {
|
||||||
throw new PasswordRequired();
|
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');
|
await using lock = await this.mutex.acquire('createFirstAdmin');
|
||||||
|
|
||||||
if (!lock) {
|
if (!lock) {
|
||||||
throw new InternalServerError();
|
throw new InternalServerError();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.server.initialized()) {
|
const user = await this.models.user.create({
|
||||||
throw new ActionForbidden('First user already created');
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await this.user.createUser({
|
|
||||||
email: input.email,
|
email: input.email,
|
||||||
password: input.password,
|
password: input.password,
|
||||||
registered: true,
|
registered: true,
|
||||||
@@ -59,7 +74,7 @@ export class CustomSetupController {
|
|||||||
await this.auth.setCookies(req, res, user.id);
|
await this.auth.setCookies(req, res, user.id);
|
||||||
res.send({ id: user.id, email: user.email, name: user.name });
|
res.send({ id: user.id, email: user.email, name: user.name });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await this.user.deleteUser(user.id);
|
await this.models.user.delete(user.id);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
packages/backend/server/src/core/user/event.ts
Normal file
61
packages/backend/server/src/core/user/event.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,16 +3,14 @@ import { Module } from '@nestjs/common';
|
|||||||
import { PermissionModule } from '../permission';
|
import { PermissionModule } from '../permission';
|
||||||
import { StorageModule } from '../storage';
|
import { StorageModule } from '../storage';
|
||||||
import { UserAvatarController } from './controller';
|
import { UserAvatarController } from './controller';
|
||||||
|
import { UserEventsListener } from './event';
|
||||||
import { UserManagementResolver, UserResolver } from './resolver';
|
import { UserManagementResolver, UserResolver } from './resolver';
|
||||||
import { UserService } from './service';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [StorageModule, PermissionModule],
|
imports: [StorageModule, PermissionModule],
|
||||||
providers: [UserResolver, UserService, UserManagementResolver],
|
providers: [UserResolver, UserManagementResolver, UserEventsListener],
|
||||||
controllers: [UserAvatarController],
|
controllers: [UserAvatarController],
|
||||||
exports: [UserService],
|
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
|
||||||
export { UserService } from './service';
|
|
||||||
export { UserType } from './types';
|
export { UserType } from './types';
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ import {
|
|||||||
Throttle,
|
Throttle,
|
||||||
UserNotFound,
|
UserNotFound,
|
||||||
} from '../../base';
|
} from '../../base';
|
||||||
|
import { Models } from '../../models';
|
||||||
import { Public } from '../auth/guard';
|
import { Public } from '../auth/guard';
|
||||||
import { sessionUser } from '../auth/service';
|
import { sessionUser } from '../auth/service';
|
||||||
import { CurrentUser } from '../auth/session';
|
import { CurrentUser } from '../auth/session';
|
||||||
import { Admin } from '../common';
|
import { Admin } from '../common';
|
||||||
import { AvatarStorage } from '../storage';
|
import { AvatarStorage } from '../storage';
|
||||||
import { validators } from '../utils/validators';
|
import { validators } from '../utils/validators';
|
||||||
import { UserService } from './service';
|
|
||||||
import {
|
import {
|
||||||
DeleteAccount,
|
DeleteAccount,
|
||||||
ManageUserInput,
|
ManageUserInput,
|
||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
export class UserResolver {
|
export class UserResolver {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly storage: AvatarStorage,
|
private readonly storage: AvatarStorage,
|
||||||
private readonly users: UserService
|
private readonly models: Models
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Throttle('strict')
|
@Throttle('strict')
|
||||||
@@ -54,7 +54,7 @@ export class UserResolver {
|
|||||||
validators.assertValidEmail(email);
|
validators.assertValidEmail(email);
|
||||||
|
|
||||||
// TODO(@forehalo): need to limit a user can only get another user witch is in the same workspace
|
// 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
|
// return empty response when user not exists
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
@@ -99,7 +99,7 @@ export class UserResolver {
|
|||||||
await this.storage.delete(user.avatarUrl);
|
await this.storage.delete(user.avatarUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.users.updateUser(user.id, { avatarUrl });
|
return this.models.user.update(user.id, { avatarUrl });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => UserType, {
|
@Mutation(() => UserType, {
|
||||||
@@ -115,7 +115,7 @@ export class UserResolver {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sessionUser(await this.users.updateUser(user.id, input));
|
return sessionUser(await this.models.user.update(user.id, input));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => RemoveAvatar, {
|
@Mutation(() => RemoveAvatar, {
|
||||||
@@ -126,7 +126,7 @@ export class UserResolver {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
throw new UserNotFound();
|
throw new UserNotFound();
|
||||||
}
|
}
|
||||||
await this.users.updateUser(user.id, { avatarUrl: null });
|
await this.models.user.update(user.id, { avatarUrl: null });
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ export class UserResolver {
|
|||||||
async deleteAccount(
|
async deleteAccount(
|
||||||
@CurrentUser() user: CurrentUser
|
@CurrentUser() user: CurrentUser
|
||||||
): Promise<DeleteAccount> {
|
): Promise<DeleteAccount> {
|
||||||
await this.users.deleteUser(user.id);
|
await this.models.user.delete(user.id);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ class CreateUserInput {
|
|||||||
export class UserManagementResolver {
|
export class UserManagementResolver {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly db: PrismaClient,
|
private readonly db: PrismaClient,
|
||||||
private readonly user: UserService
|
private readonly models: Models
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Query(() => Int, {
|
@Query(() => Int, {
|
||||||
@@ -178,11 +178,7 @@ export class UserManagementResolver {
|
|||||||
async users(
|
async users(
|
||||||
@Args({ name: 'filter', type: () => ListUserInput }) input: ListUserInput
|
@Args({ name: 'filter', type: () => ListUserInput }) input: ListUserInput
|
||||||
): Promise<UserType[]> {
|
): Promise<UserType[]> {
|
||||||
const users = await this.db.user.findMany({
|
const users = await this.models.user.pagination(input.skip, input.first);
|
||||||
select: { ...this.user.defaultUserSelect, password: true },
|
|
||||||
skip: input.skip,
|
|
||||||
take: input.first,
|
|
||||||
});
|
|
||||||
|
|
||||||
return users.map(sessionUser);
|
return users.map(sessionUser);
|
||||||
}
|
}
|
||||||
@@ -192,12 +188,7 @@ export class UserManagementResolver {
|
|||||||
description: 'Get user by id',
|
description: 'Get user by id',
|
||||||
})
|
})
|
||||||
async getUser(@Args('id') id: string) {
|
async getUser(@Args('id') id: string) {
|
||||||
const user = await this.db.user.findUnique({
|
const user = await this.models.user.get(id);
|
||||||
select: { ...this.user.defaultUserSelect, password: true },
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return null;
|
return null;
|
||||||
@@ -212,12 +203,7 @@ export class UserManagementResolver {
|
|||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
async getUserByEmail(@Args('email') email: string) {
|
async getUserByEmail(@Args('email') email: string) {
|
||||||
const user = await this.db.user.findUnique({
|
const user = await this.models.user.getUserByEmail(email);
|
||||||
select: { ...this.user.defaultUserSelect, password: true },
|
|
||||||
where: {
|
|
||||||
email,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return null;
|
return null;
|
||||||
@@ -232,7 +218,7 @@ export class UserManagementResolver {
|
|||||||
async createUser(
|
async createUser(
|
||||||
@Args({ name: 'input', type: () => CreateUserInput }) input: CreateUserInput
|
@Args({ name: 'input', type: () => CreateUserInput }) input: CreateUserInput
|
||||||
) {
|
) {
|
||||||
const { id } = await this.user.createUser({
|
const { id } = await this.models.user.create({
|
||||||
email: input.email,
|
email: input.email,
|
||||||
registered: true,
|
registered: true,
|
||||||
});
|
});
|
||||||
@@ -251,7 +237,7 @@ export class UserManagementResolver {
|
|||||||
if (user.id === id) {
|
if (user.id === id) {
|
||||||
throw new CannotDeleteOwnAccount();
|
throw new CannotDeleteOwnAccount();
|
||||||
}
|
}
|
||||||
await this.user.deleteUser(id);
|
await this.models.user.delete(id);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +262,7 @@ export class UserManagementResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return sessionUser(
|
return sessionUser(
|
||||||
await this.user.updateUser(user.id, {
|
await this.models.user.update(user.id, {
|
||||||
email: input.email,
|
email: input.email,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,10 +3,10 @@ import { PrismaClient } from '@prisma/client';
|
|||||||
import { getStreamAsBuffer } from 'get-stream';
|
import { getStreamAsBuffer } from 'get-stream';
|
||||||
|
|
||||||
import { Cache, MailService, UserNotFound } from '../../../base';
|
import { Cache, MailService, UserNotFound } from '../../../base';
|
||||||
|
import { Models } from '../../../models';
|
||||||
import { DocContentService } from '../../doc-renderer';
|
import { DocContentService } from '../../doc-renderer';
|
||||||
import { Permission, PermissionService } from '../../permission';
|
import { Permission, PermissionService } from '../../permission';
|
||||||
import { WorkspaceBlobStorage } from '../../storage';
|
import { WorkspaceBlobStorage } from '../../storage';
|
||||||
import { UserService } from '../../user';
|
|
||||||
|
|
||||||
export const defaultWorkspaceAvatar =
|
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';
|
'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 mailer: MailService,
|
||||||
private readonly permission: PermissionService,
|
private readonly permission: PermissionService,
|
||||||
private readonly prisma: PrismaClient,
|
private readonly prisma: PrismaClient,
|
||||||
private readonly user: UserService
|
private readonly models: Models
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getInviteInfo(inviteId: string): Promise<InviteInfo> {
|
async getInviteInfo(inviteId: string): Promise<InviteInfo> {
|
||||||
@@ -92,7 +92,7 @@ export class WorkspaceService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const workspace = await this.getWorkspaceInfo(workspaceId);
|
const workspace = await this.getWorkspaceInfo(workspaceId);
|
||||||
const invitee = await this.user.findUserById(inviteeUserId);
|
const invitee = await this.models.user.getPublicUser(inviteeUserId);
|
||||||
if (!invitee) {
|
if (!invitee) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Invitee user not found in workspace: ${workspaceId}, userId: ${inviteeUserId}`
|
`Invitee user not found in workspace: ${workspaceId}, userId: ${inviteeUserId}`
|
||||||
@@ -111,10 +111,10 @@ export class WorkspaceService {
|
|||||||
await this.getInviteInfo(inviteId);
|
await this.getInviteInfo(inviteId);
|
||||||
const workspace = await this.getWorkspaceInfo(workspaceId);
|
const workspace = await this.getWorkspaceInfo(workspaceId);
|
||||||
const invitee = inviteeUserId
|
const invitee = inviteeUserId
|
||||||
? await this.user.findUserById(inviteeUserId)
|
? await this.models.user.getPublicUser(inviteeUserId)
|
||||||
: null;
|
: null;
|
||||||
const inviter = inviterUserId
|
const inviter = inviterUserId
|
||||||
? await this.user.findUserById(inviterUserId)
|
? await this.models.user.getPublicUser(inviterUserId)
|
||||||
: await this.permission.getWorkspaceOwner(workspaceId);
|
: await this.permission.getWorkspaceOwner(workspaceId);
|
||||||
|
|
||||||
if (!inviter || !invitee) {
|
if (!inviter || !invitee) {
|
||||||
@@ -138,7 +138,7 @@ export class WorkspaceService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const invitee = await this.user.findUserById(inviteeUserId);
|
const invitee = await this.models.user.getPublicUser(inviteeUserId);
|
||||||
if (!invitee) {
|
if (!invitee) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Invitee user not found for inviteId: ${inviteId}, userId: ${inviteeUserId}`
|
`Invitee user not found for inviteId: ${inviteId}, userId: ${inviteeUserId}`
|
||||||
@@ -199,7 +199,7 @@ export class WorkspaceService {
|
|||||||
userId: string,
|
userId: string,
|
||||||
ws: { id: string; role: Permission }
|
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();
|
if (!user) throw new UserNotFound();
|
||||||
const workspace = await this.getWorkspaceInfo(ws.id);
|
const workspace = await this.getWorkspaceInfo(ws.id);
|
||||||
await this.mailer.sendRoleChangedEmail(user?.email, {
|
await this.mailer.sendRoleChangedEmail(user?.email, {
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ import {
|
|||||||
URLHelper,
|
URLHelper,
|
||||||
UserFriendlyError,
|
UserFriendlyError,
|
||||||
} from '../../../base';
|
} from '../../../base';
|
||||||
|
import { Models } from '../../../models';
|
||||||
import { CurrentUser } from '../../auth';
|
import { CurrentUser } from '../../auth';
|
||||||
import { Permission, PermissionService } from '../../permission';
|
import { Permission, PermissionService } from '../../permission';
|
||||||
import { QuotaManagementService } from '../../quota';
|
import { QuotaManagementService } from '../../quota';
|
||||||
import { UserService } from '../../user';
|
|
||||||
import {
|
import {
|
||||||
InviteLink,
|
InviteLink,
|
||||||
InviteResult,
|
InviteResult,
|
||||||
@@ -47,7 +47,7 @@ export class TeamWorkspaceResolver {
|
|||||||
private readonly url: URLHelper,
|
private readonly url: URLHelper,
|
||||||
private readonly prisma: PrismaClient,
|
private readonly prisma: PrismaClient,
|
||||||
private readonly permissions: PermissionService,
|
private readonly permissions: PermissionService,
|
||||||
private readonly users: UserService,
|
private readonly models: Models,
|
||||||
private readonly quota: QuotaManagementService,
|
private readonly quota: QuotaManagementService,
|
||||||
private readonly mutex: RequestMutex,
|
private readonly mutex: RequestMutex,
|
||||||
private readonly workspaceService: WorkspaceService
|
private readonly workspaceService: WorkspaceService
|
||||||
@@ -92,7 +92,7 @@ export class TeamWorkspaceResolver {
|
|||||||
for (const [idx, email] of emails.entries()) {
|
for (const [idx, email] of emails.entries()) {
|
||||||
const ret: InviteResult = { email, sentSuccess: false, inviteId: null };
|
const ret: InviteResult = { email, sentSuccess: false, inviteId: null };
|
||||||
try {
|
try {
|
||||||
let target = await this.users.findUserByEmail(email);
|
let target = await this.models.user.getUserByEmail(email);
|
||||||
if (target) {
|
if (target) {
|
||||||
const originRecord =
|
const originRecord =
|
||||||
await this.prisma.workspaceUserPermission.findFirst({
|
await this.prisma.workspaceUserPermission.findFirst({
|
||||||
@@ -104,7 +104,7 @@ export class TeamWorkspaceResolver {
|
|||||||
// only invite if the user is not already in the workspace
|
// only invite if the user is not already in the workspace
|
||||||
if (originRecord) continue;
|
if (originRecord) continue;
|
||||||
} else {
|
} else {
|
||||||
target = await this.users.createUser({
|
target = await this.models.user.create({
|
||||||
email,
|
email,
|
||||||
registered: false,
|
registered: false,
|
||||||
});
|
});
|
||||||
@@ -352,7 +352,7 @@ export class TeamWorkspaceResolver {
|
|||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
}: EventPayload<'workspace.members.requestDeclined'>) {
|
}: 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);
|
const workspace = await this.workspaceService.getWorkspaceInfo(workspaceId);
|
||||||
// send decline mail
|
// send decline mail
|
||||||
await this.workspaceService.sendReviewDeclinedEmail(
|
await this.workspaceService.sendReviewDeclinedEmail(
|
||||||
|
|||||||
@@ -30,11 +30,12 @@ import {
|
|||||||
UserFriendlyError,
|
UserFriendlyError,
|
||||||
UserNotFound,
|
UserNotFound,
|
||||||
} from '../../../base';
|
} from '../../../base';
|
||||||
|
import { Models } from '../../../models';
|
||||||
import { CurrentUser, Public } from '../../auth';
|
import { CurrentUser, Public } from '../../auth';
|
||||||
import { type Editor, PgWorkspaceDocStorageAdapter } from '../../doc';
|
import { type Editor, PgWorkspaceDocStorageAdapter } from '../../doc';
|
||||||
import { Permission, PermissionService } from '../../permission';
|
import { Permission, PermissionService } from '../../permission';
|
||||||
import { QuotaManagementService, QuotaQueryType } from '../../quota';
|
import { QuotaManagementService, QuotaQueryType } from '../../quota';
|
||||||
import { UserService, UserType } from '../../user';
|
import { UserType } from '../../user';
|
||||||
import {
|
import {
|
||||||
InvitationType,
|
InvitationType,
|
||||||
InviteUserType,
|
InviteUserType,
|
||||||
@@ -82,7 +83,7 @@ export class WorkspaceResolver {
|
|||||||
private readonly prisma: PrismaClient,
|
private readonly prisma: PrismaClient,
|
||||||
private readonly permissions: PermissionService,
|
private readonly permissions: PermissionService,
|
||||||
private readonly quota: QuotaManagementService,
|
private readonly quota: QuotaManagementService,
|
||||||
private readonly users: UserService,
|
private readonly models: Models,
|
||||||
private readonly event: EventEmitter,
|
private readonly event: EventEmitter,
|
||||||
private readonly mutex: RequestMutex,
|
private readonly mutex: RequestMutex,
|
||||||
private readonly workspaceService: WorkspaceService,
|
private readonly workspaceService: WorkspaceService,
|
||||||
@@ -407,7 +408,7 @@ export class WorkspaceResolver {
|
|||||||
// member limit check
|
// member limit check
|
||||||
await this.quota.checkWorkspaceSeat(workspaceId);
|
await this.quota.checkWorkspaceSeat(workspaceId);
|
||||||
|
|
||||||
let target = await this.users.findUserByEmail(email);
|
let target = await this.models.user.getUserByEmail(email);
|
||||||
if (target) {
|
if (target) {
|
||||||
const originRecord =
|
const originRecord =
|
||||||
await this.prisma.workspaceUserPermission.findFirst({
|
await this.prisma.workspaceUserPermission.findFirst({
|
||||||
@@ -419,7 +420,7 @@ export class WorkspaceResolver {
|
|||||||
// only invite if the user is not already in the workspace
|
// only invite if the user is not already in the workspace
|
||||||
if (originRecord) return originRecord.id;
|
if (originRecord) return originRecord.id;
|
||||||
} else {
|
} else {
|
||||||
target = await this.users.createUser({
|
target = await this.models.user.create({
|
||||||
email,
|
email,
|
||||||
registered: false,
|
registered: false,
|
||||||
});
|
});
|
||||||
@@ -480,7 +481,7 @@ export class WorkspaceResolver {
|
|||||||
|
|
||||||
const inviteeId = inviteeUserId || user?.id;
|
const inviteeId = inviteeUserId || user?.id;
|
||||||
if (!inviteeId) throw new UserNotFound();
|
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 };
|
return { workspace, user: owner, invitee };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const ModelsSymbolProvider: ExistingProvider = {
|
|||||||
providers: [...Object.values(MODELS), ModelsProvider, ModelsSymbolProvider],
|
providers: [...Object.values(MODELS), ModelsProvider, ModelsSymbolProvider],
|
||||||
exports: [ModelsProvider],
|
exports: [ModelsProvider],
|
||||||
})
|
})
|
||||||
export class ModelModules {}
|
export class ModelsModule {}
|
||||||
|
|
||||||
export * from './feature';
|
export * from './feature';
|
||||||
export * from './page';
|
export * from './page';
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import {
|
|||||||
CryptoHelper,
|
CryptoHelper,
|
||||||
EmailAlreadyUsed,
|
EmailAlreadyUsed,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
type EventPayload,
|
|
||||||
OnEvent,
|
|
||||||
WrongSignInCredentials,
|
WrongSignInCredentials,
|
||||||
WrongSignInMethod,
|
WrongSignInMethod,
|
||||||
} from '../base';
|
} from '../base';
|
||||||
@@ -248,55 +246,4 @@ export class UserModel extends BaseModel {
|
|||||||
async count() {
|
async count() {
|
||||||
return this.db.user.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
UnknownOauthProvider,
|
UnknownOauthProvider,
|
||||||
} from '../../base';
|
} from '../../base';
|
||||||
import { AuthService, Public } from '../../core/auth';
|
import { AuthService, Public } from '../../core/auth';
|
||||||
import { UserService } from '../../core/user';
|
import { Models } from '../../models';
|
||||||
import { OAuthProviderName } from './config';
|
import { OAuthProviderName } from './config';
|
||||||
import { OAuthAccount, Tokens } from './providers/def';
|
import { OAuthAccount, Tokens } from './providers/def';
|
||||||
import { OAuthProviderFactory } from './register';
|
import { OAuthProviderFactory } from './register';
|
||||||
@@ -29,7 +29,7 @@ export class OAuthController {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly auth: AuthService,
|
private readonly auth: AuthService,
|
||||||
private readonly oauth: OAuthService,
|
private readonly oauth: OAuthService,
|
||||||
private readonly user: UserService,
|
private readonly models: Models,
|
||||||
private readonly providerFactory: OAuthProviderFactory,
|
private readonly providerFactory: OAuthProviderFactory,
|
||||||
private readonly db: PrismaClient
|
private readonly db: PrismaClient
|
||||||
) {}
|
) {}
|
||||||
@@ -137,9 +137,7 @@ export class OAuthController {
|
|||||||
return connectedUser.user;
|
return connectedUser.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.user.fulfillUser(externalAccount.email, {
|
const user = await this.models.user.fulfill(externalAccount.email, {
|
||||||
emailVerifiedAt: new Date(),
|
|
||||||
registered: true,
|
|
||||||
avatarUrl: externalAccount.avatarUrl,
|
avatarUrl: externalAccount.avatarUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
} from '../../base';
|
} from '../../base';
|
||||||
import { CurrentUser } from '../../core/auth';
|
import { CurrentUser } from '../../core/auth';
|
||||||
import { FeatureManagementService } from '../../core/features';
|
import { FeatureManagementService } from '../../core/features';
|
||||||
import { UserService } from '../../core/user';
|
import { Models } from '../../models';
|
||||||
import {
|
import {
|
||||||
CheckoutParams,
|
CheckoutParams,
|
||||||
Invoice,
|
Invoice,
|
||||||
@@ -75,7 +75,7 @@ export class SubscriptionService implements OnApplicationBootstrap {
|
|||||||
private readonly stripe: Stripe,
|
private readonly stripe: Stripe,
|
||||||
private readonly db: PrismaClient,
|
private readonly db: PrismaClient,
|
||||||
private readonly feature: FeatureManagementService,
|
private readonly feature: FeatureManagementService,
|
||||||
private readonly user: UserService,
|
private readonly models: Models,
|
||||||
private readonly userManager: UserSubscriptionManager,
|
private readonly userManager: UserSubscriptionManager,
|
||||||
private readonly workspaceManager: WorkspaceSubscriptionManager,
|
private readonly workspaceManager: WorkspaceSubscriptionManager,
|
||||||
private readonly mutex: Mutex
|
private readonly mutex: Mutex
|
||||||
@@ -435,7 +435,7 @@ export class SubscriptionService implements OnApplicationBootstrap {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.user.findUserByEmail(customer.email);
|
const user = await this.models.user.getPublicUserByEmail(customer.email);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return null;
|
return null;
|
||||||
@@ -485,7 +485,9 @@ export class SubscriptionService implements OnApplicationBootstrap {
|
|||||||
return null;
|
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
|
// 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
|
// There is coming feature that allow anonymous user with only email provided to buy selfhost licenses
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"typeRoots": ["./tools/@types", "./node_modules/@types"],
|
"typeRoots": ["./tools/@types", "./node_modules/@types"],
|
||||||
|
|
||||||
// Emit
|
// Emit
|
||||||
"lib": ["ESNext"],
|
"lib": ["ES2024"],
|
||||||
"target": "ES2024",
|
"target": "ES2024",
|
||||||
"useDefineForClassFields": false,
|
"useDefineForClassFields": false,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user