From f47e6646db2ee1011d331b7ddb848e5a96e77118 Mon Sep 17 00:00:00 2001 From: danielhoward Date: Sat, 23 Sep 2023 16:17:01 +0100 Subject: [PATCH 01/23] Add base logging in --- public/auth/index.html | 44 ++++++++++++++++++++++++++++++++ public/index.html | 13 ++++++++-- src/auth.ts | 57 ++++++++++++++++++++++++++++++++++++++++++ src/auth/index.ts | 34 +++++++++++++++++++++++++ src/core.ts | 2 +- src/index.ts | 4 +++ webpack.config.js | 12 ++++++++- 7 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 public/auth/index.html create mode 100644 src/auth.ts create mode 100644 src/auth/index.ts diff --git a/public/auth/index.html b/public/auth/index.html new file mode 100644 index 0000000..494c9a5 --- /dev/null +++ b/public/auth/index.html @@ -0,0 +1,44 @@ + + + + + + + + + Logging you into your account... + + + + + + + +
+
+
+ Loading... +
+
+ + +
+ + \ No newline at end of file diff --git a/public/index.html b/public/index.html index ddd2bfb..10b4306 100644 --- a/public/index.html +++ b/public/index.html @@ -275,14 +275,23 @@
-
+
- +
+
+ + +
diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..8b21de6 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,57 @@ +import {$, makeClassToggler} from './core'; + +const loginButton = $('loginButton'); +const loginLoading = $('loginLoading'); +const loginLoadingText = $('loginLoadingText'); + +const showLoadingInfo = makeClassToggler(loginLoading, 'hidden', true, (enabled) => loginButton.disabled = enabled); + +const isDevelopment = process.env.NODE_ENV === 'development'; +const searchParams = new URLSearchParams(window.location.search); + +const ssoDevPort = searchParams.get('ssodevport'); +const ssoOrigin = ssoDevPort === null ? 'https://sso.danielhoward.me' : `http://local.danielhoward.me:${ssoDevPort}`; +const ssoPath = `${ssoOrigin}/auth?target=chaos${isDevelopment ? '&devport=3001' : ''}`; + +const backendDevPort = searchParams.get('backenddevport'); +const backendOrigin = backendDevPort === null ? 'https://chaos-backend.danielhoward.me' : `http://local.danielhoward.me:${backendDevPort}`; + +let loginWindow: Window; + +function onLoginClick() { + if (loginWindow?.closed === false) return loginWindow.focus(); + + showLoadingInfo(true); + loginLoadingText.textContent = 'Waiting for authentication in popup window'; + loginWindow = window.open(ssoPath, '', 'width=500, height=600'); + + const interval = setInterval(() => { + // Test if the page has been closed + if (loginWindow.closed) { + showLoadingInfo(false); + clearInterval(interval); + return; + } + + // Test if the origin has changed, meaning it has been authed + let originsMatch = false; + try { + originsMatch = window.location.origin === loginWindow.location.origin; + } catch (err) { + // Ignore error thrown by browser since it is expected when on a different origin + } + if (originsMatch) { + clearInterval(interval); + loginWindow.close(); + onAuth(); + } + }, 1000); +} + +function onAuth() { + loginLoadingText.textContent = 'Fetching your saves from the server'; +} + +export function onload() { + loginButton.addEventListener('click', onLoginClick); +} diff --git a/src/auth/index.ts b/src/auth/index.ts new file mode 100644 index 0000000..acaac74 --- /dev/null +++ b/src/auth/index.ts @@ -0,0 +1,34 @@ +const loadingSpinner = document.getElementById('loadingSpinner'); +const errorAlert = document.getElementById('errorAlert'); +const success = document.getElementById('success'); + +function saveAccessToken() { + const ssoParams = new URLSearchParams(window.location.hash.replace(/^#/, '')); + window.history.pushState(null, null, window.location.pathname); + + const accessToken = ssoParams.get('access_token'); + const expiresIn = ssoParams.get('expires_in'); + + let errorText = ''; + if (!accessToken || !expiresIn) { + errorText = 'Both access_token and expires_in are required in the hash'; + } else if (!Number.isInteger(parseInt(expiresIn))) { + errorText = 'expires_in should be an int'; + } + if (errorText !== '') { + console.error(errorText); + loadingSpinner.classList.add('hidden'); + errorAlert.innerHTML += errorText; + errorAlert.classList.remove('hidden'); + return; + } + + const expires = Date.now() + parseInt(expiresIn) * 1000; + + localStorage.setItem('auth', JSON.stringify({accessToken, expires})); + + loadingSpinner.classList.add('hidden'); + success.classList.remove('hidden'); +} + +saveAccessToken(); diff --git a/src/core.ts b/src/core.ts index 1c567d3..00ac23d 100644 --- a/src/core.ts +++ b/src/core.ts @@ -122,7 +122,7 @@ const helpBox = $('helpBox'); const toggleHelpBox = makeClassToggler(helpBox, 'closed', true, (isOpen) => { const newHash = isOpen ? (window.location.hash.startsWith('#help') ? window.location.hash : '#help') : ''; if (newHash !== window.location.hash) { - window.history.pushState(null, null, window.location.pathname + newHash); + window.history.pushState(null, null, `${window.location.pathname}${window.location.search}${newHash}`); } }); diff --git a/src/index.ts b/src/index.ts index 6e77c27..83fee64 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,10 @@ import './styles/tag-input.css'; import './tag-input'; +// The order here is important, otherwise it create circular dependencies import {onload as canvasOnload} from './canvas/canvas'; +// eslint-disable-next-line import/order +import {onload as authOnload} from './auth'; import {onload as coreOnload} from './core'; import {onload as savesOnload} from './saves'; import {onload as setupOnload} from './setup/setup'; @@ -12,5 +15,6 @@ window.addEventListener('DOMContentLoaded', () => { coreOnload(); canvasOnload(); savesOnload(); + authOnload(); setupOnload(); }); diff --git a/webpack.config.js b/webpack.config.js index 63e4d46..12115cf 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -18,7 +18,10 @@ const distPath = localPath('dist'); /** @type {import('webpack').Configuration} */ export default { - entry: localPath('src/index.ts'), + entry: { + main: localPath('src/index.ts'), + auth: localPath('src/auth/index.ts'), + }, devtool: (isProduction || isStaging) ? false : 'eval-source-map', module: { rules: [ @@ -56,6 +59,12 @@ export default { new HtmlWebpackPlugin({ template: localPath('public/index.html'), templateParameters: getTemplateParams(), + chunks: ['main'], + }), + new HtmlWebpackPlugin({ + template: localPath('public/auth/index.html'), + filename: 'auth/index.html', + chunks: ['auth'], }), ], resolve: { @@ -73,6 +82,7 @@ export default { }, devServer: { host: 'local.danielhoward.me', + port: 3001, }, }; From 6709e2a7c3c982122ff174eef4369313a4284fa2 Mon Sep 17 00:00:00 2001 From: danielhoward Date: Sun, 24 Sep 2023 01:41:00 +0100 Subject: [PATCH 02/23] Add account section --- public/index.html | 11 +++++++++ src/auth.ts | 60 +++++++++++++++++++++++++++++++++++++++++++++++ src/auth/index.ts | 2 ++ src/types.d.ts | 21 +++++++++++++++++ 4 files changed, 94 insertions(+) diff --git a/public/index.html b/public/index.html index 10b4306..8cebaba 100644 --- a/public/index.html +++ b/public/index.html @@ -291,6 +291,17 @@
+
diff --git a/src/auth.ts b/src/auth.ts index 8b21de6..ac1dae0 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,10 +1,17 @@ import {$, makeClassToggler} from './core'; +import type {BackendAccount, BackendAccountResponse, BackendSave, LocalStorageAuth} from './types.d'; + const loginButton = $('loginButton'); +const logoutButton = $('logoutButton'); const loginLoading = $('loginLoading'); const loginLoadingText = $('loginLoadingText'); +const loggedInView = $('loggedInView'); + +const showLoginButton = makeClassToggler(loginButton, 'hidden', true); const showLoadingInfo = makeClassToggler(loginLoading, 'hidden', true, (enabled) => loginButton.disabled = enabled); +const showLoggedInView = makeClassToggler(loggedInView, 'hidden', true); const isDevelopment = process.env.NODE_ENV === 'development'; const searchParams = new URLSearchParams(window.location.search); @@ -15,6 +22,7 @@ const ssoPath = `${ssoOrigin}/auth?target=chaos${isDevelopment ? '&devport=3001' const backendDevPort = searchParams.get('backenddevport'); const backendOrigin = backendDevPort === null ? 'https://chaos-backend.danielhoward.me' : `http://local.danielhoward.me:${backendDevPort}`; +const backendQuery = ssoDevPort === null ? '' : `?ssodevport=${ssoDevPort}`; let loginWindow: Window; @@ -49,9 +57,61 @@ function onLoginClick() { } function onAuth() { + showLoadingInfo(true); loginLoadingText.textContent = 'Fetching your saves from the server'; + fetchUserSaves(); +} + +async function fetchUserSaves() { + const auth = getAuthStorage(); + if (!auth) return; + + const res = await fetch(`${backendOrigin}/account${backendQuery}`, { + headers: { + 'Authorization': `Bearer ${auth.accessToken}`, + }, + }); + + if (!res.ok) return logout(); + + const {account, saves} = await res.json() as BackendAccountResponse; + populateAccountDetails(account); + populateUserSaves(saves); + + showLoadingInfo(false); + showLoginButton(false); + showLoggedInView(true); +} + +function populateAccountDetails(account: BackendAccount) { + loggedInView.querySelector('#username').textContent = account.username; + loggedInView.querySelector('#profilePicture').src = account.profilePicture; +} +function populateUserSaves(saves: BackendSave[]) { + // +} + +function getAuthStorage(): LocalStorageAuth | void { + try { + const auth = JSON.parse(localStorage.getItem('auth')) as LocalStorageAuth; + if (auth.expires < Date.now()) throw new Error('accessToken expired'); + return auth; + } catch (err) { + console.error(err); + logout(); + } +} + +function logout() { + localStorage.removeItem('auth'); + showLoadingInfo(false); + showLoginButton(true); + showLoggedInView(false); } export function onload() { loginButton.addEventListener('click', onLoginClick); + logoutButton.addEventListener('click', logout); + + if (localStorage.getItem('auth') !== null) onAuth(); } diff --git a/src/auth/index.ts b/src/auth/index.ts index acaac74..c9afd46 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -29,6 +29,8 @@ function saveAccessToken() { loadingSpinner.classList.add('hidden'); success.classList.remove('hidden'); + + if (!window.opener) window.location.href = '/'; } saveAccessToken(); diff --git a/src/types.d.ts b/src/types.d.ts index 02a3a20..0275569 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -132,3 +132,24 @@ export interface ShapeSettingsInputEvent { element: HTMLElement; updateGraph: boolean; } + +export interface BackendAccount { + userId: string; + username: string; + email: string; + profilePicture: string; +} +export interface BackendSave { + name: string; + data: string; + screenshot?: string; +} +export interface BackendAccountResponse { + account: BackendAccount; + saves: BackendSave[]; +} + +export interface LocalStorageAuth { + accessToken: string; + expires: number; +} From 7e41d6e6b77ccff181019a8c93bc5e65e3141a05 Mon Sep 17 00:00:00 2001 From: danielhoward Date: Sun, 24 Sep 2023 19:42:06 +0100 Subject: [PATCH 03/23] Improve displaying cloud saves --- .eslintrc.json | 3 +- public/index.html | 72 ++++++++----- src/index.ts | 5 +- src/saves/backend.ts | 36 +++++++ src/{saves.ts => saves/config.ts} | 14 +-- src/saves/paths.ts | 12 +++ src/saves/saves.ts | 9 ++ src/saves/selector.ts | 171 ++++++++++++++++++++++++++++++ src/{auth.ts => saves/sso.ts} | 82 ++++++-------- src/styles/style.css | 32 ++++++ src/types.d.ts | 11 +- webpack.config.js | 1 + 12 files changed, 357 insertions(+), 91 deletions(-) create mode 100644 src/saves/backend.ts rename src/{saves.ts => saves/config.ts} (91%) create mode 100644 src/saves/paths.ts create mode 100644 src/saves/saves.ts create mode 100644 src/saves/selector.ts rename src/{auth.ts => saves/sso.ts} (53%) diff --git a/.eslintrc.json b/.eslintrc.json index 20d9112..1869e76 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -58,6 +58,7 @@ { "patterns": ["../*", "*types"] } - ] + ], + "func-call-spacing": "off" } } \ No newline at end of file diff --git a/public/index.html b/public/index.html index 8cebaba..95f09e1 100644 --- a/public/index.html +++ b/public/index.html @@ -25,7 +25,7 @@ - + <% function playbackSettingsBar() { %> @@ -55,6 +55,46 @@

Settings

+
+ + + +
+ + + + +
<% const shapeTypes = [ 'polygon', @@ -272,37 +312,15 @@
+
-
- - +
+ +
-
- - - -
diff --git a/src/index.ts b/src/index.ts index 83fee64..6284601 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,16 +5,13 @@ import './tag-input'; // The order here is important, otherwise it create circular dependencies import {onload as canvasOnload} from './canvas/canvas'; -// eslint-disable-next-line import/order -import {onload as authOnload} from './auth'; import {onload as coreOnload} from './core'; -import {onload as savesOnload} from './saves'; +import {onload as savesOnload} from './saves/saves'; import {onload as setupOnload} from './setup/setup'; window.addEventListener('DOMContentLoaded', () => { coreOnload(); canvasOnload(); savesOnload(); - authOnload(); setupOnload(); }); diff --git a/src/saves/backend.ts b/src/saves/backend.ts new file mode 100644 index 0000000..912767c --- /dev/null +++ b/src/saves/backend.ts @@ -0,0 +1,36 @@ +import {backendOrigin, backendQuery} from './paths'; +import {getAuthStorage} from './sso'; + +import type {BackendResponse} from './../types.d'; + +function getBackendUrl(path: string): string { + let url = `${backendOrigin}${path}${backendQuery}`; + + while (url.match(/\?/g).length > 1) { + url = url.replace(/\?([^?]*)$/, '&$1'); + } + + return url; +} + +async function makeRequest(path: string, includeAuth = false): Promise { + const headers: HeadersInit = {}; + + if (includeAuth) { + const auth = getAuthStorage(); + headers['Authorization'] = `Bearer ${auth.accessToken}`; + } + + const res = await fetch(getBackendUrl(path), {headers}); + if (!res.ok) throw new Error('chaos-backend returned a non-ok response'); + + return await res.json() as T; +} + +export async function fetchUserSaves(): Promise { + return await makeRequest('/account', true); +} + +export async function deleteSave(id: string) { + return await makeRequest(`/delete?id=${id}`, true); +} diff --git a/src/saves.ts b/src/saves/config.ts similarity index 91% rename from src/saves.ts rename to src/saves/config.ts index 21f7b1b..782ba28 100644 --- a/src/saves.ts +++ b/src/saves/config.ts @@ -1,10 +1,10 @@ -import {SetupStage} from './constants'; -import {$, getInputValue, setInputValue} from './core'; -import {generatePoints} from './setup/generate-points'; -import {getStages, getSetupStage, setSetupStage} from './setup/setup'; -import {useSetEquator} from './vertex-rule'; +import {SetupStage} from './../constants'; +import {$, getInputValue, setInputValue} from './../core'; +import {generatePoints} from './../setup/generate-points'; +import {getStages, getSetupStage, setSetupStage} from './../setup/setup'; +import {useSetEquator} from './../vertex-rule'; -import type {SaveConfig} from './types.d'; +import type {SaveConfig} from './../types.d'; /* Save file version changelog: @@ -73,7 +73,7 @@ function downloadCurrentConfig() { link.click(); } -function loadConfig(config: SaveConfig) { +export function loadConfig(config: SaveConfig) { showConfigError(''); setSetupStage(0); diff --git a/src/saves/paths.ts b/src/saves/paths.ts new file mode 100644 index 0000000..cafa8b1 --- /dev/null +++ b/src/saves/paths.ts @@ -0,0 +1,12 @@ +const isDevelopment = process.env.NODE_ENV === 'development'; +const searchParams = new URLSearchParams(window.location.search); + +const ssoDevPort = searchParams.get('ssodevport'); +const ssoOrigin = ssoDevPort === null ? 'https://sso.danielhoward.me' : `http://local.danielhoward.me:${ssoDevPort}`; +const ssoPath = `${ssoOrigin}/auth?target=chaos${isDevelopment ? '&devport=3001' : ''}`; + +const backendDevPort = searchParams.get('backenddevport'); +const backendOrigin = backendDevPort === null ? 'https://chaos-backend.danielhoward.me' : `http://local.danielhoward.me:${backendDevPort}`; +const backendQuery = ssoDevPort === null ? '' : `?ssodevport=${ssoDevPort}`; + +export {ssoPath, backendOrigin, backendQuery}; diff --git a/src/saves/saves.ts b/src/saves/saves.ts new file mode 100644 index 0000000..cba137c --- /dev/null +++ b/src/saves/saves.ts @@ -0,0 +1,9 @@ +import {onload as configOnload} from './config'; +import {onload as selectorOnload} from './selector'; +import {onload as ssoOnload} from './sso'; + +export function onload() { + configOnload(); + selectorOnload(); + ssoOnload(); +} diff --git a/src/saves/selector.ts b/src/saves/selector.ts new file mode 100644 index 0000000..1d0f378 --- /dev/null +++ b/src/saves/selector.ts @@ -0,0 +1,171 @@ +import {$, makeClassToggler} from './../core'; +import {loadConfig} from './config'; +import {backendOrigin} from './paths'; + +import type {Save, SaveConfig} from './../types.d'; + +export enum SaveType { + Preset = 'preset', + Local = 'local', + Cloud = 'cloud', +} + +let currentType: SaveType | null = null; + +const savesButtons: Record = { + [SaveType.Preset]: $('presetSavesButton'), + [SaveType.Local]: $('localSavesButton'), + [SaveType.Cloud]: $('cloudSavesButton'), +}; +const savesContainers = { + [SaveType.Preset]: $('presetSaves'), + [SaveType.Local]: $('localSaves'), + [SaveType.Cloud]: $('cloudSaves'), +}; +const savesContainersTogglers = Object.entries(savesContainers).reduce void>>( + (acc, [type, container]) => { + acc[type] = makeClassToggler(container, 'hidden', true, (enabled) => { + savesButtons[type].classList.toggle('btn-outline-primary', !enabled); + savesButtons[type].classList.toggle('btn-primary', enabled); + }); + return acc; + }, {}, +); + +function hideAllContainers() { + Object.values(savesContainersTogglers).forEach((toggler) => toggler(false)); +} + +function setContainerActive(type: SaveType | null) { + currentType = currentType == type ? null : type; + + hideAllContainers(); + if (!currentType) return; + + savesContainersTogglers[type](true); +} + +export function populateSavesSection(type: SaveType, saves: null): void +export function populateSavesSection(type: SaveType, saves: Save[], deleteSave: (save: Save) => void): void +export function populateSavesSection(type: SaveType, saves: Save[] | null, deleteSave?: (save: Save) => void) { + const container = savesContainers[type].querySelector('.saves-container'); + container.innerHTML = ''; + + if (saves === null) return; + if (saves.length === 0) { + const text = document.createElement('span'); + text.classList.add('text-muted'); + text.textContent = `You don't currently have any ${type} saves`; + container.appendChild(text); + } else { + saves.forEach((save) => { + const card = createSaveCard(save, deleteSave, type, saves); + container.appendChild(card); + }); + } +} + +function createSaveCard(save: Save, deleteSaveFunc: (save: Save) => void, type: SaveType, saves: Save[]): HTMLDivElement { + const card = document.createElement('div'); + card.classList.add('card'); + card.classList.add('save-card'); + + const img = document.createElement('img'); + img.classList.add('card-img-top'); + img.alt = `${save.name} screenshot`; + img.src = save.screenshot ? `${backendOrigin}/screenshot/${save.screenshot}.jpg` : `/static/img/save-placeholder.jpg`; + card.appendChild(img); + + const body = document.createElement('div'); + body.classList.add('card-body'); + card.appendChild(body); + + const title = document.createElement('h5'); + title.classList.add('card-title'); + title.textContent = save.name; + body.appendChild(title); + + const errorText = document.createElement('div'); + errorText.classList.add('error-text'); + errorText.classList.add('hidden'); + body.appendChild(errorText); + + const footer = document.createElement('div'); + footer.classList.add('card-footer'); + card.appendChild(footer); + + const buttonContainer = document.createElement('div'); + buttonContainer.classList.add('button-container'); + footer.appendChild(buttonContainer); + + const useButton = document.createElement('button'); + useButton.classList.add('btn'); + useButton.classList.add('btn-primary'); + useButton.classList.add('load-button'); + useButton.textContent = 'Load'; + useButton.addEventListener('click', () => useSave(save, errorText)); + buttonContainer.appendChild(useButton); + + const deleteButton = document.createElement('button'); + deleteButton.classList.add('btn'); + deleteButton.classList.add('btn-danger'); + deleteButton.classList.add('delete-button'); + deleteButton.addEventListener('click', () => deleteSave(deleteSaveFunc, save, deleteButton, errorText, type, saves)); + buttonContainer.appendChild(deleteButton); + + const deleteIcon = document.createElement('i'); + deleteIcon.classList.add('bi'); + deleteIcon.classList.add('bi-trash'); + deleteButton.appendChild(deleteIcon); + + return card; +} + +function useSave(save: Save, errorText: HTMLDivElement) { + errorText.classList.add('hidden'); + + try { + const config = JSON.parse(save.data) as SaveConfig; + loadConfig(config); + + setContainerActive(null); + } catch (err) { + console.error(err); + errorText.textContent = 'There was an error when trying to load your save'; + errorText.classList.remove('hidden'); + } +} + +async function deleteSave(deleteSaveFunc: (save: Save) => void | Promise, save: Save, deleteButton: HTMLButtonElement, errorText: HTMLDivElement, type: SaveType, saves: Save[]) { + errorText.classList.add('hidden'); + + const spinner = document.createElement('span'); + spinner.classList.add('spinner-border'); + spinner.classList.add('spinner-border-sm'); + spinner.style.marginRight = '4px'; + + deleteButton.prepend(spinner); + deleteButton.disabled = true; + + try { + await deleteSaveFunc(save); + } catch (err) { + console.error(err); + errorText.textContent = 'There was an error when trying to delete your save'; + errorText.classList.remove('hidden'); + + deleteButton.removeChild(spinner); + deleteButton.disabled = false; + return; + } + + populateSavesSection(type, saves.filter(({id}) => save.id !== id), deleteSaveFunc); +} + +export function onload() { + Object.entries(savesButtons).forEach(([type, button]) => { + button.addEventListener('click', () => { + setContainerActive(type as SaveType); + }); + }); +} diff --git a/src/auth.ts b/src/saves/sso.ts similarity index 53% rename from src/auth.ts rename to src/saves/sso.ts index ac1dae0..b06e587 100644 --- a/src/auth.ts +++ b/src/saves/sso.ts @@ -1,34 +1,34 @@ -import {$, makeClassToggler} from './core'; +import {$, makeClassToggler} from './../core'; +import {fetchUserSaves, deleteSave} from './backend'; +import {ssoPath} from './paths'; +import {SaveType, populateSavesSection} from './selector'; -import type {BackendAccount, BackendAccountResponse, BackendSave, LocalStorageAuth} from './types.d'; +import type {Account, BackendResponse, LocalStorageAuth} from './../types.d'; const loginButton = $('loginButton'); const logoutButton = $('logoutButton'); const loginLoading = $('loginLoading'); const loginLoadingText = $('loginLoadingText'); +const loginError = $('loginError'); +const refreshCloudSavesButton = $('refreshCloudSavesButton'); const loggedInView = $('loggedInView'); const showLoginButton = makeClassToggler(loginButton, 'hidden', true); const showLoadingInfo = makeClassToggler(loginLoading, 'hidden', true, (enabled) => loginButton.disabled = enabled); const showLoggedInView = makeClassToggler(loggedInView, 'hidden', true); - -const isDevelopment = process.env.NODE_ENV === 'development'; -const searchParams = new URLSearchParams(window.location.search); - -const ssoDevPort = searchParams.get('ssodevport'); -const ssoOrigin = ssoDevPort === null ? 'https://sso.danielhoward.me' : `http://local.danielhoward.me:${ssoDevPort}`; -const ssoPath = `${ssoOrigin}/auth?target=chaos${isDevelopment ? '&devport=3001' : ''}`; - -const backendDevPort = searchParams.get('backenddevport'); -const backendOrigin = backendDevPort === null ? 'https://chaos-backend.danielhoward.me' : `http://local.danielhoward.me:${backendDevPort}`; -const backendQuery = ssoDevPort === null ? '' : `?ssodevport=${ssoDevPort}`; +const showLoginError = makeClassToggler(loginError, 'hidden', true); +const setLoginError = (value: string | undefined) => { + loginError.textContent = value; + showLoginError(!!value); +}; let loginWindow: Window; function onLoginClick() { if (loginWindow?.closed === false) return loginWindow.focus(); + setLoginError(null); showLoadingInfo(true); loginLoadingText.textContent = 'Waiting for authentication in popup window'; loginWindow = window.open(ssoPath, '', 'width=500, height=600'); @@ -48,58 +48,44 @@ function onLoginClick() { } catch (err) { // Ignore error thrown by browser since it is expected when on a different origin } - if (originsMatch) { + if (originsMatch && localStorage.getItem('auth') !== null) { clearInterval(interval); loginWindow.close(); - onAuth(); + refreshServerResponse(); } }, 1000); } -function onAuth() { +async function refreshServerResponse() { showLoadingInfo(true); loginLoadingText.textContent = 'Fetching your saves from the server'; - fetchUserSaves(); -} -async function fetchUserSaves() { - const auth = getAuthStorage(); - if (!auth) return; - - const res = await fetch(`${backendOrigin}/account${backendQuery}`, { - headers: { - 'Authorization': `Bearer ${auth.accessToken}`, - }, - }); - - if (!res.ok) return logout(); + let res: BackendResponse; + try { + res = await fetchUserSaves(); + } catch (err) { + console.error(err); + setLoginError(`There was an error when logging you in. Please try again later.`); + return logout(); + } - const {account, saves} = await res.json() as BackendAccountResponse; - populateAccountDetails(account); - populateUserSaves(saves); + populateAccountDetails(res.account); + populateSavesSection(SaveType.Cloud, res.saves, (save) => deleteSave(save.id)); showLoadingInfo(false); showLoginButton(false); showLoggedInView(true); } -function populateAccountDetails(account: BackendAccount) { +function populateAccountDetails(account: Account) { loggedInView.querySelector('#username').textContent = account.username; - loggedInView.querySelector('#profilePicture').src = account.profilePicture; -} -function populateUserSaves(saves: BackendSave[]) { - // + loggedInView.querySelector('#profilePicture').src = `${account.profilePicture}&s=50`; } -function getAuthStorage(): LocalStorageAuth | void { - try { - const auth = JSON.parse(localStorage.getItem('auth')) as LocalStorageAuth; - if (auth.expires < Date.now()) throw new Error('accessToken expired'); - return auth; - } catch (err) { - console.error(err); - logout(); - } +export function getAuthStorage(): LocalStorageAuth { + const auth = JSON.parse(localStorage.getItem('auth')) as LocalStorageAuth; + if (auth.expires < Date.now()) throw new Error('accessToken expired'); + return auth; } function logout() { @@ -107,11 +93,13 @@ function logout() { showLoadingInfo(false); showLoginButton(true); showLoggedInView(false); + populateSavesSection(SaveType.Cloud, null); } export function onload() { loginButton.addEventListener('click', onLoginClick); logoutButton.addEventListener('click', logout); + refreshCloudSavesButton.addEventListener('click', refreshServerResponse); - if (localStorage.getItem('auth') !== null) onAuth(); + if (localStorage.getItem('auth') !== null) refreshServerResponse(); } diff --git a/src/styles/style.css b/src/styles/style.css index de8cc4e..be4217e 100644 --- a/src/styles/style.css +++ b/src/styles/style.css @@ -242,3 +242,35 @@ a.text-muted { pointer-events: all; cursor: help; } + +.error-text { + color: #dc3545; + font-size: 0.9em; +} + +.saves-container { + display: flex; + flex-wrap: wrap; +} +.saves-container:has(> span) { + justify-content: center; +} +.save-card { + margin: 5px; + width: calc(50% - 10px); + box-shadow: 0 6px 10px rgba(0,0,0,.08), 0 0 6px rgba(0,0,0,.05); +} +.save-card:hover { + transition: .3s transform cubic-bezier(.155,1.105,.295,1.12), .3s box-shadow; + transform: scale(1.05); + box-shadow: 0 10px 20px rgba(0,0,0,.12), 0 4px 8px rgba(0,0,0,.06); +} +.save-card .button-container { + display: flex; + align-items: center; + position: relative; +} +.save-card .delete-button { + position: absolute; + right: 0; +} diff --git a/src/types.d.ts b/src/types.d.ts index 0275569..1edc800 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -133,20 +133,21 @@ export interface ShapeSettingsInputEvent { updateGraph: boolean; } -export interface BackendAccount { +export interface Account { userId: string; username: string; email: string; profilePicture: string; } -export interface BackendSave { +export interface Save { + id: string; name: string; data: string; screenshot?: string; } -export interface BackendAccountResponse { - account: BackendAccount; - saves: BackendSave[]; +export interface BackendResponse { + account: Account; + saves: Save[]; } export interface LocalStorageAuth { diff --git a/webpack.config.js b/webpack.config.js index 12115cf..093ddbe 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -83,6 +83,7 @@ export default { devServer: { host: 'local.danielhoward.me', port: 3001, + watchFiles: ['src/**/*', 'public/**/*'], }, }; From 699caf1c681e5805f09966f92a26fffb0d4f354f Mon Sep 17 00:00:00 2001 From: danielhoward Date: Mon, 25 Sep 2023 00:18:19 +0100 Subject: [PATCH 04/23] Add local saves --- public/index.html | 14 +++++++ src/saves/backend.ts | 6 ++- src/saves/local.ts | 93 +++++++++++++++++++++++++++++++++++++++++++ src/saves/presets.ts | 34 ++++++++++++++++ src/saves/saves.ts | 4 ++ src/saves/selector.ts | 30 +++++++------- src/saves/sso.ts | 2 + src/styles/style.css | 1 + 8 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 src/saves/local.ts create mode 100644 src/saves/presets.ts diff --git a/public/index.html b/public/index.html index 95f09e1..fdbfccc 100644 --- a/public/index.html +++ b/public/index.html @@ -62,9 +62,23 @@

Settings