Skip to content

Commit

Permalink
feat(server): runtime setting support
Browse files Browse the repository at this point in the history
  • Loading branch information
forehalo committed May 12, 2024
1 parent 931e996 commit 0f53b93
Show file tree
Hide file tree
Showing 108 changed files with 1,676 additions and 976 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- CreateTable
CREATE TABLE "app_runtime_settings" (
"id" VARCHAR NOT NULL,
"module" VARCHAR NOT NULL,
"key" VARCHAR NOT NULL,
"value" JSON NOT NULL,
"description" TEXT NOT NULL,
"updated_at" TIMESTAMPTZ(6) NOT NULL,
"deleted_at" TIMESTAMPTZ(6),
"last_updated_by" VARCHAR(36),

CONSTRAINT "app_runtime_settings_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "app_runtime_settings_module_key_key" ON "app_runtime_settings"("module", "key");

-- AddForeignKey
ALTER TABLE "app_runtime_settings" ADD CONSTRAINT "app_runtime_settings_last_updated_by_fkey" FOREIGN KEY ("last_updated_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
17 changes: 17 additions & 0 deletions packages/backend/server/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ model User {
connectedAccounts ConnectedAccount[]
sessions UserSession[]
aiSessions AiSession[]
AppRuntimeSetting AppRuntimeSetting[]
@@index([email])
@@map("users")
Expand Down Expand Up @@ -502,3 +503,19 @@ model DataMigration {
@@map("_data_migrations")
}

model AppRuntimeSetting {
id String @id @db.VarChar
module String @db.VarChar
key String @db.VarChar
value Json @db.Json
description String @db.Text
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
lastUpdatedBy String? @map("last_updated_by") @db.VarChar(36)
lastUpdatedByUser User? @relation(fields: [lastUpdatedBy], references: [id])
@@unique([module, key])
@@map("app_runtime_settings")
}
15 changes: 11 additions & 4 deletions packages/backend/server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ import { UserModule } from './core/user';
import { WorkspaceModule } from './core/workspaces';
import { getOptionalModuleMetadata } from './fundamentals';
import { CacheModule } from './fundamentals/cache';
import type { AvailablePlugins } from './fundamentals/config';
import { Config, ConfigModule } from './fundamentals/config';
import {
Config,
ConfigModule,
mergeConfigOverride,
} from './fundamentals/config';
import { EventModule } from './fundamentals/event';
import { GqlModule } from './fundamentals/graphql';
import { HelpersModule } from './fundamentals/helpers';
Expand All @@ -30,8 +33,10 @@ import { StorageProviderModule } from './fundamentals/storage';
import { RateLimiterModule } from './fundamentals/throttler';
import { WebSocketModule } from './fundamentals/websocket';
import { REGISTERED_PLUGINS } from './plugins';
import { ENABLED_PLUGINS } from './plugins/registry';

export const FunctionalityModules = [
ConfigModule.forRoot(),
ConfigModule.forRoot(),
ScheduleModule.forRoot(),
EventModule,
Expand Down Expand Up @@ -112,6 +117,8 @@ export class AppModuleBuilder {
}

function buildAppModule() {
AFFiNE = mergeConfigOverride(AFFiNE);
// @ts-expect-error runtime instance will be injected after config instance created
const factor = new AppModuleBuilder(AFFiNE);

factor
Expand Down Expand Up @@ -147,8 +154,8 @@ function buildAppModule() {
);

// plugin modules
AFFiNE.plugins.enabled.forEach(name => {
const plugin = REGISTERED_PLUGINS.get(name as AvailablePlugins);
ENABLED_PLUGINS.forEach(name => {
const plugin = REGISTERED_PLUGINS.get(name);
if (!plugin) {
throw new Error(`Unknown plugin ${name}`);
}
Expand Down
10 changes: 6 additions & 4 deletions packages/backend/server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ export async function createApp() {
app.useWebSocketAdapter(adapter);
}

if (AFFiNE.isSelfhosted && AFFiNE.telemetry.enabled) {
if (AFFiNE.isSelfhosted && AFFiNE.metrics.telemetry.enabled) {
const mixpanel = await import('mixpanel');
mixpanel.init(AFFiNE.telemetry.token).track('selfhost-server-started', {
version: AFFiNE.version,
});
mixpanel
.init(AFFiNE.metrics.telemetry.token)
.track('selfhost-server-started', {
version: AFFiNE.version,
});
}

return app;
Expand Down
32 changes: 9 additions & 23 deletions packages/backend/server/src/config/affine.env.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
// Convenient way to map environment variables to config values.
AFFiNE.ENV_MAP = {
AFFINE_SERVER_PORT: ['port', 'int'],
AFFINE_SERVER_HOST: 'host',
AFFINE_SERVER_SUB_PATH: 'path',
AFFINE_SERVER_HTTPS: ['https', 'boolean'],
DATABASE_URL: 'db.url',
ENABLE_CAPTCHA: ['auth.captcha.enable', 'boolean'],
CAPTCHA_TURNSTILE_SECRET: ['auth.captcha.turnstile.secret', 'string'],
OAUTH_GOOGLE_CLIENT_ID: 'plugins.oauth.providers.google.clientId',
OAUTH_GOOGLE_CLIENT_SECRET: 'plugins.oauth.providers.google.clientSecret',
OAUTH_GITHUB_CLIENT_ID: 'plugins.oauth.providers.github.clientId',
OAUTH_GITHUB_CLIENT_SECRET: 'plugins.oauth.providers.github.clientSecret',
AFFINE_SERVER_PORT: ['server.port', 'int'],
AFFINE_SERVER_HOST: 'server.host',
AFFINE_SERVER_SUB_PATH: 'server.path',
AFFINE_SERVER_HTTPS: ['server.https', 'boolean'],
TELEMETRY_ENABLE: ['metrics.telemetry.enabled', 'boolean'],
MAILER_HOST: 'mailer.host',
MAILER_PORT: ['mailer.port', 'int'],
MAILER_USER: 'mailer.auth.user',
MAILER_PASSWORD: 'mailer.auth.pass',
MAILER_SENDER: 'mailer.from.address',
MAILER_SECURE: ['mailer.secure', 'boolean'],
THROTTLE_TTL: ['rateLimiter.ttl', 'int'],
THROTTLE_LIMIT: ['rateLimiter.limit', 'int'],
OAUTH_GOOGLE_CLIENT_ID: 'plugins.oauth.providers.google.clientId',
OAUTH_GOOGLE_CLIENT_SECRET: 'plugins.oauth.providers.google.clientSecret',
OAUTH_GITHUB_CLIENT_ID: 'plugins.oauth.providers.github.clientId',
OAUTH_GITHUB_CLIENT_SECRET: 'plugins.oauth.providers.github.clientSecret',
COPILOT_OPENAI_API_KEY: 'plugins.copilot.openai.apiKey',
COPILOT_FAL_API_KEY: 'plugins.copilot.fal.apiKey',
COPILOT_UNSPLASH_API_KEY: 'plugins.copilot.unsplashKey',
Expand All @@ -28,16 +24,6 @@ AFFiNE.ENV_MAP = {
REDIS_SERVER_PASSWORD: 'plugins.redis.password',
REDIS_SERVER_DATABASE: ['plugins.redis.db', 'int'],
DOC_MERGE_INTERVAL: ['doc.manager.updatePollInterval', 'int'],
DOC_MERGE_USE_JWST_CODEC: [
'doc.manager.experimentalMergeWithYOcto',
'boolean',
],
STRIPE_API_KEY: 'plugins.payment.stripe.keys.APIKey',
STRIPE_WEBHOOK_KEY: 'plugins.payment.stripe.keys.webhookKey',
FEATURES_EARLY_ACCESS_PREVIEW: ['featureFlags.earlyAccessPreview', 'boolean'],
FEATURES_SYNC_CLIENT_VERSION_CHECK: [
'featureFlags.syncClientVersionCheck',
'boolean',
],
TELEMETRY_ENABLE: ['telemetry.enabled', 'boolean'],
};
40 changes: 23 additions & 17 deletions packages/backend/server/src/config/affine.self.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,35 +20,41 @@ const env = process.env;
AFFiNE.metrics.enabled = !AFFiNE.node.test;

if (env.R2_OBJECT_STORAGE_ACCOUNT_ID) {
AFFiNE.plugins.use('cloudflare-r2', {
AFFiNE.use('cloudflare-r2', {
accountId: env.R2_OBJECT_STORAGE_ACCOUNT_ID,
credentials: {
accessKeyId: env.R2_OBJECT_STORAGE_ACCESS_KEY_ID!,
secretAccessKey: env.R2_OBJECT_STORAGE_SECRET_ACCESS_KEY!,
},
});
AFFiNE.storage.storages.avatar.provider = 'cloudflare-r2';
AFFiNE.storage.storages.avatar.bucket = 'account-avatar';
AFFiNE.storage.storages.avatar.publicLinkFactory = key =>
AFFiNE.storages.avatar.provider = 'cloudflare-r2';
AFFiNE.storages.avatar.bucket = 'account-avatar';
AFFiNE.storages.avatar.publicLinkFactory = key =>
`https://avatar.affineassets.com/${key}`;

AFFiNE.storage.storages.blob.provider = 'cloudflare-r2';
AFFiNE.storage.storages.blob.bucket = `workspace-blobs-${
AFFiNE.storages.blob.provider = 'cloudflare-r2';
AFFiNE.storages.blob.bucket = `workspace-blobs-${
AFFiNE.affine.canary ? 'canary' : 'prod'
}`;

AFFiNE.storage.storages.copilot.provider = 'cloudflare-r2';
AFFiNE.storage.storages.copilot.bucket = `workspace-copilot-${
AFFiNE.affine.canary ? 'canary' : 'prod'
}`;
AFFiNE.use('copilot', {
storage: {
provider: 'cloudflare-r2',
bucket: `workspace-copilot-${AFFiNE.affine.canary ? 'canary' : 'prod'}`,
},
});
}

AFFiNE.plugins.use('copilot', {
openai: {},
fal: {},
AFFiNE.use('copilot', {
openai: {
apiKey: '',
},
fal: {
apiKey: '',
},
});
AFFiNE.plugins.use('redis');
AFFiNE.plugins.use('payment', {
AFFiNE.use('redis');
AFFiNE.use('payment', {
stripe: {
keys: {
// fake the key to ensure the server generate full GraphQL Schema even env vars are not set
Expand All @@ -57,7 +63,7 @@ AFFiNE.plugins.use('payment', {
},
},
});
AFFiNE.plugins.use('oauth');
AFFiNE.use('oauth');

if (AFFiNE.deploy) {
AFFiNE.mailer = {
Expand All @@ -68,5 +74,5 @@ if (AFFiNE.deploy) {
},
};

AFFiNE.plugins.use('gcloud');
AFFiNE.use('gcloud');
}
6 changes: 3 additions & 3 deletions packages/backend/server/src/config/affine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@
// AFFiNE.serverName = 'Your Cool AFFiNE Selfhosted Cloud';
//
// /* Whether the server is deployed behind a HTTPS proxied environment */
AFFiNE.https = false;
AFFiNE.server.https = false;
// /* Domain of your server that your server will be available at */
AFFiNE.host = 'localhost';
AFFiNE.server.host = 'localhost';
// /* The local port of your server that will listen on */
AFFiNE.port = 3010;
AFFiNE.server.port = 3010;
// /* The sub path of your server */
// /* For example, if you set `AFFiNE.path = '/affine'`, then the server will be available at `${domain}/affine` */
// AFFiNE.path = '/affine';
Expand Down
81 changes: 81 additions & 0 deletions packages/backend/server/src/core/auth/config.ts
Original file line number Diff line number Diff line change
@@ -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,
},
});
16 changes: 7 additions & 9 deletions packages/backend/server/src/core/auth/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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`
);
}
Expand All @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/server/src/core/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import './config';

import { Module } from '@nestjs/common';

import { FeatureModule } from '../features';
Expand Down

0 comments on commit 0f53b93

Please sign in to comment.