mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
feat(server): setup api for selfhost deployment (#7569)
This commit is contained in:
@@ -16,6 +16,7 @@ import { ADD_ENABLED_FEATURES, ServerConfigModule } from './core/config';
|
||||
import { DocModule } from './core/doc';
|
||||
import { FeatureModule } from './core/features';
|
||||
import { QuotaModule } from './core/quota';
|
||||
import { CustomSetupModule } from './core/setup';
|
||||
import { StorageModule } from './core/storage';
|
||||
import { SyncModule } from './core/sync';
|
||||
import { UserModule } from './core/user';
|
||||
@@ -175,13 +176,11 @@ function buildAppModule() {
|
||||
// self hosted server only
|
||||
.useIf(
|
||||
config => config.isSelfhosted,
|
||||
CustomSetupModule,
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join('/app', 'static'),
|
||||
exclude: ['/admin*'],
|
||||
})
|
||||
)
|
||||
.useIf(
|
||||
config => config.isSelfhosted,
|
||||
}),
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join('/app', 'static', 'admin'),
|
||||
serveRoot: '/admin',
|
||||
|
||||
@@ -5,14 +5,7 @@ import { PrismaClient } from '@prisma/client';
|
||||
import type { CookieOptions, Request, Response } from 'express';
|
||||
import { assign, omit } from 'lodash-es';
|
||||
|
||||
import {
|
||||
Config,
|
||||
CryptoHelper,
|
||||
EmailAlreadyUsed,
|
||||
MailService,
|
||||
WrongSignInCredentials,
|
||||
WrongSignInMethod,
|
||||
} from '../../fundamentals';
|
||||
import { Config, EmailAlreadyUsed, MailService } from '../../fundamentals';
|
||||
import { FeatureManagementService } from '../features/management';
|
||||
import { QuotaService } from '../quota/service';
|
||||
import { QuotaType } from '../quota/types';
|
||||
@@ -74,20 +67,19 @@ export class AuthService implements OnApplicationBootstrap {
|
||||
private readonly mailer: MailService,
|
||||
private readonly feature: FeatureManagementService,
|
||||
private readonly quota: QuotaService,
|
||||
private readonly user: UserService,
|
||||
private readonly crypto: CryptoHelper
|
||||
private readonly user: UserService
|
||||
) {}
|
||||
|
||||
async onApplicationBootstrap() {
|
||||
if (this.config.node.dev) {
|
||||
try {
|
||||
const [email, name, pwd] = ['dev@affine.pro', 'Dev User', 'dev'];
|
||||
const [email, name, password] = ['dev@affine.pro', 'Dev User', 'dev'];
|
||||
let devUser = await this.user.findUserByEmail(email);
|
||||
if (!devUser) {
|
||||
devUser = await this.user.createUser({
|
||||
email,
|
||||
name,
|
||||
password: await this.crypto.encryptPassword(pwd),
|
||||
password,
|
||||
});
|
||||
}
|
||||
await this.quota.switchUserQuota(devUser.id, QuotaType.ProPlanV1);
|
||||
@@ -114,36 +106,17 @@ export class AuthService implements OnApplicationBootstrap {
|
||||
throw new EmailAlreadyUsed();
|
||||
}
|
||||
|
||||
const hashedPassword = await this.crypto.encryptPassword(password);
|
||||
|
||||
return this.user
|
||||
.createUser({
|
||||
name,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
password,
|
||||
})
|
||||
.then(sessionUser);
|
||||
}
|
||||
|
||||
async signIn(email: string, password: string) {
|
||||
const user = await this.user.findUserWithHashedPasswordByEmail(email);
|
||||
|
||||
if (!user) {
|
||||
throw new WrongSignInCredentials();
|
||||
}
|
||||
|
||||
if (!user.password) {
|
||||
throw new WrongSignInMethod();
|
||||
}
|
||||
|
||||
const passwordMatches = await this.crypto.verifyPassword(
|
||||
password,
|
||||
user.password
|
||||
);
|
||||
|
||||
if (!passwordMatches) {
|
||||
throw new WrongSignInCredentials();
|
||||
}
|
||||
const user = await this.user.signIn(email, password);
|
||||
|
||||
return sessionUser(user);
|
||||
}
|
||||
@@ -382,8 +355,7 @@ export class AuthService implements OnApplicationBootstrap {
|
||||
id: string,
|
||||
newPassword: string
|
||||
): Promise<Omit<User, 'password'>> {
|
||||
const hashedPassword = await this.crypto.encryptPassword(newPassword);
|
||||
return this.user.updateUser(id, { password: hashedPassword });
|
||||
return this.user.updateUser(id, { password: newPassword });
|
||||
}
|
||||
|
||||
async changeEmail(
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import { RuntimeConfig, RuntimeConfigType } from '@prisma/client';
|
||||
import { PrismaClient, RuntimeConfig, RuntimeConfigType } from '@prisma/client';
|
||||
import { GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars';
|
||||
|
||||
import { Config, DeploymentType, URLHelper } from '../../fundamentals';
|
||||
@@ -115,7 +115,8 @@ export class ServerFlagsType implements ServerFlags {
|
||||
export class ServerConfigResolver {
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly url: URLHelper
|
||||
private readonly url: URLHelper,
|
||||
private readonly db: PrismaClient
|
||||
) {}
|
||||
|
||||
@Public()
|
||||
@@ -165,6 +166,13 @@ export class ServerConfigResolver {
|
||||
return flags;
|
||||
}, {} as ServerFlagsType);
|
||||
}
|
||||
|
||||
@ResolveField(() => Boolean, {
|
||||
description: 'whether server has been initialized',
|
||||
})
|
||||
async initialized() {
|
||||
return (await this.db.user.count()) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Resolver(() => ServerRuntimeConfigType)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { Config } from '../../fundamentals';
|
||||
import { Config, type EventPayload, OnEvent } from '../../fundamentals';
|
||||
import { UserService } from '../user/service';
|
||||
import { FeatureService } from './service';
|
||||
import { FeatureType } from './types';
|
||||
@@ -167,4 +167,9 @@ export class FeatureManagementService {
|
||||
async listFeatureWorkspaces(feature: FeatureType) {
|
||||
return this.feature.listFeatureWorkspaces(feature);
|
||||
}
|
||||
|
||||
@OnEvent('user.admin.created')
|
||||
async onAdminUserCreated({ id }: EventPayload<'user.admin.created'>) {
|
||||
await this.addAdmin(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@ export class FeatureManagementResolver {
|
||||
if (user) {
|
||||
return this.feature.addEarlyAccess(user.id, type);
|
||||
} else {
|
||||
const user = await this.users.createAnonymousUser(email, {
|
||||
const user = await this.users.createUser({
|
||||
email,
|
||||
registered: false,
|
||||
});
|
||||
return this.feature.addEarlyAccess(user.id, type);
|
||||
|
||||
66
packages/backend/server/src/core/setup/controller.ts
Normal file
66
packages/backend/server/src/core/setup/controller.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Body, Controller, Post, Req, Res } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
import {
|
||||
ActionForbidden,
|
||||
EventEmitter,
|
||||
InternalServerError,
|
||||
MutexService,
|
||||
PasswordRequired,
|
||||
} from '../../fundamentals';
|
||||
import { AuthService, Public } from '../auth';
|
||||
import { UserService } from '../user/service';
|
||||
|
||||
interface CreateUserInput {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
@Controller('/api/setup')
|
||||
export class CustomSetupController {
|
||||
constructor(
|
||||
private readonly db: PrismaClient,
|
||||
private readonly user: UserService,
|
||||
private readonly auth: AuthService,
|
||||
private readonly event: EventEmitter,
|
||||
private readonly mutex: MutexService
|
||||
) {}
|
||||
|
||||
@Public()
|
||||
@Post('/create-admin-user')
|
||||
async createAdmin(
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
@Body() input: CreateUserInput
|
||||
) {
|
||||
if (!input.password) {
|
||||
throw new PasswordRequired();
|
||||
}
|
||||
|
||||
await using lock = await this.mutex.lock('createFirstAdmin');
|
||||
|
||||
if (!lock) {
|
||||
throw new InternalServerError();
|
||||
}
|
||||
|
||||
if ((await this.db.user.count()) > 0) {
|
||||
throw new ActionForbidden('First user already created');
|
||||
}
|
||||
|
||||
const user = await this.user.createUser({
|
||||
email: input.email,
|
||||
password: input.password,
|
||||
registered: true,
|
||||
});
|
||||
|
||||
try {
|
||||
await this.event.emitAsync('user.admin.created', user);
|
||||
await this.auth.setCookie(req, res, user);
|
||||
res.send({ id: user.id, email: user.email, name: user.name });
|
||||
} catch (e) {
|
||||
await this.user.deleteUser(user.id);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
packages/backend/server/src/core/setup/index.ts
Normal file
11
packages/backend/server/src/core/setup/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { AuthModule } from '../auth';
|
||||
import { UserModule } from '../user';
|
||||
import { CustomSetupController } from './controller';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule, UserModule],
|
||||
controllers: [CustomSetupController],
|
||||
})
|
||||
export class CustomSetupModule {}
|
||||
@@ -12,13 +12,7 @@ import { PrismaClient } from '@prisma/client';
|
||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||
import { isNil, omitBy } from 'lodash-es';
|
||||
|
||||
import {
|
||||
Config,
|
||||
CryptoHelper,
|
||||
type FileUpload,
|
||||
Throttle,
|
||||
UserNotFound,
|
||||
} from '../../fundamentals';
|
||||
import { type FileUpload, Throttle, UserNotFound } from '../../fundamentals';
|
||||
import { CurrentUser } from '../auth/current-user';
|
||||
import { Public } from '../auth/guard';
|
||||
import { sessionUser } from '../auth/service';
|
||||
@@ -177,9 +171,7 @@ class CreateUserInput {
|
||||
export class UserManagementResolver {
|
||||
constructor(
|
||||
private readonly db: PrismaClient,
|
||||
private readonly user: UserService,
|
||||
private readonly crypto: CryptoHelper,
|
||||
private readonly config: Config
|
||||
private readonly user: UserService
|
||||
) {}
|
||||
|
||||
@Query(() => [UserType], {
|
||||
@@ -222,22 +214,9 @@ export class UserManagementResolver {
|
||||
async createUser(
|
||||
@Args({ name: 'input', type: () => CreateUserInput }) input: CreateUserInput
|
||||
) {
|
||||
validators.assertValidEmail(input.email);
|
||||
if (input.password) {
|
||||
const config = await this.config.runtime.fetchAll({
|
||||
'auth/password.max': true,
|
||||
'auth/password.min': true,
|
||||
});
|
||||
validators.assertValidPassword(input.password, {
|
||||
max: config['auth/password.max'],
|
||||
min: config['auth/password.min'],
|
||||
});
|
||||
}
|
||||
|
||||
const { id } = await this.user.createAnonymousUser(input.email, {
|
||||
password: input.password
|
||||
? await this.crypto.encryptPassword(input.password)
|
||||
: undefined,
|
||||
const { id } = await this.user.createUser({
|
||||
email: input.email,
|
||||
password: input.password,
|
||||
registered: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -3,12 +3,16 @@ import { Prisma, PrismaClient } from '@prisma/client';
|
||||
|
||||
import {
|
||||
Config,
|
||||
CryptoHelper,
|
||||
EmailAlreadyUsed,
|
||||
EventEmitter,
|
||||
type EventPayload,
|
||||
OnEvent,
|
||||
WrongSignInCredentials,
|
||||
WrongSignInMethod,
|
||||
} from '../../fundamentals';
|
||||
import { Quota_FreePlanV1_1 } from '../quota/schema';
|
||||
import { validators } from '../utils/validators';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
@@ -26,6 +30,7 @@ export class UserService {
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly crypto: CryptoHelper,
|
||||
private readonly prisma: PrismaClient,
|
||||
private readonly emitter: EventEmitter
|
||||
) {}
|
||||
@@ -35,7 +40,7 @@ export class UserService {
|
||||
name: 'Unnamed',
|
||||
features: {
|
||||
create: {
|
||||
reason: 'created by invite sign up',
|
||||
reason: 'sign up',
|
||||
activated: true,
|
||||
feature: {
|
||||
connect: {
|
||||
@@ -47,7 +52,33 @@ export class UserService {
|
||||
};
|
||||
}
|
||||
|
||||
async createUser(data: Prisma.UserCreateInput) {
|
||||
async createUser(
|
||||
data: Omit<Prisma.UserCreateInput, 'name'> & { name?: string }
|
||||
) {
|
||||
validators.assertValidEmail(data.email);
|
||||
const user = await this.findUserByEmail(data.email);
|
||||
|
||||
if (user) {
|
||||
throw new EmailAlreadyUsed();
|
||||
}
|
||||
|
||||
if (data.password) {
|
||||
const config = await this.config.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.name) {
|
||||
data.name = data.email.split('@')[0];
|
||||
}
|
||||
|
||||
return this.prisma.user.create({
|
||||
select: this.defaultUserSelect,
|
||||
data: {
|
||||
@@ -57,23 +88,6 @@ export class UserService {
|
||||
});
|
||||
}
|
||||
|
||||
async createAnonymousUser(
|
||||
email: string,
|
||||
data?: Partial<Prisma.UserCreateInput>
|
||||
) {
|
||||
const user = await this.findUserByEmail(email);
|
||||
|
||||
if (user) {
|
||||
throw new EmailAlreadyUsed();
|
||||
}
|
||||
|
||||
return this.createUser({
|
||||
email,
|
||||
name: email.split('@')[0],
|
||||
...data,
|
||||
});
|
||||
}
|
||||
|
||||
async findUserById(id: string) {
|
||||
return this.prisma.user
|
||||
.findUnique({
|
||||
@@ -86,6 +100,7 @@ export class UserService {
|
||||
}
|
||||
|
||||
async findUserByEmail(email: string) {
|
||||
validators.assertValidEmail(email);
|
||||
return this.prisma.user.findFirst({
|
||||
where: {
|
||||
email: {
|
||||
@@ -101,6 +116,7 @@ export class UserService {
|
||||
* supposed to be used only for `Credential SignIn`
|
||||
*/
|
||||
async findUserWithHashedPasswordByEmail(email: string) {
|
||||
validators.assertValidEmail(email);
|
||||
return this.prisma.user.findFirst({
|
||||
where: {
|
||||
email: {
|
||||
@@ -111,15 +127,27 @@ export class UserService {
|
||||
});
|
||||
}
|
||||
|
||||
async findOrCreateUser(
|
||||
email: string,
|
||||
data?: Partial<Prisma.UserCreateInput>
|
||||
) {
|
||||
const user = await this.findUserByEmail(email);
|
||||
if (user) {
|
||||
return user;
|
||||
async signIn(email: string, password: string) {
|
||||
const user = await this.findUserWithHashedPasswordByEmail(email);
|
||||
|
||||
if (!user) {
|
||||
throw new WrongSignInCredentials();
|
||||
}
|
||||
return this.createAnonymousUser(email, data);
|
||||
|
||||
if (!user.password) {
|
||||
throw new WrongSignInMethod();
|
||||
}
|
||||
|
||||
const passwordMatches = await this.crypto.verifyPassword(
|
||||
password,
|
||||
user.password
|
||||
);
|
||||
|
||||
if (!passwordMatches) {
|
||||
throw new WrongSignInCredentials();
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async fulfillUser(
|
||||
@@ -160,9 +188,23 @@ export class UserService {
|
||||
|
||||
async updateUser(
|
||||
id: string,
|
||||
data: Prisma.UserUpdateInput,
|
||||
data: Omit<Prisma.UserUpdateInput, 'password'> & {
|
||||
password?: string | null;
|
||||
},
|
||||
select: Prisma.UserSelect = this.defaultUserSelect
|
||||
) {
|
||||
if (data.password) {
|
||||
const config = await this.config.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);
|
||||
}
|
||||
const user = await this.prisma.user.update({ where: { id }, data, select });
|
||||
|
||||
this.emitter.emit('user.updated', user);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '@nestjs/graphql';
|
||||
import type { User } from '@prisma/client';
|
||||
|
||||
import type { Payload } from '../../fundamentals/event/def';
|
||||
import { CurrentUser } from '../auth/current-user';
|
||||
|
||||
@ObjectType()
|
||||
@@ -81,3 +82,11 @@ export class UpdateUserInput implements Partial<User> {
|
||||
@Field({ description: 'User name', nullable: true })
|
||||
name?: string;
|
||||
}
|
||||
|
||||
declare module '../../fundamentals/event/def' {
|
||||
interface UserEvents {
|
||||
admin: {
|
||||
created: Payload<{ id: string }>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +342,8 @@ export class WorkspaceResolver {
|
||||
// only invite if the user is not already in the workspace
|
||||
if (originRecord) return originRecord.id;
|
||||
} else {
|
||||
target = await this.users.createAnonymousUser(email, {
|
||||
target = await this.users.createUser({
|
||||
email,
|
||||
registered: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,33 +2,14 @@ import { ModuleRef } from '@nestjs/core';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { FeatureManagementService } from '../../core/features';
|
||||
import { UserService } from '../../core/user';
|
||||
import { Config, CryptoHelper } from '../../fundamentals';
|
||||
import { Config } from '../../fundamentals';
|
||||
|
||||
export class SelfHostAdmin1 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient, ref: ModuleRef) {
|
||||
const config = ref.get(Config, { strict: false });
|
||||
if (config.isSelfhosted) {
|
||||
const crypto = ref.get(CryptoHelper, { strict: false });
|
||||
const user = ref.get(UserService, { strict: false });
|
||||
const feature = ref.get(FeatureManagementService, { strict: false });
|
||||
if (
|
||||
!process.env.AFFINE_ADMIN_EMAIL ||
|
||||
!process.env.AFFINE_ADMIN_PASSWORD
|
||||
) {
|
||||
throw new Error(
|
||||
'You have to set AFFINE_ADMIN_EMAIL and AFFINE_ADMIN_PASSWORD environment variables to generate the initial user for self-hosted AFFiNE Server.'
|
||||
);
|
||||
}
|
||||
|
||||
await user.findOrCreateUser(process.env.AFFINE_ADMIN_EMAIL, {
|
||||
name: 'AFFINE First User',
|
||||
emailVerifiedAt: new Date(),
|
||||
password: await crypto.encryptPassword(
|
||||
process.env.AFFINE_ADMIN_PASSWORD
|
||||
),
|
||||
});
|
||||
|
||||
const firstUser = await db.user.findFirst({
|
||||
orderBy: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
Inject,
|
||||
Injectable,
|
||||
Logger,
|
||||
OnApplicationBootstrap,
|
||||
OnModuleInit,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { difference, keyBy } from 'lodash-es';
|
||||
@@ -45,7 +45,7 @@ function validateConfigType<K extends keyof FlattenedAppRuntimeConfig>(
|
||||
* })
|
||||
*/
|
||||
@Injectable()
|
||||
export class Runtime implements OnApplicationBootstrap {
|
||||
export class Runtime implements OnModuleInit {
|
||||
private readonly logger = new Logger('App:RuntimeConfig');
|
||||
|
||||
constructor(
|
||||
@@ -54,7 +54,7 @@ export class Runtime implements OnApplicationBootstrap {
|
||||
@Inject(forwardRef(() => Cache)) private readonly cache: Cache
|
||||
) {}
|
||||
|
||||
async onApplicationBootstrap() {
|
||||
async onModuleInit() {
|
||||
await this.upgradeDB();
|
||||
}
|
||||
|
||||
|
||||
@@ -254,6 +254,10 @@ export const USER_FRIENDLY_ERRORS = {
|
||||
message: ({ min, max }) =>
|
||||
`Password must be between ${min} and ${max} characters`,
|
||||
},
|
||||
password_required: {
|
||||
type: 'invalid_input',
|
||||
message: 'Password is required.',
|
||||
},
|
||||
wrong_sign_in_method: {
|
||||
type: 'invalid_input',
|
||||
message:
|
||||
|
||||
@@ -101,6 +101,12 @@ export class InvalidPasswordLength extends UserFriendlyError {
|
||||
}
|
||||
}
|
||||
|
||||
export class PasswordRequired extends UserFriendlyError {
|
||||
constructor(message?: string) {
|
||||
super('invalid_input', 'password_required', message);
|
||||
}
|
||||
}
|
||||
|
||||
export class WrongSignInMethod extends UserFriendlyError {
|
||||
constructor(message?: string) {
|
||||
super('invalid_input', 'wrong_sign_in_method', message);
|
||||
@@ -496,6 +502,7 @@ export enum ErrorNames {
|
||||
OAUTH_ACCOUNT_ALREADY_CONNECTED,
|
||||
INVALID_EMAIL,
|
||||
INVALID_PASSWORD_LENGTH,
|
||||
PASSWORD_REQUIRED,
|
||||
WRONG_SIGN_IN_METHOD,
|
||||
EARLY_ACCESS_REQUIRED,
|
||||
SIGN_UP_FORBIDDEN,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { Inject, Injectable, Logger, Scope } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { CONTEXT } from '@nestjs/graphql';
|
||||
import { ModuleRef, REQUEST } from '@nestjs/core';
|
||||
import type { Request } from 'express';
|
||||
|
||||
import type { GraphqlContext } from '../graphql';
|
||||
import { GraphqlContext } from '../graphql';
|
||||
import { retryable } from '../utils/promise';
|
||||
import { Locker } from './local-lock';
|
||||
|
||||
@@ -17,7 +17,7 @@ export class MutexService {
|
||||
private readonly locker: Locker;
|
||||
|
||||
constructor(
|
||||
@Inject(CONTEXT) private readonly context: GraphqlContext,
|
||||
@Inject(REQUEST) private readonly request: Request | GraphqlContext,
|
||||
private readonly ref: ModuleRef
|
||||
) {
|
||||
// nestjs will always find and injecting the locker from local module
|
||||
@@ -31,11 +31,12 @@ export class MutexService {
|
||||
}
|
||||
|
||||
protected getId() {
|
||||
let id = this.context.req.headers['x-transaction-id'] as string;
|
||||
const req = 'req' in this.request ? this.request.req : this.request;
|
||||
let id = req.headers['x-transaction-id'] as string;
|
||||
|
||||
if (!id) {
|
||||
id = randomUUID();
|
||||
this.context.req.headers['x-transaction-id'] = id;
|
||||
req.headers['x-transaction-id'] = id;
|
||||
}
|
||||
|
||||
return id;
|
||||
|
||||
@@ -249,6 +249,7 @@ enum ErrorNames {
|
||||
OAUTH_ACCOUNT_ALREADY_CONNECTED
|
||||
OAUTH_STATE_EXPIRED
|
||||
PAGE_IS_NOT_PUBLIC
|
||||
PASSWORD_REQUIRED
|
||||
RUNTIME_CONFIG_NOT_FOUND
|
||||
SAME_EMAIL_PROVIDED
|
||||
SAME_SUBSCRIPTION_RECURRING
|
||||
@@ -622,6 +623,9 @@ type ServerConfigType {
|
||||
"""server flavor"""
|
||||
flavor: String! @deprecated(reason: "use `features`")
|
||||
|
||||
"""whether server has been initialized"""
|
||||
initialized: Boolean!
|
||||
|
||||
"""server identical name could be shown as badge on user interface"""
|
||||
name: String!
|
||||
oauthProviders: [OAuthProviderType!]!
|
||||
|
||||
@@ -7,7 +7,7 @@ import { createTestingModule } from './utils';
|
||||
let config: Config;
|
||||
let module: TestingModule;
|
||||
test.beforeEach(async () => {
|
||||
module = await createTestingModule();
|
||||
module = await createTestingModule({}, false);
|
||||
config = module.get(Config);
|
||||
});
|
||||
|
||||
@@ -33,4 +33,6 @@ test('should be able to override config', async t => {
|
||||
const config = module.get(Config);
|
||||
|
||||
t.is(config.server.host, 'testing');
|
||||
|
||||
await module.close();
|
||||
});
|
||||
|
||||
@@ -13,9 +13,12 @@ import { Config } from '../src/fundamentals/config';
|
||||
import { createTestingModule } from './utils';
|
||||
|
||||
const createModule = () => {
|
||||
return createTestingModule({
|
||||
imports: [QuotaModule, StorageModule, DocModule],
|
||||
});
|
||||
return createTestingModule(
|
||||
{
|
||||
imports: [QuotaModule, StorageModule, DocModule],
|
||||
},
|
||||
false
|
||||
);
|
||||
};
|
||||
|
||||
let m: TestingModule;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { hashSync } from '@node-rs/argon2';
|
||||
import request, { type Response } from 'supertest';
|
||||
|
||||
import {
|
||||
@@ -54,7 +53,7 @@ export async function signUp(
|
||||
const user = await app.get(UserService).createUser({
|
||||
name,
|
||||
email,
|
||||
password: hashSync(password),
|
||||
password,
|
||||
emailVerifiedAt: autoVerifyEmail ? new Date() : null,
|
||||
});
|
||||
const { sessionId } = await app.get(AuthService).createUserSession(user);
|
||||
|
||||
@@ -11,7 +11,7 @@ import supertest from 'supertest';
|
||||
import { AppModule, FunctionalityModules } from '../../src/app.module';
|
||||
import { AuthGuard, AuthModule } from '../../src/core/auth';
|
||||
import { UserFeaturesInit1698652531198 } from '../../src/data/migrations/1698652531198-user-features-init';
|
||||
import { GlobalExceptionFilter } from '../../src/fundamentals';
|
||||
import { Config, GlobalExceptionFilter } from '../../src/fundamentals';
|
||||
import { GqlModule } from '../../src/fundamentals/graphql';
|
||||
|
||||
async function flushDB(client: PrismaClient) {
|
||||
@@ -67,7 +67,8 @@ class MockResolver {
|
||||
}
|
||||
|
||||
export async function createTestingModule(
|
||||
moduleDef: TestingModuleMeatdata = {}
|
||||
moduleDef: TestingModuleMeatdata = {},
|
||||
init = true
|
||||
) {
|
||||
// setting up
|
||||
let imports = moduleDef.imports ?? [];
|
||||
@@ -105,11 +106,19 @@ export async function createTestingModule(
|
||||
await initTestingDB(prisma);
|
||||
}
|
||||
|
||||
if (init) {
|
||||
await m.init();
|
||||
|
||||
const config = m.get(Config);
|
||||
// by pass password min length validation
|
||||
await config.runtime.set('auth/password.min', 1);
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) {
|
||||
const m = await createTestingModule(moduleDef);
|
||||
const m = await createTestingModule(moduleDef, false);
|
||||
|
||||
const app = m.createNestApplication({
|
||||
cors: true,
|
||||
@@ -134,6 +143,10 @@ export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) {
|
||||
|
||||
await app.init();
|
||||
|
||||
const config = app.get(Config);
|
||||
// by pass password min length validation
|
||||
await config.runtime.set('auth/password.min', 1);
|
||||
|
||||
return {
|
||||
module: m,
|
||||
app,
|
||||
|
||||
Reference in New Issue
Block a user