Initial service setup
commit
75a7bcfa48
|
@ -0,0 +1,25 @@
|
|||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
sourceType: 'module',
|
||||
tsconfigRootDir: __dirname
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# local development environment files
|
||||
.env
|
||||
/devdocker
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo_text.svg" width="320" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<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>
|
||||
<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>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
|
||||
<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>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
## Running the app
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ npm run start
|
||||
|
||||
# watch mode
|
||||
$ npm run start:dev
|
||||
|
||||
# production mode
|
||||
$ npm run start:prod
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ npm run test
|
||||
|
||||
# e2e tests
|
||||
$ npm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ npm 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).
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src"
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,88 @@
|
|||
{
|
||||
"name": "icynet-auth-server",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"prebuild": "rimraf dist",
|
||||
"build": "nest build && npm run build:fe",
|
||||
"build:fe": "rimraf public/css && sass --no-source-map --style=compressed src/scss/_index.scss:public/css/index.css",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"start:fe": "rimraf public/css && sass --watch --update --style=expanded src/scss/_index.scss:public/css/index.css",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@icynet/oauth2-provider": "git+ssh://git@gitlab.icynet.eu:IcyNetwork/oauth2-provider.git",
|
||||
"@levminer/speakeasy": "^1.3.1",
|
||||
"@nestjs/common": "^8.0.0",
|
||||
"@nestjs/core": "^8.0.0",
|
||||
"@nestjs/platform-express": "^8.0.0",
|
||||
"bcrypt": "^5.0.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.13.2",
|
||||
"dotenv": "^16.0.0",
|
||||
"express-session": "^1.17.2",
|
||||
"mysql2": "^2.3.3",
|
||||
"pug": "^3.0.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.2.0",
|
||||
"typeorm": "^0.2.45",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^8.0.0",
|
||||
"@nestjs/schematics": "^8.0.0",
|
||||
"@nestjs/testing": "^8.0.0",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express-session": "^1.17.4",
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/node": "^16.0.0",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^27.2.5",
|
||||
"prettier": "^2.3.2",
|
||||
"sass": "^1.49.9",
|
||||
"source-map-support": "^0.5.20",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "^27.0.3",
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsconfig-paths": "^3.10.1",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
*,*::before,*::after{box-sizing:border-box}html,body{width:100%;height:100%;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Open Sans","Helvetica Neue",sans-serif}
|
|
@ -0,0 +1,22 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
import { Controller, Get, Res, Session } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { SessionData } from 'express-session';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
getHello(
|
||||
@Session() session: SessionData,
|
||||
@Res() res: Response,
|
||||
): Record<string, any> {
|
||||
if (!session.user) {
|
||||
res.redirect('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
res.render('index', { user: session.user });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { CSRFMiddleware } from './middleware/csrf.middleware';
|
||||
import { FlashMiddleware } from './middleware/flash.middleware';
|
||||
import { LoginModule } from './modules/features/login/login.module';
|
||||
import { OAuth2Module } from './modules/features/oauth2/oauth2.module';
|
||||
import { RegisterModule } from './modules/features/register/register.module';
|
||||
import { DatabaseModule } from './modules/objects/database/database.module';
|
||||
import { OAuth2ClientModule } from './modules/objects/oauth2-client/oauth2-client.module';
|
||||
import { OAuth2TokenModule } from './modules/objects/oauth2-token/oauth2-token.module';
|
||||
import { UploadModule } from './modules/objects/upload/upload.module';
|
||||
import { UserModule } from './modules/objects/user/user.module';
|
||||
import { UtilityModule } from './modules/utility/utility.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UtilityModule,
|
||||
DatabaseModule,
|
||||
UserModule,
|
||||
UploadModule,
|
||||
OAuth2ClientModule,
|
||||
OAuth2TokenModule,
|
||||
LoginModule,
|
||||
RegisterModule,
|
||||
OAuth2Module,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService, CSRFMiddleware],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(CSRFMiddleware).forRoutes('*');
|
||||
consumer
|
||||
.apply(FlashMiddleware)
|
||||
.forRoutes('login', 'register', 'login/verify');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as session from 'express-session';
|
||||
import { join } from 'path';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||
app.use(
|
||||
session({
|
||||
secret: process.env.SESSION_SECRET,
|
||||
resave: true,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
sameSite: 'lax',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
app.useStaticAssets(join(__dirname, '..', 'public'), {
|
||||
prefix: '/public/',
|
||||
});
|
||||
app.setBaseViewsDir(join(__dirname, '..', 'views'));
|
||||
app.setViewEngine('pug');
|
||||
|
||||
await app.listen(3000);
|
||||
}
|
||||
bootstrap();
|
|
@ -0,0 +1,21 @@
|
|||
import {
|
||||
Injectable,
|
||||
NestMiddleware,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class AuthMiddleware implements NestMiddleware {
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
if (!req.session.user) {
|
||||
if (req.header('content-type')?.includes('application/json')) {
|
||||
throw new UnauthorizedException('Unauthorized');
|
||||
}
|
||||
|
||||
res.redirect('/login?redirectTo=' + encodeURIComponent(req.originalUrl));
|
||||
return;
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||
|
||||
@Injectable()
|
||||
export class CSRFMiddleware implements NestMiddleware {
|
||||
constructor(private readonly tokenService: TokenService) {}
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
if (!req.session.csrf) {
|
||||
req.session.csrf = this.tokenService.generateString(64);
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { SessionData } from 'express-session';
|
||||
import { format } from 'util';
|
||||
|
||||
@Injectable()
|
||||
export class FlashMiddleware implements NestMiddleware {
|
||||
private _flash(
|
||||
session: SessionData,
|
||||
type: string,
|
||||
...msg: any[]
|
||||
): Record<string, any> {
|
||||
const msgs = (session.flash = session.flash || {});
|
||||
|
||||
if (type && msg?.length) {
|
||||
let result: string;
|
||||
|
||||
if (Array.isArray(msg[0])) {
|
||||
msg[0].forEach((val) => {
|
||||
(msgs[type] = msgs[type] || []).push(val);
|
||||
});
|
||||
|
||||
return msgs;
|
||||
} else {
|
||||
result = msg.length > 1 ? format(...msg) : msg[0];
|
||||
}
|
||||
|
||||
return (msgs[type] = msgs[type] || []).push(result);
|
||||
} else if (type) {
|
||||
const arr = msgs[type];
|
||||
delete msgs[type];
|
||||
return arr || [];
|
||||
} else {
|
||||
session.flash = {};
|
||||
return msgs;
|
||||
}
|
||||
}
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
if (req.flash) {
|
||||
return next();
|
||||
}
|
||||
|
||||
req.flash = this._flash.bind(this, req.session);
|
||||
next();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class ValidateCSRFMiddleware implements NestMiddleware {
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
if (req.body.csrf !== req.session.csrf) {
|
||||
return next(new Error('Invalid session'));
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Query,
|
||||
Render,
|
||||
Req,
|
||||
Res,
|
||||
Session,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { SessionData } from 'express-session';
|
||||
import { User } from 'src/modules/objects/user/user.entity';
|
||||
import { UserService } from 'src/modules/objects/user/user.service';
|
||||
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||
|
||||
@Controller('/login')
|
||||
export class LoginController {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly formUtil: FormUtilityService,
|
||||
private readonly token: TokenService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Render('login')
|
||||
public loginView(
|
||||
@Session() session: SessionData,
|
||||
@Req() req: Request,
|
||||
): Record<string, any> {
|
||||
return this.formUtil.populateTemplate(req, session);
|
||||
}
|
||||
|
||||
@Post()
|
||||
public async loginRequest(
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
@Body() body: { username: string; password: string },
|
||||
@Query() query: { redirectTo?: string },
|
||||
) {
|
||||
const { username, password } = body;
|
||||
const user = await this.userService.getByUsername(username);
|
||||
|
||||
// User exists and password matches
|
||||
if (
|
||||
!user ||
|
||||
!user.activated ||
|
||||
!(await this.userService.comparePasswords(user.password, password))
|
||||
) {
|
||||
req.flash('form', { username });
|
||||
req.flash('message', {
|
||||
error: true,
|
||||
text: 'Invalid username or password',
|
||||
});
|
||||
|
||||
res.redirect(req.originalUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.userService.userHasTOTP(user)) {
|
||||
const challenge = { type: 'totp', user: user.uuid };
|
||||
req.session.challenge = await this.token.encryptChallenge(challenge);
|
||||
res.redirect(
|
||||
'/login/verify' +
|
||||
(query.redirectTo ? '?redirectTo=' + query.redirectTo : ''),
|
||||
);
|
||||
}
|
||||
|
||||
req.session.user = user;
|
||||
res.redirect(query.redirectTo ? decodeURIComponent(query.redirectTo) : '/');
|
||||
}
|
||||
|
||||
@Get('verify')
|
||||
public verifyUserTokenView(
|
||||
@Session() session: SessionData,
|
||||
@Query() query: { redirectTo?: string },
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
if (!session.challenge) {
|
||||
req.flash('message', {
|
||||
error: true,
|
||||
text: 'An unexpected error occured, please log in again.',
|
||||
});
|
||||
|
||||
res.redirect(
|
||||
'/login' + (query.redirectTo ? '?redirectTo=' + query.redirectTo : ''),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
res.render('totp-verify', this.formUtil.populateTemplate(req, session));
|
||||
}
|
||||
|
||||
@Post('verify')
|
||||
public async verifyUserToken(
|
||||
@Session() session: SessionData,
|
||||
@Query() query: { redirectTo?: string },
|
||||
@Body() body: { totp: string },
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
let user: User;
|
||||
|
||||
try {
|
||||
if (!session.challenge) {
|
||||
throw new Error('No challenge');
|
||||
}
|
||||
|
||||
const challenge = await this.token.decryptChallenge(session.challenge);
|
||||
if (!challenge || challenge.type !== 'totp' || !challenge.user) {
|
||||
throw new Error('Bad challenge');
|
||||
}
|
||||
|
||||
user = await this.userService.getByUUID(challenge.user);
|
||||
if (!user) {
|
||||
throw new Error('Bad challenge');
|
||||
}
|
||||
} catch (e: any) {
|
||||
req.flash('message', {
|
||||
error: true,
|
||||
text: 'An unexpected error occured, please log in again.',
|
||||
});
|
||||
|
||||
res.redirect(
|
||||
'/login' + (query.redirectTo ? '?redirectTo=' + query.redirectTo : ''),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const totp = await this.userService.getUserTOTP(user);
|
||||
|
||||
if (!this.userService.validateTOTP(totp.token, body.totp)) {
|
||||
throw new Error('Invalid code!');
|
||||
}
|
||||
} catch (e: any) {
|
||||
req.flash('message', {
|
||||
error: true,
|
||||
text: e.message,
|
||||
});
|
||||
res.redirect(req.originalUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
session.challenge = null;
|
||||
res.redirect(query.redirectTo ? decodeURIComponent(query.redirectTo) : '/');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import {
|
||||
MiddlewareConsumer,
|
||||
Module,
|
||||
NestModule,
|
||||
RequestMethod,
|
||||
} from '@nestjs/common';
|
||||
import { ValidateCSRFMiddleware } from 'src/middleware/validate-csrf.middleware';
|
||||
import { UserModule } from 'src/modules/objects/user/user.module';
|
||||
import { LoginController } from './login.controller';
|
||||
|
||||
@Module({
|
||||
imports: [UserModule],
|
||||
controllers: [LoginController],
|
||||
})
|
||||
export class LoginModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer
|
||||
.apply(ValidateCSRFMiddleware)
|
||||
.forRoutes({ path: '*', method: RequestMethod.POST });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import {
|
||||
OAuth2AccessTokenAdapter,
|
||||
OAuth2AccessToken,
|
||||
} from '@icynet/oauth2-provider';
|
||||
import { OAuth2TokenType } from 'src/modules/objects/oauth2-token/oauth2-token.entity';
|
||||
import { OAuth2Service } from '../oauth2.service';
|
||||
|
||||
export class AccessTokenAdapter implements OAuth2AccessTokenAdapter {
|
||||
constructor(private _service: OAuth2Service) {}
|
||||
|
||||
public ttl = 3600;
|
||||
|
||||
getToken(token: OAuth2AccessToken): string {
|
||||
return token.token;
|
||||
}
|
||||
|
||||
async create(
|
||||
userId: number,
|
||||
clientId: string,
|
||||
scope: string | string[],
|
||||
ttl: number,
|
||||
): Promise<string> {
|
||||
const client = await this._service.clientService.getById(clientId);
|
||||
const user = await this._service.userService.getById(userId);
|
||||
const accessToken = this._service.token.generateString(64);
|
||||
|
||||
// Standardize scope value
|
||||
const scopes = (
|
||||
!Array.isArray(scope) ? this._service.splitScope(scope) : scope
|
||||
).join(' ');
|
||||
|
||||
const expiresAt = new Date(Date.now() + ttl * 1000);
|
||||
|
||||
this._service.tokenService.insertToken(
|
||||
accessToken,
|
||||
OAuth2TokenType.ACCESS_TOKEN,
|
||||
client,
|
||||
scopes,
|
||||
expiresAt,
|
||||
user,
|
||||
);
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
async fetchByToken(
|
||||
token: string | OAuth2AccessToken,
|
||||
): Promise<OAuth2AccessToken> {
|
||||
const findBy = typeof token === 'string' ? token : token.token;
|
||||
const find = await this._service.tokenService.fetchByToken(
|
||||
findBy,
|
||||
OAuth2TokenType.ACCESS_TOKEN,
|
||||
);
|
||||
|
||||
return {
|
||||
...find,
|
||||
client_id: find.client.client_id,
|
||||
user_id: find.user.id,
|
||||
};
|
||||
}
|
||||
|
||||
checkTTL(token: OAuth2AccessToken): boolean {
|
||||
return new Date() < token.expires_at;
|
||||
}
|
||||
|
||||
getTTL(token: OAuth2AccessToken): number {
|
||||
return token.expires_at.getTime() - Date.now();
|
||||
}
|
||||
|
||||
async fetchByUserIdClientId(
|
||||
userId: number,
|
||||
clientId: string,
|
||||
): Promise<OAuth2AccessToken> {
|
||||
const find = await this._service.tokenService.fetchByUserIdClientId(
|
||||
userId,
|
||||
clientId,
|
||||
OAuth2TokenType.ACCESS_TOKEN,
|
||||
);
|
||||
|
||||
return {
|
||||
...find,
|
||||
client_id: find.client.client_id,
|
||||
user_id: find.user.id,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { OAuth2ClientAdapter, OAuth2Client } from '@icynet/oauth2-provider';
|
||||
import { OAuth2ClientURLType } from 'src/modules/objects/oauth2-client/oauth2-client-url.entity';
|
||||
import { OAuth2Service } from '../oauth2.service';
|
||||
|
||||
export class ClientAdapter implements OAuth2ClientAdapter {
|
||||
constructor(private _service: OAuth2Service) {}
|
||||
|
||||
getId(client: OAuth2Client): string {
|
||||
return client.id as string;
|
||||
}
|
||||
|
||||
async fetchById(id: string): Promise<OAuth2Client> {
|
||||
const find = await this._service.clientService.getById(id);
|
||||
return {
|
||||
id: find.client_id,
|
||||
scope: this._service.splitScope(find.scope),
|
||||
grants: find.grants.split(' '),
|
||||
secret: find.client_secret,
|
||||
};
|
||||
}
|
||||
|
||||
checkSecret(client: OAuth2Client, secret: string): boolean {
|
||||
return client.secret === secret;
|
||||
}
|
||||
|
||||
checkGrantType(client: OAuth2Client, grant: string): boolean {
|
||||
return client.grants.includes(grant);
|
||||
}
|
||||
|
||||
async hasRedirectUri(client: OAuth2Client): Promise<boolean> {
|
||||
const redirectUris = await this._service.clientService.getClientURLs(
|
||||
client.id as string,
|
||||
OAuth2ClientURLType.REDIRECT_URI,
|
||||
);
|
||||
|
||||
return !!redirectUris.length;
|
||||
}
|
||||
|
||||
async checkRedirectUri(
|
||||
client: OAuth2Client,
|
||||
redirectUri: string,
|
||||
): Promise<boolean> {
|
||||
return this._service.clientService.checkRedirectURI(
|
||||
client.id as string,
|
||||
redirectUri,
|
||||
);
|
||||
}
|
||||
|
||||
transformScope(scope: string | string[]): string[] {
|
||||
return Array.isArray(scope) ? scope : this._service.splitScope(scope);
|
||||
}
|
||||
|
||||
checkScope(client: OAuth2Client, scope: string[]): boolean {
|
||||
return scope.every((one) => client.scope.includes(one));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
import { OAuth2CodeAdapter, OAuth2Code } from '@icynet/oauth2-provider';
|
||||
import { OAuth2TokenType } from 'src/modules/objects/oauth2-token/oauth2-token.entity';
|
||||
import { OAuth2Service } from '../oauth2.service';
|
||||
|
||||
export class CodeAdapter implements OAuth2CodeAdapter {
|
||||
constructor(private _service: OAuth2Service) {}
|
||||
|
||||
ttl = 3600;
|
||||
|
||||
async create(
|
||||
userId: number,
|
||||
clientId: string,
|
||||
scope: string | string[],
|
||||
ttl: number,
|
||||
): Promise<string> {
|
||||
const client = await this._service.clientService.getById(clientId);
|
||||
const user = await this._service.userService.getById(userId);
|
||||
const accessToken = this._service.token.generateString(64);
|
||||
|
||||
// Standardize scope value
|
||||
const scopes = (
|
||||
!Array.isArray(scope) ? this._service.splitScope(scope) : scope
|
||||
).join(' ');
|
||||
|
||||
const expiresAt = new Date(Date.now() + ttl * 1000);
|
||||
|
||||
this._service.tokenService.insertToken(
|
||||
accessToken,
|
||||
OAuth2TokenType.CODE,
|
||||
client,
|
||||
scopes,
|
||||
expiresAt,
|
||||
user,
|
||||
);
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
async fetchByCode(code: string | OAuth2Code): Promise<OAuth2Code> {
|
||||
const findBy = typeof code === 'string' ? code : code.code;
|
||||
const find = await this._service.tokenService.fetchByToken(
|
||||
findBy,
|
||||
OAuth2TokenType.CODE,
|
||||
);
|
||||
|
||||
return {
|
||||
...find,
|
||||
code: find.token,
|
||||
client_id: find.client.client_id,
|
||||
user_id: find.user.id,
|
||||
};
|
||||
}
|
||||
|
||||
async removeByCode(code: string | OAuth2Code): Promise<boolean> {
|
||||
const findBy = typeof code === 'string' ? code : code.code;
|
||||
const find = await this._service.tokenService.fetchByToken(
|
||||
findBy,
|
||||
OAuth2TokenType.CODE,
|
||||
);
|
||||
this._service.tokenService.remove(find);
|
||||
return true;
|
||||
}
|
||||
|
||||
getUserId(code: OAuth2Code): string {
|
||||
return code.user_id as string;
|
||||
}
|
||||
|
||||
getClientId(code: OAuth2Code): string {
|
||||
return code.client_id as string;
|
||||
}
|
||||
|
||||
getScope(code: OAuth2Code): string {
|
||||
return code.scope;
|
||||
}
|
||||
|
||||
checkTTL(code: OAuth2Code): boolean {
|
||||
return code.expires_at.getTime() > Date.now();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
import {
|
||||
OAuth2RefreshTokenAdapter,
|
||||
OAuth2RefreshToken,
|
||||
} from '@icynet/oauth2-provider';
|
||||
import { OAuth2TokenType } from 'src/modules/objects/oauth2-token/oauth2-token.entity';
|
||||
import { OAuth2Service } from '../oauth2.service';
|
||||
|
||||
export class RefreshTokenAdapter implements OAuth2RefreshTokenAdapter {
|
||||
constructor(private _service: OAuth2Service) {}
|
||||
|
||||
invalidateOld = false;
|
||||
|
||||
async create(
|
||||
userId: number,
|
||||
clientId: string,
|
||||
scope: string | string[],
|
||||
): Promise<string> {
|
||||
const client = await this._service.clientService.getById(clientId);
|
||||
const user = await this._service.userService.getById(userId);
|
||||
const accessToken = this._service.token.generateString(64);
|
||||
|
||||
// Standardize scope value
|
||||
const scopes = (
|
||||
!Array.isArray(scope) ? this._service.splitScope(scope) : scope
|
||||
).join(' ');
|
||||
|
||||
const expiresAt = new Date(Date.now() + 3.154e7 * 1000);
|
||||
|
||||
this._service.tokenService.insertToken(
|
||||
accessToken,
|
||||
OAuth2TokenType.REFRESH_TOKEN,
|
||||
client,
|
||||
scopes,
|
||||
expiresAt,
|
||||
user,
|
||||
);
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
async fetchByToken(
|
||||
token: string | OAuth2RefreshToken,
|
||||
): Promise<OAuth2RefreshToken> {
|
||||
const findBy = typeof token === 'string' ? token : token.token;
|
||||
const find = await this._service.tokenService.fetchByToken(
|
||||
findBy,
|
||||
OAuth2TokenType.REFRESH_TOKEN,
|
||||
);
|
||||
|
||||
return {
|
||||
...find,
|
||||
client_id: find.client.client_id,
|
||||
user_id: find.user.id,
|
||||
};
|
||||
}
|
||||
|
||||
async removeByUserIdClientId(
|
||||
userId: number,
|
||||
clientId: string,
|
||||
): Promise<boolean> {
|
||||
const find = await this._service.tokenService.fetchByUserIdClientId(
|
||||
userId,
|
||||
clientId,
|
||||
OAuth2TokenType.REFRESH_TOKEN,
|
||||
);
|
||||
|
||||
await this._service.tokenService.remove(find);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async removeByRefreshToken(token: string): Promise<boolean> {
|
||||
const find = await this._service.tokenService.fetchByToken(
|
||||
token,
|
||||
OAuth2TokenType.REFRESH_TOKEN,
|
||||
);
|
||||
|
||||
await this._service.tokenService.remove(find);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getUserId(code: OAuth2RefreshToken): number {
|
||||
return code.user_id as number;
|
||||
}
|
||||
|
||||
getClientId(code: OAuth2RefreshToken): string {
|
||||
return code.client_id as string;
|
||||
}
|
||||
|
||||
getScope(code: OAuth2RefreshToken): string {
|
||||
return code.scope;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import { OAuth2UserAdapter, OAuth2User } from '@icynet/oauth2-provider';
|
||||
import { OAuth2Service } from '../oauth2.service';
|
||||
import { Request } from 'express';
|
||||
import { ParamsDictionary } from 'express-serve-static-core';
|
||||
import { ParsedQs } from 'qs';
|
||||
|
||||
export class UserAdapter implements OAuth2UserAdapter {
|
||||
constructor(private _service: OAuth2Service) {}
|
||||
|
||||
getId(user: OAuth2User): number {
|
||||
return user.id as number;
|
||||
}
|
||||
|
||||
async fetchById(id: number): Promise<OAuth2User> {
|
||||
const find = await this._service.userService.getById(id);
|
||||
return {
|
||||
id: find.id,
|
||||
username: find.username,
|
||||
password: find.password,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchByUsername(username: string): Promise<OAuth2User> {
|
||||
const find = await this._service.userService.getByUsername(username);
|
||||
return {
|
||||
id: find.id,
|
||||
username: find.username,
|
||||
password: find.password,
|
||||
};
|
||||
}
|
||||
|
||||
checkPassword(user: OAuth2User, password: string): Promise<boolean> {
|
||||
return this._service.userService.comparePasswords(user.password, password);
|
||||
}
|
||||
|
||||
async fetchFromRequest(
|
||||
req: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>,
|
||||
): Promise<OAuth2User> {
|
||||
return req.session.user;
|
||||
}
|
||||
|
||||
async consented(
|
||||
userId: number,
|
||||
clientId: string,
|
||||
scope: string | string[],
|
||||
): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async consent(
|
||||
userId: number,
|
||||
clientId: string,
|
||||
scope: string | string[],
|
||||
): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { Controller, Get, Next, Post, Req, Res } from '@nestjs/common';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { OAuth2Service } from './oauth2.service';
|
||||
|
||||
@Controller('oauth2')
|
||||
export class OAuth2Controller {
|
||||
constructor(private _service: OAuth2Service) {}
|
||||
|
||||
// These requests are just passed straight on to the provider controller
|
||||
|
||||
@Get('authorize')
|
||||
public authorizeGetWrapper(
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
@Next() next: NextFunction,
|
||||
): void {
|
||||
return this._service.oauth.controller.authorization(req, res, next);
|
||||
}
|
||||
|
||||
@Post('authorize')
|
||||
public authorizePostWrapper(
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
@Next() next: NextFunction,
|
||||
): void {
|
||||
return this._service.oauth.controller.authorization(req, res, next);
|
||||
}
|
||||
|
||||
@Post('token')
|
||||
public tokenWrapper(
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
@Next() next: NextFunction,
|
||||
): void {
|
||||
return this._service.oauth.controller.token(req, res, next);
|
||||
}
|
||||
|
||||
@Post('introspect')
|
||||
public introspectWrapper(
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
@Next() next: NextFunction,
|
||||
): void {
|
||||
return this._service.oauth.controller.introspection(req, res, next);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { AuthMiddleware } from 'src/middleware/auth.middleware';
|
||||
import { OAuth2ClientModule } from 'src/modules/objects/oauth2-client/oauth2-client.module';
|
||||
import { OAuth2TokenModule } from 'src/modules/objects/oauth2-token/oauth2-token.module';
|
||||
import { UploadModule } from 'src/modules/objects/upload/upload.module';
|
||||
import { UserModule } from 'src/modules/objects/user/user.module';
|
||||
import { OAuth2Controller } from './oauth2.controller';
|
||||
import { OAuth2Service } from './oauth2.service';
|
||||
|
||||
@Module({
|
||||
imports: [UserModule, UploadModule, OAuth2ClientModule, OAuth2TokenModule],
|
||||
controllers: [OAuth2Controller],
|
||||
providers: [OAuth2Service],
|
||||
exports: [OAuth2Service],
|
||||
})
|
||||
export class OAuth2Module implements NestModule {
|
||||
constructor(private _service: OAuth2Service) {}
|
||||
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(this._service.oauth.express()).forRoutes('oauth2/*');
|
||||
consumer.apply(AuthMiddleware).forRoutes('oauth2/authorize');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import { OAuth2AdapterModel, OAuth2Provider } from '@icynet/oauth2-provider';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service';
|
||||
import { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service';
|
||||
import { UserService } from 'src/modules/objects/user/user.service';
|
||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||
import { AccessTokenAdapter } from './adapter/access-token.adapter';
|
||||
import { ClientAdapter } from './adapter/client.adapter';
|
||||
import { CodeAdapter } from './adapter/code.adapter';
|
||||
import { RefreshTokenAdapter } from './adapter/refresh-token.adapter';
|
||||
import { UserAdapter } from './adapter/user.adapter';
|
||||
|
||||
@Injectable()
|
||||
export class OAuth2Service {
|
||||
private _oauthAdapter: OAuth2AdapterModel = {
|
||||
accessToken: new AccessTokenAdapter(this),
|
||||
refreshToken: new RefreshTokenAdapter(this),
|
||||
user: new UserAdapter(this),
|
||||
client: new ClientAdapter(this),
|
||||
code: new CodeAdapter(this),
|
||||
};
|
||||
|
||||
public oauth = new OAuth2Provider(this._oauthAdapter, async (req, res) => {
|
||||
res.render('authorize');
|
||||
});
|
||||
|
||||
constructor(
|
||||
public token: TokenService,
|
||||
public userService: UserService,
|
||||
public clientService: OAuth2ClientService,
|
||||
public tokenService: OAuth2TokenService,
|
||||
) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
this.oauth.logger.setLogLevel('debug');
|
||||
}
|
||||
}
|
||||
|
||||
public splitScope(scope: string): string[] {
|
||||
return scope.includes(',')
|
||||
? scope.split(',').map((item) => item.trim())
|
||||
: scope.split(' ');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Query,
|
||||
Render,
|
||||
Req,
|
||||
Res,
|
||||
Session,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { SessionData } from 'express-session';
|
||||
import { UserService } from 'src/modules/objects/user/user.service';
|
||||
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
||||
import { RegisterDto } from './register.interfaces';
|
||||
|
||||
@Controller('/register')
|
||||
export class RegisterController {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly formUtil: FormUtilityService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Render('register')
|
||||
public registerView(
|
||||
@Session() session: SessionData,
|
||||
@Req() req: Request,
|
||||
): Record<string, any> {
|
||||
return this.formUtil.populateTemplate(req, session);
|
||||
}
|
||||
|
||||
@Post()
|
||||
public async registerRequest(
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
@Body() body: RegisterDto,
|
||||
@Query() query: { redirectTo?: string },
|
||||
) {
|
||||
const { username, display_name, email, password, password_repeat } = body;
|
||||
|
||||
try {
|
||||
if (
|
||||
!username ||
|
||||
!display_name ||
|
||||
!email ||
|
||||
!password ||
|
||||
!password_repeat
|
||||
) {
|
||||
throw new Error('Please fill out all of the fields!');
|
||||
}
|
||||
|
||||
if (!username || !username.match(this.formUtil.usernameRegex)) {
|
||||
throw new Error(
|
||||
'Username must be alphanumeric and between 3 to 26 characters long (_, - and . are also allowed)',
|
||||
);
|
||||
}
|
||||
|
||||
if (display_name.length < 3 || display_name.length > 32) {
|
||||
throw new Error(
|
||||
'Display name must be between 3 and 32 characters long.',
|
||||
);
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/21456918
|
||||
if (!password.match(this.formUtil.passwordRegex)) {
|
||||
throw new Error(
|
||||
'Password must be at least 8 characters long, contain a capital and lowercase letter and a number',
|
||||
);
|
||||
}
|
||||
|
||||
if (!email.match(this.formUtil.emailRegex)) {
|
||||
throw new Error('Invalid email address!');
|
||||
}
|
||||
|
||||
if (password !== password_repeat) {
|
||||
throw new Error('The passwords do not match!');
|
||||
}
|
||||
|
||||
await this.userService.userRegistration(body);
|
||||
|
||||
req.flash('message', {
|
||||
error: false,
|
||||
text: `An activation email has been sent to ${email}!`,
|
||||
});
|
||||
|
||||
res.redirect(
|
||||
'/login' + (query.redirectTo ? '?redirectTo=' + query.redirectTo : ''),
|
||||
);
|
||||
} catch (e: any) {
|
||||
req.flash('message', { error: true, text: e.message });
|
||||
req.flash('form', { ...body, password_repeat: undefined });
|
||||
res.redirect(req.originalUrl);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export interface RegisterDto {
|
||||
username: string;
|
||||
display_name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
password_repeat: string;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import {
|
||||
MiddlewareConsumer,
|
||||
Module,
|
||||
NestModule,
|
||||
RequestMethod,
|
||||
} from '@nestjs/common';
|
||||
import { ValidateCSRFMiddleware } from 'src/middleware/validate-csrf.middleware';
|
||||
import { UserModule } from 'src/modules/objects/user/user.module';
|
||||
import { RegisterController } from './register.controller';
|
||||
|
||||
@Module({
|
||||
imports: [UserModule],
|
||||
controllers: [RegisterController],
|
||||
})
|
||||
export class RegisterModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer
|
||||
.apply(ValidateCSRFMiddleware)
|
||||
.forRoutes({ path: '*', method: RequestMethod.POST });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { databaseProviders } from './database.providers';
|
||||
|
||||
@Module({
|
||||
providers: [...databaseProviders],
|
||||
exports: [...databaseProviders],
|
||||
})
|
||||
export class DatabaseModule {}
|
|
@ -0,0 +1,18 @@
|
|||
import { createConnection } from 'typeorm';
|
||||
|
||||
export const databaseProviders = [
|
||||
{
|
||||
provide: 'DATABASE_CONNECTION',
|
||||
useFactory: async () =>
|
||||
await createConnection({
|
||||
type: 'mysql',
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
username: 'icyauth',
|
||||
password: 'icyauth',
|
||||
database: 'icyauth',
|
||||
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
|
||||
synchronize: true,
|
||||
}),
|
||||
},
|
||||
];
|
|
@ -0,0 +1,33 @@
|
|||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '../user/user.entity';
|
||||
import { OAuth2Client } from './oauth2-client.entity';
|
||||
|
||||
@Entity()
|
||||
export class OAuth2ClientAuthorization {
|
||||
@PrimaryGeneratedColumn()
|
||||
public id: number;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
public scope: string;
|
||||
|
||||
@Column({ type: 'timestamp' })
|
||||
public expires_at: Date;
|
||||
|
||||
@CreateDateColumn({
|
||||
type: 'timestamp',
|
||||
default: () => 'CURRENT_TIMESTAMP(6)',
|
||||
})
|
||||
public created_at: Date;
|
||||
|
||||
@ManyToOne(() => OAuth2Client)
|
||||
public client: OAuth2Client;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
public user: User;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { OAuth2Client } from './oauth2-client.entity';
|
||||
|
||||
export enum OAuth2ClientURLType {
|
||||
REDIRECT_URI = 'redirect_uri',
|
||||
TERMS = 'terms',
|
||||
PRIVACY = 'privacy',
|
||||
WEBSITE = 'website',
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class OAuth2ClientURL {
|
||||
@PrimaryGeneratedColumn()
|
||||
public id: number;
|
||||
|
||||
@Column({ nullable: false })
|
||||
public url: string;
|
||||
|
||||
@Column({ type: 'enum', enum: OAuth2ClientURLType, nullable: false })
|
||||
public type: OAuth2ClientURLType;
|
||||
|
||||
@CreateDateColumn({
|
||||
type: 'timestamp',
|
||||
default: () => 'CURRENT_TIMESTAMP(6)',
|
||||
})
|
||||
public created_at: Date;
|
||||
|
||||
@UpdateDateColumn({
|
||||
type: 'timestamp',
|
||||
default: () => 'CURRENT_TIMESTAMP(6)',
|
||||
onUpdate: 'CURRENT_TIMESTAMP(6)',
|
||||
})
|
||||
public updated_at: Date;
|
||||
|
||||
@ManyToOne(() => OAuth2Client, (client) => client.urls)
|
||||
public client: OAuth2Client;
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Upload } from '../upload/upload.entity';
|
||||
import { User } from '../user/user.entity';
|
||||
import { OAuth2ClientURL } from './oauth2-client-url.entity';
|
||||
|
||||
@Entity()
|
||||
export class OAuth2Client {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ type: 'uuid', length: 36, nullable: false, unique: true })
|
||||
client_id: string;
|
||||
|
||||
@Column({ type: 'text', nullable: false })
|
||||
client_secret: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
title: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
scope: string;
|
||||
|
||||
@Column({ type: 'text', default: 'authorization_code' })
|
||||
grants: string;
|
||||
|
||||
@Column({ default: false })
|
||||
activated: boolean;
|
||||
|
||||
@Column({ default: false })
|
||||
verified: boolean;
|
||||
|
||||
@CreateDateColumn({
|
||||
type: 'timestamp',
|
||||
default: () => 'CURRENT_TIMESTAMP(6)',
|
||||
})
|
||||
public created_at: Date;
|
||||
|
||||
@UpdateDateColumn({
|
||||
type: 'timestamp',
|
||||
|