Yggdrasil/services/oauth2Service.js
2026-01-24 22:22:33 +01:00

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, YggdrasilError } = 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 YggdrasilError(404, "NotLinkedError", `No ${provider} account linked to any player.`)
}
if (error instanceof DefaultError) throw error
throw new DefaultError(500, `${provider} authentication failed: + ${error.message}`)
}
}
module.exports = {
unlinkAccount,
handleLoginCallback,
generateLoginDiscordURL,
handleAssociationCallback,
generateAssociationDiscordURL
}