import { Queue } from 'bull'; import * as admin from 'firebase-admin'; import { Repository } from 'typeorm'; import { InjectQueue } from '@nestjs/bull'; import { BadRequestException, Inject, Injectable, InternalServerErrorException, NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { User } from './database/entities'; import { DiscordAuthorizeDto, QuestType } from './funki-id.dto'; import { FunkiDiscordConfiguration } from './funki-id.module'; type DiscordUser = { id: string; username: string; avatar: string; email?: string }; @Injectable() export class FunkiIdDiscordService { constructor( @Inject('DISCORD_CONFIG') private readonly discordConfig: FunkiDiscordConfiguration, @InjectRepository(User, 'funki-id') private readonly userRepo: Repository, @Inject('FIREBASE_AUTH') private readonly firebaseAuth: admin.auth.Auth, @InjectQueue('validate-quest-by-type-queue') private readonly validateQueue: Queue, ) {} async authorize(dto: DiscordAuthorizeDto, userData?: User): Promise<{ customToken: string }> { const body = new URLSearchParams({ client_id: this.discordConfig.clientId, client_secret: this.discordConfig.clientSecret, code: dto.code, grant_type: 'authorization_code', redirect_uri: this.discordConfig.redirectUrl, scope: 'identify', }); const tokenResponseData = await fetch('https://discord.com/api/v10/oauth2/token', { method: 'POST', body: body, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); if (!tokenResponseData.ok) { const parsedErr = await tokenResponseData.json(); if (parsedErr.error === 'invalid_grant') { throw new BadRequestException(parsedErr.error_description); } throw new InternalServerErrorException(`Cannot get discord token ${JSON.stringify(parsedErr)}`); } const oauthDAta = await tokenResponseData.json(); const me = await this.getMe(oauthDAta.token_type, oauthDAta.access_token); if (!me) { throw new NotFoundException('Discord user not found'); } const user = await this.findOrCreateUserByDiscordInfo(me, userData); const customToken = await this.firebaseAuth.createCustomToken(user.uid, { discordId: me.id }); await this.validateQueue.add('validate-quest-by-type-job', { funkiId: user.uid, type: QuestType.LINK_SOCIAL, }); return { customToken, }; } private async getMe(tokenType: string, token: string) { const response = await fetch('https://discord.com/api/users/@me', { headers: { Authorization: `${tokenType} ${token}`, }, }); if (!response.ok) { return null; } const me: DiscordUser = await response.json(); return me; } private getAges(xId: string): number { const creationDate = new Date(parseInt(xId) / 4194304 + 1420070400000); const now = new Date(); const ageInMilliseconds = now.getTime() - creationDate.getTime(); return Math.floor(ageInMilliseconds / (365.25 * 24 * 60 * 60 * 1000) + 0.5); } public async findOrCreateUserByDiscordInfo(discordUser: DiscordUser, userData?: User) { let user = await this.userRepo.findOne({ where: { discordId: discordUser.id } }); if (!!userData?.uid) { if (user) throw new BadRequestException('Discord Id already linked'); const ages = this.getAges(discordUser.id); return await this.userRepo.save({ uid: userData.uid, discordId: discordUser.id, discord: { ages: ages > 0 ? ages : 1, id: discordUser.id, username: discordUser.username, avatar: discordUser.avatar, email: discordUser.email, }, }); } if (!user) { const firebaseUser = await this.firebaseAuth.createUser({ displayName: discordUser.username, email: discordUser.email, }); user = await this.userRepo.save({ uid: firebaseUser.uid, name: firebaseUser.displayName, email: firebaseUser.email, discordId: discordUser.id, discord: { ages: this.getAges(discordUser.id), id: discordUser.id, username: discordUser.username, avatar: discordUser.avatar, email: discordUser.email, }, }); } return user; } }