aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.d.ts18
-rw-r--r--src/hooks.ts28
-rw-r--r--src/lib/APITypes.d.ts30
-rw-r--r--src/lib/LoginButton.svelte32
-rw-r--r--src/lib/api.ts2
-rw-r--r--src/routes/index.svelte21
-rw-r--r--src/routes/loggedin.ts8
-rw-r--r--src/routes/logout.ts34
-rw-r--r--src/routes/player/[player]/[profile].svelte2
-rw-r--r--src/routes/player/index.ts4
-rw-r--r--src/routes/profile.svelte79
-rw-r--r--src/routes/verify.svelte63
-rw-r--r--src/routes/verify.ts104
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&current=${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