feat(server): setup api for selfhost deployment (#7569)

This commit is contained in:
forehalo
2024-07-23 10:39:33 +00:00
parent 14fbeb7879
commit dddbfe6473
21 changed files with 243 additions and 136 deletions

View File

@@ -16,6 +16,7 @@ import { ADD_ENABLED_FEATURES, ServerConfigModule } from './core/config';
import { DocModule } from './core/doc'; import { DocModule } from './core/doc';
import { FeatureModule } from './core/features'; import { FeatureModule } from './core/features';
import { QuotaModule } from './core/quota'; import { QuotaModule } from './core/quota';
import { CustomSetupModule } from './core/setup';
import { StorageModule } from './core/storage'; 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';
@@ -175,13 +176,11 @@ function buildAppModule() {
// self hosted server only // self hosted server only
.useIf( .useIf(
config => config.isSelfhosted, config => config.isSelfhosted,
CustomSetupModule,
ServeStaticModule.forRoot({ ServeStaticModule.forRoot({
rootPath: join('/app', 'static'), rootPath: join('/app', 'static'),
exclude: ['/admin*'], exclude: ['/admin*'],
}) }),
)
.useIf(
config => config.isSelfhosted,
ServeStaticModule.forRoot({ ServeStaticModule.forRoot({
rootPath: join('/app', 'static', 'admin'), rootPath: join('/app', 'static', 'admin'),
serveRoot: '/admin', serveRoot: '/admin',

View File

@@ -5,14 +5,7 @@ import { PrismaClient } from '@prisma/client';
import type { CookieOptions, Request, Response } from 'express'; import type { CookieOptions, Request, Response } from 'express';
import { assign, omit } from 'lodash-es'; import { assign, omit } from 'lodash-es';
import { import { Config, EmailAlreadyUsed, MailService } from '../../fundamentals';
Config,
CryptoHelper,
EmailAlreadyUsed,
MailService,
WrongSignInCredentials,
WrongSignInMethod,
} from '../../fundamentals';
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';
@@ -74,20 +67,19 @@ export class AuthService implements OnApplicationBootstrap {
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, private readonly user: UserService
private readonly crypto: CryptoHelper
) {} ) {}
async onApplicationBootstrap() { async onApplicationBootstrap() {
if (this.config.node.dev) { if (this.config.node.dev) {
try { 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); let devUser = await this.user.findUserByEmail(email);
if (!devUser) { if (!devUser) {
devUser = await this.user.createUser({ devUser = await this.user.createUser({
email, email,
name, name,
password: await this.crypto.encryptPassword(pwd), password,
}); });
} }
await this.quota.switchUserQuota(devUser.id, QuotaType.ProPlanV1); await this.quota.switchUserQuota(devUser.id, QuotaType.ProPlanV1);
@@ -114,36 +106,17 @@ export class AuthService implements OnApplicationBootstrap {
throw new EmailAlreadyUsed(); throw new EmailAlreadyUsed();
} }
const hashedPassword = await this.crypto.encryptPassword(password);
return this.user return this.user
.createUser({ .createUser({
name, name,
email, email,
password: hashedPassword, password,
}) })
.then(sessionUser); .then(sessionUser);
} }
async signIn(email: string, password: string) { async signIn(email: string, password: string) {
const user = await this.user.findUserWithHashedPasswordByEmail(email); const user = await this.user.signIn(email, password);
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();
}
return sessionUser(user); return sessionUser(user);
} }
@@ -382,8 +355,7 @@ export class AuthService implements OnApplicationBootstrap {
id: string, id: string,
newPassword: string newPassword: string
): Promise<Omit<User, 'password'>> { ): Promise<Omit<User, 'password'>> {
const hashedPassword = await this.crypto.encryptPassword(newPassword); return this.user.updateUser(id, { password: newPassword });
return this.user.updateUser(id, { password: hashedPassword });
} }
async changeEmail( async changeEmail(

View File

@@ -9,7 +9,7 @@ import {
ResolveField, ResolveField,
Resolver, Resolver,
} from '@nestjs/graphql'; } from '@nestjs/graphql';
import { RuntimeConfig, RuntimeConfigType } from '@prisma/client'; import { PrismaClient, RuntimeConfig, RuntimeConfigType } from '@prisma/client';
import { GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars'; import { GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars';
import { Config, DeploymentType, URLHelper } from '../../fundamentals'; import { Config, DeploymentType, URLHelper } from '../../fundamentals';
@@ -115,7 +115,8 @@ export class ServerFlagsType implements ServerFlags {
export class ServerConfigResolver { export class ServerConfigResolver {
constructor( constructor(
private readonly config: Config, private readonly config: Config,
private readonly url: URLHelper private readonly url: URLHelper,
private readonly db: PrismaClient
) {} ) {}
@Public() @Public()
@@ -165,6 +166,13 @@ export class ServerConfigResolver {
return flags; return flags;
}, {} as ServerFlagsType); }, {} as ServerFlagsType);
} }
@ResolveField(() => Boolean, {
description: 'whether server has been initialized',
})
async initialized() {
return (await this.db.user.count()) > 0;
}
} }
@Resolver(() => ServerRuntimeConfigType) @Resolver(() => ServerRuntimeConfigType)

View File

@@ -1,6 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { Config } from '../../fundamentals'; import { Config, type EventPayload, OnEvent } from '../../fundamentals';
import { UserService } from '../user/service'; import { UserService } from '../user/service';
import { FeatureService } from './service'; import { FeatureService } from './service';
import { FeatureType } from './types'; import { FeatureType } from './types';
@@ -167,4 +167,9 @@ export class FeatureManagementService {
async listFeatureWorkspaces(feature: FeatureType) { async listFeatureWorkspaces(feature: FeatureType) {
return this.feature.listFeatureWorkspaces(feature); return this.feature.listFeatureWorkspaces(feature);
} }
@OnEvent('user.admin.created')
async onAdminUserCreated({ id }: EventPayload<'user.admin.created'>) {
await this.addAdmin(id);
}
} }

View File

@@ -47,7 +47,8 @@ export class FeatureManagementResolver {
if (user) { if (user) {
return this.feature.addEarlyAccess(user.id, type); return this.feature.addEarlyAccess(user.id, type);
} else { } else {
const user = await this.users.createAnonymousUser(email, { const user = await this.users.createUser({
email,
registered: false, registered: false,
}); });
return this.feature.addEarlyAccess(user.id, type); return this.feature.addEarlyAccess(user.id, type);

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

View 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 {}

View File

@@ -12,13 +12,7 @@ import { PrismaClient } from '@prisma/client';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { isNil, omitBy } from 'lodash-es'; import { isNil, omitBy } from 'lodash-es';
import { import { type FileUpload, Throttle, UserNotFound } from '../../fundamentals';
Config,
CryptoHelper,
type FileUpload,
Throttle,
UserNotFound,
} from '../../fundamentals';
import { CurrentUser } from '../auth/current-user'; import { CurrentUser } from '../auth/current-user';
import { Public } from '../auth/guard'; import { Public } from '../auth/guard';
import { sessionUser } from '../auth/service'; import { sessionUser } from '../auth/service';
@@ -177,9 +171,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 user: UserService
private readonly crypto: CryptoHelper,
private readonly config: Config
) {} ) {}
@Query(() => [UserType], { @Query(() => [UserType], {
@@ -222,22 +214,9 @@ export class UserManagementResolver {
async createUser( async createUser(
@Args({ name: 'input', type: () => CreateUserInput }) input: CreateUserInput @Args({ name: 'input', type: () => CreateUserInput }) input: CreateUserInput
) { ) {
validators.assertValidEmail(input.email); const { id } = await this.user.createUser({
if (input.password) { email: input.email,
const config = await this.config.runtime.fetchAll({ password: input.password,
'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,
registered: true, registered: true,
}); });

View File

@@ -3,12 +3,16 @@ import { Prisma, PrismaClient } from '@prisma/client';
import { import {
Config, Config,
CryptoHelper,
EmailAlreadyUsed, EmailAlreadyUsed,
EventEmitter, EventEmitter,
type EventPayload, type EventPayload,
OnEvent, OnEvent,
WrongSignInCredentials,
WrongSignInMethod,
} from '../../fundamentals'; } from '../../fundamentals';
import { Quota_FreePlanV1_1 } from '../quota/schema'; import { Quota_FreePlanV1_1 } from '../quota/schema';
import { validators } from '../utils/validators';
@Injectable() @Injectable()
export class UserService { export class UserService {
@@ -26,6 +30,7 @@ export class UserService {
constructor( constructor(
private readonly config: Config, private readonly config: Config,
private readonly crypto: CryptoHelper,
private readonly prisma: PrismaClient, private readonly prisma: PrismaClient,
private readonly emitter: EventEmitter private readonly emitter: EventEmitter
) {} ) {}
@@ -35,7 +40,7 @@ export class UserService {
name: 'Unnamed', name: 'Unnamed',
features: { features: {
create: { create: {
reason: 'created by invite sign up', reason: 'sign up',
activated: true, activated: true,
feature: { feature: {
connect: { 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({ return this.prisma.user.create({
select: this.defaultUserSelect, select: this.defaultUserSelect,
data: { 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) { async findUserById(id: string) {
return this.prisma.user return this.prisma.user
.findUnique({ .findUnique({
@@ -86,6 +100,7 @@ export class UserService {
} }
async findUserByEmail(email: string) { async findUserByEmail(email: string) {
validators.assertValidEmail(email);
return this.prisma.user.findFirst({ return this.prisma.user.findFirst({
where: { where: {
email: { email: {
@@ -101,6 +116,7 @@ export class UserService {
* supposed to be used only for `Credential SignIn` * supposed to be used only for `Credential SignIn`
*/ */
async findUserWithHashedPasswordByEmail(email: string) { async findUserWithHashedPasswordByEmail(email: string) {
validators.assertValidEmail(email);
return this.prisma.user.findFirst({ return this.prisma.user.findFirst({
where: { where: {
email: { email: {
@@ -111,15 +127,27 @@ export class UserService {
}); });
} }
async findOrCreateUser( async signIn(email: string, password: string) {
email: string, const user = await this.findUserWithHashedPasswordByEmail(email);
data?: Partial<Prisma.UserCreateInput>
) { if (!user) {
const user = await this.findUserByEmail(email); throw new WrongSignInCredentials();
if (user) {
return user;
} }
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( async fulfillUser(
@@ -160,9 +188,23 @@ export class UserService {
async updateUser( async updateUser(
id: string, id: string,
data: Prisma.UserUpdateInput, data: Omit<Prisma.UserUpdateInput, 'password'> & {
password?: string | null;
},
select: Prisma.UserSelect = this.defaultUserSelect 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 }); const user = await this.prisma.user.update({ where: { id }, data, select });
this.emitter.emit('user.updated', user); this.emitter.emit('user.updated', user);

View File

@@ -7,6 +7,7 @@ import {
} from '@nestjs/graphql'; } from '@nestjs/graphql';
import type { User } from '@prisma/client'; import type { User } from '@prisma/client';
import type { Payload } from '../../fundamentals/event/def';
import { CurrentUser } from '../auth/current-user'; import { CurrentUser } from '../auth/current-user';
@ObjectType() @ObjectType()
@@ -81,3 +82,11 @@ export class UpdateUserInput implements Partial<User> {
@Field({ description: 'User name', nullable: true }) @Field({ description: 'User name', nullable: true })
name?: string; name?: string;
} }
declare module '../../fundamentals/event/def' {
interface UserEvents {
admin: {
created: Payload<{ id: string }>;
};
}
}

View File

@@ -342,7 +342,8 @@ 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.createAnonymousUser(email, { target = await this.users.createUser({
email,
registered: false, registered: false,
}); });
} }

View File

@@ -2,33 +2,14 @@ import { ModuleRef } from '@nestjs/core';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import { FeatureManagementService } from '../../core/features'; import { FeatureManagementService } from '../../core/features';
import { UserService } from '../../core/user'; import { Config } from '../../fundamentals';
import { Config, CryptoHelper } from '../../fundamentals';
export class SelfHostAdmin1 { export class SelfHostAdmin1 {
// do the migration // do the migration
static async up(db: PrismaClient, ref: ModuleRef) { static async up(db: PrismaClient, ref: ModuleRef) {
const config = ref.get(Config, { strict: false }); const config = ref.get(Config, { strict: false });
if (config.isSelfhosted) { if (config.isSelfhosted) {
const crypto = ref.get(CryptoHelper, { strict: false });
const user = ref.get(UserService, { strict: false });
const feature = ref.get(FeatureManagementService, { 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({ const firstUser = await db.user.findFirst({
orderBy: { orderBy: {

View File

@@ -3,7 +3,7 @@ import {
Inject, Inject,
Injectable, Injectable,
Logger, Logger,
OnApplicationBootstrap, OnModuleInit,
} from '@nestjs/common'; } from '@nestjs/common';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import { difference, keyBy } from 'lodash-es'; import { difference, keyBy } from 'lodash-es';
@@ -45,7 +45,7 @@ function validateConfigType<K extends keyof FlattenedAppRuntimeConfig>(
* }) * })
*/ */
@Injectable() @Injectable()
export class Runtime implements OnApplicationBootstrap { export class Runtime implements OnModuleInit {
private readonly logger = new Logger('App:RuntimeConfig'); private readonly logger = new Logger('App:RuntimeConfig');
constructor( constructor(
@@ -54,7 +54,7 @@ export class Runtime implements OnApplicationBootstrap {
@Inject(forwardRef(() => Cache)) private readonly cache: Cache @Inject(forwardRef(() => Cache)) private readonly cache: Cache
) {} ) {}
async onApplicationBootstrap() { async onModuleInit() {
await this.upgradeDB(); await this.upgradeDB();
} }

View File

@@ -254,6 +254,10 @@ export const USER_FRIENDLY_ERRORS = {
message: ({ min, max }) => message: ({ min, max }) =>
`Password must be between ${min} and ${max} characters`, `Password must be between ${min} and ${max} characters`,
}, },
password_required: {
type: 'invalid_input',
message: 'Password is required.',
},
wrong_sign_in_method: { wrong_sign_in_method: {
type: 'invalid_input', type: 'invalid_input',
message: message:

View File

@@ -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 { export class WrongSignInMethod extends UserFriendlyError {
constructor(message?: string) { constructor(message?: string) {
super('invalid_input', 'wrong_sign_in_method', message); super('invalid_input', 'wrong_sign_in_method', message);
@@ -496,6 +502,7 @@ export enum ErrorNames {
OAUTH_ACCOUNT_ALREADY_CONNECTED, OAUTH_ACCOUNT_ALREADY_CONNECTED,
INVALID_EMAIL, INVALID_EMAIL,
INVALID_PASSWORD_LENGTH, INVALID_PASSWORD_LENGTH,
PASSWORD_REQUIRED,
WRONG_SIGN_IN_METHOD, WRONG_SIGN_IN_METHOD,
EARLY_ACCESS_REQUIRED, EARLY_ACCESS_REQUIRED,
SIGN_UP_FORBIDDEN, SIGN_UP_FORBIDDEN,

View File

@@ -1,10 +1,10 @@
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { Inject, Injectable, Logger, Scope } from '@nestjs/common'; import { Inject, Injectable, Logger, Scope } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef, REQUEST } from '@nestjs/core';
import { CONTEXT } from '@nestjs/graphql'; import type { Request } from 'express';
import type { GraphqlContext } from '../graphql'; import { GraphqlContext } from '../graphql';
import { retryable } from '../utils/promise'; import { retryable } from '../utils/promise';
import { Locker } from './local-lock'; import { Locker } from './local-lock';
@@ -17,7 +17,7 @@ export class MutexService {
private readonly locker: Locker; private readonly locker: Locker;
constructor( constructor(
@Inject(CONTEXT) private readonly context: GraphqlContext, @Inject(REQUEST) private readonly request: Request | GraphqlContext,
private readonly ref: ModuleRef private readonly ref: ModuleRef
) { ) {
// nestjs will always find and injecting the locker from local module // nestjs will always find and injecting the locker from local module
@@ -31,11 +31,12 @@ export class MutexService {
} }
protected getId() { 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) { if (!id) {
id = randomUUID(); id = randomUUID();
this.context.req.headers['x-transaction-id'] = id; req.headers['x-transaction-id'] = id;
} }
return id; return id;

View File

@@ -249,6 +249,7 @@ enum ErrorNames {
OAUTH_ACCOUNT_ALREADY_CONNECTED OAUTH_ACCOUNT_ALREADY_CONNECTED
OAUTH_STATE_EXPIRED OAUTH_STATE_EXPIRED
PAGE_IS_NOT_PUBLIC PAGE_IS_NOT_PUBLIC
PASSWORD_REQUIRED
RUNTIME_CONFIG_NOT_FOUND RUNTIME_CONFIG_NOT_FOUND
SAME_EMAIL_PROVIDED SAME_EMAIL_PROVIDED
SAME_SUBSCRIPTION_RECURRING SAME_SUBSCRIPTION_RECURRING
@@ -622,6 +623,9 @@ type ServerConfigType {
"""server flavor""" """server flavor"""
flavor: String! @deprecated(reason: "use `features`") flavor: String! @deprecated(reason: "use `features`")
"""whether server has been initialized"""
initialized: Boolean!
"""server identical name could be shown as badge on user interface""" """server identical name could be shown as badge on user interface"""
name: String! name: String!
oauthProviders: [OAuthProviderType!]! oauthProviders: [OAuthProviderType!]!

View File

@@ -7,7 +7,7 @@ import { createTestingModule } from './utils';
let config: Config; let config: Config;
let module: TestingModule; let module: TestingModule;
test.beforeEach(async () => { test.beforeEach(async () => {
module = await createTestingModule(); module = await createTestingModule({}, false);
config = module.get(Config); config = module.get(Config);
}); });
@@ -33,4 +33,6 @@ test('should be able to override config', async t => {
const config = module.get(Config); const config = module.get(Config);
t.is(config.server.host, 'testing'); t.is(config.server.host, 'testing');
await module.close();
}); });

View File

@@ -13,9 +13,12 @@ import { Config } from '../src/fundamentals/config';
import { createTestingModule } from './utils'; import { createTestingModule } from './utils';
const createModule = () => { const createModule = () => {
return createTestingModule({ return createTestingModule(
{
imports: [QuotaModule, StorageModule, DocModule], imports: [QuotaModule, StorageModule, DocModule],
}); },
false
);
}; };
let m: TestingModule; let m: TestingModule;

View File

@@ -1,5 +1,4 @@
import type { INestApplication } from '@nestjs/common'; import type { INestApplication } from '@nestjs/common';
import { hashSync } from '@node-rs/argon2';
import request, { type Response } from 'supertest'; import request, { type Response } from 'supertest';
import { import {
@@ -54,7 +53,7 @@ export async function signUp(
const user = await app.get(UserService).createUser({ const user = await app.get(UserService).createUser({
name, name,
email, email,
password: hashSync(password), password,
emailVerifiedAt: autoVerifyEmail ? new Date() : null, emailVerifiedAt: autoVerifyEmail ? new Date() : null,
}); });
const { sessionId } = await app.get(AuthService).createUserSession(user); const { sessionId } = await app.get(AuthService).createUserSession(user);

View File

@@ -11,7 +11,7 @@ import supertest from 'supertest';
import { AppModule, FunctionalityModules } from '../../src/app.module'; import { AppModule, FunctionalityModules } from '../../src/app.module';
import { AuthGuard, AuthModule } from '../../src/core/auth'; import { AuthGuard, AuthModule } from '../../src/core/auth';
import { UserFeaturesInit1698652531198 } from '../../src/data/migrations/1698652531198-user-features-init'; 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'; import { GqlModule } from '../../src/fundamentals/graphql';
async function flushDB(client: PrismaClient) { async function flushDB(client: PrismaClient) {
@@ -67,7 +67,8 @@ class MockResolver {
} }
export async function createTestingModule( export async function createTestingModule(
moduleDef: TestingModuleMeatdata = {} moduleDef: TestingModuleMeatdata = {},
init = true
) { ) {
// setting up // setting up
let imports = moduleDef.imports ?? []; let imports = moduleDef.imports ?? [];
@@ -105,11 +106,19 @@ export async function createTestingModule(
await initTestingDB(prisma); 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; return m;
} }
export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) { export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) {
const m = await createTestingModule(moduleDef); const m = await createTestingModule(moduleDef, false);
const app = m.createNestApplication({ const app = m.createNestApplication({
cors: true, cors: true,
@@ -134,6 +143,10 @@ export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) {
await app.init(); await app.init();
const config = app.get(Config);
// by pass password min length validation
await config.runtime.set('auth/password.min', 1);
return { return {
module: m, module: m,
app, app,