Skip to content

Commit

Permalink
Merge 62b0f11 into a6d0e79
Browse files Browse the repository at this point in the history
  • Loading branch information
kondanna authored Aug 23, 2021
2 parents a6d0e79 + 62b0f11 commit d3dee69
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 0 deletions.
132 changes: 132 additions & 0 deletions helpers/discordAuth.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
jest.mock('node-fetch')
jest.mock('prisma')
import fetch from 'node-fetch'
import prisma from '../prisma'
import { URLSearchParams } from 'url'

import {
getTokenFromAuthCode,
getUserInfoFromRefreshToken
} from './discordAuth'

describe('getTokenFromAuthCode function', () => {
test('fetch should be called once', async () => {
fetch.mockResolvedValue({ json: () => {} })
const mockRefreshToken = await getTokenFromAuthCode('123')

expect(fetch.mock.calls[0][0]).toBe(
'https://discordapp.com/api/oauth2/token'
)
expect(fetch.mock.calls[0][1]).toEqual({
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded;charset=utf-8'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: undefined,
client_secret: undefined,
code: '123',
redirect_uri: 'https://c0d3.com/discord/redir',
scope: 'email guilds.join gdm.join identify'
})
})
})
})

describe('getUserInfoFromRefreshToken function', () => {
beforeEach(() => {
jest.clearAllMocks()
prisma.user.update = jest.fn()
})
it('should call the correct functions and update refresh token in database and return the correct object if refresh token valid', async () => {
fetch.mockResolvedValueOnce({
json: () => ({
refresh_token: 'fakeRefreshToken',
access_token: 'fakeAccessToken'
})
})
fetch.mockResolvedValueOnce({
json: () => ({
id: 'discord123',
username: 'discord-fakeuser',
avatar: 'ea8f5f59aff14450e892321ba128745d'
})
})
const result = await getUserInfoFromRefreshToken(123, 'mockRefreshToken')

expect(fetch.mock.calls.length).toBe(2)

// first fetch function call
expect(fetch.mock.calls[0][0]).toBe(
'https://discordapp.com/api/oauth2/token'
)
expect(fetch.mock.calls[0][1]).toEqual({
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded;charset=utf-8'
},
body: new URLSearchParams({
client_id: undefined,
client_secret: undefined,
grant_type: 'refresh_token',
refresh_token: 'mockRefreshToken'
})
})

// second fetch function call
expect(fetch.mock.calls[1][0]).toBe('https://discordapp.com/api/users/@me')
expect(fetch.mock.calls[1][1]).toEqual({
method: 'GET',
headers: {
'content-type': 'application/x-www-form-urlencoded;charset=utf-8',
Authorization: 'Bearer fakeAccessToken'
}
})

// database call
expect(prisma.user.update.mock.calls.length).toBe(1)
expect(prisma.user.update.mock.calls[0][0]).toEqual({
where: {
id: 123
},
data: {
discordRefreshToken: 'fakeRefreshToken'
}
})

expect(result).toEqual({
userId: 'discord123',
username: 'discord-fakeuser',
avatarUrl:
'https://cdn.discordapp.com/avatars/discord123/ea8f5f59aff14450e892321ba128745d.png',
refreshToken: 'fakeRefreshToken'
})
})

it('should throw error and update database with empty string if refresh token invalid', async () => {
fetch.mockResolvedValueOnce({
json: () => ({})
})
try {
const result = await getUserInfoFromRefreshToken(
123,
'invalidRefreshToken'
)
expect(false).toEqual(true) // force test to fail
} catch (error) {
expect(error.message).toBe('refresh token invalid')
}

expect(fetch.mock.calls.length).toBe(1)
expect(prisma.user.update.mock.calls.length).toBe(1)
expect(prisma.user.update.mock.calls[0][0]).toEqual({
where: {
id: 123
},
data: {
discordRefreshToken: ''
}
})
})
})
116 changes: 116 additions & 0 deletions helpers/discordAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import prisma from '../prisma'
import { URLSearchParams } from 'url'
import { User } from '.prisma/client'
import fetch from 'node-fetch'

const discordAPI = 'https://discordapp.com/api'
const client_id = process.env.DISCORD_KEY
const client_secret = process.env.DISCORD_SECRET

type AccessTokenResponse = {
access_token: string
token_type: string
expires_in: number
refresh_token: string
scope: string
}

type UserInfoResponse = {
id: string
username: string
avatar: string
discriminator: string
public_flags: number
locale: string
mfa_enabled: boolean
email: string
verified: boolean
}

type DiscordUserInfo = {
userId: string
username: string
avatarUrl: string
refreshToken: string
}

export const getTokenFromAuthCode = (
code: string
): Promise<AccessTokenResponse> => {
return fetch(`${discordAPI}/oauth2/token`, {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded;charset=utf-8'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id,
client_secret,
code,
redirect_uri: 'https://c0d3.com/discord/redir',
scope: 'email guilds.join gdm.join identify'
})
}).then(r => r.json())
}

const getTokenFromRefreshToken = (
refresh_token: string
): Promise<AccessTokenResponse> => {
return fetch(`${discordAPI}/oauth2/token`, {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded;charset=utf-8'
},
body: new URLSearchParams({
client_id,
client_secret,
grant_type: 'refresh_token',
refresh_token
})
}).then(r => r.json())
}

const getUserInfo = (accessToken: string): Promise<UserInfoResponse> => {
return fetch(`${discordAPI}/users/@me`, {
method: 'GET',
headers: {
'content-type': 'application/x-www-form-urlencoded;charset=utf-8',
Authorization: `Bearer ${accessToken}`
}
}).then(r => r.json())
}

const updateUserRefreshToken = (
userId: number,
refreshToken: string
): Promise<User> => {
return prisma.user.update({
where: {
id: userId
},
data: {
discordRefreshToken: refreshToken
}
})
}

export const getUserInfoFromRefreshToken = async (
userId: number,
refreshToken: string
): Promise<DiscordUserInfo> => {
const tokenResponse = await getTokenFromRefreshToken(refreshToken)
const updatedRefreshToken = tokenResponse.refresh_token || ''
// if updatedRefreshToken is undefined, empty string is stored in db to remove invalid refresh tokens
await updateUserRefreshToken(userId, updatedRefreshToken)

if (!updatedRefreshToken) throw new Error('refresh token invalid')

const { id, username, avatar } = await getUserInfo(tokenResponse.access_token)

return {
userId: id,
username,
avatarUrl: `https://cdn.discordapp.com/avatars/${id}/${avatar}.png`,
refreshToken: updatedRefreshToken
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "discordRefreshToken" VARCHAR(255);
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ model User {
cliToken String? @db.VarChar(255)
emailVerificationToken String? @db.VarChar(255)
tokenExpiration DateTime? @db.Timestamptz(6)
discordRefreshToken String? @db.VarChar(255)
starsMentor Star[] @relation("starMentor")
starsGiven Star[] @relation("starStudent")
submissionsReviewed Submission[] @relation("userReviewedSubmissions")
Expand Down

0 comments on commit d3dee69

Please sign in to comment.