Skip to content

Commit

Permalink
feat: separate interaction endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
csuvajit committed Aug 25, 2024
1 parent 05a7155 commit 14912e8
Show file tree
Hide file tree
Showing 17 changed files with 525 additions and 82 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
"editor.tabSize": 2,
"editor.detectIndentation": false,
"editor.insertSpaces": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "esbenp.prettier-vscode",
"jest.runMode": "on-demand"
}
145 changes: 145 additions & 0 deletions apps/service-auth/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { ClashClientService } from '@app/clash-client';
import { Collections } from '@app/constants';
import { DiscordOAuthService } from '@app/discord-oauth';
import { PlayerLinksEntity } from '@app/entities';
import {
Body,
Controller,
Get,
Headers,
HttpCode,
Inject,
InternalServerErrorException,
NotFoundException,
Post,
Query,
RawBodyRequest,
Req,
Res,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ApiTags } from '@nestjs/swagger';
import {
InteractionResponseFlags,
InteractionResponseType,
InteractionType,
verifyKey,
} from 'discord-interactions';
import { Request, Response } from 'express';
import { Collection } from 'mongodb';

@ApiTags('DISCORD')
@Controller({ path: '/' })
export class AppController {
private readonly discordPublicKey: string;
constructor(
private configService: ConfigService,
private discordOAuthService: DiscordOAuthService,
@Inject(Collections.PLAYER_LINKS)
private playerLinksEntity: Collection<PlayerLinksEntity>,
private clashClientService: ClashClientService,
) {
this.discordPublicKey = this.configService.getOrThrow<string>('DISCORD_PUBLIC_KEY');
}

@Post('/interactions')
@HttpCode(200)
handleDiscordInteractions(
@Req() req: RawBodyRequest<Request>,
@Body() body: Record<string, string | number>,
@Headers('X-Signature-Ed25519') signature: string,
@Headers('X-Signature-Timestamp') timestamp: string,
@Query('message') message: string,
) {
const isValidRequest = verifyKey(
req.rawBody as Buffer,
signature,
timestamp,
this.discordPublicKey,
);
if (!isValidRequest) return new UnauthorizedException();

if (body.type === InteractionType.PING) {
return { type: InteractionResponseType.PONG };
}

if (body.type === InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE) {
return {
type: InteractionResponseType.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT,
data: {
choices: [
{
name: message || 'The application is currently rebooting, please try again later.',
value: '0',
},
],
},
};
}

return {
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content:
message ||
'The application is currently rebooting, please try again later in a few minutes.',
flags: InteractionResponseFlags.EPHEMERAL,
},
};
}

@Get('/connect')
connect(@Res() res: Response) {
const { state, url } = this.discordOAuthService.getOAuth2Url();

// Store the signed state param in the user's cookies so we can verify the value later
// https://this.discordOAuthService.com/developers/docs/topics/oauth2#state-and-security
res.cookie('clientState', state, { maxAge: 1000 * 60 * 5, signed: true });

return res.redirect(url);
}

@Get('/discord-oauth-callback')
async onCallback(@Req() req: Request, @Res() res: Response) {
try {
// 1. Uses the code and state to acquire Discord OAuth2 tokens
const code = req.query['code'];
const discordState = req.query['state'];

// Make sure the state parameter exists
const { clientState } = req.signedCookies;
if (clientState !== discordState) {
return res.status(403).json({ message: 'State verification failed.' });
}

const tokens = await this.discordOAuthService.getOAuthTokens(code as string);

// 2. Uses the Discord Access Token to fetch the user profile
const meData = await this.discordOAuthService.getUserData(tokens);
const userId = meData.user.id;
await this.discordOAuthService.storeDiscordTokens(userId, {
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
expires_at: Date.now() + tokens.expires_in * 1000,
});

const link = await this.playerLinksEntity.findOne({ userId }, { sort: { order: 1 } });
const player = link ? await this.clashClientService.getPlayer(link.tag) : null;
if (!player) throw new NotFoundException('No linked player found.');

const metadata = {
trophies: player.trophies,
verified: link?.verified ? 1 : 0,
username: `${player.name} (${player.tag})`,
};

// 3. Update the users metadata, assuming future updates will be posted to the `/update-metadata` endpoint
await this.discordOAuthService.pushMetadata(userId, metadata, tokens);

return res.send('<h1>You did it! Now go back to Discord.</h1>');
} catch {
throw new InternalServerErrorException();
}
}
}
9 changes: 7 additions & 2 deletions apps/service-auth/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { DiscordOAuthModule } from '@app/discord-oauth';
import { MongoDbModule } from '@app/mongodb';
import { RedisModule } from '@app/redis';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import { AppController } from './app.controller';
import { AuthModule } from './auth/auth.module';
import { ClansModule } from './clans/clans.module';
import { GuildsModule } from './guilds/guilds.module';
import { LinksModule } from './links/links.module';
import { PlayersModule } from './players/players.module';
import { RostersModule } from './rosters/rosters.module';
import { ScheduleModule } from '@nestjs/schedule';
import { ClashClientModule } from '@app/clash-client';

