New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Discord verification to user profiles #12
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -96,8 +96,8 @@ export async function createProfileForUser(user: User) { | |
} | ||
|
||
/** | ||
* Updates a User in the database. The following fields are stripped from the | ||
* payload: | ||
* Updates a User Profile in the database. The following fields are stripped | ||
* from the payload: | ||
* - createdAt | ||
* - updatedAt (filled in automatically) | ||
* - userId | ||
|
@@ -111,7 +111,6 @@ export async function updateProfileForUser( | |
|
||
// Delete the fields we don't want to alter | ||
delete updatedProfile.createdAt; | ||
delete updatedProfile.updatedAt; | ||
delete updatedProfile.userId; | ||
delete updatedProfile.githubUrl; | ||
|
||
|
@@ -124,6 +123,26 @@ export async function updateProfileForUser( | |
.set(updatedProfile, { merge: true }); | ||
} | ||
|
||
/** | ||
* Updates a User in the database. The following fields are stripped from the | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Praise: Thanks for writing good docstrings! |
||
* payload: | ||
* - createdAt | ||
* - updatedAt (filled in automatically) | ||
* - uid | ||
* - githubLogin | ||
*/ | ||
export async function updateUser(uid: string, user: Partial<User>) { | ||
// Delete the fields we don't want to alter | ||
delete user.createdAt; | ||
delete user.uid; | ||
delete user.githubLogin; | ||
|
||
// Send the correct time stamp | ||
user.updatedAt = new Date().toISOString(); | ||
|
||
await db.collection(USERS_COLLECTION).doc(uid).set(user, { merge: true }); | ||
} | ||
|
||
export async function getUserProfileBySlug( | ||
slug: string | ||
): Promise<UserProfile | null> { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import crypto from "node:crypto"; | ||
import DiscordOauth2 from "discord-oauth2"; | ||
|
||
const DISCORD_REDIRECT_URI = | ||
process.env.NODE_ENV === "production" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thought (non-blocking): If we do checks like this in many places, we might want to adopt something like the codesee_metaconfig.json file for storing non-sensitive constants that are env specific. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea. This is the only place where we're doing this so far but I'll keep an eye on it. |
||
? "https://opensourcehub.io/verify-discord" | ||
: "http://localhost:3000/verify-discord"; | ||
|
||
/** | ||
* List of Discord scopes we need for the OAuth handshake. In our case, all | ||
* we want is the user's ID, so we use a single scope. | ||
* @see https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes | ||
*/ | ||
const DISCORD_SCOPES = ["identify"]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion (non-blocking): Is there any documentation of Discord's available scopes? Maybe add a link to such documentation in a comment? |
||
|
||
function assertDiscordEnvironmentVariables() { | ||
if (!process.env.DISCORD_CLIENT_ID) { | ||
throw new Error("Missing environment variable DISCORD_CLIENT_ID"); | ||
} | ||
if (!process.env.DISCORD_CLIENT_SECRET) { | ||
throw new Error("Missing environment variable DISCORD_CLIENT_SECRET"); | ||
} | ||
} | ||
|
||
export async function getDiscordAccessToken(code: string) { | ||
const discordAuth = new DiscordOauth2(); | ||
|
||
const token = await discordAuth.tokenRequest({ | ||
clientId: process.env.DISCORD_CLIENT_ID, | ||
clientSecret: process.env.DISCORD_CLIENT_SECRET, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question (maybe blocking): How are we keeping these values secret? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They're pulled from a |
||
code, | ||
scope: DISCORD_SCOPES, | ||
grantType: "authorization_code", | ||
redirectUri: DISCORD_REDIRECT_URI, | ||
}); | ||
|
||
return token.access_token; | ||
} | ||
|
||
export async function getDiscordUserId(accessToken: string) { | ||
const discordAuth = new DiscordOauth2(); | ||
const discordUser = await discordAuth.getUser(accessToken); | ||
|
||
return discordUser.id; | ||
} | ||
|
||
export function getDiscordOAuthUrl() { | ||
assertDiscordEnvironmentVariables(); | ||
|
||
const oauth = new DiscordOauth2({ | ||
clientId: process.env.DISCORD_CLIENT_ID, | ||
clientSecret: process.env.DISCORD_CLIENT_SECRET, | ||
redirectUri: DISCORD_REDIRECT_URI, | ||
}); | ||
|
||
const url = oauth.generateAuthUrl({ | ||
scope: ["identify"], | ||
state: crypto.randomBytes(16).toString("hex"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Praise: Thanks for using a real source of entropy! |
||
}); | ||
|
||
return url; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { ActionFunction, redirect } from "@remix-run/node"; | ||
import { getDiscordOAuthUrl } from "~/discord.server"; | ||
|
||
/** | ||
* Generates an OAuth URL to authenticate with Discord, and then redirects to | ||
* it. Once a user authenticates, we send them back to this website. | ||
*/ | ||
export const action: ActionFunction = () => { | ||
const url = getDiscordOAuthUrl(); | ||
return redirect(url); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { LoaderFunction, redirect } from "@remix-run/node"; | ||
import { destroySession, getCurrentUser, getSession } from "~/session.server"; | ||
import { getDiscordAccessToken, getDiscordUserId } from "~/discord.server"; | ||
import { getProfileRouteForUser } from "~/utils/routes"; | ||
import { updateUser } from "~/database.server"; | ||
|
||
export const loader: LoaderFunction = async ({ request }) => { | ||
const session = await getSession(request.headers.get("Cookie")); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thought/suggestion (non-blocking): The code to get the session and currentUser, and redirect to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, and they do! https://linear.app/codesee/issue/ENG-615/middleware-for-user-auth |
||
const currentUser = await getCurrentUser(session); | ||
|
||
if (!currentUser) { | ||
// Redirect to the login page | ||
return redirect("/login", { | ||
headers: { | ||
// It's possible that there is not current user but that the session still | ||
// exists. So to avoid infinite redirects, we destroy the session before | ||
// redirecting. | ||
"Set-Cookie": await destroySession(session), | ||
}, | ||
}); | ||
} | ||
|
||
// Get the `code` param from the URL -- this was added by Discord after | ||
// successful authentication | ||
const url = new URL(request.url); | ||
const code = url.searchParams.get("code"); | ||
|
||
if (!code) { | ||
throw new Error("No code"); | ||
} | ||
|
||
const accessToken = await getDiscordAccessToken(code); | ||
|
||
const discordUserId = await getDiscordUserId(accessToken); | ||
|
||
await updateUser(currentUser.uid, { discordUserId }); | ||
|
||
return redirect(getProfileRouteForUser(currentUser)); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Praise: Thanks for writing good docstrings!