feat(server): runtime setting support (#5602)

---

<details open="true"><summary>Generated summary (powered by <a href="https://app.graphite.dev">Graphite</a>)</summary>

> ## TL;DR
> This pull request adds a new migration file, a new model, and new modules related to runtime settings. It also introduces a new `Runtime` service that allows getting, setting, and updating runtime configurations.
>
> ## What changed
> - Added a new migration file `migration.sql` that creates a table called `application_settings` with columns `key` and `value`.
> - Added a new model `ApplicationSetting` with properties `key` and `value`.
> - Added a new module `RuntimeSettingModule` that exports the `Runtime` service.
> - Added a new service `Runtime` that provides methods for getting, setting, and updating runtime configurations.
> - Modified the `app.module.ts` file to import the `RuntimeSettingModule`.
> - Modified the `index.ts` file in the `fundamentals` directory to export the `Runtime` service.
> - Added a new file `def.ts` in the `runtime` directory that defines the runtime configurations and provides a default implementation.
> - Added a new file `service.ts` in the `runtime` directory that implements the `Runtime` service.
>
> ## How to test
> 1. Run the migration script to create the `application_settings` table.
> 2. Use the `Runtime` service to get, set, and update runtime configurations.
> 3. Verify that the runtime configurations are stored correctly in the database and can be retrieved and modified using the `Runtime` service.
>
> ## Why make this change
> This change introduces a new feature related to runtime settings. The `Runtime` service allows the application to dynamically manage and modify runtime configurations without requiring a restart. This provides flexibility and allows for easier customization and configuration of the application.
</details>
This commit is contained in:
forehalo
2024-05-28 06:43:53 +00:00
parent 9d296c4b62
commit 638fc62601
116 changed files with 1907 additions and 1106 deletions

View File

@@ -0,0 +1,81 @@
import {
defineRuntimeConfig,
defineStartupConfig,
ModuleConfig,
} from '../../fundamentals/config';
export interface AuthStartupConfigurations {
/**
* auth session config
*/
session: {
/**
* Application auth expiration time in seconds
*/
ttl: number;
/**
* Application auth time to refresh in seconds
*/
ttr: number;
};
/**
* Application access token config
*/
accessToken: {
/**
* Application access token expiration time in seconds
*/
ttl: number;
/**
* Application refresh token expiration time in seconds
*/
refreshTokenTtl: number;
};
}
export interface AuthRuntimeConfigurations {
/**
* Whether allow anonymous users to sign up
*/
allowSignup: boolean;
/**
* The minimum and maximum length of the password when registering new users
*/
password: {
min: number;
max: number;
};
}
declare module '../../fundamentals/config' {
interface AppConfig {
auth: ModuleConfig<AuthStartupConfigurations, AuthRuntimeConfigurations>;
}
}
defineStartupConfig('auth', {
session: {
ttl: 60 * 60 * 24 * 15, // 15 days
ttr: 60 * 60 * 24 * 7, // 7 days
},
accessToken: {
ttl: 60 * 60 * 24 * 7, // 7 days
refreshTokenTtl: 60 * 60 * 24 * 30, // 30 days
},
});
defineRuntimeConfig('auth', {
allowSignup: {
desc: 'Whether allow new registrations',
default: true,
},
'password.min': {
desc: 'The minimum length of user password',
default: 8,
},
'password.max': {
desc: 'The maximum length of user password',
default: 32,
},
});

View File

@@ -14,12 +14,7 @@ import {
} from '@nestjs/common';
import type { Request, Response } from 'express';
import {
Config,
PaymentRequiredException,
Throttle,
URLHelper,
} from '../../fundamentals';
import { Config, Throttle, URLHelper } from '../../fundamentals';
import { UserService } from '../user';
import { validators } from '../utils/validators';
import { CurrentUser } from './current-user';
@@ -60,7 +55,7 @@ export class AuthController {
validators.assertValidEmail(credential.email);
const canSignIn = await this.auth.canSignIn(credential.email);
if (!canSignIn) {
throw new PaymentRequiredException(
throw new BadRequestException(
`You don't have early access permission\nVisit https://community.affine.pro/c/insider-general/ for more information`
);
}
@@ -76,8 +71,11 @@ export class AuthController {
} else {
// send email magic link
const user = await this.user.findUserByEmail(credential.email);
if (!user && !this.config.auth.allowSignup) {
throw new BadRequestException('You are not allows to sign up.');
if (!user) {
const allowSignup = await this.config.runtime.fetch('auth/allowSignup');
if (!allowSignup) {
throw new BadRequestException('You are not allows to sign up.');
}
}
const result = await this.sendSignInEmail(

View File

@@ -1,3 +1,5 @@
import './config';
import { Module } from '@nestjs/common';
import { FeatureModule } from '../features';

View File

@@ -10,7 +10,7 @@ import {
Resolver,
} from '@nestjs/graphql';
import { Config, SkipThrottle, Throttle } from '../../fundamentals';
import { Config, SkipThrottle, Throttle, URLHelper } from '../../fundamentals';
import { UserService } from '../user';
import { UserType } from '../user/types';
import { validators } from '../utils/validators';
@@ -36,6 +36,7 @@ export class ClientTokenType {
export class AuthResolver {
constructor(
private readonly config: Config,
private readonly url: URLHelper,
private readonly auth: AuthService,
private readonly user: UserService,
private readonly token: TokenService
@@ -83,7 +84,14 @@ export class AuthResolver {
@Args('token') token: string,
@Args('newPassword') newPassword: string
) {
validators.assertValidPassword(newPassword);
const config = await this.config.runtime.fetchAll({
'auth/password.max': true,
'auth/password.min': true,
});
validators.assertValidPassword(newPassword, {
min: config['auth/password.min'],
max: config['auth/password.max'],
});
// NOTE: Set & Change password are using the same token type.
const valid = await this.token.verifyToken(
TokenType.ChangePassword,
@@ -144,13 +152,9 @@ export class AuthResolver {
user.id
);
const url = new URL(callbackUrl, this.config.baseUrl);
url.searchParams.set('token', token);
const url = this.url.link(callbackUrl, { token });
const res = await this.auth.sendChangePasswordEmail(
user.email,
url.toString()
);
const res = await this.auth.sendChangePasswordEmail(user.email, url);
return !res.rejected.length;
}
@@ -170,13 +174,9 @@ export class AuthResolver {
user.id
);
const url = new URL(callbackUrl, this.config.baseUrl);
url.searchParams.set('token', token);
const url = this.url.link(callbackUrl, { token });
const res = await this.auth.sendSetPasswordEmail(
user.email,
url.toString()
);
const res = await this.auth.sendSetPasswordEmail(user.email, url);
return !res.rejected.length;
}
@@ -200,10 +200,9 @@ export class AuthResolver {
const token = await this.token.createToken(TokenType.ChangeEmail, user.id);
const url = new URL(callbackUrl, this.config.baseUrl);
url.searchParams.set('token', token);
const url = this.url.link(callbackUrl, { token });
const res = await this.auth.sendChangeEmail(user.email, url.toString());
const res = await this.auth.sendChangeEmail(user.email, url);
return !res.rejected.length;
}
@@ -240,11 +239,8 @@ export class AuthResolver {
user.id
);
const url = new URL(callbackUrl, this.config.baseUrl);
url.searchParams.set('token', verifyEmailToken);
url.searchParams.set('email', email);
const res = await this.auth.sendVerifyChangeEmail(email, url.toString());
const url = this.url.link(callbackUrl, { token: verifyEmailToken, email });
const res = await this.auth.sendVerifyChangeEmail(email, url);
return !res.rejected.length;
}
@@ -256,10 +252,9 @@ export class AuthResolver {
) {
const token = await this.token.createToken(TokenType.VerifyEmail, user.id);
const url = new URL(callbackUrl, this.config.baseUrl);
url.searchParams.set('token', token);
const url = this.url.link(callbackUrl, { token });
const res = await this.auth.sendVerifyEmail(user.email, url.toString());
const res = await this.auth.sendVerifyEmail(user.email, url);
return !res.rejected.length;
}

View File

@@ -61,7 +61,7 @@ export class AuthService implements OnApplicationBootstrap {
sameSite: 'lax',
httpOnly: true,
path: '/',
secure: this.config.https,
secure: this.config.server.https,
};
static readonly sessionCookieName = 'affine_session';
static readonly authUserSeqHeaderName = 'x-auth-user';

View File

@@ -1,102 +0,0 @@
import { Module } from '@nestjs/common';
import { Field, ObjectType, Query, registerEnumType } from '@nestjs/graphql';
import { DeploymentType } from '../fundamentals';
import { Public } from './auth';
export enum ServerFeature {
Copilot = 'copilot',
Payment = 'payment',
OAuth = 'oauth',
}
registerEnumType(ServerFeature, {
name: 'ServerFeature',
});
registerEnumType(DeploymentType, {
name: 'ServerDeploymentType',
});
const ENABLED_FEATURES: Set<ServerFeature> = new Set();
export function ADD_ENABLED_FEATURES(feature: ServerFeature) {
ENABLED_FEATURES.add(feature);
}
@ObjectType()
export class PasswordLimitsType {
@Field()
minLength!: number;
@Field()
maxLength!: number;
}
@ObjectType()
export class CredentialsRequirementType {
@Field()
password!: PasswordLimitsType;
}
@ObjectType()
export class ServerConfigType {
@Field({
description:
'server identical name could be shown as badge on user interface',
})
name!: string;
@Field({ description: 'server version' })
version!: string;
@Field({ description: 'server base url' })
baseUrl!: string;
@Field(() => DeploymentType, { description: 'server type' })
type!: DeploymentType;
/**
* @deprecated
*/
@Field({ description: 'server flavor', deprecationReason: 'use `features`' })
flavor!: string;
@Field(() => [ServerFeature], { description: 'enabled server features' })
features!: ServerFeature[];
@Field(() => CredentialsRequirementType, {
description: 'credentials requirement',
})
credentialsRequirement!: CredentialsRequirementType;
@Field({ description: 'enable telemetry' })
enableTelemetry!: boolean;
}
export class ServerConfigResolver {
@Public()
@Query(() => ServerConfigType, {
description: 'server config',
})
serverConfig(): ServerConfigType {
return {
name: AFFiNE.serverName,
version: AFFiNE.version,
baseUrl: AFFiNE.baseUrl,
type: AFFiNE.type,
// BACKWARD COMPATIBILITY
// the old flavors contains `selfhosted` but it actually not flavor but deployment type
// this field should be removed after frontend feature flags implemented
flavor: AFFiNE.type,
features: Array.from(ENABLED_FEATURES),
credentialsRequirement: {
password: AFFiNE.auth.password,
},
enableTelemetry: AFFiNE.telemetry.enabled,
};
}
}
@Module({
providers: [ServerConfigResolver],
})
export class ServerConfigModule {}

View File

@@ -0,0 +1,23 @@
import { defineRuntimeConfig, ModuleConfig } from '../../fundamentals/config';
export interface ServerFlags {
earlyAccessControl: boolean;
syncClientVersionCheck: boolean;
}
declare module '../../fundamentals/config' {
interface AppConfig {
flags: ModuleConfig<never, ServerFlags>;
}
}
defineRuntimeConfig('flags', {
earlyAccessControl: {
desc: 'Only allow users with early access features to access the app',
default: false,
},
syncClientVersionCheck: {
desc: 'Only allow client with exact the same version with server to establish sync connections',
default: false,
},
});

View File

@@ -0,0 +1,12 @@
import './config';
import { Module } from '@nestjs/common';
import { ServerConfigResolver, ServerRuntimeConfigResolver } from './resolver';
@Module({
providers: [ServerConfigResolver, ServerRuntimeConfigResolver],
})
export class ServerConfigModule {}
export { ADD_ENABLED_FEATURES, ServerConfigType } from './resolver';
export { ServerFeature } from './types';

View File

@@ -0,0 +1,207 @@
import {
Args,
Field,
GraphQLISODateTime,
Mutation,
ObjectType,
Query,
registerEnumType,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { RuntimeConfig, RuntimeConfigType } from '@prisma/client';
import { GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars';
import { Config, DeploymentType, URLHelper } from '../../fundamentals';
import { Public } from '../auth';
import { Admin } from '../common';
import { ServerFlags } from './config';
import { ServerFeature } from './types';
const ENABLED_FEATURES: Set<ServerFeature> = new Set();
export function ADD_ENABLED_FEATURES(feature: ServerFeature) {
ENABLED_FEATURES.add(feature);
}
registerEnumType(ServerFeature, {
name: 'ServerFeature',
});
registerEnumType(DeploymentType, {
name: 'ServerDeploymentType',
});
@ObjectType()
export class PasswordLimitsType {
@Field()
minLength!: number;
@Field()
maxLength!: number;
}
@ObjectType()
export class CredentialsRequirementType {
@Field()
password!: PasswordLimitsType;
}
@ObjectType()
export class ServerConfigType {
@Field({
description:
'server identical name could be shown as badge on user interface',
})
name!: string;
@Field({ description: 'server version' })
version!: string;
@Field({ description: 'server base url' })
baseUrl!: string;
@Field(() => DeploymentType, { description: 'server type' })
type!: DeploymentType;
/**
* @deprecated
*/
@Field({ description: 'server flavor', deprecationReason: 'use `features`' })
flavor!: string;
@Field(() => [ServerFeature], { description: 'enabled server features' })
features!: ServerFeature[];
@Field({ description: 'enable telemetry' })
enableTelemetry!: boolean;
}
registerEnumType(RuntimeConfigType, {
name: 'RuntimeConfigType',
});
@ObjectType()
export class ServerRuntimeConfigType implements Partial<RuntimeConfig> {
@Field()
id!: string;
@Field()
module!: string;
@Field()
key!: string;
@Field()
description!: string;
@Field(() => GraphQLJSON)
value!: any;
@Field(() => RuntimeConfigType)
type!: RuntimeConfigType;
@Field(() => GraphQLISODateTime)
updatedAt!: Date;
}
@ObjectType()
export class ServerFlagsType implements ServerFlags {
@Field()
earlyAccessControl!: boolean;
@Field()
syncClientVersionCheck!: boolean;
}
@Resolver(() => ServerConfigType)
export class ServerConfigResolver {
constructor(
private readonly config: Config,
private readonly url: URLHelper
) {}
@Public()
@Query(() => ServerConfigType, {
description: 'server config',
})
serverConfig(): ServerConfigType {
return {
name: this.config.serverName,
version: this.config.version,
baseUrl: this.url.home,
type: this.config.type,
// BACKWARD COMPATIBILITY
// the old flavors contains `selfhosted` but it actually not flavor but deployment type
// this field should be removed after frontend feature flags implemented
flavor: this.config.type,
features: Array.from(ENABLED_FEATURES),
enableTelemetry: this.config.metrics.telemetry.enabled,
};
}
@ResolveField(() => CredentialsRequirementType, {
description: 'credentials requirement',
})
async credentialsRequirement() {
const config = await this.config.runtime.fetchAll({
'auth/password.max': true,
'auth/password.min': true,
});
return {
password: {
minLength: config['auth/password.min'],
maxLength: config['auth/password.max'],
},
};
}
@ResolveField(() => ServerFlagsType, {
description: 'server flags',
})
async flags(): Promise<ServerFlagsType> {
const records = await this.config.runtime.list('flags');
return records.reduce((flags, record) => {
flags[record.key as keyof ServerFlagsType] = record.value as any;
return flags;
}, {} as ServerFlagsType);
}
}
@Resolver(() => ServerRuntimeConfigType)
export class ServerRuntimeConfigResolver {
constructor(private readonly config: Config) {}
@Admin()
@Query(() => [ServerRuntimeConfigType], {
description: 'get all server runtime configurable settings',
})
serverRuntimeConfig(): Promise<ServerRuntimeConfigType[]> {
return this.config.runtime.list();
}
@Admin()
@Mutation(() => ServerRuntimeConfigType, {
description: 'update server runtime configurable setting',
})
async updateRuntimeConfig(
@Args('id') id: string,
@Args({ type: () => GraphQLJSON, name: 'value' }) value: any
): Promise<ServerRuntimeConfigType> {
return await this.config.runtime.set(id as any, value);
}
@Admin()
@Mutation(() => [ServerRuntimeConfigType], {
description: 'update multiple server runtime configurable settings',
})
async updateRuntimeConfigs(
@Args({ type: () => GraphQLJSONObject, name: 'updates' }) updates: any
): Promise<ServerRuntimeConfigType[]> {
const keys = Object.keys(updates);
const results = await Promise.all(
keys.map(key => this.config.runtime.set(key as any, updates[key]))
);
return results;
}
}

View File

@@ -0,0 +1,5 @@
export enum ServerFeature {
Copilot = 'copilot',
Payment = 'payment',
OAuth = 'oauth',
}

View File

@@ -0,0 +1,71 @@
import {
defineRuntimeConfig,
defineStartupConfig,
ModuleConfig,
} from '../../fundamentals/config';
interface DocStartupConfigurations {
manager: {
/**
* Whether auto merge updates into doc snapshot.
*/
enableUpdateAutoMerging: boolean;
/**
* How often the [DocManager] will start a new turn of merging pending updates into doc snapshot.
*
* This is not the latency a new joint client will take to see the latest doc,
* but the buffer time we introduced to reduce the load of our service.
*
* in {ms}
*/
updatePollInterval: number;
/**
* The maximum number of updates that will be pulled from the server at once.
* Existing for avoiding the server to be overloaded when there are too many updates for one doc.
*/
maxUpdatesPullCount: number;
};
history: {
/**
* How long the buffer time of creating a new history snapshot when doc get updated.
*
* in {ms}
*/
interval: number;
};
}
interface DocRuntimeConfigurations {
/**
* Use `y-octo` to merge updates at the same time when merging using Yjs.
*
* This is an experimental feature, and aimed to check the correctness of JwstCodec.
*/
experimentalMergeWithYOcto: boolean;
}
declare module '../../fundamentals/config' {
interface AppConfig {
doc: ModuleConfig<DocStartupConfigurations, DocRuntimeConfigurations>;
}
}
defineStartupConfig('doc', {
manager: {
enableUpdateAutoMerging: true,
updatePollInterval: 1000,
maxUpdatesPullCount: 100,
},
history: {
interval: 1000,
},
});
defineRuntimeConfig('doc', {
experimentalMergeWithYOcto: {
desc: 'Use `y-octo` to merge updates at the same time when merging using Yjs.',
default: false,
},
});

View File

@@ -1,3 +1,5 @@
import './config';
import { Module } from '@nestjs/common';
import { QuotaModule } from '../quota';

View File

@@ -133,8 +133,11 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
private async applyUpdates(guid: string, ...updates: Buffer[]): Promise<Doc> {
const doc = await this.recoverDoc(...updates);
const useYocto = await this.config.runtime.fetch(
'doc/experimentalMergeWithYOcto'
);
// test jwst codec
if (this.config.doc.manager.experimentalMergeWithYOcto) {
if (useYocto) {
metrics.jwst.counter('codec_merge_counter').add(1);
const yjsResult = Buffer.from(encodeStateAsUpdate(doc));
let log = false;
@@ -185,11 +188,6 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
}, this.config.doc.manager.updatePollInterval);
this.logger.log('Automation started');
if (this.config.doc.manager.experimentalMergeWithYOcto) {
this.logger.warn(
'Experimental feature enabled: merge updates with jwst codec is enabled'
);
}
}
/**

View File

@@ -95,7 +95,11 @@ export class FeatureManagementService {
email: string,
type: EarlyAccessType = EarlyAccessType.App
) {
if (this.config.featureFlags.earlyAccessPreview && !this.isStaff(email)) {
const earlyAccessControlEnabled = await this.config.runtime.fetch(
'flags/earlyAccessControl'
);
if (earlyAccessControlEnabled && !this.isStaff(email)) {
const user = await this.user.findUserByEmail(email);
if (!user) {
return false;

View File

@@ -0,0 +1,30 @@
import { defineStartupConfig, ModuleConfig } from '../../fundamentals/config';
import { StorageProviderType } from '../../fundamentals/storage';
export type StorageConfig<Ext = unknown> = {
provider: StorageProviderType;
bucket: string;
} & Ext;
export interface StorageStartupConfigurations {
avatar: StorageConfig<{ publicLinkFactory: (key: string) => string }>;
blob: StorageConfig;
}
declare module '../../fundamentals/config' {
interface AppConfig {
storages: ModuleConfig<StorageStartupConfigurations>;
}
}
defineStartupConfig('storages', {
avatar: {
provider: 'fs',
bucket: 'avatars',
publicLinkFactory: key => `/api/avatars/${key}`,
},
blob: {
provider: 'fs',
bucket: 'blobs',
},
});

View File

@@ -1,3 +1,5 @@
import './config';
import { Module } from '@nestjs/common';
import { AvatarStorage, WorkspaceBlobStorage } from './wrappers';

View File

@@ -6,19 +6,25 @@ import type {
PutObjectMetadata,
StorageProvider,
} from '../../../fundamentals';
import { Config, OnEvent, StorageProviderFactory } from '../../../fundamentals';
import {
Config,
OnEvent,
StorageProviderFactory,
URLHelper,
} from '../../../fundamentals';
@Injectable()
export class AvatarStorage {
public readonly provider: StorageProvider;
private readonly storageConfig: Config['storage']['storages']['avatar'];
private readonly storageConfig: Config['storages']['avatar'];
constructor(
private readonly config: Config,
private readonly url: URLHelper,
private readonly storageFactory: StorageProviderFactory
) {
this.provider = this.storageFactory.create('avatar');
this.storageConfig = this.config.storage.storages.avatar;
this.storageConfig = this.config.storages.avatar;
this.provider = this.storageFactory.create(this.storageConfig);
}
async put(key: string, blob: BlobInputType, metadata?: PutObjectMetadata) {
@@ -26,7 +32,7 @@ export class AvatarStorage {
let link = this.storageConfig.publicLinkFactory(key);
if (link.startsWith('/')) {
link = this.config.baseUrl + link;
link = this.url.link(link);
}
return link;

View File

@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
import {
type BlobInputType,
Cache,
Config,
EventEmitter,
type EventPayload,
type ListObjectsMetadata,
@@ -16,11 +17,12 @@ export class WorkspaceBlobStorage {
public readonly provider: StorageProvider;
constructor(
private readonly config: Config,
private readonly event: EventEmitter,
private readonly storageFactory: StorageProviderFactory,
private readonly cache: Cache
) {
this.provider = this.storageFactory.create('blob');
this.provider = this.storageFactory.create(this.config.storages.blob);
}
async put(workspaceId: string, key: string, blob: BlobInputType) {

View File

@@ -11,7 +11,7 @@ import {
import { Server, Socket } from 'socket.io';
import { encodeStateAsUpdate, encodeStateVector } from 'yjs';
import { CallTimer, metrics } from '../../../fundamentals';
import { CallTimer, Config, metrics } from '../../../fundamentals';
import { Auth, CurrentUser } from '../../auth';
import { DocManager } from '../../doc';
import { DocID } from '../../utils/doc';
@@ -98,6 +98,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
private connectionCount = 0;
constructor(
private readonly config: Config,
private readonly docManager: DocManager,
private readonly permissions: PermissionService
) {}
@@ -115,10 +116,13 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
metrics.socketio.gauge('realtime_connections').record(this.connectionCount);
}
assertVersion(client: Socket, version?: string) {
async assertVersion(client: Socket, version?: string) {
const shouldCheckClientVersion = await this.config.runtime.fetch(
'flags/syncClientVersionCheck'
);
if (
// @todo(@darkskygit): remove this flag after 0.12 goes stable
AFFiNE.featureFlags.syncClientVersionCheck &&
shouldCheckClientVersion &&
version !== AFFiNE.version
) {
client.emit('server-version-rejected', {
@@ -180,7 +184,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
@MessageBody('version') version: string | undefined,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
this.assertVersion(client, version);
await this.assertVersion(client, version);
await this.assertWorkspaceAccessible(
workspaceId,
user.id,
@@ -203,7 +207,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
@MessageBody('version') version: string | undefined,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
this.assertVersion(client, version);
await this.assertVersion(client, version);
await this.assertWorkspaceAccessible(
workspaceId,
user.id,

View File

@@ -1,26 +1,6 @@
import { BadRequestException } from '@nestjs/common';
import z from 'zod';
function getAuthCredentialValidator() {
const email = z.string().email({ message: 'Invalid email address' });
let password = z.string();
password = password
.min(AFFiNE.auth.password.minLength, {
message: `Password must be ${AFFiNE.auth.password.minLength} or more charactors long`,
})
.max(AFFiNE.auth.password.maxLength, {
message: `Password must be ${AFFiNE.auth.password.maxLength} or fewer charactors long`,
});
return z
.object({
email,
password,
})
.required();
}
function assertValid<T>(z: z.ZodType<T>, value: unknown) {
const result = z.safeParse(value);
@@ -35,22 +15,25 @@ function assertValid<T>(z: z.ZodType<T>, value: unknown) {
}
export function assertValidEmail(email: string) {
assertValid(getAuthCredentialValidator().shape.email, email);
assertValid(z.string().email({ message: 'Invalid email address' }), email);
}
export function assertValidPassword(password: string) {
assertValid(getAuthCredentialValidator().shape.password, password);
}
export function assertValidCredential(credential: {
email: string;
password: string;
}) {
assertValid(getAuthCredentialValidator(), credential);
export function assertValidPassword(
password: string,
{ min, max }: { min: number; max: number }
) {
assertValid(
z
.string()
.min(min, { message: `Password must be ${min} or more charactors long` })
.max(max, {
message: `Password must be ${max} or fewer charactors long`,
}),
password
);
}
export const validators = {
assertValidEmail,
assertValidPassword,
assertValidCredential,
};