@Module({
imports: [
Expand All @@ -22,6 +25,8 @@ import { ScheduleModule } from '@nestjs/schedule';
LinksModule,
PlayersModule,
RostersModule,
ClashClientModule,
DiscordOAuthModule,
// KafkaProducerModule.forRootAsync({
// useFactory: (configService: ConfigService) => {
// return {
Expand All @@ -48,7 +53,7 @@ import { ScheduleModule } from '@nestjs/schedule';
// }),
// ConsumerModule,
],
controllers: [],
controllers: [AppController],
providers: [],
})
export class AppModule {}
76 changes: 2 additions & 74 deletions apps/service-auth/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,14 @@
import { CurrentUserExpanded, JwtAuthGuard, JwtUser, Role, Roles, RolesGuard } from '@app/auth';
import { getAppHealth } from '@app/helper';
import {
Body,
Controller,
Get,
Headers,
HttpCode,
Post,
Query,
RawBodyRequest,
Req,
UnauthorizedException,
UseGuards,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
import {
InteractionResponseFlags,
InteractionResponseType,
InteractionType,
verifyKey,
} from 'discord-interactions';
import { Request } from 'express';
import { AuthService } from './auth.service';
import { LoginInput } from './dto';

@ApiTags('AUTH')
@Controller({ path: '/auth' })
export class AuthController {
private readonly discordPublicKey: string;
constructor(
private authService: AuthService,
private configService: ConfigService,
) {
this.discordPublicKey = this.configService.getOrThrow<string>('DISCORD_PUBLIC_KEY');
}
constructor(private authService: AuthService) {}

@ApiExcludeEndpoint()
@Get()
Expand Down Expand Up @@ -67,50 +41,4 @@ export class AuthController {
getCustomBots() {
return this.authService.getCustomBots();
}

@Post('/interactions')
@HttpCode(200)
handleInteractions(
@Req() req: RawBodyRequest<Request>,
@Body() body: Record<string, string | number>,
@Headers('X-Signature-Ed25519') signature: string,
@Headers('X-Signature-Timestamp') timestamp: string,
@Query('message') message: string,
) {
const isValidRequest = verifyKey(
req.rawBody as Buffer,
signature,
timestamp,
this.discordPublicKey,
);
if (!isValidRequest) return new UnauthorizedException();

if (body.type === InteractionType.PING) {
return { type: InteractionResponseType.PONG };
}

if (body.type === InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE) {
return {
type: InteractionResponseType.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT,
data: {
choices: [
{
name: message || 'The application is currently rebooting, please try again later.',
value: '0',
},
],
},
};
}

return {
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content:
message ||
'The application is currently rebooting, please try again later in a few minutes.',
flags: InteractionResponseFlags.EPHEMERAL,
},
};
}
}
2 changes: 1 addition & 1 deletion apps/service-auth/src/clans/clans.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class ClansController {
}

@Public()
@Get('/:clanTag/badges/:size')
@Get('/:clanTag/badges')
@Header('Cache-Control', 'max-age=600')
@ApiResponse({ type: CWLStatsOutput, status: 200 })
async getClanBadges(@Param('clanTag') clanTag: string, @Res() res: Response) {
Expand Down
12 changes: 9 additions & 3 deletions apps/service-auth/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,42 @@
import { morganLogger } from '@app/helper';
import { Logger, ValidationPipe, VersioningType } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import cookieParser from 'cookie-parser';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, { rawBody: true });
const logger = new Logger(AppModule.name);

const config = app.get(ConfigService);

app.enableShutdownHooks();
app.set('trust proxy', true);
app.enableCors();
app.use(morganLogger(logger));
app.use(cookieParser(config.getOrThrow('COOKIE_SECRET')));
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));

const config = new DocumentBuilder()
const builder = new DocumentBuilder()
.setTitle('Service Backend API')
.setDescription('Public and Private Routes for the Service Backend API')
.setVersion('v1')
.addTag('AUTH')
.addTag('DISCORD')
.addTag('LINKS')
.addTag('PLAYERS')
.addTag('CLANS')
.addTag('GUILDS')
.build();
const document = SwaggerModule.createDocument(app, config);
const document = SwaggerModule.createDocument(app, builder);
SwaggerModule.setup('/', app, document);

app.enableVersioning({ type: VersioningType.URI });

const port = process.env.PORT || 8081;
const port = config.get('PORT', 8081);
await app.listen(port);

logger.log(`Service-Auth: http://localhost:${port}`);
Expand Down
2 changes: 2 additions & 0 deletions libs/constants/src/constants.values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,6 @@ export enum Collections {
BOT_STATS = 'BotStats',
BOT_COMMANDS = 'BotCommands',
BOT_INTERACTIONS = 'BotInteractions',

DISCORD_OAUTH_USERS = 'DiscordOAuthUsers',
}
8 changes: 8 additions & 0 deletions libs/discord-oauth/src/discord-oauth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { DiscordOAuthService } from './discord-oauth.service';

@Module({
providers: [DiscordOAuthService],
exports: [DiscordOAuthService],
})
export class DiscordOAuthModule {}
18 changes: 18 additions & 0 deletions libs/discord-oauth/src/discord-oauth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DiscordOAuthService } from './discord-oauth.service';

describe('DiscordOAuthService', () => {
let service: DiscordOAuthService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DiscordOAuthService],
}).compile();

service = module.get<DiscordOAuthService>(DiscordOAuthService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
Loading

0 comments on commit 14912e8

Please sign in to comment.