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';