-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
251 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: '' | ||
} | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
2 changes: 2 additions & 0 deletions
2
prisma/migrations/20210609140541_adds_refresh_token_to_user/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
-- AlterTable | ||
ALTER TABLE "users" ADD COLUMN "discordRefreshToken" VARCHAR(255); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters