Add Discord OAuth2 account linking and login support
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.
This commit is contained in:
parent
5b81f57adb
commit
c5b6f6c107
3
.gitignore
vendored
3
.gitignore
vendored
@ -133,4 +133,5 @@ dist
|
||||
#Modun Globals
|
||||
logs
|
||||
tests
|
||||
data/keys
|
||||
data/keys
|
||||
tests
|
||||
@ -45,7 +45,7 @@ async function setupDatabase() {
|
||||
name VARCHAR(256) NOT NULL,
|
||||
value VARCHAR(512) NOT NULL,
|
||||
uuid VARCHAR(36) NOT NULL,
|
||||
UNIQUE KEY unique_property (uuid, name),
|
||||
UNIQUE KEY uniqueProperty (uuid, name),
|
||||
FOREIGN KEY (uuid) REFERENCES players(uuid) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
@ -142,7 +142,7 @@ async function setupDatabase() {
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS banReasons (
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
reason_key VARCHAR(512) UNIQUE NOT NULL
|
||||
reasonKey VARCHAR(512) UNIQUE NOT NULL
|
||||
)
|
||||
`)
|
||||
logger.log(`${"banReasons".bold} table ready`, ["MariaDB", "yellow"])
|
||||
@ -347,32 +347,42 @@ async function setupDatabase() {
|
||||
logger.log(`${"clean_expired_certificates".bold} event ready`, ["MariaDB", "yellow"])
|
||||
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS api_administrators (
|
||||
CREATE TABLE IF NOT EXISTS apiAdministrators (
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(255) UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
logger.log(`${"api_administrators".bold} table ready`, ["MariaDB", "yellow"])
|
||||
logger.log(`${"apiAdministrators".bold} table ready`, ["MariaDB", "yellow"])
|
||||
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS api_administrators_permissions_list (
|
||||
permission_key VARCHAR(64) PRIMARY KEY
|
||||
CREATE TABLE IF NOT EXISTS apiAdministratorsPermissionsList (
|
||||
permissionKey VARCHAR(64) PRIMARY KEY
|
||||
)
|
||||
`)
|
||||
logger.log(`${"api_administrators_permissions_list".bold} table ready`, ["MariaDB", "yellow"])
|
||||
logger.log(`${"apiAdministratorsPermissionsList".bold} table ready`, ["MariaDB", "yellow"])
|
||||
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS api_administrators_permissions (
|
||||
administrator_id INTEGER NOT NULL,
|
||||
permission_key VARCHAR(64) NOT NULL,
|
||||
PRIMARY KEY (administrator_id, permission_key),
|
||||
FOREIGN KEY (administrator_id) REFERENCES api_administrators(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (permission_key) REFERENCES api_administrators_permissions_list(permission_key) ON DELETE CASCADE
|
||||
CREATE TABLE IF NOT EXISTS apiAdministratorsPermissions (
|
||||
administratorId INTEGER NOT NULL,
|
||||
permissionKey VARCHAR(64) NOT NULL,
|
||||
PRIMARY KEY (administratorId, permissionKey),
|
||||
FOREIGN KEY (administratorId) REFERENCES apiAdministrators(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (permissionkey) REFERENCES apiAdministratorsPermissionsList(permissionKey) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
logger.log(`${"api_administrators_permissions".bold} table ready`, ["MariaDB", "yellow"])
|
||||
logger.log(`${"apiAdministratorsPermissions".bold} table ready`, ["MariaDB", "yellow"])
|
||||
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS oaauth2LinkAttempts (
|
||||
OAuth2LinkId VARCHAR(255) NOT NULL,
|
||||
playerUuid VARCHAR(255) NOT NULL,
|
||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (playerUuid) REFERENCES players(uuid) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
logger.log(`${"oaauth2LinkAttempts".bold} table ready`, ["MariaDB", "yellow"])
|
||||
|
||||
logger.log("MariaDB database successfully initialised!", ["MariaDB", "yellow"])
|
||||
|
||||
|
||||
@ -55,7 +55,13 @@ function isTrueFromDotEnv(key) {
|
||||
return (process.env[key] || "").trim().toLowerCase() === "true"
|
||||
}
|
||||
|
||||
function getUrlParam(url, param) {
|
||||
const urlParams = new URLSearchParams(url)
|
||||
return urlParams.get(param)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getUrlParam,
|
||||
signProfileData,
|
||||
addDashesToUUID,
|
||||
isTrueFromDotEnv,
|
||||
|
||||
238
package-lock.json
generated
238
package-lock.json
generated
@ -1,16 +1,18 @@
|
||||
{
|
||||
"name": "base-rest-api",
|
||||
"name": "yggdrasil",
|
||||
"version": "0.0.1-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "base-rest-api",
|
||||
"name": "yggdrasil",
|
||||
"version": "0.0.1-alpha",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@mgalacyber/discord-oauth2": "^1.9.6",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"colors": "^1.4.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.2.1",
|
||||
@ -240,6 +242,36 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@mgalacyber/discord-oauth2": {
|
||||
"version": "1.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@mgalacyber/discord-oauth2/-/discord-oauth2-1.9.6.tgz",
|
||||
"integrity": "sha512-sSzi5aikr+32WnVvkfDpSu0SDtBh0anwvWRB0WfsjUrd4/WqfXPXTu0G2kqQOmcvXEQzws1/8wFbDymklUELJw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@mgalacyber/package-notifier": "^1.0.1",
|
||||
"undici": "^6.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mgalacyber/package-notifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mgalacyber/package-notifier/-/package-notifier-1.0.1.tgz",
|
||||
"integrity": "sha512-s/QgExSx4RRbVV73hMbBBZvkHjQ6WzOAc7tYJ/82JHMEabwbiQYoIdRJAH6y3MmAKo11+wKFCovHaM6oCPnpYA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@mgalacyber/termbox": "^1.0.0",
|
||||
"axios": "^1.6.7",
|
||||
"string-width": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mgalacyber/termbox": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@mgalacyber/termbox/-/termbox-1.0.0.tgz",
|
||||
"integrity": "sha512-0tQL5lJw9NW1cND5loLgj5hrvJmIIY2vBWVsXsIz4pQEZR3KasMUKBcOxAKclMaoy19Y5sgqge018M67AgqkhA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"string-width": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@ -322,6 +354,15 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
|
||||
"integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
@ -365,6 +406,23 @@
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -607,6 +665,18 @@
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@ -660,6 +730,25 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
|
||||
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie-signature": "1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser/node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
@ -721,6 +810,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
@ -819,6 +917,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@ -1181,6 +1294,63 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@ -1320,6 +1490,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@ -1524,6 +1709,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
|
||||
"integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
@ -2157,6 +2351,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
@ -2498,6 +2698,31 @@
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
||||
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-fullwidth-code-point": "^2.0.0",
|
||||
"strip-ansi": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
|
||||
"integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
@ -2596,6 +2821,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "base-rest-api",
|
||||
"name": "yggdrasil",
|
||||
"version": "0.0.1-alpha",
|
||||
"description": "",
|
||||
"repository": {
|
||||
@ -23,8 +23,10 @@
|
||||
"homepage": "https://gitea.azures.fr/azures04/Yggdrasil",
|
||||
"readme": "https://gitea.azures.fr/azures04/Yggdrasil/src/branch/main/README.md",
|
||||
"dependencies": {
|
||||
"@mgalacyber/discord-oauth2": "^1.9.6",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"colors": "^1.4.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.2.1",
|
||||
|
||||
@ -4,7 +4,7 @@ const { DefaultError } = require("../errors/errors")
|
||||
|
||||
async function getAdminById(id) {
|
||||
try {
|
||||
const sql = "SELECT id, username, createdAt FROM api_administrators WHERE id = ?"
|
||||
const sql = "SELECT id, username, createdAt FROM apiAdministrators WHERE id = ?"
|
||||
const rows = await database.query(sql, [id])
|
||||
return rows[0] || null
|
||||
} catch (error) {
|
||||
@ -15,7 +15,7 @@ async function getAdminById(id) {
|
||||
|
||||
async function createAdmin(username, hashedPassword) {
|
||||
try {
|
||||
const sql = "INSERT INTO api_administrators (username, password) VALUES (?, ?)"
|
||||
const sql = "INSERT INTO apiAdministrators (username, password) VALUES (?, ?)"
|
||||
const result = await database.query(sql, [username, hashedPassword])
|
||||
|
||||
if (result.affectedRows > 0) {
|
||||
@ -36,7 +36,7 @@ async function hasPermission(adminId, permissionKey) {
|
||||
try {
|
||||
const sql = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM api_administrators_permissions
|
||||
FROM apiAdministrators_permissions
|
||||
WHERE administrator_id = ? AND permission_key = ?
|
||||
`
|
||||
const rows = await database.query(sql, [adminId, permissionKey])
|
||||
@ -49,7 +49,7 @@ async function hasPermission(adminId, permissionKey) {
|
||||
|
||||
async function assignPermission(adminId, permissionKey) {
|
||||
try {
|
||||
const sql = "INSERT INTO api_administrators_permissions (administrator_id, permission_key) VALUES (?, ?)"
|
||||
const sql = "INSERT INTO apiAdministrators_permissions (administrator_id, permission_key) VALUES (?, ?)"
|
||||
const result = await database.query(sql, [adminId, permissionKey])
|
||||
|
||||
return result.affectedRows > 0
|
||||
@ -62,7 +62,7 @@ async function assignPermission(adminId, permissionKey) {
|
||||
|
||||
async function revokePermission(adminId, permissionKey) {
|
||||
try {
|
||||
const sql = "DELETE FROM api_administrators_permissions WHERE administrator_id = ? AND permission_key = ?"
|
||||
const sql = "DELETE FROM apiAdministrators_permissions WHERE administrator_id = ? AND permission_key = ?"
|
||||
const result = await database.query(sql, [adminId, permissionKey])
|
||||
|
||||
return result.affectedRows > 0
|
||||
@ -76,7 +76,7 @@ async function getAdminPermissions(adminId) {
|
||||
try {
|
||||
const sql = `
|
||||
SELECT permission_key
|
||||
FROM api_administrators_permissions
|
||||
FROM apiAdministrators_permissions
|
||||
WHERE administrator_id = ?
|
||||
`
|
||||
const rows = await database.query(sql, [adminId])
|
||||
@ -89,7 +89,7 @@ async function getAdminPermissions(adminId) {
|
||||
|
||||
async function updateAdminPassword(adminId, newHashedPassword) {
|
||||
try {
|
||||
const sql = "UPDATE api_administrators SET password = ? WHERE id = ?"
|
||||
const sql = "UPDATE apiAdministrators SET password = ? WHERE id = ?"
|
||||
const result = await database.query(sql, [newHashedPassword, adminId])
|
||||
|
||||
if (result.affectedRows > 0) {
|
||||
@ -109,7 +109,7 @@ async function updateAdminPassword(adminId, newHashedPassword) {
|
||||
|
||||
async function getAdminByUsername(username) {
|
||||
try {
|
||||
const sql = "SELECT id, username, password, createdAt FROM api_administrators WHERE username = ?"
|
||||
const sql = "SELECT id, username, password, createdAt FROM apiAdministrators WHERE username = ?"
|
||||
const rows = await database.query(sql, [username])
|
||||
|
||||
return rows[0] || null
|
||||
|
||||
55
repositories/oauth2Repository.js
Normal file
55
repositories/oauth2Repository.js
Normal file
@ -0,0 +1,55 @@
|
||||
const logger = require("../modules/logger")
|
||||
const database = require("../modules/database")
|
||||
const { DefaultError } = require("../errors/errors")
|
||||
|
||||
async function createLinkAttempt(OAuth2LinkId, playerUuid) {
|
||||
try {
|
||||
const sql = `
|
||||
INSERT INTO oaauth2LinkAttempts (OAuth2LinkId, playerUuid)
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE playerUuid = VALUES(playerUuid), createdAt = NOW()
|
||||
`
|
||||
const result = await database.query(sql, [OAuth2LinkId, playerUuid])
|
||||
return result.affectedRows > 0
|
||||
} catch (error) {
|
||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function popLinkAttempt(OAuth2LinkId) {
|
||||
try {
|
||||
const selectSql = "SELECT playerUuid FROM oaauth2LinkAttempts WHERE OAuth2LinkId = ?"
|
||||
const rows = await database.query(selectSql, [OAuth2LinkId])
|
||||
|
||||
if (rows.length === 0) return null
|
||||
|
||||
const playerUuid = rows[0].playerUuid
|
||||
|
||||
const deleteSql = "DELETE FROM oaauth2LinkAttempts WHERE OAuth2LinkId = ?"
|
||||
await database.query(deleteSql, [OAuth2LinkId])
|
||||
|
||||
return playerUuid
|
||||
} catch (error) {
|
||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function unlinkProviderAccount(provider, playerUuid) {
|
||||
try {
|
||||
const sql = `DELETE FROM playersProperties WHERE name = '${provider}Id' AND uuid = ?`
|
||||
const result = await database.query(sql, [playerUuid])
|
||||
|
||||
return result.affectedRows > 0
|
||||
} catch (error) {
|
||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
popLinkAttempt,
|
||||
createLinkAttempt,
|
||||
unlinkProviderAccount
|
||||
}
|
||||
@ -90,6 +90,31 @@ async function getPlayerProperty(key, uuid) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getPlayerPropertyByValue(key, value) {
|
||||
try {
|
||||
const sql = `SELECT * FROM playersProperties WHERE name = ? AND value = ?`
|
||||
const rows = await database.query(sql, [key, value])
|
||||
const property = rows[0]
|
||||
|
||||
if (!property) {
|
||||
throw new DefaultError(404, "No property found with this value for the specified key")
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
property: {
|
||||
name: property.name,
|
||||
value: property.value,
|
||||
uuid: property.uuid
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof DefaultError) throw error
|
||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
||||
}
|
||||
}
|
||||
|
||||
async function getPlayerSettingsSchema() {
|
||||
const RAW_SCHEMA_CACHE = {
|
||||
privileges: {},
|
||||
@ -213,11 +238,11 @@ async function banUser(uuid, { reasonKey, reasonMessage, expires = null }) {
|
||||
}
|
||||
|
||||
let reasonId
|
||||
const reasonRows = await database.query("SELECT id FROM banReasons WHERE reason_key = ?", [reasonKey])
|
||||
const reasonRows = await database.query("SELECT id FROM banReasons WHERE reasonKey = ?", [reasonKey])
|
||||
if (reasonRows.length > 0) {
|
||||
reasonId = reasonRows[0].id
|
||||
} else {
|
||||
const insertReason = await database.query("INSERT INTO banReasons (reason_key) VALUES (?)", [reasonKey])
|
||||
const insertReason = await database.query("INSERT INTO banReasons (reasonKey) VALUES (?)", [reasonKey])
|
||||
reasonId = insertReason.insertId
|
||||
}
|
||||
|
||||
@ -279,7 +304,7 @@ async function getPlayerBans(uuid) {
|
||||
b.banId,
|
||||
b.expires,
|
||||
b.reasonMessage,
|
||||
r.reason_key as reason
|
||||
r.reasonKey as reason
|
||||
FROM bans b
|
||||
JOIN banReasons r ON b.reason = r.id
|
||||
WHERE b.uuid = ?
|
||||
@ -770,5 +795,6 @@ module.exports = {
|
||||
updatePropertyToPlayer,
|
||||
getPlayerSettingsSchema,
|
||||
updatePlayerPreferences,
|
||||
getPlayerPropertyByValue,
|
||||
deleteExpiredCertificates,
|
||||
}
|
||||
15
routes/auth/provider/discord.js
Normal file
15
routes/auth/provider/discord.js
Normal file
@ -0,0 +1,15 @@
|
||||
const express = require("express")
|
||||
const router = express.Router()
|
||||
const oauth2Service = require("../../../services/oauth2Service")
|
||||
|
||||
router.get("/login", async (req, res) => {
|
||||
const redirectObject = await oauth2Service.generateLoginDiscordURL()
|
||||
return res.status(200).redirect(redirectObject.url)
|
||||
})
|
||||
|
||||
router.get("/login/callback", async (req, res) => {
|
||||
const result = await oauth2Service.handleLoginCallback("discord", req.query.code, req.query.requestUser)
|
||||
return res.status(result.code).json(result.response)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@ -1,6 +1,6 @@
|
||||
const express = require("express")
|
||||
const router = express.Router()
|
||||
const { YggdrasilError } = require("../../errors/errors")
|
||||
const { YggdrasilError, DefaultError } = require("../../errors/errors")
|
||||
const rateLimit = require("express-rate-limit")
|
||||
const authService = require("../../services/authService")
|
||||
const logger = require("../../modules/logger")
|
||||
|
||||
26
routes/link/discord.js
Normal file
26
routes/link/discord.js
Normal file
@ -0,0 +1,26 @@
|
||||
const express = require("express")
|
||||
const router = express.Router()
|
||||
const authService = require("../../services/authService")
|
||||
const oauth2Service = require("../../services/oauth2Service")
|
||||
|
||||
router.get("/redirect", async (req, res) => {
|
||||
const accessToken = req.headers.authorization.replace("Bearer ", "")
|
||||
const player = await authService.verifyAccessToken({ accessToken })
|
||||
const redirectObject = await oauth2Service.generateAssociationDiscordURL(player.user.uuid)
|
||||
return res.json({ url: redirectObject.url })
|
||||
})
|
||||
|
||||
router.get("/link", async (req, res) => {
|
||||
const { code, state } = req.query
|
||||
const result = await oauth2Service.handleAssociationCallback("discord", code, state)
|
||||
return res.status(200).json(result)
|
||||
})
|
||||
|
||||
router.delete("/link", async (req, res) => {
|
||||
const accessToken = req.headers.authorization.replace("Bearer ", "")
|
||||
const player = await authService.verifyAccessToken({ accessToken })
|
||||
const result = await oauth2Service.unlinkAccount("discord", player.user.uuid)
|
||||
return res.status(result.code).json(result)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@ -5,7 +5,7 @@ const logger = require("../modules/logger")
|
||||
const authService = require("../services/authService")
|
||||
const adminService = require("../services/adminService")
|
||||
|
||||
if (utils.isTrueFromDotEnv("SUPPORT_REGISTER")) {
|
||||
if (!utils.isTrueFromDotEnv("SUPPORT_REGISTER")) {
|
||||
router.post("/", adminService.hasPermission("REGISTER_USER"), async (req, res) => {
|
||||
const { username, password, email, registrationCountry, preferredLanguage } = req.body
|
||||
const clientIp = req.headers["x-forwarded-for"] || req.connection.remoteAddress
|
||||
|
||||
24
schemas/link/discord/link.js
Normal file
24
schemas/link/discord/link.js
Normal file
@ -0,0 +1,24 @@
|
||||
const z = require("zod")
|
||||
|
||||
module.exports = {
|
||||
GET: {
|
||||
query: z.object({
|
||||
code: z.string({ required_error: "Authorisation code required" }),
|
||||
state: z.string({ required_error: "The state parameter is required." })
|
||||
}),
|
||||
error: {
|
||||
code: 400,
|
||||
message: "Invalid Discord callback settings"
|
||||
}
|
||||
},
|
||||
DELETE: {
|
||||
headers: z.object({
|
||||
authorization: z.string({ required_error: "The authentication token is required." })
|
||||
.regex(/^Bearer\s.+/, { message: "Invalid Authorization header format (Bearer token expected)" })
|
||||
}),
|
||||
error: {
|
||||
code: 401,
|
||||
message: "Authentication required for disassociation"
|
||||
}
|
||||
}
|
||||
}
|
||||
14
schemas/link/discord/redirect.js
Normal file
14
schemas/link/discord/redirect.js
Normal file
@ -0,0 +1,14 @@
|
||||
const z = require("zod")
|
||||
|
||||
module.exports = {
|
||||
GET: {
|
||||
headers: z.object({
|
||||
authorization: z.string({ required_error: "The authentication token is required." })
|
||||
.regex(/^Bearer\s.+/, { message: "Invalid Authorization header format (Bearer token expected)" })
|
||||
}),
|
||||
error: {
|
||||
code: 401,
|
||||
message: "Authentication required to generate the link URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -26,6 +26,7 @@ app.use(cors({ origin: "*" }))
|
||||
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
// app.use(cookieParser())
|
||||
|
||||
app.set("trust proxy", true)
|
||||
|
||||
|
||||
@ -139,13 +139,13 @@ async function logPlayerAction(playerUuid, actionCode) {
|
||||
module.exports = {
|
||||
loginAdmin,
|
||||
uploadCape,
|
||||
deleteCape,
|
||||
registerAdmin,
|
||||
hasPermission,
|
||||
getAdminProfile,
|
||||
grantPermission,
|
||||
logPlayerAction,
|
||||
revokePermission,
|
||||
checkAdminAccess,
|
||||
deleteCape,
|
||||
logPlayerAction,
|
||||
changeAdminPassword,
|
||||
changeAdminPassword
|
||||
}
|
||||
@ -61,6 +61,7 @@ async function authenticate({ identifier, password, clientToken, requireUser })
|
||||
uuid: userResult.user.uuid,
|
||||
username: userResult.user.username,
|
||||
clientToken: $clientToken,
|
||||
type: "Minecraft"
|
||||
}, keys.authenticationKeys.private, {
|
||||
subject: userResult.user.uuid,
|
||||
issuer: "LentiaYggdrasil",
|
||||
@ -105,6 +106,70 @@ async function authenticate({ identifier, password, clientToken, requireUser })
|
||||
}
|
||||
}
|
||||
|
||||
async function authenticateWithoutPassword({ identifier, requireUser }) {
|
||||
let userResult
|
||||
try {
|
||||
userResult = await authRepository.getUser(identifier, true)
|
||||
} catch (error) {
|
||||
if (error.code === 404) {
|
||||
throw new DefaultError(403, "Invalid credentials. Invalid username or password.", "ForbiddenOperationException")
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
delete userResult.user.password
|
||||
|
||||
const $clientToken = crypto.randomUUID()
|
||||
const accessToken = jwt.sign({
|
||||
uuid: userResult.user.uuid,
|
||||
username: userResult.user.username,
|
||||
clientToken: $clientToken,
|
||||
type: "Minecraft_OAuth2"
|
||||
}, keys.authenticationKeys.private, {
|
||||
subject: userResult.user.uuid,
|
||||
issuer: "LentiaYggdrasil",
|
||||
expiresIn: "1d",
|
||||
algorithm: "RS256"
|
||||
})
|
||||
|
||||
const clientSessionProcess = await authRepository.insertClientSession(accessToken, $clientToken, userResult.user.uuid)
|
||||
|
||||
const userObject = {
|
||||
clientToken: clientSessionProcess.clientToken,
|
||||
accessToken: clientSessionProcess.accessToken,
|
||||
availableProfiles: [{
|
||||
name: userResult.user.username,
|
||||
id: userResult.user.uuid,
|
||||
}],
|
||||
selectedProfile: {
|
||||
name: userResult.user.username,
|
||||
id: userResult.user.uuid,
|
||||
}
|
||||
}
|
||||
|
||||
if (requireUser) {
|
||||
try {
|
||||
const propertiesRequest = await authRepository.getPlayerProperties(userResult.user.uuid)
|
||||
userObject.user = {
|
||||
username: userResult.user.username,
|
||||
properties: propertiesRequest.properties
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== 404) throw error
|
||||
userObject.user = {
|
||||
username: userResult.user.username,
|
||||
properties: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
response: userObject
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function refreshToken({ previousAccessToken, clientToken, requireUser }) {
|
||||
let sessionCheck
|
||||
try {
|
||||
@ -125,6 +190,7 @@ async function refreshToken({ previousAccessToken, clientToken, requireUser }) {
|
||||
uuid: userResult.user.uuid,
|
||||
username: userResult.user.username,
|
||||
clientToken: $clientToken,
|
||||
type: "Minecraft"
|
||||
}, keys.authenticationKeys.private, {
|
||||
subject: userResult.user.uuid,
|
||||
issuer: "LentiaYggdrasil",
|
||||
@ -281,9 +347,10 @@ module.exports = {
|
||||
signout,
|
||||
validate,
|
||||
invalidate,
|
||||
registerUser,
|
||||
authenticate,
|
||||
refreshToken,
|
||||
registerUser,
|
||||
verifyAccessToken,
|
||||
checkUsernameAvailability
|
||||
checkUsernameAvailability,
|
||||
authenticateWithoutPassword,
|
||||
}
|
||||
134
services/oauth2Service.js
Normal file
134
services/oauth2Service.js
Normal file
@ -0,0 +1,134 @@
|
||||
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
|
||||
}
|
||||
@ -29,6 +29,10 @@ async function getPlayerProperty(uuid, key) {
|
||||
return await userRepository.getPlayerProperty(key, uuid)
|
||||
}
|
||||
|
||||
async function getPlayerPropertyByValue(key, value) {
|
||||
return await userRepository.getPlayerPropertyByValue(key, value)
|
||||
}
|
||||
|
||||
async function addPlayerProperty(uuid, key, value) {
|
||||
return await userRepository.addPropertyToPlayer(key, value, uuid)
|
||||
}
|
||||
@ -561,6 +565,7 @@ module.exports = {
|
||||
getPlayerCertificate,
|
||||
savePlayerCertificate,
|
||||
clearAllPlayerActions,
|
||||
getPlayerPropertyByValue,
|
||||
getPlayerNameChangeStatus,
|
||||
getPlayerUsernamesHistory,
|
||||
deleteExpiredCertificates,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user