feat: implement mailer support
feat: basic structure
This commit is contained in:
parent
60532e83cf
commit
ca8ee728fb
26 changed files with 2410 additions and 285 deletions
92
README.md
92
README.md
|
@ -1,73 +1,19 @@
|
||||||
<p align="center">
|
## Waterwolf Identity Solution
|
||||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
|
|
||||||
</p>
|
This is a re-imagination on how BOOP works with an optimized OAUTH 2.0 flow. This will hook into the existing user database to read usernames and passwords.
|
||||||
|
|
||||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
|
||||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
|
||||||
|
## Installation
|
||||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
|
||||||
<p align="center">
|
Use pnmp to install the required packages.
|
||||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
|
||||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
```bash
|
||||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
pnpm install
|
||||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
```
|
||||||
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
|
||||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
## Usage
|
||||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
|
||||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
```bash
|
||||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
|
pnpm start
|
||||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
```
|
||||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
|
||||||
</p>
|
|
||||||
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
|
|
||||||
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running the app
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# development
|
|
||||||
$ pnpm run start
|
|
||||||
|
|
||||||
# watch mode
|
|
||||||
$ pnpm run start:dev
|
|
||||||
|
|
||||||
# production mode
|
|
||||||
$ pnpm run start:prod
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# unit tests
|
|
||||||
$ pnpm run test
|
|
||||||
|
|
||||||
# e2e tests
|
|
||||||
$ pnpm run test:e2e
|
|
||||||
|
|
||||||
# test coverage
|
|
||||||
$ pnpm run test:cov
|
|
||||||
```
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
|
||||||
|
|
||||||
## Stay in touch
|
|
||||||
|
|
||||||
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
|
|
||||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
|
||||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Nest is [MIT licensed](LICENSE).
|
|
||||||
|
|
65
package.json
65
package.json
|
@ -17,37 +17,52 @@
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
|
"prepare": "ts-patch install && typia patch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^10.0.0",
|
"@atech/postal": "^1.0.0",
|
||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/common": "^10.3.10",
|
||||||
"@nestjs/platform-express": "^10.0.0",
|
"@nestjs/config": "^3.2.3",
|
||||||
"reflect-metadata": "^0.2.0",
|
"@nestjs/core": "^10.3.10",
|
||||||
"rxjs": "^7.8.1"
|
"@nestjs/platform-express": "^10.3.10",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.1",
|
||||||
|
"mysql2": "^3.10.2",
|
||||||
|
"nanoid": "^5.0.7",
|
||||||
|
"nestjs-postal-client": "^0.0.5",
|
||||||
|
"oidc-provider": "^8.5.1",
|
||||||
|
"pug": "^3.0.3",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"sequelize": "^6.37.3",
|
||||||
|
"sequelize-typescript": "^2.1.6",
|
||||||
|
"typia": "^6.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.4.2",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.1.2",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.3.10",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.21",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.14.10",
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/sequelize": "^4.28.20",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
"@types/supertest": "^6.0.2",
|
||||||
"@typescript-eslint/parser": "^7.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||||
"eslint": "^8.42.0",
|
"@typescript-eslint/parser": "^7.16.1",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"jest": "^29.5.0",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"prettier": "^3.0.0",
|
"jest": "^29.7.0",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.2.2",
|
||||||
"ts-loader": "^9.4.3",
|
"ts-loader": "^9.5.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.2",
|
||||||
|
"ts-patch": "^3.2.1",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typescript": "^5.1.3"
|
"typescript": "^5.5.3"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
|
@ -66,4 +81,4 @@
|
||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage",
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
}
|
}
|
||||||
}
|
}
|
2160
pnpm-lock.yaml
2160
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1,12 +1,30 @@
|
||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get } from '@nestjs/common';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
import { PostalClientService } from 'nestjs-postal-client';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private readonly appService: AppService) {}
|
constructor(
|
||||||
|
private readonly appService: AppService,
|
||||||
|
private readonly postal: PostalClientService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
getHello(): string {
|
getHello(): string {
|
||||||
return this.appService.getHello();
|
return this.appService.getHello();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('email_test')
|
||||||
|
async getEmailTest(): Promise<any> {
|
||||||
|
await this.postal.sendMessage({
|
||||||
|
to: ['kakious@kakio.us'],
|
||||||
|
subject: 'Test email',
|
||||||
|
from: 'kakious@kakio.us',
|
||||||
|
plain_body: 'This is a test email',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Email sent',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import config from './config/config';
|
||||||
|
import { MailModule } from './mail/mail.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
cache: true,
|
||||||
|
isGlobal: true,
|
||||||
|
load: [config],
|
||||||
|
}),
|
||||||
|
MailModule,
|
||||||
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
|
|
10
src/auth/auth.module.ts
Normal file
10
src/auth/auth.module.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [],
|
||||||
|
controllers: [],
|
||||||
|
providers: [ConfigService],
|
||||||
|
exports: [],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
0
src/auth/controllers/auth.controller.ts
Normal file
0
src/auth/controllers/auth.controller.ts
Normal file
0
src/auth/controllers/oidc.controller.ts
Normal file
0
src/auth/controllers/oidc.controller.ts
Normal file
0
src/auth/mailers/postal.service.ts
Normal file
0
src/auth/mailers/postal.service.ts
Normal file
0
src/auth/oidc/adapter.ts
Normal file
0
src/auth/oidc/adapter.ts
Normal file
26
src/auth/oidc/helper/nanoid.helper.ts
Normal file
26
src/auth/oidc/helper/nanoid.helper.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
let nanoidImport: typeof import('nanoid') | null = null;
|
||||||
|
|
||||||
|
const getNanoId = async () => {
|
||||||
|
if (!nanoidImport) {
|
||||||
|
// This is an async import as it's only esm and we need to use it in a commonjs environment because
|
||||||
|
// nestjs doesn't work very well with esm yet.
|
||||||
|
nanoidImport = await (eval(
|
||||||
|
"import('nanoid')",
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||||
|
) as Promise<typeof import('nanoid')>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nanoidImport;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateId = async (length = 21, charset?: string): Promise<string> => {
|
||||||
|
const nanoid = await getNanoId();
|
||||||
|
|
||||||
|
if (charset) {
|
||||||
|
return nanoid.customAlphabet(charset, length)();
|
||||||
|
}
|
||||||
|
|
||||||
|
return nanoid.nanoid(length);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default generateId;
|
13
src/auth/oidc/types/accessToken.type.ts
Normal file
13
src/auth/oidc/types/accessToken.type.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export interface AccessToken {
|
||||||
|
iat: number;
|
||||||
|
exp: number;
|
||||||
|
accountId?: string;
|
||||||
|
expiresWithSession: boolean;
|
||||||
|
grantId: string;
|
||||||
|
gty: string;
|
||||||
|
sessionUid: string;
|
||||||
|
clientId: string;
|
||||||
|
scope: string;
|
||||||
|
kind: string;
|
||||||
|
jti: string;
|
||||||
|
}
|
15
src/auth/oidc/types/adapter.type.ts
Normal file
15
src/auth/oidc/types/adapter.type.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export enum adapterTypes {
|
||||||
|
Session = 'Session',
|
||||||
|
AccessToken = 'AccessToken',
|
||||||
|
AuthorizationCode = 'AuthorizationCode',
|
||||||
|
RefreshToken = 'RefreshToken',
|
||||||
|
DeviceCode = 'DeviceCode',
|
||||||
|
ClientCredentials = 'ClientCredentials',
|
||||||
|
Client = 'Client',
|
||||||
|
InitalAccessToken = 'InitalAccessToken',
|
||||||
|
RegistrationAccessToken = 'RegistrationAccessToken',
|
||||||
|
Interaction = 'Interaction',
|
||||||
|
ReplayDetection = 'ReplayDetection',
|
||||||
|
PushedAuthorizationRequest = 'PushedAuthorizationRequest',
|
||||||
|
Grant = 'Grant',
|
||||||
|
}
|
77
src/auth/oidc/types/interaction.type.ts
Normal file
77
src/auth/oidc/types/interaction.type.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
export interface Interaction {
|
||||||
|
readonly kind: string;
|
||||||
|
|
||||||
|
iat: number;
|
||||||
|
exp: number;
|
||||||
|
|
||||||
|
session?: InteractionSession | undefined;
|
||||||
|
|
||||||
|
params: InteractionParams;
|
||||||
|
prompt: PromptDetail;
|
||||||
|
result?: InteractionResults | undefined;
|
||||||
|
returnTo: string;
|
||||||
|
deviceCode?: string | undefined;
|
||||||
|
|
||||||
|
trusted?: string[] | undefined;
|
||||||
|
uid: string;
|
||||||
|
lastSubmission?: InteractionResults | undefined;
|
||||||
|
grantId?: string | undefined;
|
||||||
|
|
||||||
|
save(ttl: number): Promise<string>;
|
||||||
|
persist(): Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InteractionSession {
|
||||||
|
accountId: string;
|
||||||
|
uid: string;
|
||||||
|
cookie: string;
|
||||||
|
acr?: string | undefined;
|
||||||
|
amr?: string[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UnknownObject = Record<string, unknown>;
|
||||||
|
|
||||||
|
export interface PromptDetail {
|
||||||
|
name: Prompts | string;
|
||||||
|
reasons: string[];
|
||||||
|
details: InteractionDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Prompts {
|
||||||
|
login = 'login',
|
||||||
|
consent = 'consent',
|
||||||
|
select_account = 'select_account',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InteractionResults {
|
||||||
|
login?:
|
||||||
|
| {
|
||||||
|
remember?: boolean | undefined;
|
||||||
|
accountId: string;
|
||||||
|
ts?: number | undefined;
|
||||||
|
amr?: string[] | undefined;
|
||||||
|
acr?: string | undefined;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
consent?:
|
||||||
|
| {
|
||||||
|
grantId?: string | undefined;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InteractionParams extends UnknownObject {
|
||||||
|
scope?: string | undefined;
|
||||||
|
client_id?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InteractionDetails extends UnknownObject {
|
||||||
|
missingOIDCScope?: string[] | undefined;
|
||||||
|
missingOIDCClaims?: string[] | undefined;
|
||||||
|
missingResourceScopes?: UnknownObject;
|
||||||
|
}
|
59
src/auth/oidc/types/session.type.ts
Normal file
59
src/auth/oidc/types/session.type.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
export interface VerifiedSessionFromRequest {
|
||||||
|
session: Session;
|
||||||
|
//USER: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Session {
|
||||||
|
iat: number;
|
||||||
|
exp: number;
|
||||||
|
uid: string;
|
||||||
|
jti: string;
|
||||||
|
|
||||||
|
accountId?: string | undefined;
|
||||||
|
acr?: string | undefined;
|
||||||
|
amr?: string[] | undefined;
|
||||||
|
loginTs?: number | undefined;
|
||||||
|
transient?: boolean | undefined;
|
||||||
|
state?: UnknownObject | undefined;
|
||||||
|
authorizations?: Record<string, ClientAuthorizationState> | undefined;
|
||||||
|
clientId?: string | undefined;
|
||||||
|
|
||||||
|
authTime(): string | void;
|
||||||
|
past(age: number): boolean;
|
||||||
|
|
||||||
|
ensureClientContainer(clientId: string): void;
|
||||||
|
loginAccount(details: {
|
||||||
|
accountId: string;
|
||||||
|
acr?: string | undefined;
|
||||||
|
amr?: string[] | undefined;
|
||||||
|
loginTs?: number | undefined;
|
||||||
|
transient?: boolean | undefined;
|
||||||
|
}): void;
|
||||||
|
authorizationFor(clientId: string): ClientAuthorizationState | void;
|
||||||
|
sidFor(clientId: string): string;
|
||||||
|
sidFor(clientId: string, value: string): void;
|
||||||
|
grantIdFor(clientId: string): string;
|
||||||
|
grantIdFor(clientId: string, value: string): void;
|
||||||
|
|
||||||
|
save(ttl: number): Promise<string>;
|
||||||
|
persist(): Promise<string>;
|
||||||
|
destroy(): Promise<void>;
|
||||||
|
resetIdentifier(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UnknownObject = Record<string, unknown>;
|
||||||
|
|
||||||
|
export interface ClientAuthorizationState {
|
||||||
|
persistsLogout?: boolean | undefined;
|
||||||
|
sid?: string | undefined;
|
||||||
|
grantId?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* interface BaseModel {
|
||||||
|
jti: string;
|
||||||
|
kind: string;
|
||||||
|
iat?: number | undefined;
|
||||||
|
exp?: number | undefined;
|
||||||
|
}
|
||||||
|
*/
|
0
src/auth/resources/emails/registration.pug
Normal file
0
src/auth/resources/emails/registration.pug
Normal file
0
src/auth/resources/views/layout.pug
Normal file
0
src/auth/resources/views/layout.pug
Normal file
0
src/auth/resources/views/login.pug
Normal file
0
src/auth/resources/views/login.pug
Normal file
0
src/auth/services/auth.service.ts
Normal file
0
src/auth/services/auth.service.ts
Normal file
0
src/auth/services/core.service.ts
Normal file
0
src/auth/services/core.service.ts
Normal file
6
src/config/config.ts
Normal file
6
src/config/config.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default async () => ({
|
||||||
|
mail: {
|
||||||
|
baseUrl: process.env.MAIL_BASE_URL,
|
||||||
|
apiKey: process.env.MAIL_API_KEY,
|
||||||
|
},
|
||||||
|
});
|
19
src/mail/mail.module.ts
Normal file
19
src/mail/mail.module.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { PostalClientModule } from 'nestjs-postal-client';
|
||||||
|
import postalConfig from './postal.config';
|
||||||
|
import { PostalConfigService } from './postal_config.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
PostalClientModule.registerAsync({
|
||||||
|
imports: [ConfigModule.forFeature(postalConfig)],
|
||||||
|
useClass: PostalConfigService,
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [],
|
||||||
|
providers: [ConfigService],
|
||||||
|
exports: [PostalClientModule],
|
||||||
|
})
|
||||||
|
export class MailModule {}
|
23
src/mail/postal.config.ts
Normal file
23
src/mail/postal.config.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
import type { tags } from 'typia';
|
||||||
|
import typia from 'typia';
|
||||||
|
|
||||||
|
import { validateOrThrow } from '../util/validation.util';
|
||||||
|
|
||||||
|
export const POSTAL_CONFIG_KEY = 'postal';
|
||||||
|
|
||||||
|
const validateEqualsPostalConfig = typia.createValidateEquals<PostalConfig>();
|
||||||
|
|
||||||
|
export interface PostalConfig {
|
||||||
|
POSTAL_BASE_URL: string & tags.Format<'url'>;
|
||||||
|
POSTAL_API_KEY: string & tags.MinLength<1>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default registerAs(
|
||||||
|
POSTAL_CONFIG_KEY,
|
||||||
|
(): PostalConfig =>
|
||||||
|
validateOrThrow(validateEqualsPostalConfig, {
|
||||||
|
POSTAL_BASE_URL: process.env['POSTAL_BASE_URL'],
|
||||||
|
POSTAL_API_KEY: process.env['POSTAL_API_KEY'],
|
||||||
|
}),
|
||||||
|
);
|
32
src/mail/postal_config.service.ts
Normal file
32
src/mail/postal_config.service.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import type {
|
||||||
|
PostalModuleOptions,
|
||||||
|
PostalModuleOptionsFactory,
|
||||||
|
} from 'nestjs-postal-client';
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
import type { PostalConfig } from './postal.config';
|
||||||
|
import { POSTAL_CONFIG_KEY } from './postal.config';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PostalConfigService implements PostalModuleOptionsFactory {
|
||||||
|
constructor(private readonly configService: ConfigService) {}
|
||||||
|
|
||||||
|
createPostalOptions(): Promise<PostalModuleOptions> | PostalModuleOptions {
|
||||||
|
const postalConfig =
|
||||||
|
this.configService.getOrThrow<PostalConfig>(POSTAL_CONFIG_KEY);
|
||||||
|
|
||||||
|
return {
|
||||||
|
http: {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'OIDC-Client',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
postal: {
|
||||||
|
baseURL: postalConfig.POSTAL_BASE_URL,
|
||||||
|
apiKey: postalConfig.POSTAL_API_KEY,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
55
src/util/validation.util.ts
Normal file
55
src/util/validation.util.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import type {
|
||||||
|
ClassConstructor,
|
||||||
|
ClassTransformOptions,
|
||||||
|
} from 'class-transformer';
|
||||||
|
import { plainToClass } from 'class-transformer';
|
||||||
|
import type { ValidatorOptions } from 'class-validator';
|
||||||
|
import { validate } from 'class-validator';
|
||||||
|
import type { IValidation } from 'typia';
|
||||||
|
|
||||||
|
export const VALIDATOR_OPTIONS: ValidatorOptions = {
|
||||||
|
forbidNonWhitelisted: true,
|
||||||
|
forbidUnknownValues: true,
|
||||||
|
whitelist: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TRANSFORMER_OPTIONS: ClassTransformOptions = {
|
||||||
|
excludeExtraneousValues: true,
|
||||||
|
exposeDefaultValues: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function transformAndValidate<T extends object>(
|
||||||
|
cls: ClassConstructor<T>,
|
||||||
|
data: unknown,
|
||||||
|
): Promise<T> {
|
||||||
|
const instance = plainToClass(cls, data, TRANSFORMER_OPTIONS);
|
||||||
|
const errors = await validate(instance, VALIDATOR_OPTIONS);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new Error(errors.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyIError(error: IValidation.IError) {
|
||||||
|
return ` - Invalid type on ${error.path}, expect to be ${error.expected}, was ${JSON.stringify(
|
||||||
|
error.value,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PartialUndefined<T> = {
|
||||||
|
[P in keyof T]?: T[P] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function validateOrThrow<T>(
|
||||||
|
validator: (input: unknown) => IValidation<T>,
|
||||||
|
input: PartialUndefined<T>,
|
||||||
|
) {
|
||||||
|
const result = validator(input);
|
||||||
|
if (result.success) {
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = result.errors.map(stringifyIError).join('\n');
|
||||||
|
throw new Error(`Validation errors:\n${body}`);
|
||||||
|
}
|
|
@ -12,10 +12,15 @@
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strictNullChecks": false,
|
"strictNullChecks": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"strictBindCallApply": false,
|
"strictBindCallApply": false,
|
||||||
"forceConsistentCasingInFileNames": false,
|
"forceConsistentCasingInFileNames": false,
|
||||||
"noFallthroughCasesInSwitch": false
|
"noFallthroughCasesInSwitch": false,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"transform": "typia/lib/transform"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue