diff options
author | mat <github@matdoes.dev> | 2022-03-18 16:59:58 -0500 |
---|---|---|
committer | mat <github@matdoes.dev> | 2022-03-18 16:59:58 -0500 |
commit | 5c9ccb3ab759526e84079f1c49abb87c995c7021 (patch) | |
tree | dd5e0a3b88c8ea1add26de926a9bbd1aa7402130 | |
parent | 557423e887a363d0d1be1dfc6db613f83fd7cec0 (diff) | |
download | skyblock-stats-5c9ccb3ab759526e84079f1c49abb87c995c7021.tar.gz skyblock-stats-5c9ccb3ab759526e84079f1c49abb87c995c7021.tar.bz2 skyblock-stats-5c9ccb3ab759526e84079f1c49abb87c995c7021.zip |
Add customization page
-rw-r--r-- | CONTRIBUTING.md | 6 | ||||
-rw-r--r-- | package.json | 5 | ||||
-rw-r--r-- | scripts/updateBackgrounds.js | 10 | ||||
-rw-r--r-- | src/_admins.json | 3 | ||||
-rw-r--r-- | src/_backgrounds.json | 1 | ||||
-rw-r--r-- | src/hooks.ts | 5 | ||||
-rw-r--r-- | src/lib/APITypes.d.ts | 1 | ||||
-rw-r--r-- | src/lib/Tooltip.svelte | 6 | ||||
-rw-r--r-- | src/routes/loggedin.ts | 3 | ||||
-rw-r--r-- | src/routes/profile.svelte | 79 | ||||
-rw-r--r-- | src/routes/profile/index.svelte | 218 | ||||
-rw-r--r-- | src/routes/profile/update.ts | 136 | ||||
-rw-r--r-- | src/routes/verify.svelte | 8 | ||||
-rw-r--r-- | src/routes/verify.ts | 33 |
14 files changed, 394 insertions, 120 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..df61cf9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# Contributing + +this is extremely wip i'll add more stuff here later + +If you want to add yourself as an admin for testing, add your undashed Minecraft UUID to src/_admins.json. The _backgrounds.json and _donators.json files are automatically updated, you can add donators from donators.txt. + diff --git a/package.json b/package.json index 2e7dfd2..82bb7ea 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,9 @@ "version": "0.0.1", "license": "MIT", "scripts": { - "dev": "node ./scripts/updateDonators.js && svelte-kit dev", - "build": "node ./scripts/updateDonators.js && svelte-kit build", + "init": "node ./scripts/updateDonators.js && node ./scripts/updateBackgrounds.js", + "dev": "npm run init && svelte-kit dev", + "build": "npm run init && svelte-kit build", "package": "svelte-kit package", "preview": "svelte-kit preview", "check": "svelte-check --tsconfig ./tsconfig.json", diff --git a/scripts/updateBackgrounds.js b/scripts/updateBackgrounds.js new file mode 100644 index 0000000..e2c74f2 --- /dev/null +++ b/scripts/updateBackgrounds.js @@ -0,0 +1,10 @@ +import fs from 'fs' + +// read the file names in the backgrounds folder +const backgrounds = fs.readdirSync('static/backgrounds') + +await fs.promises.writeFile( + 'src/_backgrounds.json', + JSON.stringify(backgrounds), + { encoding: 'utf8' } +) diff --git a/src/_admins.json b/src/_admins.json new file mode 100644 index 0000000..b264374 --- /dev/null +++ b/src/_admins.json @@ -0,0 +1,3 @@ +[ + "6536bfed869548fd83a1ecd24cf2a0fd" +]
\ No newline at end of file diff --git a/src/_backgrounds.json b/src/_backgrounds.json new file mode 100644 index 0000000..e3737b5 --- /dev/null +++ b/src/_backgrounds.json @@ -0,0 +1 @@ +["1.jpg","10.jpg","11.jpg","12.jpg","13.jpg","14.jpg","15.jpg","16.jpg","17.jpg","18.jpg","19.jpg","2.jpg","20.jpg","21.jpg","22.jpg","23.jpg","24.jpg","25.jpg","26.jpg","27.jpg","28.jpg","29.jpg","3.jpg","30.jpg","31.jpg","32.jpg","33.jpg","34.jpg","35.jpg","36.jpg","37.jpg","38.jpg","39.jpg","4.jpg","40.jpg","41.jpg","42.jpg","43.jpg","44.jpg","45.jpg","46.jpg","47.jpg","48.jpg","49.jpg","5.jpg","50.jpg","51.jpg","52.jpg","53.jpg","54.jpg","55.jpg","56.jpg","57.jpg","58.jpg","59.jpg","6.jpg","60.jpg","61.jpg","62.jpg","63.jpg","64.jpg","65.jpg","66.jpg","67.jpg","68.jpg","69.jpg","7.jpg","70.jpg","71.jpg","8.jpg","9.jpg"]
\ No newline at end of file diff --git a/src/hooks.ts b/src/hooks.ts index 5b3464c..ac53658 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -9,6 +9,7 @@ export const handle: Handle = async ({ event, resolve }) => { const cookies = cookie.parse(event.request.headers.get('cookie') || '') // event.locals.userid = cookies.userid || uuid(); + console.log(cookies) event.locals.sid = cookies.sid const response = await resolve(event) @@ -40,7 +41,7 @@ export const externalFetch: ExternalFetch = async (request) => { request.headers.set('key', SKYBLOCK_STATS_API_KEY) } - console.log('request', request.url) + const response = await fetch(request) - return fetch(request) + return response }
\ No newline at end of file diff --git a/src/lib/APITypes.d.ts b/src/lib/APITypes.d.ts index b0deee0..328f4cd 100644 --- a/src/lib/APITypes.d.ts +++ b/src/lib/APITypes.d.ts @@ -97,6 +97,7 @@ export interface CleanBasicProfile { export interface AccountCustomization { backgroundUrl?: string pack?: string + blurBackground?: boolean emoji?: string } diff --git a/src/lib/Tooltip.svelte b/src/lib/Tooltip.svelte index b354d42..14fa90d 100644 --- a/src/lib/Tooltip.svelte +++ b/src/lib/Tooltip.svelte @@ -4,9 +4,12 @@ A tooltip that works without requiring JavaScript to be enabled. When you hover or click on the element, it shows the content in a box above the element. --> +<script lang="ts"> + export let width = '10em' +</script> <span class="tooltip-container" tabindex="-1"> - <span class="tooltip-content"> + <span class="tooltip-content" style="max-width: {width}"> <slot name="tooltip">No tooltip!</slot> </span> <span class="tooltip-inner"> @@ -47,7 +50,6 @@ width: max-content; text-align: center; - max-width: 10em; cursor: auto; box-shadow: 0 0 1em 0.5em #0002; pointer-events: none; diff --git a/src/routes/loggedin.ts b/src/routes/loggedin.ts index 9eadf67..32dd6eb 100644 --- a/src/routes/loggedin.ts +++ b/src/routes/loggedin.ts @@ -20,12 +20,13 @@ export const get: RequestHandler = async ({ url }) => { return res.json() }) console.log(response) + if (response.ok) { return { status: 303, headers: { location: '/verify', - 'Set-Cookie': `sid=${response.session_id}; Max-Age=31536000000; Path=/; HttpOnly; SameSite=Strict` + 'Set-Cookie': `sid=${response.session_id}; Max-Age=31536000000; Path=/; HttpOnly` } } } diff --git a/src/routes/profile.svelte b/src/routes/profile.svelte deleted file mode 100644 index c88b8fe..0000000 --- a/src/routes/profile.svelte +++ /dev/null @@ -1,79 +0,0 @@ -<script lang="ts" context="module"> - import type { Load } from '@sveltejs/kit' - import { API_URL } from '$lib/api' - import type { AccountSchema, CleanMemberProfile, CleanUser, SessionSchema } from '$lib/APITypes' - import Head from '$lib/Head.svelte' - import Header from '$lib/Header.svelte' - - export const load: Load = async ({ params, fetch, session }) => { - const sessionResponse: { session: SessionSchema | null; account: AccountSchema | null } | null = - await fetch(`${API_URL}accounts/session`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - uuid: session.sid, - }), - }).then(r => r.json()) - - const playerResponse = sessionResponse?.account - ? await fetch( - `${API_URL}player/${sessionResponse.account.minecraftUuid}?customization=true` - ).then(r => r.json()) - : null - - // redirect to /login if the user is not logged in - if (!sessionResponse?.account) { - return { redirect: '/login', status: 303 } - } - - return { - props: { - session: sessionResponse.session, - account: sessionResponse.account, - player: playerResponse, - }, - } - } -</script> - -<script lang="ts"> - import Emoji from '$lib/Emoji.svelte' - - export let session: SessionSchema | null - export let account: AccountSchema | null - export let player: CleanUser | null -</script> - -<Head title="Customize Profile" /> -<Header /> - -<main> - {#if session && session._id} - <a href="/logout?sid={session._id}" class="logout"><button>Log out</button></a> - {/if} - <h1>Customize Profile</h1> - <noscript> - <p><Emoji value="⚠" /> Please enable JavaScript to use this page.</p> - </noscript> - {#if player && player.player} - <a href="/player/{player.player.username}">View profile</a> - {:else} - <p><Emoji value="⚠" /> No linked Minecraft account</p> - {/if} - <p> - Pack: <select> - <option value="default">Default</option> - <option value="custom">Custom</option> - </select> - </p> -</main> - -<style> - .logout { - position: absolute; - top: 0.5em; - right: 0.5em; - } -</style> diff --git a/src/routes/profile/index.svelte b/src/routes/profile/index.svelte new file mode 100644 index 0000000..5fc937f --- /dev/null +++ b/src/routes/profile/index.svelte @@ -0,0 +1,218 @@ +<script lang="ts" context="module"> + import type { Load } from '@sveltejs/kit' + import { API_URL } from '$lib/api' + import type { AccountCustomization, AccountSchema, CleanUser, SessionSchema } from '$lib/APITypes' + import Head from '$lib/Head.svelte' + import Header from '$lib/Header.svelte' + import donators from '../../_donators.json' + import admins from '../../_admins.json' + + export const load: Load = async ({ fetch, session }) => { + const sessionResponse: { session: SessionSchema | null; account: AccountSchema | null } | null = + await fetch(`${API_URL}accounts/session`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + uuid: session.sid, + }), + }).then(r => r.json()) + + const playerResponse = sessionResponse?.account + ? await fetch(`${API_URL}player/${sessionResponse.account.minecraftUuid}`).then(r => r.json()) + : null + + // redirect to /login if the user is not logged in + if ( + !sessionResponse || + !sessionResponse.account || + !sessionResponse.session || + !playerResponse.player + ) { + return { redirect: '/login', status: 303 } + } + + const isDonator = + donators.find(d => d.uuid === sessionResponse.account?.minecraftUuid) !== undefined + const isAdmin = admins.find(a => a === sessionResponse.account?.minecraftUuid) !== undefined + + return { + props: { + session: sessionResponse.session, + account: sessionResponse.account, + player: playerResponse, + isDonator: isDonator || isAdmin, + }, + } + } +</script> + +<script lang="ts"> + import Emoji from '$lib/Emoji.svelte' + import { browser } from '$app/env' + import Tooltip from '$lib/Tooltip.svelte' + import { onDestroy, onMount } from 'svelte' + import backgroundNames from '../../_backgrounds.json' + + export let session: SessionSchema + export let account: AccountSchema + export let player: CleanUser + + export let isDonator: boolean + + let pack: AccountCustomization['pack'] = account?.customization?.pack ?? 'furfsky_reborn' + let blurBackground: AccountCustomization['blurBackground'] = + account?.customization?.blurBackground ?? false + let backgroundName: string | undefined = account?.customization?.backgroundUrl?.replace( + /^\/backgrounds\//, + '' + ) + let emoji: AccountCustomization['emoji'] = account?.customization?.emoji + + let mounted = false + onMount(() => { + mounted = true + }) + onDestroy(() => { + mounted = false + }) + + let error: string | undefined + let loading: boolean = false + + async function updateProfile() { + if (!browser || !mounted) return + error = undefined + loading = true + const response = await fetch('/profile/update', { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + pack, + blurBackground, + backgroundName, + emoji, + }), + }) + .then(r => { + loading = false + return r.json() + }) + .catch(e => { + loading = false + error = e.message + }) + if (!response.ok) { + error = response.error + } + } + + // call updateProfile whenever anything is changed + $: [pack, blurBackground, backgroundName, emoji, updateProfile()] +</script> + +<Head title="Customize Profile" /> +<Header /> + +<main> + {#if session && session._id} + <a href="/logout?sid={session._id}" class="logout"><button>Log out</button></a> + {/if} + <h1>Customize Profile</h1> + <noscript> + <p><Emoji value="⚠" /> Please enable JavaScript to use this page.</p> + </noscript> + + <p class="status"> + {#if error} + <Emoji value="🚫" /> {error} + {:else if loading} + <Emoji value="🔄" /> Loading... + {/if} + </p> + + <a href="/player/{player.player?.username}">View profile</a> + <p> + <label for="pack-selector">Pack: </label> + <select bind:value={pack} id="pack-selector"> + <option value="ectoplasm">Ectoplasm</option> + <option value="furfsky">Furfsky</option> + <option value="furfsky_reborn">Furfsky Reborn</option> + <option value="hypixel+">Hypixel+</option> + <option value="packshq">PacksHQ</option> + <option value="rnbw">RNBW</option> + <option value="vanilla">Vanilla</option> + <option value="worlds_and_beyond">Worlds and Beyond</option> + </select> + </p> + <p> + <label for="blur-background-toggle">Blur and darken background behind content: </label> + <input type="checkbox" id="blur-background-toggle" bind:checked={blurBackground} /> + </p> + {#if isDonator} + <label for="profile-emoji-selector">Emoji next to username: </label> + <input type="text" name="emoji" id="profile-emoji-selector" bind:value={emoji} /> + <Tooltip width="15em"> + <span slot="tooltip"> + <p>Windows: <code>win+.</code></p> + <p>Mobile: Emoji keyboard</p> + </span> + <span>ⓘ</span> + </Tooltip> + {/if} + <h2>Background</h2> + <div id="background-selector-list"> + {#each backgroundNames as thisBackgroundName} + <span + class="selectable-background-option" + class:selected={thisBackgroundName === backgroundName} + style="background-image: url(/backgrounds/{thisBackgroundName})" + on:click={() => (backgroundName = thisBackgroundName)} + /> + {/each} + </div> +</main> + +<style> + .logout { + position: absolute; + top: 0.5em; + right: 0.5em; + } + .status { + position: relative; + top: -1.6em; + height: 0; + margin: 0; + } + #profile-emoji-selector { + width: 2em; + } + p { + margin: 0.2em 0; + } + #background-selector-list { + display: flex; + flex-wrap: wrap; + } + .selectable-background-option { + display: inline-block; + height: 10em; + width: 18em; + background-position: center; + background-size: 110%; + margin: 0.5em; + border-radius: 1em; + transition: background-size 500ms; + cursor: pointer; + } + .selectable-background-option:hover { + background-size: 125%; + } + .selectable-background-option.selected { + box-shadow: 0 0 0 2pt #06ec; + } +</style> diff --git a/src/routes/profile/update.ts b/src/routes/profile/update.ts new file mode 100644 index 0000000..b906810 --- /dev/null +++ b/src/routes/profile/update.ts @@ -0,0 +1,136 @@ +import { API_URL } from '$lib/api' +import type { AccountSchema, SessionSchema } from '$lib/APITypes' +import type { RequestHandler } from '@sveltejs/kit' +import backgroundFileNames from '../../_backgrounds.json' +import donators from '../../_donators.json' +import admins from '../../_admins.json' +import type { JSONValue } from '@sveltejs/kit/types/internal' + +const emojiRegex = /^(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])$/ + +function isValidEmoji(emoji: string) { + const match = emojiRegex.exec(emoji) + return match && match[0] === emoji && match.index === 0 +} + +console.log(isValidEmoji('😎')) +console.log(isValidEmoji('😎')) +console.log(isValidEmoji('😎')) +console.log(isValidEmoji('😎')) + +export const patch: RequestHandler = async ({ request, locals }) => { + console.log('updating profile...') + if (locals.sid === undefined) { + return { + body: { ok: false, error: 'You are not logged in.' }, + status: 401, + } + } + if (!process.env.SKYBLOCK_STATS_API_KEY) { + return { + body: { ok: false, error: 'The SKYBLOCK_STATS_API_KEY environment variable is not set.' }, + status: 500, + } + } + const data = await request.json() + + console.log('sending request to get session') + const sessionResponse: { session: SessionSchema | null, account: AccountSchema | null } = await fetch(`${API_URL}accounts/session`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + uuid: locals.sid, + }), + }).then(r => r.json()) + if (!sessionResponse.session || !sessionResponse.account?.minecraftUuid) + return { + body: { ok: false, error: 'Invalid session.' }, + status: 401, + } + + const backgroundName = data.backgroundName + const pack = data.pack + const blurBackground = data.blurBackground + const emoji = data.emoji + + const isDonator = donators.find(d => d.uuid === sessionResponse.account?.minecraftUuid) !== undefined + console.log(sessionResponse.account?.minecraftUuid) + const isAdmin = admins.includes(sessionResponse.account?.minecraftUuid) + + if (typeof backgroundName !== 'undefined' && typeof backgroundName !== 'string') { + return { + body: { ok: false, error: 'Invalid background.' }, + status: 400, + } + } + if (typeof pack !== 'string') { + return { + body: { ok: false, error: 'Invalid pack.' }, + status: 400, + } + } + if (typeof blurBackground !== 'boolean') { + return { + body: { ok: false, error: 'Invalid blurBackground.' }, + status: 400, + } + } + console.log('emoji', emoji) + if (typeof emoji !== 'undefined' && typeof emoji !== 'string') { + console.log('a') + return { + body: { ok: false, error: 'Invalid emoji.' }, + status: 400, + } + } + + // prevent people from putting non-existent backgrounds + if (backgroundName && !backgroundFileNames.includes(backgroundName)) + return { + body: { ok: false, error: 'Invalid background.' }, + status: 400, + } + const backgroundUrl = backgroundName ? `/backgrounds/${backgroundName}` : undefined + + console.log('emoji?', emoji) + if (emoji) { + if (!isDonator && !isAdmin) + return { + body: { ok: false, error: 'You are not allowed to use emojis.' }, + status: 401, + } + if (!isValidEmoji(emoji)) + return { + body: { ok: false, error: 'Invalid emoji.' }, + status: 400, + } + } + + const updatedAccount: AccountSchema = { + discordId: sessionResponse.account.discordId, + customization: { + backgroundUrl, + pack, + blurBackground, + emoji, + }, + } + + const response = await fetch(`${API_URL}accounts/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + key: process.env.SKYBLOCK_STATS_API_KEY + }, + body: JSON.stringify(updatedAccount), + }).then(r => r.json()) + + console.log(response) + + + return { + body: { ok: true } as JSONValue, + } +}
\ No newline at end of file diff --git a/src/routes/verify.svelte b/src/routes/verify.svelte index e900a5f..9675779 100644 --- a/src/routes/verify.svelte +++ b/src/routes/verify.svelte @@ -1,6 +1,7 @@ <script lang="ts" context="module"> import type { Load } from '@sveltejs/kit' - export const load: Load = async ({ params, fetch, session, url }) => { + export const load: Load = async ({ session, url }) => { + console.log(session.sid) if (session.sid === undefined) { return { redirect: '/login', status: 303 } } @@ -39,7 +40,10 @@ <main> <h1>Verify Minecraft account</h1> <p>Please enter your Minecraft username to verify that this is your account.</p> - <p>This will check with the Hypixel API that your Discord username matches your Hypixel name.</p> + <p> + This will check with the Hypixel API that your Discord username matches the Discord name set in + your Hypixel settings. + </p> {#if errorCode && errorCode in errorCodes} <div class="error"> <Emoji value="🚫" /> diff --git a/src/routes/verify.ts b/src/routes/verify.ts index 5951314..a6a21d7 100644 --- a/src/routes/verify.ts +++ b/src/routes/verify.ts @@ -12,16 +12,11 @@ function redirect(status: number, location: string) { } export const post: RequestHandler = async ({ request, locals }) => { - console.log('ok!') + console.log('verify', locals.sid) if (!process.env.SKYBLOCK_STATS_API_KEY) { return redirect(303, `/verify?error=NO_KEY`) } - console.log('sid check') if (locals.sid === undefined) { - // return { - // status: 303, - // redirect: '/login', - // } return redirect(303, '/login') } @@ -29,12 +24,7 @@ export const post: RequestHandler = async ({ request, locals }) => { // username or uuid const playerIdentifier = form.get('ign') - console.log('ign check') if (!playerIdentifier) { - // return { - // status: 303, - // redirect: '/verify?error=NO_IGN', - // } return redirect(303, `/verify?error=NO_IGN`) } @@ -49,36 +39,20 @@ export const post: RequestHandler = async ({ request, locals }) => { }), }).then(r => r.json()) - console.log('session check') if (!sessionResponse.session) - // return { - // status: 303, - // redirect: '/login', - // } return redirect(303, '/login') const hypixelDiscordName = playerResponse.player?.socials.discord - console.log('discord check') if (!hypixelDiscordName) - // return { - // status: 303, - // redirect: '/verify?error=NOT_LINKED', - // } return redirect(303, `/verify?error=NOT_LINKED`) - const discordUser = sessionResponse.session.discord_user const actualDiscordName = discordUser.name // some people link themselves as <id>#<discrim> instead of <name>#<discrim> const actualDiscordIdDiscrim = `${discordUser.id}#${discordUser.name.split('#')[1]}` - console.log('name check') if (!(hypixelDiscordName === actualDiscordName || hypixelDiscordName === actualDiscordIdDiscrim)) - // return { - // status: 303, - // redirect: `/verify?error=WRONG_NAME?current=${hypixelDiscordName}&correct=${actualDiscordName}`, - // } return redirect(303, `/verify?error=WRONG_NAME¤t=${encodeURIComponent(hypixelDiscordName)}&correct=${encodeURIComponent(actualDiscordName)}`) const updatedAccount: AccountSchema = { @@ -95,10 +69,5 @@ export const post: RequestHandler = async ({ request, locals }) => { body: JSON.stringify(updatedAccount), }).then(r => r.json()) - console.log('epic') - // return { - // status: 303, - // redirect: '/profile' - // } return redirect(303, '/profile') }
\ No newline at end of file |