diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e69de29..66fbc2e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/.github/workflows" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/main.yml similarity index 92% rename from .github/workflows/deploy.yml rename to .github/workflows/main.yml index 68ac073..bc59d5b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/main.yml @@ -65,7 +65,10 @@ jobs: run: rsync -avz -e 'ssh -i ~/.ssh/staging.key -o StrictHostKeyChecking=no' --progress ./build.tar.gz $SSH_USER@$SSH_HOST:/home/$SSH_USER/$REPO_NAME - name: Unzip - run: ssh staging "cd /home/$SSH_USER/$REPO_NAME/ && sudo tar --no-same-owner --no-same-permissions -xvf build.tar.gz" + run: ssh staging "cd /home/$SSH_USER/$REPO_NAME/ && sudo tar --no-same-owner --no-same-permissions -xf build.tar.gz" + + - name: Delete build zip + run: ssh staging "cd /home/$SSH_USER/$REPO_NAME/ && sudo rm build.tar.gz" - name: Delete current deployed files run: ssh staging 'sudo rm -r -d /var/www/netdbBeta' diff --git a/Assets/css/dialog.css b/Assets/css/dialog.css new file mode 100644 index 0000000..57a3b8f --- /dev/null +++ b/Assets/css/dialog.css @@ -0,0 +1,141 @@ +.dialog-container { + width: 100%; + background-color: rgba(0, 0, 0, .5); + height: 100vh; + position: fixed; + top: 0; + left: 0; + z-index: 2000; + justify-content: center; + align-items: center; + display: none; + opacity: 0; + animation: fadein .5s forwards; + } + + .dialog { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + background-color: #444; + flex-direction: column; + border-radius: 1em; + overflow: hidden; + min-width: 20%; + padding-bottom: 1em; + opacity: 0; + animation: fadein .5s forwards; + /* animation: scale .5s forwards; */ + /* transform: scale(.75); */ + } + + @keyframes fadein { + from { + opacity: 0; + } + + to { + opacity: 1; + } + } + + @keyframes scale { + from { + transform: scale(.75); + } + + to { + transform: scale(1); + } + } + + .dialog-container.visible { + display: block; + } + + .dialog-container.visible .dialog { + opacity: 1; + } + + .dialog-title { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1rem; + width: 100%; + color: white; + padding: .5em; + margin: 0; + } + + .dialog-content { + margin-bottom: 1rem; + width: 100%; + padding: .5em 2em; + } + + .dialog-button { + padding: 0.5rem 1rem; + border: 1px solid #ccc; + background-color: #fff; + cursor: pointer; + color: black; + width: 30%; + align-self: center; + border-radius: 11em; + } + + .dialog-button:hover { + background-color: #ccc; + } + + .dialog.info .dialog-title { + background-color: #3077d2; + } + + .dialog.error .dialog-title { + background-color: #c11f1f; + } + + .dialog.warning .dialog-title { + background-color: #ffa500; + } + + .dialog.debug .dialog-title { + background-color: #888; + } + + .dialog.pink .dialog-title { + background: linear-gradient(124deg, #ff2400, #e81d1d, #e8b71d, #e3e81d, #1de840, #1ddde8, #2b1de8, #dd00f3, #dd00f3); + background-size: 1800% 1800%; + animation: rainbow 18s ease infinite; + } + + @keyframes rainbow { + 0% { + background-position: 0% 82% + } + + 50% { + background-position: 100% 19% + } + + 100% { + background-position: 0% 82% + } + } + + .custom-dialog { + display: none; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + position: fixed; + z-index: 2100; + } + + .custom-dialog.visible { + display: block; + } + \ No newline at end of file diff --git a/Assets/css/profile.css b/Assets/css/profile.css index 7c99627..47a34cc 100644 --- a/Assets/css/profile.css +++ b/Assets/css/profile.css @@ -45,6 +45,10 @@ li a { text-decoration: none; } +h4, h5 { + margin: 0; +} + .settings-container { width: 100%; overflow: hidden; @@ -118,6 +122,38 @@ button:active { background-color: #393939; } +button[disabled] { + background-color: #222; + color: #4d4d4d; + cursor: pointer; +} + +select { + padding: 1em; + margin: .5em 0; + background-color: #393939; + border-radius: .2em; + color: #fff; + outline: none; + border: none; + outline: 2px solid transparent; + transition: all .2s ease-in-out; +} + +select:focus { + outline: 2px solid white; +} + +select:hover { + background-color: #4d4d4d; +} + +select[disabled] { + background-color: #222; + color: #4d4d4d; + cursor: pointer; +} + .connected-accounts { display: flex; gap: 1em; @@ -187,7 +223,6 @@ button:active { .connected-accounts h1:nth-child(4) { background-color: white; - color: black; } .connected-accounts h1:nth-child(5) { @@ -214,6 +249,7 @@ button:active { display: flex; align-items: center; margin-top: .5em; + gap: .5em; } .flex.end { @@ -290,20 +326,26 @@ th { overflow: hidden; } -.apiKeysTable td:nth-child(3) { +.apiKeysTable td:nth-child(2) { display: flex; gap: 1em; } -.apiKeysTable td:nth-child(3) button { +.apiKeysTable td:nth-child(2) button { padding: .8em; } +.apiKeysTable td { + overflow: hidden; + text-overflow: ellipsis; +} + .collapsible-content { padding: 0 18px; max-height: 0; overflow: hidden; transition: max-height 0.2s ease-out; + overflow-y: auto; } .collapsible-content > div:nth-child(1) { @@ -345,6 +387,11 @@ th { margin-left: auto; } +.ssoClients a { + color: #fff; + text-decoration: none; +} + .ssoRedirects > div{ display: flex; align-items: center; diff --git a/Assets/css/style.css b/Assets/css/style.css index 7edd5f0..b37dee6 100644 --- a/Assets/css/style.css +++ b/Assets/css/style.css @@ -6,115 +6,115 @@ body { } .stroke { - fill: none; - stroke-width: 3px; - stroke: white; - } + fill: none; + stroke-width: 3px; + stroke: white; +} - h1 { - margin: 0; - } +h1 { + margin: 0; +} - .st0 { - fill: none; - } +.st0 { + fill: none; +} - .st1 { - fill: url(#SVGID_1_); - } +.st1 { + fill: url(#SVGID_1_); +} - nav { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1em; - z-index: 10; - } +nav { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1em; + z-index: 10; +} - a { - color: white; - text-decoration: none; - } +a { + color: white; + text-decoration: none; +} - a:hover { - text-decoration: underline; - } +a:hover { + text-decoration: underline; +} - .logo { - width: 60vw; - margin-left: auto; - margin-right: auto; - position: relative; - margin-top: 25vh; - } +.logo { + width: 60vw; + margin-left: auto; + margin-right: auto; + position: relative; + margin-top: 25vh; +} - footer { - padding: 1em 12vw; - padding-top: 3em; - border-top: #666 solid 1px; - } +footer { + padding: 1em 12vw; + padding-top: 3em; + border-top: #666 solid 1px; +} - .logo-background { - position: absolute; - filter: blur(15vw); - z-index: -1; - } +.logo-background { + position: absolute; + filter: blur(15vw); + z-index: -1; +} - .logo-container { - background-repeat: no-repeat; - background-color: #ff000000; - background-image: radial-gradient(at 2% 0%, hsla(1, 0%, 34%, 1) 0px, black 50%); - height: 90vh; - width: 100%; - position: absolute; - top: 0; - z-index: -1; - } +.logo-container { + background-repeat: no-repeat; + background-color: #ff000000; + background-image: radial-gradient(at 2% 0%, hsla(1, 0%, 34%, 1) 0px, black 50%); + height: 90vh; + width: 100%; + position: absolute; + top: 0; + z-index: -1; +} - .container { - position: relative; - padding: 0 15vw; - } +.container { + position: relative; + padding: 0 15vw; +} - .feature-container { - display: flex; - justify-content: space-between; - gap: 1em; - flex-wrap: wrap; - width: 100%; - } +.feature-container { + display: flex; + justify-content: space-between; + gap: 1em; + flex-wrap: wrap; + width: 100%; +} - .feature-container .item { - background-color: #222222b0; - border-radius: 15px; - border: 1px solid #666; - padding: 1em; - width: calc(33% - 2em - 1em); - box-shadow: 0 0 10px #060606; - backdrop-filter: blur(16px); - } +.feature-container .item { + background-color: #222222b0; + border-radius: 15px; + border: 1px solid #666; + padding: 1em; + width: calc(33% - 2em - 1em); + box-shadow: 0 0 10px #060606; + backdrop-filter: blur(16px); +} - .service-container { - display: flex; - padding: 1em; - gap: 1em; - flex-wrap: wrap; - justify-content: center; - } +.service-container { + display: flex; + padding: 1em; + gap: 1em; + flex-wrap: wrap; + justify-content: center; +} - .service-container .item { - width: calc(33% - 2em - 1em); - border: 1px solid #444; - border-radius: 1em; - padding: 1em; - box-shadow: 0 0 10px #060606; - position: relative; - min-height: 10em; - background: linear-gradient(180deg,#242424,#121212 65.62%); - overflow: hidden; - } +.service-container .item { + width: calc(33% - 2em - 1em); + border: 1px solid #444; + border-radius: 1em; + padding: 1em; + box-shadow: 0 0 10px #060606; + position: relative; + min-height: 10em; + background: linear-gradient(180deg, #242424, #121212 65.62%); + overflow: hidden; +} - /* .service-container .item::before { +/* .service-container .item::before { content: ""; position: absolute; inset: 0; @@ -129,128 +129,130 @@ body { background: conic-gradient(from 100deg at 50% 50%,var(--accents-2) 0deg,var(--accents-2) 176deg,#fff 193deg,var(--accents-2) 217deg,var(--accents-2) 1turn); } */ - .service-container .item .footer { - font-size: .8em; - display: flex; - gap: .5em; - align-items: center; - position: absolute; - bottom: 1em; - } - - .service-container .item .footer hr { - border: 0; - background: #ffffff; - height: 19px; - width: 1px; - margin: 0; - } +.service-container .item .footer { + font-size: .8em; + display: flex; + gap: .5em; + align-items: center; + position: absolute; + bottom: 1em; +} - .service-container .item:nth-child(1) { - background: linear-gradient(180deg,#ffad1e69,#ffa2008a 65.62%); - } +.service-container .item .footer hr { + border: 0; + background: #ffffff; + height: 19px; + width: 1px; + margin: 0; +} - .service-container .item:nth-child(2) { - background: linear-gradient(180deg,#63a0d969,#63a0d98a 65.62%); - } +.service-container .item:nth-child(1) { + background: linear-gradient(180deg, #ffad1e69, #ffa2008a 65.62%); +} - .service-container .item:nth-child(5) { - background: linear-gradient(180deg,#c5120b69,#c5120b8a 65.62%); - } +.service-container .item:nth-child(2) { + background: linear-gradient(180deg, #63a0d969, #63a0d98a 65.62%); +} - .service-container .item:nth-child(6) { - background: linear-gradient(180deg,#6600ff69,#6600ff8a 65.62%); - } +.service-container .item:nth-child(5) { + background: linear-gradient(180deg, #c5120b69, #c5120b8a 65.62%); +} - footer hr { - border: 0; - background: gray; - height: 19px; - width: 1px; - margin: 0; - } +.service-container .item:nth-child(6) { + background: linear-gradient(180deg, #6600ff69, #6600ff8a 65.62%); +} - footer > div { - display: flex; - width: 100%; - gap: 1em; - } +footer hr { + border: 0; + background: gray; + height: 19px; + width: 1px; + margin: 0; +} - footer > div:nth-child(1) { - flex-wrap: wrap; - } +footer>div { + display: flex; + width: 100%; + gap: 1em; +} - @media (max-width: 600px) { - footer > div:nth-child(2) { - justify-content: center; - } +footer>div:nth-child(1) { + flex-wrap: wrap; +} - footer > div:nth-child(1) > div { - width: 100%; - } +@media (max-width: 600px) { + footer>div:nth-child(2) { + justify-content: center; } - @media (max-width: 1100px) { - .feature-container .item, .service-container .item { - width: 100%; - } + footer>div:nth-child(1)>div { + width: 100%; } +} - .service-outer { - padding-top: 6em; - padding-bottom: 5em; - } +@media (max-width: 1100px) { - .divider { - margin-top: 65vh; - margin-bottom: -36em; + .feature-container .item, + .service-container .item { + width: 100%; } +} - .divider svg { - margin-bottom: -.5em; - } +.service-outer { + padding-top: 6em; + padding-bottom: 5em; +} - .divider > div { - background: #222; - background: linear-gradient(180deg, #222 34%, rgba(66,53,53,0) 100%); - height: 20em; - } +.divider { + margin-top: 65vh; + margin-bottom: -36em; +} - .pfb { - width: 2.5em; - height: 2.5em; - border-radius: 50%; - background-color: #333; - animation: skeleton 2s infinite; - overflow: hidden; - border: 1px solid #666; - } +.divider svg { + margin-bottom: -.5em; +} - .name.loading { - width: 10em; - height: 1em; - border-radius: 0.5em; - background-color: #333; - animation: skeleton 2s infinite; - } +.divider>div { + background: #222; + background: linear-gradient(180deg, #222 34%, rgba(66, 53, 53, 0) 100%); + height: 20em; +} - .profile { - display: flex; - align-items: center; - justify-content: end; - gap: 1em; - } +.pfb { + width: 2.5em; + height: 2.5em; + border-radius: 50%; + background-color: #333; + animation: skeleton 2s infinite; + overflow: hidden; + border: 1px solid #666; +} - .profile img { - object-fit: cover; - } +.name.loading { + width: 10em; + height: 1em; + border-radius: 0.5em; + background-color: #333; + animation: skeleton 2s infinite; +} - @keyframes skeleton { - 50% { - background-color: #555; - } +.profile { + display: flex; + align-items: center; + justify-content: end; + gap: 1em; +} + +.profile img { + object-fit: cover; +} + +@keyframes skeleton { + 50% { + background-color: #555; } +} - .d-none { - display: none; - } \ No newline at end of file +.d-none { + display: none; +} \ No newline at end of file diff --git a/Assets/js/dialog.js b/Assets/js/dialog.js new file mode 100644 index 0000000..5020b05 --- /dev/null +++ b/Assets/js/dialog.js @@ -0,0 +1,119 @@ +export function createDialog(title = 'Information', text = 'No information available', type = 'info') { + const container = document.getElementById('dialog-container'); + + if (container) { + const dialog = document.createElement('div'); + dialog.classList.add('dialog'); + dialog.classList.add(type); + + const dialogTitle = document.createElement('h2'); + dialogTitle.classList.add('dialog-title'); + dialogTitle.textContent = title; + + const dialogText = document.createElement('p'); + dialogText.classList.add('dialog-content'); + dialogText.textContent = text; + + const dialogButton = document.createElement('button'); + dialogButton.classList.add('dialog-button'); + dialogButton.textContent = 'OK'; + + dialog.appendChild(dialogTitle); + dialog.appendChild(dialogText); + dialog.appendChild(dialogButton); + + dialogButton.addEventListener('click', () => { + if (container.children.length === 1) container.classList.remove('visible'); + + dialog.remove(); + }); + + container.appendChild(dialog); + container.classList.add('visible'); + } else console.error('Dialog container not found'); + } + + export function initDialog(element = undefined) { + element = element || window.document; + + Array.from(element.querySelectorAll('[target-dialog]')).forEach((button) => { + const target = button.getAttribute('target'); + const dialog = document.getElementById(target); + const submitButton = dialog.querySelector('[type="submit"]'); + + dialog.hide = () => { + dialog.classList.remove('visible'); + document.getElementById('dialog-container').classList.remove('visible'); + dialog.dispatchEvent(new CustomEvent('onHide', { + detail: { + reason: 'submit' + } + })); + }; + + dialog.show = () => { + dialog.classList.add('visible'); + document.getElementById('dialog-container').classList.add('visible'); + dialog.dispatchEvent(new CustomEvent('onShow')); + }; + + if (dialog === undefined) { + console.error('Dialog not found'); + return; + } + + const onShowEvent = dialog.getAttribute('onShow'); + if (onShowEvent !== undefined) dialog.addEventListener('onShow', () => eval(onShowEvent)); + + submitButton.addEventListener('click', (event) => { + dialog.hide(); + }); + + button.addEventListener('click', () => { + dialog.classList.add('visible'); + document.getElementById('dialog-container').classList.add('visible'); + + dialog.dispatchEvent(new CustomEvent('onShow')); + + setTimeout(() => { + window.addEventListener( + 'click', + function _listener(e) { + if (e.target.closest('#' + dialog.id) === null) { + dialog.classList.remove('visible'); + dialog.dispatchEvent(new CustomEvent('onHide')); + window.removeEventListener('click', _listener, true); + } + }, + true + ); + }, 100); + }); + }); + + if (document.getElementById('dialog-container') !== null) return; + + const container = document.createElement('div'); + container.id = 'dialog-container'; + container.classList.add('dialog-container'); + + //add event listener to window + container.addEventListener('click', (event) => { + if (event.target === container) { + container.classList.remove('visible'); + + Array.from(container.querySelectorAll('.dialog')).forEach((dialog) => { + dialog.classList.remove('visible'); + dialog.dispatchEvent(new CustomEvent('onHide', { detail: { reason: 'canceled' }})); + }); + + Array.from(document.querySelectorAll('.custom-dialog.visible')).forEach((dialog) => { + dialog.classList.remove('visible'); + dialog.dispatchEvent(new CustomEvent('onHide', { detail: { reason: 'canceled' }})); + }); + } + }); + + document.body.appendChild(container); + } + \ No newline at end of file diff --git a/Assets/js/profile.js b/Assets/js/profile.js index 02c02c8..a205bfc 100644 --- a/Assets/js/profile.js +++ b/Assets/js/profile.js @@ -1,3 +1,5 @@ +import { createDialog, initDialog } from './dialog.js'; + const languages = [ { key: 'en-us', name: 'English' }, { key: 'de-at', name: 'German' }, @@ -63,16 +65,18 @@ const countries = [ ]; var currentUser; +initDialog(); document.getElementById('pi_save').addEventListener('click', savePersonalInformation); document.getElementById('cp_save').addEventListener('click', changePassword); document.getElementById('createApiKey').addEventListener('click', createApiKey); -document.getElementById('ca_spotify_link').addEventListener('click', () => LinkAccounts("spotify")); -document.getElementById('ca_twitch_link').addEventListener('click', () => LinkAccounts("twitch")); -document.getElementById('ca_discord_link').addEventListener('click', () => LinkAccounts("discord")); -document.getElementById('ca_google_link').addEventListener('click', () => LinkAccounts("google")); -document.getElementById('ca_github_link').addEventListener('click', () => LinkAccounts("github")); +document.getElementById('ca_spotify_link').addEventListener('click', () => LinkAccounts('spotify')); +document.getElementById('ca_twitch_link').addEventListener('click', () => LinkAccounts('twitch')); +document.getElementById('ca_discord_link').addEventListener('click', () => LinkAccounts('discord')); +document.getElementById('ca_google_link').addEventListener('click', () => LinkAccounts('google')); +document.getElementById('ca_github_link').addEventListener('click', () => LinkAccounts('github')); document.getElementById('logout').addEventListener('click', () => LoginManager.logout()); document.getElementById('deleteAccount').addEventListener('click', async (e) => await doubleClickButton(e, deleteAccount)); +document.getElementById('createSsoCredentials').addEventListener('click', async () => await createSsoCredentials()); LoginManager.isLoggedIn().then(async (e) => { if (!e) { @@ -83,30 +87,11 @@ LoginManager.isLoggedIn().then(async (e) => { const token = LoginManager.getCookie('token'); const urlParams = new URLSearchParams(window.location.search); - + if (urlParams.has('code')) { const code = urlParams.get('code'); - const provider = localStorage.getItem('linkType'); - - const req = await fetch('https://api.login.netdb.at/link/' + provider + '?code=' + code, { - method: 'GET', - headers: { - Authorization: 'Bearer ' + token, - 'Content-Type': 'application/json', - }, - }); - - if (req.status == 401) { - window.location.href = 'https://login.netdb.at?redirect=' + encodeURIComponent(window.location.href); - return; - } - - localStorage.removeItem('linkType'); - urlParams.delete('code'); - - window.history.replaceState({}, document.title, window.location.pathname + window.location.search); - - alert("Account successfully linked!"); + + LinkAccount(code); } const req = await fetch('https://api.login.netdb.at/user', { @@ -145,37 +130,38 @@ LoginManager.isLoggedIn().then(async (e) => { if (user.twitchId !== null) document.getElementById('ca_twitch').classList.add('connected'); - if (user.githubId !== null) - document.getElementById("ca_github").classList.add("connected"); + if (user.githubId !== null) document.getElementById('ca_github').classList.add('connected'); - if (user.googleId !== null) - document.getElementById("ca_google").classList.add("connected"); + if (user.googleId !== null) document.getElementById('ca_google').classList.add('connected'); Array.from(document.getElementsByClassName('connected')).forEach((element) => { element.addEventListener('click', disconnectAccount); }); if (user['2fa'] && user['2faType'] == 'App') document.getElementById('cp_2fa').classList.remove('d-none'); - if (user['2fa'] && user['2faType'] == 'App') document.getElementById('deleteAccount2fa').classList.remove('d-none'); - if (user['2fa']) - document.getElementById('2fa_enable').checked = true; - else - document.getElementById('2fa_disable').checked = true; + if (user['2fa']) { + document.getElementById('2fa_status').innerText = 'Enabled'; + document.getElementById('2fa_type').value = user['2faType'] == 'App' ? 0 : user['2faType'] == 'Mail' ? 1 : 2; + document.getElementById('2fa_type').disabled = true; + document.getElementById('2fa_enable').innerText = 'Disable'; + document.getElementById('2fa_enable').addEventListener('click', disable2fa); + } else { + document.getElementById('2fa_enable').addEventListener('click', enable2fa); + } - initSearchbar(countries, "country_search"); - initSearchbar(languages, "language_search"); + initSearchbar(countries, 'country_search'); + initSearchbar(languages, 'language_search'); user.api_keys.forEach((key) => { - document.getElementById('apiKeysTable').appendChild(createApiKeyRow(key, "*************")); + document.getElementById('apiKeysTable').appendChild(createApiKeyRow(key, '*************')); }); - if(user.trusted_sso_clients.length > 0) - document.getElementById('thirdPartyAppsContainer').innerHTML = ''; + if (user.trusted_sso_clients.length > 0) document.getElementById('thirdPartyAppsContainer').innerHTML = ''; user.trusted_sso_clients.forEach((client) => { const row = document.createElement('div'); - row.id = "trusted_" + client.id; + row.id = 'trusted_' + client.id; const img = document.createElement('img'); img.src = client.logo; img.alt = client.name; @@ -191,30 +177,58 @@ LoginManager.isLoggedIn().then(async (e) => { document.getElementById('thirdPartyAppsContainer').appendChild(row); }); - if(user.sso_clients.length > 0) - document.getElementById('ssoClientsContainer').innerHTML = ''; + if (user.sso_clients.length > 0) document.getElementById('ssoClientsContainer').innerHTML = ''; user.sso_clients.forEach((client) => { document.getElementById('ssoClientsContainer').appendChild(createSSOClient(client.logo, client.name, client.url, client.id, client.secret, client.redirects)); }); Array.from(document.getElementsByClassName('collapsible')).forEach((element) => { - element.addEventListener("click", function() { - this.classList.toggle("active"); - - if (this.nextElementSibling.style.maxHeight) - this.nextElementSibling.style.maxHeight = null; - else - this.nextElementSibling.style.maxHeight = this.nextElementSibling.scrollHeight + "px"; - + element.addEventListener('click', function () { + this.classList.toggle('active'); + + if (this.nextElementSibling.style.maxHeight) this.nextElementSibling.style.maxHeight = null; + else this.nextElementSibling.style.maxHeight = this.nextElementSibling.scrollHeight + 'px'; }); }); - + Array.from(document.getElementsByTagName('input')).forEach((element) => { element.addEventListener('keyup', (e) => e.target.classList.remove('invalid')); }); }); +async function LinkAccount(code) { + const provider = localStorage.getItem('linkType'); + + await LoginManager.validateToken(); + const req = await fetch('https://api.login.netdb.at/link/' + provider + '?code=' + code, { + method: 'GET', + headers: { + Authorization: 'Bearer ' + LoginManager.getCookie('token'), + 'Content-Type': 'application/json', + }, + }); + + if (req.status == 401) { + window.location.href = 'https://login.netdb.at?redirect=' + encodeURIComponent(window.location.href); + return; + } + + const res = await req.json(); + + if (res.statusCode != 200) { + createDialog('Error', 'An error occured while linking your account!', 'error'); + return; + } + + localStorage.removeItem('linkType'); + urlParams.delete('code'); + + window.history.replaceState({}, document.title, window.location.pathname + window.location.search); + + createDialog('Success', 'Account successfully linked!', 'info'); +} + async function deleteTrustedSsoClient(clientId) { await LoginManager.validateToken(); const req = await fetch('https://api.login.netdb.at/oauth/untrust', { @@ -223,7 +237,7 @@ async function deleteTrustedSsoClient(clientId) { Authorization: 'Bearer ' + LoginManager.getCookie('token'), 'Content-Type': 'application/json', }, - body: "\"" + clientId + "\"", + body: '"' + clientId + '"', }); if (req.status == 401) { @@ -238,7 +252,80 @@ async function deleteTrustedSsoClient(clientId) { return; } - document.getElementById("trusted_" + clientId).remove(); + document.getElementById('trusted_' + clientId).remove(); + + const container = document.getElementById('thirdPartyAppsContainer'); + container.innerHTML = '

No third party apps connected!

'; +} + +async function createSsoCredentials() { + document + .getElementById('createSSODialog') + .querySelectorAll('input') + .forEach((element) => { + element.value = ''; + }); + + document.getElementById('createSSODialog').show(); + + const canceled = await new Promise((resolve) => { + document.getElementById('createSSODialog').addEventListener( + 'onHide', + (e) => { + if (e.detail.reason == 'canceled') { + resolve(false); + return; + } + + resolve(true); + }, + { once: true } + ); + }); + + if (!canceled) return; + + const name = document.getElementById('c_sso_name').value; + const websiteUrl = document.getElementById('c_sso_url').value; + const logoUrl = document.getElementById('c_sso_logoUrl').value; + + await LoginManager.validateToken(); + const req = await fetch('https://api.login.netdb.at/user/sso', { + method: 'POST', + headers: { + Authorization: 'Bearer ' + LoginManager.getCookie('token'), + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: name, + url: websiteUrl, + logoUrl: logoUrl, + }), + }); + + if (req.status == 401) { + window.location.href = 'https://login.netdb.at?redirect=' + encodeURIComponent(window.location.href); + return; + } + + const res = await req.json(); + + if (req.status != 200 || res.statusCode != 203) { + createDialog('Error', 'An error occured while creating the SSO credentials!', 'error'); + return; + } + + if (document.getElementById('ssoClientsContainer').children[0].tagName == 'P') document.getElementById('ssoClientsContainer').innerHTML = ''; + + const item = createSSOClient(logoUrl, name, websiteUrl, res.data.clientId, res.data.clientSecret, []); + document.getElementById('ssoClientsContainer').appendChild(item); + + item.children[0].addEventListener('click', function () { + item.classList.toggle('active'); + + if (item.children[0].nextElementSibling.style.maxHeight) item.children[0].nextElementSibling.style.maxHeight = null; + else item.children[0].nextElementSibling.style.maxHeight = item.children[0].nextElementSibling.scrollHeight + 'px'; + }); } function createSSOClient(logoUrl, clientName, websiteUrl, clientId, clientSecret, redirects) { @@ -252,7 +339,6 @@ function createSSOClient(logoUrl, clientName, websiteUrl, clientId, clientSecret item.querySelector('h1').innerText = clientName; item.querySelector('a').href = websiteUrl; item.querySelector('a').innerText = websiteUrl; - // item.querySelector('button').addEventListener('click', () => deleteSSOClient(clientId)); const content = item.querySelector('.collapsible-content'); content.querySelector('#sso_clientId').value = clientId; @@ -261,13 +347,14 @@ function createSSOClient(logoUrl, clientName, websiteUrl, clientId, clientSecret content.querySelector('#sso_websiteUrl').value = websiteUrl; content.querySelector('#sso_name').value = clientName; content.querySelector('#sso_save').addEventListener('click', async () => await saveSSOClient(clientId)); + item.querySelector('#sso_delete').addEventListener('click', async () => await deleteSSOClient(clientId)); content.querySelector('#sso_addRedirect').addEventListener('click', async () => await addSSORedirect(clientId)); const redirectsContainer = content.querySelector('#sso_redirects'); redirects.forEach((redirect) => { const redirectItem = document.createElement('div'); - redirectItem.id = "sso_redirect_" + redirect.id; + redirectItem.id = 'sso_redirect_' + redirect.id; const url = document.createElement('input'); url.type = 'text'; @@ -286,6 +373,32 @@ function createSSOClient(logoUrl, clientName, websiteUrl, clientId, clientSecret return item; } +async function deleteSSOClient(clientId) { + await LoginManager.validateToken(); + const req = await fetch('https://api.login.netdb.at/user/sso', { + method: 'DELETE', + headers: { + Authorization: 'Bearer ' + LoginManager.getCookie('token'), + 'Content-Type': 'application/json', + }, + body: '"' + clientId + '"', + }); + + if (req.status == 401) { + window.location.href = 'https://login.netdb.at?redirect=' + encodeURIComponent(window.location.href); + return; + } + + const res = await req.json(); + + if (req.status != 200 || res.statusCode != 200) { + createDialog('Error', 'An error occured while deleting the SSO credentials!', 'error'); + return; + } + + document.getElementById('sso_' + clientId).remove(); +} + async function deleteSSORedirect(clientId, redirectId) { await LoginManager.validateToken(); const req = await fetch('https://api.login.netdb.at/user/sso/redirects', { @@ -300,9 +413,9 @@ async function deleteSSORedirect(clientId, redirectId) { }), }); - if (req.status == 401) { - window.location.href = 'https://login.netdb.at?redirect=' + encodeURIComponent(window.location.href); - return; + if (req.status == 401) { + window.location.href = 'https://login.netdb.at?redirect=' + encodeURIComponent(window.location.href); + return; } const res = await req.json(); @@ -312,11 +425,14 @@ async function deleteSSORedirect(clientId, redirectId) { return; } - document.getElementById("sso_redirect_" + redirectId).remove(); + document.getElementById('sso_redirect_' + redirectId).remove(); } async function addSSORedirect(clientId) { - const item = document.getElementById("sso_" + clientId); + const item = document.getElementById('sso_' + clientId); + const redirects = item.querySelector('#sso_redirects'); + + //TODO: add button feedback await LoginManager.validateToken(); const req = await fetch('https://api.login.netdb.at/user/sso/redirects', { @@ -343,8 +459,10 @@ async function addSSORedirect(clientId) { return; } + item.querySelector('#sso_addRedirect').previousElementSibling.value = ''; + const redirect = document.createElement('div'); - redirect.id = "sso_redirect_" + res.data.id; + redirect.id = 'sso_redirect_' + res.data.id; const url = document.createElement('input'); url.type = 'text'; url.value = res.data.url; @@ -355,12 +473,16 @@ async function addSSORedirect(clientId) { deleteBtn.addEventListener('click', async () => await deleteSSORedirect(clientId, res.data.id)); redirect.appendChild(deleteBtn); - console.log(redirect); - document.getElementById("sso_" + clientId).querySelector('#sso_redirects').appendChild(redirect); + if (redirects.children.length > 1) { + redirects.insertBefore(redirect, redirects.children[1]); + return; + } + + redirects.append(redirect); } async function saveSSOClient(clientId) { - const item = document.getElementById("sso_" + clientId); + const item = document.getElementById('sso_' + clientId); await LoginManager.validateToken(); const req = await fetch('https://api.login.netdb.at/user/sso', { @@ -405,10 +527,10 @@ async function doubleClickButton(e, func) { }, 3000); } -function createApiKeyRow(client_id, client_secret) { +function createApiKeyRow(client_id) { const row = document.createElement('tr'); row.id = client_id; - row.innerHTML = '' + client_id + '' + client_secret + ''; + row.innerHTML = '' + client_id + ''; const td = document.createElement('td'); const deleteBtn = document.createElement('button'); const regenBtn = document.createElement('button'); @@ -447,7 +569,11 @@ async function createApiKey() { return; } - document.getElementById('apiKeysTable').appendChild(createApiKeyRow(res.data.clientId, res.data.clientSecret)); + document.getElementById('apiKeysTable').appendChild(createApiKeyRow(res.data.clientId)); + + //TODO: Dialog + alert('Successfully created API key! Please save it now, as it will not be shown again!'); + alert(res.data.clientSecret); } async function regenerateApiKey(clientId) { @@ -458,7 +584,7 @@ async function regenerateApiKey(clientId) { Authorization: 'Bearer ' + LoginManager.getCookie('token'), 'Content-Type': 'application/json', }, - body: "\"" + clientId + "\"", + body: '"' + clientId + '"', }); if (req.status == 401) { @@ -473,7 +599,9 @@ async function regenerateApiKey(clientId) { return; } - document.getElementById(clientId).children[1].innerText = res.data.clientSecret; + //TODO: Dialog + alert('Successfully regenerated API key! Please save it now, as it will not be shown again!'); + alert(res.data.clientSecret); } async function deleteApiKey(clientId) { @@ -484,7 +612,7 @@ async function deleteApiKey(clientId) { Authorization: 'Bearer ' + LoginManager.getCookie('token'), 'Content-Type': 'application/json', }, - body: "\"" + clientId + "\"", + body: '"' + clientId + '"', }); if (req.status == 401) { @@ -496,27 +624,28 @@ async function deleteApiKey(clientId) { } function LinkAccounts(type) { - localStorage.setItem("linkType", type); + localStorage.setItem('linkType', type); switch (type) { - case "spotify": { - window.location.href = "https://accounts.spotify.com/de/authorize?client_id=a7c2014c0531405983d7050277dee3cb&response_type=code&redirect_uri=https://new.netdb.at/profile&scope=user-read-private%20user-read-email"; + case 'spotify': { + window.location.href = 'https://accounts.spotify.com/de/authorize?client_id=a7c2014c0531405983d7050277dee3cb&response_type=code&redirect_uri=https://new.netdb.at/profile&scope=user-read-private%20user-read-email'; break; } - case "discord": { - window.location.href = "https://discord.com/api/oauth2/authorize?client_id=802237562625196084&redirect_uri=https://new.netdb.at/profile&response_type=code&scope=identify%20email"; + case 'discord': { + window.location.href = 'https://discord.com/api/oauth2/authorize?client_id=802237562625196084&redirect_uri=https://new.netdb.at/profile&response_type=code&scope=identify%20email'; break; } - case "twitch": { - window.location.href = "https://id.twitch.tv/oauth2/authorize?client_id=okxhfdyyoyx724c5zf0h869x9ry1sx&redirect_uri=https://new.netdb.at/profile&response_type=code&scope=user_read"; + case 'twitch': { + window.location.href = 'https://id.twitch.tv/oauth2/authorize?client_id=okxhfdyyoyx724c5zf0h869x9ry1sx&redirect_uri=https://new.netdb.at/profile&response_type=code&scope=user_read'; break; } - case "github": { - window.location.href = "https://github.com/login/oauth/authorize?scope=user:email&client_id=de5e22518d66ab50a805"; + case 'github': { + window.location.href = 'https://github.com/login/oauth/authorize?scope=user:email&client_id=de5e22518d66ab50a805'; break; } - case "google": { - window.location.href = "https://accounts.google.com/o/oauth2/v2/auth?scope=https%3A//www.googleapis.com/auth/userinfo.email&access_type=offline&include_granted_scopes=true&response_type=code&state=state_parameter_passthrough_value&redirect_uri=https://new.netdb.at/profile&client_id=736018590984-nh2ifch6ps8art9v35avipv16se1b720.apps.googleusercontent.com"; + case 'google': { + window.location.href = + 'https://accounts.google.com/o/oauth2/v2/auth?scope=https%3A//www.googleapis.com/auth/userinfo.email&access_type=offline&include_granted_scopes=true&response_type=code&state=state_parameter_passthrough_value&redirect_uri=https://new.netdb.at/profile&client_id=736018590984-nh2ifch6ps8art9v35avipv16se1b720.apps.googleusercontent.com'; break; } } @@ -524,25 +653,28 @@ function LinkAccounts(type) { function initSearchbar(data, id) { let searchbar = document.getElementById(id); - let inputBox = searchbar.querySelector("input"); + let inputBox = searchbar.querySelector('input'); - if (inputBox.dataset.key && inputBox.dataset.key != "null") - inputBox.value = data.find((e) => e.key == inputBox.dataset.key).name; + try { + if (inputBox.dataset.key && inputBox.dataset.key != 'null') inputBox.value = data.find((e) => e.key == inputBox.dataset.key).name; + } catch (e) { + console.log(e); + } showSuggestions(data, searchbar); - inputBox.addEventListener("keyup", (e) => filterSearch(e.target.value, data, searchbar)); - inputBox.addEventListener("focus", () => searchbar.querySelector('.autocom-box').classList.add("active")); - searchbar.addEventListener("focusout", (e) => { + inputBox.addEventListener('keyup', (e) => filterSearch(e.target.value, data, searchbar)); + inputBox.addEventListener('focus', () => searchbar.querySelector('.autocom-box').classList.add('active')); + searchbar.addEventListener('focusout', (e) => { setTimeout(() => { - searchbar.querySelector('.autocom-box').classList.remove("active"); - }, 200); + searchbar.querySelector('.autocom-box').classList.remove('active'); + }, 300); }); inputBox.onkeydown = (e) => { - if (e.key == "Enter") { - searchbar.querySelector('input').value = document.querySelector('.autocom-box h1').innerText; - searchbar.querySelector('input').dataset.key = document.querySelector('.autocom-box h1').dataset.key; + if (e.key == 'Enter') { + searchbar.querySelector('input').value = searchbar.querySelector('.autocom-box h1').innerText; + searchbar.querySelector('input').dataset.key = searchbar.querySelector('.autocom-box h1').dataset.key; } }; } @@ -570,7 +702,7 @@ function showSuggestions(list, searchbar) { return (data = '

' + data.name + '

'); }); - let listData; + let listData = ''; if (!list.length) { // const userValue = searchbar.querySelector('input').value; // listData = '

' + userValue + '

'; @@ -663,12 +795,14 @@ async function changePassword() { return; } - LoginManager.deleteCookie("token"); - LoginManager.deleteCookie("refreshToken"); + LoginManager.deleteCookie('token'); + LoginManager.deleteCookie('refreshToken'); window.location.href = 'https://login.netdb.at?redirect=' + encodeURIComponent(window.location.href); } function validatePw(oldPw, pw, rpw) { + if (pw == null || rpw == null) return 'Please fill out all fields'; + if (pw.length < 8) return 'The password has to be at least 8 characters long'; if (!isUpperCase(pw)) return 'The password has to contain at least one uppercase letter'; @@ -699,10 +833,10 @@ async function disconnectAccount(e) { await LoginManager.validateToken(); const req = await fetch('https://api.login.netdb.at/unlink/' + element.dataset.type, { - method: "GET", + method: 'GET', headers: { - Authorization: "Bearer " + LoginManager.getCookie("token"), - } + Authorization: 'Bearer ' + LoginManager.getCookie('token'), + }, }); if (req.status == 401) { @@ -714,17 +848,21 @@ async function disconnectAccount(e) { } async function deleteAccount() { + const creds = await getCreds(true); + + if (!creds) return; + await LoginManager.validateToken(); const req = await fetch('https://api.login.netdb.at/user', { - method: "DELETE", + method: 'DELETE', headers: { - Authorization: "Bearer " + LoginManager.getCookie("token"), + Authorization: 'Bearer ' + LoginManager.getCookie('token'), 'Content-Type': 'application/json', }, body: JSON.stringify({ - Password: document.getElementById('deleteAccountPassword').value, - TwoFaToken: currentUser['2fa'] ? document.getElementById('deleteAccount2fa').value : null - }) + Password: creds.password, + TwoFaToken: creds.mfaToken, + }), }); if (req.status == 401) { @@ -735,4 +873,201 @@ async function deleteAccount() { LoginManager.deleteCookie('token', '/', '.netdb.at'); LoginManager.deleteCookie('refreshToken', '/', '.netdb.at'); window.location.href = 'https://login.netdb.at?redirect=' + encodeURIComponent(window.location.href); -} \ No newline at end of file +} + +async function enable2fa() { + const mfaType = document.getElementById('2fa_type').value; + + if (mfaType == 2) { + alert('Select a valid 2FA type!'); + return; + } + + await LoginManager.validateToken(); + const req = await fetch('https://api.login.netdb.at/2fa/activate', { + method: 'POST', + headers: { + Authorization: 'Bearer ' + LoginManager.getCookie('token'), + 'Content-Type': 'application/json', + }, + body: mfaType == 0 ? '"app"' : mfaType == 1 ? '"mail"' : null, + }); + + if (req.status == 401) { + window.location.href = 'https://login.netdb.at?redirect=' + encodeURIComponent(window.location.href); + return; + } + + if (req.status != 200) { + createDialog('Error', 'An error occured while enabling 2FA!', 'error'); + return; + } + + const res = await req.json(); + + if (mfaType == 0) { //App + const qr = res.data.qrCodeSetupImageUrl; + const secret = res.data.manualEntryKey; + + document.getElementById('authenticatorDialog').querySelector('img').src = qr; + document.getElementById('authenticatorDialog').querySelector('h1').innerText = secret; + document.getElementById('authenticatorDialog').show(); + + const canceled = await new Promise((resolve) => { + document.getElementById('authenticatorDialog').addEventListener( + 'onHide', + (e) => { + if (e.detail.reason == 'canceled') { + resolve(false); + return; + } + + resolve(true); + }, + { once: true } + ); + }); + + if (!canceled) return false; + } + + await verify2fa(); +} + +async function verify2fa() { + const creds = await getCreds(true, false, true); + + if (!creds) return; + + await LoginManager.validateToken(); + const req = await fetch('https://api.login.netdb.at/2fa/verify', { + method: 'POST', + headers: { + Authorization: 'Bearer ' + LoginManager.getCookie('token'), + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + MFAToken: creds.mfaToken, + Password: creds.password, + }), +}); + + if (req.status == 401) { + window.location.href = 'https://login.netdb.at?redirect=' + encodeURIComponent(window.location.href); + return false; + } + + if (req.status != 200) { + createDialog('Error', 'An error occured while verifying 2FA!', 'error'); + return false; + } + + const res = await req.json(); + + if (res.statusCode != 200) { + createDialog('Error', 'An error occured while verifying 2FA!', 'error'); + return false; + } + + createDialog('Success', '2FA successfully enabled!', 'info'); +} + +async function disable2fa() { + const creds = await getCreds(true); + + if (!creds) return; + + const success = await disableMfaRequest(creds.password, creds.mfaToken); + + if (!success) { + const mfaToken = await getCreds(true, true); + + if (!mfaToken) return; + + await disableMfaRequest(creds.password, mfaToken.mfaToken); + } +} + +async function disableMfaRequest(password, mfaToken) { + await LoginManager.validateToken(); + const req = await fetch('https://api.login.netdb.at/2fa/deactivate', { + method: 'POST', + headers: { + Authorization: 'Bearer ' + LoginManager.getCookie('token'), + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + Password: password, + MFAToken: mfaToken, + }), + }); + + if (req.status == 401) { + window.location.href = 'https://login.netdb.at?redirect=' + encodeURIComponent(window.location.href); + return; + } + + const res = await req.json(); + + if (res.statusCode == 409) + return false; + + if (req.status != 200 || res.statusCode != 200) + createDialog('Error', 'An error occured while disabling 2FA!', 'error'); + + return true; +} + +async function getCreds(mfa = false, mfaOnly = false, forceMfa = false) { + document.getElementById('passwordInput').value = ''; + document.getElementById('2faInput').value = ''; + + if ((mfa && (currentUser['2fa'] == "App" || mfaOnly)) || forceMfa) document.getElementById('2faInput').classList.remove('d-none'); + else document.getElementById('2faInput').classList.add('d-none'); + + if (mfaOnly) document.getElementById('passwordInput').classList.add('d-none'); + else document.getElementById('passwordInput').classList.remove('d-none'); + + document.getElementById('passwordDialog').show(); + + const res = await new Promise((resolve) => { + document.getElementById('passwordDialog').addEventListener( + 'onHide', + (e) => { + if (e.detail.reason == 'canceled') { + resolve(false); + return; + } + + resolve(true); + }, + { once: true } + ); + }); + + if (!res) return false; + + const pw = document.getElementById('passwordInput').classList.contains('d-none') ? null : document.getElementById('passwordInput').value; + const mfaToken = document.getElementById('2faInput').classList.contains('d-none') ? null : document.getElementById('2faInput').value; + + if (pw != null && validatePw(null, pw, pw)) { + createDialog('Invalid password', 'The password you entered is invalid!', 'error'); + return false; + } + if (mfaToken != null && !validateMfaToken(mfaToken)) { + createDialog('Invalid 2FA token', 'The 2FA token you entered is invalid!', 'error'); + return false; + } + + return { + password: pw, + mfaToken: mfaToken, + }; +} + +function validateMfaToken(token) { + if (token.length != 6) return false; + if (!isNumber(token)) return false; + + return true; +} diff --git a/profile/index.html b/profile/index.html index 8d50e68..a0dcb51 100644 --- a/profile/index.html +++ b/profile/index.html @@ -37,8 +37,8 @@ - - + + @@ -57,12 +57,11 @@

Security

Developer

  • Api Keys
  • Sso Credentials
  • +
  • +
    Logout
    +
    Delete Account
    +
  • - -
    -
    Logout
    -
    Delete Account
    -
    @@ -75,7 +74,7 @@

    Personal Information

    Two Factor Authentication

    -
    - - +

    Status:

    +

    Disabled

    - - +

    Type:

    + +
    + +
    +
    @@ -205,7 +211,6 @@

    Api Keys

    -
    KeySecret Actions
    @@ -258,6 +263,7 @@

    +
    @@ -280,15 +286,32 @@

    Danger Zone

    Warning: Deleting your account is irreversible. All your data will be deleted and cannot be restored.

    -
    - - -
    -
    + + + +
    + + + + +
    +
    + Qr Code +

    + + +
    +
    + + + + + +
    \ No newline at end of file