diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 72f8e06..01d7a0f 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -11,13 +11,13 @@ import { ApiQuery } from '@nestjs/swagger'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { catchError, lastValueFrom, map } from 'rxjs'; +import { AuthService } from './auth.service'; @Controller('auth') export class AuthController { constructor( private config: ConfigService, - private httpService: HttpService, - private jwtService: JwtService, + private authService: AuthService, ) {} @Get('/vatsim') @@ -35,58 +35,15 @@ export class AuthController { ); } - const token = await lastValueFrom( - this.httpService - .post( - `${this.config.get( - 'vatsim-auth.base-url', - )}/${this.config.get('vatsim-auth.token-endpoint')}`, - { - grant_type: 'authorization_code', - client_id: this.config.get('vatsim-auth.client-id'), - client_secret: this.config.get('vatsim-auth.client-secret'), - redirect_uri: 'http://localhost:3000/auth/vatsim', - code, - }, - ) - .pipe( - map((response) => response.data.access_token), - catchError((err) => { - throw new HttpException(err.response.data, err.response.status); - }), - ), - ); + const token = await this.authService.login(code); - const userdata = await lastValueFrom( - this.httpService - .get( - `${this.config.get( - 'vatsim-auth.base-url', - )}/${this.config.get('vatsim-auth.user-endpoint')}`, - { - headers: { - Authorization: `Bearer ${token}`, - Accept: 'application/json', - }, - }, - ) - .pipe( - map((response) => response.data.data), - catchError((err) => { - throw new HttpException(err.response.data, err.response.status); - }), - ), - ); - - if (userdata.oauth.token_valid) { - const payload = { username: userdata.cid, sub: token }; - const accessToken = this.jwtService.sign(payload); + if (token !== undefined) { return { url: `${this.config.get( 'frontend.base-url', )}/${this.config.get( 'frontend.login-endpoint', - )}?token=${accessToken}`, + )}?token=${token}`, }; } else { return { diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 97e394a..311ad5a 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -2,8 +2,11 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; +import { MongooseModule } from '@nestjs/mongoose'; import { AuthController } from './auth.controller'; +import { UserSchema } from './models/user.model'; import { JwtStrategy } from './strategies/jwt.strategy'; +import { AuthService } from './auth.service'; @Module({ imports: [ @@ -15,8 +18,9 @@ import { JwtStrategy } from './strategies/jwt.strategy'; signOptions: { expiresIn: '1h' }, }), }), + MongooseModule.forFeature([{ name: 'user', schema: UserSchema }]), ], - providers: [JwtStrategy], + providers: [JwtStrategy, AuthService], controllers: [AuthController], }) export class AuthModule {} diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts new file mode 100644 index 0000000..800ab66 --- /dev/null +++ b/src/auth/auth.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 0000000..46a80ca --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,72 @@ +import { HttpService } from '@nestjs/axios'; +import { HttpException, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { catchError, lastValueFrom, map } from 'rxjs'; + +import { UserDocument } from './models/user.model'; + +@Injectable() +export class AuthService { + constructor( + @InjectModel('user') + private readonly userModel: Model, + private config: ConfigService, + private httpService: HttpService, + private jwtService: JwtService, + ) {} + + async login(code: string): Promise { + const token = await lastValueFrom( + this.httpService + .post( + `${this.config.get( + 'vatsim-auth.base-url', + )}/${this.config.get('vatsim-auth.token-endpoint')}`, + { + grant_type: 'authorization_code', + client_id: this.config.get('vatsim-auth.client-id'), + client_secret: this.config.get('vatsim-auth.client-secret'), + redirect_uri: 'http://localhost:3000/auth/vatsim', + code, + }, + ) + .pipe( + map((response) => response.data.access_token), + catchError((err) => { + throw new HttpException(err.response.data, err.response.status); + }), + ), + ); + + const userdata = await lastValueFrom( + this.httpService + .get( + `${this.config.get( + 'vatsim-auth.base-url', + )}/${this.config.get('vatsim-auth.user-endpoint')}`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + }, + ) + .pipe( + map((response) => response.data.data), + catchError((err) => { + throw new HttpException(err.response.data, err.response.status); + }), + ), + ); + + if (userdata.oauth.token_valid) { + const payload = { username: userdata.cid, sub: token }; + return this.jwtService.sign(payload); + } + + return undefined; + } +} diff --git a/src/auth/models/user.model.ts b/src/auth/models/user.model.ts new file mode 100644 index 0000000..99669f8 --- /dev/null +++ b/src/auth/models/user.model.ts @@ -0,0 +1,44 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document } from 'mongoose'; + +export type UserDocument = User & Document; + +@Schema() +export class User { + @Prop({ + required: true, + index: true, + type: String, + }) + vatsimId: string; + + @Prop({ + type: String, + }) + fullName: string; + + @Prop({ + required: true, + type: String, + }) + vatsimToken: string; + + @Prop({ + type: String, + }) + vatsimRefreshToken: string; + + @Prop({ + type: Boolean, + default: false, + }) + administrator: boolean; + + @Prop({ + type: [String], + default: [], + }) + airportConfigurationAccess: string[]; +} + +export const UserSchema = SchemaFactory.createForClass(User);