diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/app.d.ts | 18 | ||||
-rw-r--r-- | src/hooks.ts | 28 | ||||
-rw-r--r-- | src/lib/APITypes.d.ts | 30 | ||||
-rw-r--r-- | src/lib/LoginButton.svelte | 32 | ||||
-rw-r--r-- | src/lib/api.ts | 2 | ||||
-rw-r--r-- | src/routes/index.svelte | 21 | ||||
-rw-r--r-- | src/routes/loggedin.ts | 8 | ||||
-rw-r--r-- | src/routes/logout.ts | 34 | ||||
-rw-r--r-- | src/routes/player/[player]/[profile].svelte | 2 | ||||
-rw-r--r-- | src/routes/player/index.ts | 4 | ||||
-rw-r--r-- | src/routes/profile.svelte | 79 | ||||
-rw-r--r-- | src/routes/verify.svelte | 63 | ||||
-rw-r--r-- | src/routes/verify.ts | 104 |
13 files changed, 404 insertions, 21 deletions
diff --git a/src/app.d.ts b/src/app.d.ts index efdb902..ddaae39 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -2,14 +2,16 @@ // // See https://kit.svelte.dev/docs/typescript // // for information about these interfaces -// declare namespace App { -// interface Locals { -// userid: string; -// } +declare namespace App { + interface Locals { + sid: string | undefined + } -// interface Platform {} + // interface Platform {} -// interface Session {} + interface Session { + sid: string | undefined + } -// interface Stuff {} -// } + // interface Stuff {} +} diff --git a/src/hooks.ts b/src/hooks.ts index 9cd6ca4..5b3464c 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,11 +1,16 @@ import cookie from 'cookie' -import { v4 as uuid } from '@lukeed/uuid' -import type { Handle } from '@sveltejs/kit' +import type { ExternalFetch, GetSession, Handle } from '@sveltejs/kit' + +const SKYBLOCK_STATS_API_KEY = process.env.SKYBLOCK_STATS_API_KEY +if (!SKYBLOCK_STATS_API_KEY) + console.warn('SKYBLOCK_STATS_API_KEY is not set as an environment variable. This is required for logging in with Skyblock Stats to work. It should be the same as the `key` environment variable in skyblock-api.') export const handle: Handle = async ({ event, resolve }) => { - // const cookies = cookie.parse(event.request.headers.get('cookie') || ''); + const cookies = cookie.parse(event.request.headers.get('cookie') || '') // event.locals.userid = cookies.userid || uuid(); + event.locals.sid = cookies.sid + const response = await resolve(event) // if (!cookies.userid) { @@ -22,3 +27,20 @@ export const handle: Handle = async ({ event, resolve }) => { return response } + +export const getSession: GetSession = async ({ locals }) => { + return { + sid: locals.sid + } +} + +export const externalFetch: ExternalFetch = async (request) => { + if (SKYBLOCK_STATS_API_KEY && request.url.startsWith('https://skyblock-api.matdoes.dev/')) { + // add the key as a header + request.headers.set('key', SKYBLOCK_STATS_API_KEY) + } + + console.log('request', request.url) + + return fetch(request) +}
\ No newline at end of file diff --git a/src/lib/APITypes.d.ts b/src/lib/APITypes.d.ts index 3a54cea..b0deee0 100644 --- a/src/lib/APITypes.d.ts +++ b/src/lib/APITypes.d.ts @@ -162,4 +162,32 @@ export interface ElectionData { year: number candidates: Candidate[] } | null -}
\ No newline at end of file +} + +interface SessionSchema { + _id?: string + refresh_token: string + discord_user: { + id: string + name: string + } + lastUpdated: Date +} + +export interface AccountCustomization { + backgroundUrl?: string + pack?: string + emoji?: string +} + +export interface AccountSchema { + _id?: string + discordId: string + minecraftUuid?: string + customization?: AccountCustomization +} + +export interface CleanSocialMedia { + discord: string | null + forums: string | null +} diff --git a/src/lib/LoginButton.svelte b/src/lib/LoginButton.svelte new file mode 100644 index 0000000..bba078e --- /dev/null +++ b/src/lib/LoginButton.svelte @@ -0,0 +1,32 @@ +<script lang="ts"> + export let loggedIn: boolean +</script> + +<div class="login-button-container"> + {#if loggedIn} + <a href="/profile"><button class="login-button">Edit profile</button></a> + {:else} + <a href="/login"> + <button class="login-button" + ><img src="/discord-mark-light.svg" alt="Discord logo" />Log in with Discord</button + > + </a> + {/if} +</div> + +<style> + .login-button-container { + position: absolute; + top: 0.5em; + right: 0.5em; + } + .login-button { + color: var(--theme-main-text); + } + img { + height: 1em; + vertical-align: middle; + margin-right: 0.3em; + opacity: 0.9; + } +</style> diff --git a/src/lib/api.ts b/src/lib/api.ts index db6bf81..243cf2b 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1 +1 @@ -export const API_URL = 'https://skyblock-api.matdoes.dev/'
\ No newline at end of file +export const API_URL = 'https://skyblock-api.matdoes.dev/' diff --git a/src/routes/index.svelte b/src/routes/index.svelte index 6f6bae1..54843bd 100644 --- a/src/routes/index.svelte +++ b/src/routes/index.svelte @@ -1,13 +1,28 @@ +<script lang="ts" context="module"> + import type { Load } from '@sveltejs/kit' + import { API_URL } from '$lib/api' + + export const load: Load = async ({ params, fetch, session }) => { + return { + props: { + loggedIn: session.sid !== undefined, + }, + } + } +</script> + <script lang="ts"> import Username from '$lib/minecraft/Username.svelte' import SearchUser from '$lib/SearchUser.svelte' - import type { CleanUser } from '$lib/APITypes' import donators from '../_donators.json' import Head from '$lib/Head.svelte' import Emoji from '$lib/Emoji.svelte' + import LoginButton from '$lib/LoginButton.svelte' - export const prerender = true + // export const prerender = true export const hydrate = false + + export let loggedIn: boolean </script> <svelte:head> @@ -20,6 +35,7 @@ /> <main> + <LoginButton {loggedIn} /> <section class="title-section"> <h1>SkyBlock Stats</h1> <SearchUser> @@ -56,7 +72,6 @@ <section> <h2>Info</h2> <p>Website made by <a href="https://matdoes.dev">mat</a>.</p> - <p>Join the <a href="https://discord.gg/nWQKpzPCPJ">Forum Sweats</a> Discord server.</p> <p> Resource packs: <a href="//packshq.com">PacksHQ</a> (default), <a href="//hypixel.net/threads/2138599">Furfsky</a>, diff --git a/src/routes/loggedin.ts b/src/routes/loggedin.ts index eb7236d..9eadf67 100644 --- a/src/routes/loggedin.ts +++ b/src/routes/loggedin.ts @@ -2,15 +2,17 @@ import { API_URL } from '$lib/api' import type { RequestHandler } from '@sveltejs/kit' import type { JSONValue } from '@sveltejs/kit/types/internal' -export const get: RequestHandler = async ({ params }) => { - const code = params.code +export const get: RequestHandler = async ({ url }) => { + const code = url.searchParams.get('code') + const redirectUri = `${url.protocol}//${url.host}/loggedin` const response = await fetch(`${API_URL}accounts/createsession`, { method: 'POST', headers: { 'content-type': 'application/json', }, body: JSON.stringify({ - code + code, + redirectUri: redirectUri }), }).then(res => { if (res.status !== 200) diff --git a/src/routes/logout.ts b/src/routes/logout.ts new file mode 100644 index 0000000..f2f5e33 --- /dev/null +++ b/src/routes/logout.ts @@ -0,0 +1,34 @@ +import { API_URL } from '$lib/api' +import type { RequestHandler } from '@sveltejs/kit' + +const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID +if (!DISCORD_CLIENT_ID) + console.warn('DISCORD_CLIENT_ID is not set as an environment variable. This is required for logging in with Discord to work.') + +export const get: RequestHandler = async ({ request, params, locals, url }) => { + // if the sid is wrong, nothing to do + console.log(url.searchParams.get('sid'), locals.sid) + if (url.searchParams.has('sid') && url.searchParams.get('sid') === locals.sid) { + console.log('ok sent logout') + await fetch(`${API_URL}accounts/session`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + uuid: locals.sid + }), + }).then(res => { + if (res.status !== 200) + throw new Error(res.statusText) + }) + } + return { + status: 303, + headers: { + location: '/', + 'Set-Cookie': 'sid=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/;' + } + } + +} diff --git a/src/routes/player/[player]/[profile].svelte b/src/routes/player/[player]/[profile].svelte index b87741c..02c46bb 100644 --- a/src/routes/player/[player]/[profile].svelte +++ b/src/routes/player/[player]/[profile].svelte @@ -39,7 +39,7 @@ pack = (await import('skyblock-assets/matchers/worlds_and_beyond.json')) as any break default: - // packshq is the default pack + // furfsky reborn is the default pack pack = (await import('skyblock-assets/matchers/furfsky_reborn.json')) as any break } diff --git a/src/routes/player/index.ts b/src/routes/player/index.ts index c9d1b90..4644499 100644 --- a/src/routes/player/index.ts +++ b/src/routes/player/index.ts @@ -1,4 +1,6 @@ -export async function post({ request }) { +import type { RequestHandler } from '@sveltejs/kit' + +export const post: RequestHandler = async ({ request }) => { const form = await request.formData() const player = form.get('user-search') diff --git a/src/routes/profile.svelte b/src/routes/profile.svelte new file mode 100644 index 0000000..c88b8fe --- /dev/null +++ b/src/routes/profile.svelte @@ -0,0 +1,79 @@ +<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/verify.svelte b/src/routes/verify.svelte new file mode 100644 index 0000000..e900a5f --- /dev/null +++ b/src/routes/verify.svelte @@ -0,0 +1,63 @@ +<script lang="ts" context="module"> + import type { Load } from '@sveltejs/kit' + export const load: Load = async ({ params, fetch, session, url }) => { + if (session.sid === undefined) { + return { redirect: '/login', status: 303 } + } + return { + props: { + errorCode: url.searchParams.get('error'), + current: url.searchParams.get('current'), + correct: url.searchParams.get('correct'), + }, + } + } +</script> + +<script lang="ts"> + import Emoji from '$lib/Emoji.svelte' + import Head from '$lib/Head.svelte' + import Header from '$lib/Header.svelte' + + export let errorCode: string | null + export let current: string | null + export let correct: string | null + + const errorCodes = { + NO_IGN: 'Please enter a valid Minecraft username.', + NOT_LINKED: + 'Please link your Discord in Hypixel by doing /profile -> Social media -> Discord. If you just changed it, wait a few minutes and try again.', + WRONG_NAME: `You're linked to ${current} on Hypixel. Please change this to ${correct} by doing /profile -> Social media -> Discord. If you just changed it, wait a few minutes and try again.`, + NO_KEY: + "This instance of skyblock-stats doesn't have a skyblock-api key set. Please contact the owner of the website if you believe this to be a mistake.", + } +</script> + +<Head title="Verify Account" /> +<Header /> + +<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> + {#if errorCode && errorCode in errorCodes} + <div class="error"> + <Emoji value="🚫" /> + {errorCodes[errorCode]} + </div> + {/if} + <form method="post" action="/verify"> + <input placeholder="Username or UUID" name="ign" required /> + <input type="submit" value="Enter" /> + </form> +</main> + +<style> + p { + margin: 0; + } + .error { + font-weight: bold; + margin: 1em 0; + } +</style> diff --git a/src/routes/verify.ts b/src/routes/verify.ts new file mode 100644 index 0000000..5951314 --- /dev/null +++ b/src/routes/verify.ts @@ -0,0 +1,104 @@ +import { API_URL } from '$lib/api' +import type { AccountSchema, CleanUser, SessionSchema } from '$lib/APITypes' +import type { RequestHandler } from '@sveltejs/kit' + +function redirect(status: number, location: string) { + return { + status, + headers: { + location, + }, + } +} + +export const post: RequestHandler = async ({ request, locals }) => { + console.log('ok!') + 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') + } + + const form = await request.formData() + + // 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`) + } + + const playerResponse: CleanUser = await fetch(`${API_URL}player/${playerIdentifier}`).then(res => res.json()) + 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()) + + 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 = { + discordId: sessionResponse.session.discord_user.id, + minecraftUuid: playerResponse.player?.uuid + } + + 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('epic') + // return { + // status: 303, + // redirect: '/profile' + // } + return redirect(303, '/profile') +}
\ No newline at end of file |