Introduces Discord OAuth2 integration for account association and login, including new routes for linking, unlinking, and authenticating via Discord. Adds supporting services, repositories, and schema validation for the OAuth2 flow. Refactors database schema and queries for consistency, and updates dependencies to include required OAuth2 libraries.
134 lines
4.9 KiB
JavaScript
134 lines
4.9 KiB
JavaScript
const { DiscordOAuth2 } = require("@mgalacyber/discord-oauth2")
|
|
const oauth2Repository = require("../repositories/oauth2Repository")
|
|
const userService = require("./userService")
|
|
const authService = require("./authService")
|
|
const { StateTypes, Scopes, PromptTypes, ResponseCodeTypes } = require("@mgalacyber/discord-oauth2")
|
|
const { DefaultError } = require("../errors/errors")
|
|
|
|
const oauth2_association = new DiscordOAuth2({
|
|
clientId: process.env.DISCORD_CLIENT_ID,
|
|
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
|
redirectUri: process.env.DISCORD_ASSOCIATION_REDIRECT_URL
|
|
})
|
|
|
|
const oauth2_login = new DiscordOAuth2({
|
|
clientId: process.env.DISCORD_CLIENT_ID,
|
|
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
|
redirectUri: process.env.DISCORD_LOGIN_REDIRECT_URL
|
|
})
|
|
|
|
async function generateAssociationDiscordURL(playerUuid) {
|
|
const redirectObject = await oauth2_association.GenerateOAuth2Url({
|
|
state: StateTypes.UserAuth,
|
|
scope: [
|
|
Scopes.Identify
|
|
],
|
|
prompt: PromptTypes.Consent,
|
|
responseCode: ResponseCodeTypes.Code,
|
|
})
|
|
await oauth2Repository.createLinkAttempt(redirectObject.state, playerUuid)
|
|
return redirectObject
|
|
}
|
|
|
|
async function handleAssociationCallback(provider, code, state) {
|
|
const playerUuid = await oauth2Repository.popLinkAttempt(state)
|
|
if (!playerUuid) {
|
|
throw new DefaultError(400, "Invalid or expired session state.", "InvalidStateError")
|
|
}
|
|
|
|
let isProviderAlreadyLinked = false
|
|
|
|
try {
|
|
await userService.getPlayerProperty(playerUuid, `${provider}Id`)
|
|
isProviderAlreadyLinked = true
|
|
} catch (error) {
|
|
if (error.code !== 404) throw error
|
|
}
|
|
|
|
if (isProviderAlreadyLinked) {
|
|
throw new DefaultError(409, `Account from ${provider} already linked to that player`, "AlreadyLinkedException")
|
|
}
|
|
|
|
try {
|
|
const tokenResponse = await oauth2_association.GetAccessToken(code)
|
|
const userProfile = await oauth2_association.UserDataSchema.GetUserProfile(tokenResponse.accessToken)
|
|
if (!userProfile || !userProfile.id) {
|
|
throw new DefaultError(500, `Failed to retrieve ${provider} profile.`)
|
|
}
|
|
|
|
await userService.addPlayerProperty(playerUuid, `${provider}Id`, userProfile.id)
|
|
return {
|
|
code: 200,
|
|
message: "Account linked successfully",
|
|
provider: {
|
|
id: userProfile.id,
|
|
username: userProfile.username
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof DefaultError) throw error
|
|
throw new DefaultError(500, `${provider} authentication failed: + ${error.message}`)
|
|
}
|
|
}
|
|
|
|
async function unlinkAccount(provider, playerUuid) {
|
|
try {
|
|
const property = await userService.getPlayerProperty(playerUuid, `${provider}Id`).catch(() => null)
|
|
if (!property) {
|
|
throw new DefaultError(404, `No ${provider} account linked to this player.`, "NotLinkedError")
|
|
}
|
|
|
|
const success = await oauth2Repository.unlinkProviderAccount(provider, playerUuid)
|
|
if (!success) {
|
|
throw new DefaultError(500, "Failed to unlink the account. Please try again.")
|
|
}
|
|
|
|
return {
|
|
code: 200,
|
|
message: `${provider} account successfully unlinked.`
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof DefaultError) throw error;
|
|
throw new DefaultError(500, "An error occurred during unlinking: " + error.message)
|
|
}
|
|
}
|
|
|
|
async function generateLoginDiscordURL() {
|
|
const redirectObject = await oauth2_login.GenerateOAuth2Url({
|
|
state: StateTypes.UserAuth,
|
|
scope: [
|
|
Scopes.Identify
|
|
],
|
|
prompt: PromptTypes.Consent,
|
|
responseCode: ResponseCodeTypes.Code,
|
|
})
|
|
return redirectObject
|
|
}
|
|
|
|
async function handleLoginCallback(provider, code, requestUser) {
|
|
try {
|
|
const tokenResponse = await oauth2_login.GetAccessToken(code)
|
|
const userProfile = await oauth2_login.UserDataSchema.GetUserProfile(tokenResponse.accessToken)
|
|
if (!userProfile || !userProfile.id) {
|
|
throw new DefaultError(500, `Failed to retrieve ${provider} profile.`)
|
|
}
|
|
|
|
const propertyObject = await userService.getPlayerPropertyByValue(`${provider}Id`, userProfile.id)
|
|
return await authService.authenticateWithoutPassword({ identifier: propertyObject.property.uuid, requireUser: requestUser || true })
|
|
|
|
} catch (error) {
|
|
if (error.code == 404) {
|
|
throw new DefaultError(404, `No ${provider} account linked to any player.`, "NotLinkedError")
|
|
}
|
|
if (error instanceof DefaultError) throw error
|
|
throw new DefaultError(500, `${provider} authentication failed: + ${error.message}`)
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
unlinkAccount,
|
|
handleLoginCallback,
|
|
generateLoginDiscordURL,
|
|
handleAssociationCallback,
|
|
generateAssociationDiscordURL
|
|
} |