Implemented SuperTokens

This commit is contained in:
2022-09-07 10:09:17 +02:00
parent 6bb920861a
commit c44be1bcc1
13 changed files with 254 additions and 544 deletions

View File

@@ -1,22 +0,0 @@
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!');
});
});
});

View File

@@ -1,5 +1,7 @@
import { Controller, Get } from '@nestjs/common';
import { Controller, Get, Session, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
import { AuthGuard } from './auth.guard';
import { SessionContainer } from "supertokens-node/recipe/session";
@Controller()
export class AppController {
@@ -9,4 +11,11 @@ export class AppController {
getHello(): string {
return this.appService.getHello();
}
@Get('test')
@UseGuards(AuthGuard)
async getTest(@Session() session: SessionContainer): Promise<string> {
// TODO: magic
return "magic";
}
}

View File

@@ -1,9 +1,24 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [],
imports: [
ConfigModule.forRoot(),
AuthModule.forRoot({
connectionURI: process.env.ST_API_URL,
apiKey: process.env.ST_API_KEY,
appInfo: {
appName: process.env.APP_NAME,
apiDomain: process.env.APP_URL,
websiteDomain: process.env.WEBAPP_URL,
apiBasePath: '/auth/api',
websiteBasePath: '/auth',
},
}),
],
controllers: [AppController],
providers: [AppService],
})

31
backend/src/auth.guard.ts Normal file
View File

@@ -0,0 +1,31 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Error as STError } from 'supertokens-node';
import { verifySession } from 'supertokens-node/recipe/session/framework/express';
@Injectable()
export class AuthGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const ctx = context.switchToHttp();
let err = undefined;
const resp = ctx.getResponse();
// You can create an optional version of this by passing {sessionRequired: false} to verifySession
await verifySession()(ctx.getRequest(), resp, (res) => {
err = res;
});
if (resp.headersSent) {
throw new STError({
message: 'RESPONSE_SENT',
type: 'RESPONSE_SENT',
});
}
if (err) {
throw err;
}
return true;
}
}

View File

@@ -0,0 +1,29 @@
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { errorHandler } from 'supertokens-node/framework/express';
import { Error as STError } from 'supertokens-node';
import { Request, Response, NextFunction, ErrorRequestHandler } from 'express';
@Catch(STError)
export class SupertokensExceptionFilter implements ExceptionFilter {
handler: ErrorRequestHandler;
constructor() {
this.handler = errorHandler();
}
catch(exception: Error, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const resp = ctx.getResponse<Response>();
if (resp.headersSent) {
return;
}
this.handler(
exception,
ctx.getRequest<Request>(),
resp,
ctx.getNext<NextFunction>(),
);
}
}

View File

@@ -0,0 +1,15 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { middleware } from 'supertokens-node/framework/express';
@Injectable()
export class AuthMiddleware implements NestMiddleware {
supertokensMiddleware: any;
constructor() {
this.supertokensMiddleware = middleware();
}
use(req: Request, res: any, next: () => void) {
return this.supertokensMiddleware(req, res, next);
}
}

View File

@@ -0,0 +1,43 @@
import {
DynamicModule,
MiddlewareConsumer,
Module,
NestModule,
} from '@nestjs/common';
import { AuthMiddleware } from './auth.middleware';
import { AuthModuleConfig, ConfigInjectionToken } from './config.interface';
import { SupertokensService } from './supertokens/supertokens.service';
@Module({
providers: [],
exports: [],
controllers: [],
})
export class AuthModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(AuthMiddleware).forRoutes('*');
}
static forRoot({
connectionURI,
apiKey,
appInfo,
}: AuthModuleConfig): DynamicModule {
return {
providers: [
{
useValue: {
appInfo,
connectionURI,
apiKey,
},
provide: ConfigInjectionToken,
},
SupertokensService,
],
exports: [],
imports: [],
module: AuthModule,
};
}
}

View File

@@ -0,0 +1,9 @@
import { AppInfo } from 'supertokens-node/types';
export const ConfigInjectionToken = 'ConfigInjectionToken';
export type AuthModuleConfig = {
appInfo: AppInfo;
connectionURI: string;
apiKey?: string;
};

View File

@@ -0,0 +1,8 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const Session = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.session;
},
);

View File

@@ -0,0 +1,48 @@
import { Inject, Injectable } from '@nestjs/common';
import { AuthModuleConfig, ConfigInjectionToken } from '../config.interface';
import supertokens from 'supertokens-node';
import ThirdPartyEmailPassword from 'supertokens-node/recipe/thirdpartyemailpassword';
import Session from 'supertokens-node/recipe/session';
@Injectable()
export class SupertokensService {
constructor(@Inject(ConfigInjectionToken) private config: AuthModuleConfig) {
supertokens.init({
appInfo: config.appInfo,
supertokens: {
connectionURI: config.connectionURI,
apiKey: config.apiKey,
},
recipeList: [
ThirdPartyEmailPassword.init({
providers: [
ThirdPartyEmailPassword.Google({
clientId:
'1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com',
clientSecret: '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',
}),
ThirdPartyEmailPassword.Github({
clientId: '467101b197249757c71f',
clientSecret: 'e97051221f4b6426e8fe8d51486396703012f5bd',
}),
ThirdPartyEmailPassword.Apple({
clientId: '4398792-io.supertokens.example.service',
clientSecret: {
keyId: '7M48Y4RYDL',
privateKey:
'-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----',
teamId: 'YWQCXGJRJL',
},
}),
],
}),
Session.init({
jwt: {
enable: true,
issuer: `${process.env.APP_URL}/auth/api`,
},
}),
],
});
}
}

View File

@@ -1,8 +1,17 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import supertokens from 'supertokens-node';
import { SupertokensExceptionFilter } from './auth/auth.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
app.enableCors({
origin: [process.env.WEBAPP_URL],
allowedHeaders: ['content-type', ...supertokens.getAllCORSHeaders()],
credentials: true,
});
app.useGlobalFilters(new SupertokensExceptionFilter());
await app.listen(process.env.APP_PORT);
}
bootstrap();