From 4cc473b43e4690f6c71fb587c91b6d9ece1bde8c Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 6 May 2025 20:00:27 +0200 Subject: [PATCH 1/7] fix: Improve semantics and fine tune responsive layout parameters --- src/app.html | 41 +++++++++++++------------ src/layout/Hero/HeroImage.svelte | 8 +---- src/layout/Hero/HeroSection.svelte | 46 ++++++++++++++++------------ src/layout/Navbar/NavHost.svelte | 2 -- src/lib/components/Wave.svelte | 2 +- src/routes/+layout.svelte | 27 ++++++++-------- src/routes/+page.svelte | 15 ++++++--- src/routes/contributors/+page.svelte | 3 -- src/routes/donate/+page.svelte | 3 -- src/routes/download/+page.svelte | 7 ++--- src/routes/patches/+page.svelte | 2 -- 11 files changed, 74 insertions(+), 82 deletions(-) diff --git a/src/app.html b/src/app.html index cb442f9..197516e 100644 --- a/src/app.html +++ b/src/app.html @@ -1,26 +1,27 @@ + + + + + + - - - - - - + + + - - - + + + - - - + %sveltekit.head% + - %sveltekit.head% - - - -
%sveltekit.body%
- - - \ No newline at end of file + + %sveltekit.body% + + diff --git a/src/layout/Hero/HeroImage.svelte b/src/layout/Hero/HeroImage.svelte index 6ab34cf..faa47fd 100644 --- a/src/layout/Hero/HeroImage.svelte +++ b/src/layout/Hero/HeroImage.svelte @@ -14,16 +14,10 @@ } .hero-img { - height: 70vh; + height: max(100vh, 600px); padding: 0.5rem 0.5rem; border-radius: 1.75rem; background-color: var(--surface-seven); user-select: none; } - @media screen and (max-width: 1700px) { - .hero-img { - height: 100vh; - right: 6rem; - } - } diff --git a/src/layout/Hero/HeroSection.svelte b/src/layout/Hero/HeroSection.svelte index 506c255..3133b8e 100644 --- a/src/layout/Hero/HeroSection.svelte +++ b/src/layout/Hero/HeroSection.svelte @@ -60,6 +60,7 @@ } .hero { + padding-top: 10vh; display: flex; flex-direction: column; gap: 1rem; @@ -69,27 +70,9 @@ color: var(--primary); } - @media screen and (max-width: 1700px) { + @media screen and (max-width: 1100px) { .hero { - height: 80vh; - } - } - - @media screen and (max-height: 820px) { - .social-buttons { - bottom: initial; - left: initial; - position: initial; - width: initial; - opacity: 100% !important; - } - } - @media screen and (max-width: 1100px) or (min-height: 820px) { - .social-buttons { - transform: translateX(-50%); - width: 100%; - position: absolute; - justify-content: center; + padding-top: initial; } } @@ -100,11 +83,34 @@ } .social-buttons { + left: 50%; + transform: translateX(-50%); justify-content: center; + width: 100%; } .hero { height: initial; } } + + @media screen and (max-width: 1100px) or (min-height: 780px) { + .social-buttons { + transform: translateX(-50%); + width: 90%; + position: absolute; + left: initial; + transform: initial; + } + } + + @media screen and (max-height: 780px) { + .social-buttons { + transform: initial; + left: initial; + position: initial; + width: initial; + opacity: 100% !important; + } + } diff --git a/src/layout/Navbar/NavHost.svelte b/src/layout/Navbar/NavHost.svelte index c6a8548..0317f0a 100644 --- a/src/layout/Navbar/NavHost.svelte +++ b/src/layout/Navbar/NavHost.svelte @@ -203,8 +203,6 @@ #nav-container { z-index: 5; - top: 0; - position: sticky; width: 100%; } diff --git a/src/lib/components/Wave.svelte b/src/lib/components/Wave.svelte index bf52e61..bfc2e62 100644 --- a/src/lib/components/Wave.svelte +++ b/src/lib/components/Wave.svelte @@ -22,7 +22,7 @@ height: 40vh; } - @media screen and (max-height: 820px) { + @media screen and (max-height: 780px) { svg { opacity: 0 !important; } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 5bef71a..a09a4ff 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -25,6 +25,7 @@ import { events as themeEvents } from '$util/themeEvents'; import { RV_GOOGLE_TAG_MANAGER_ID } from '$env/static/public'; + import FooterHost from '$layout/Footer/FooterHost.svelte'; const queryClient = new QueryClient({ defaultOptions: { @@ -108,21 +109,19 @@ {/if} + + It's your choice + + We use analytics to improve your experience on this site. By clicking "Allow", you allow us to + collect anonymous data about your visit. + + + + + + - - - It's your choice - - We use analytics to improve your experience on this site. By clicking "Allow", you allow us to - collect anonymous data about your visit. - - - - - - -
{#if $show_loading_animation} @@ -130,5 +129,5 @@ {/if}
- +
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index e03b860..a9d4683 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,7 +1,6 @@ diff --git a/src/layout/Navbar/Modals/LoginModal.svelte b/src/layout/Navbar/Modals/LoginModal.svelte new file mode 100644 index 0000000..70f2119 --- /dev/null +++ b/src/layout/Navbar/Modals/LoginModal.svelte @@ -0,0 +1,86 @@ + + + +
+

Login

+

This login is reserved for site administrators. Go back!

+ {#if wrong_credentials} +

Username or password do not match. Try again.

+ {/if} +
+
+ + event.key === 'Enter' && loginForm.requestSubmit()} + required + /> +
+
+
+ + + + + +
+ + diff --git a/src/layout/Navbar/Modals/LoginSuccessfulModal.svelte b/src/layout/Navbar/Modals/LoginSuccessfulModal.svelte new file mode 100644 index 0000000..0df09b1 --- /dev/null +++ b/src/layout/Navbar/Modals/LoginSuccessfulModal.svelte @@ -0,0 +1,28 @@ + + + + Successfully logged in! + + + + + + + diff --git a/src/layout/Navbar/Modals/SessionExpiredModal.svelte b/src/layout/Navbar/Modals/SessionExpiredModal.svelte new file mode 100644 index 0000000..d553d01 --- /dev/null +++ b/src/layout/Navbar/Modals/SessionExpiredModal.svelte @@ -0,0 +1,35 @@ + + + + Expired session +
+ This session has expired, log in again to renew or lose all access to administrative power. +
+ + + + +
+ + diff --git a/src/layout/Navbar/Modals/SettingsModal.svelte b/src/layout/Navbar/Modals/SettingsModal.svelte new file mode 100644 index 0000000..eda7800 --- /dev/null +++ b/src/layout/Navbar/Modals/SettingsModal.svelte @@ -0,0 +1,116 @@ + + + + + + + Settings +
+

Configure the API for this website.

+
+ + +
+
+ + +
+ +
+ + +
+
+
+
+ + diff --git a/src/layout/Navbar/NavButton.svelte b/src/layout/Navbar/NavButton.svelte index 7138bb2..5144649 100644 --- a/src/layout/Navbar/NavButton.svelte +++ b/src/layout/Navbar/NavButton.svelte @@ -14,12 +14,12 @@ if (queryKey !== null) { if (Array.isArray(queryKey)) { queryKey.forEach((key) => { - const query = queries[key]; + const query = (queries[key] as Function)(); dev_log('Prefetching', query); client.prefetchQuery(query as any); }); } else { - const query = queries[queryKey]; + const query = (queries[queryKey] as Function)(); dev_log('Prefetching', query); client.prefetchQuery(query as any); } @@ -27,20 +27,40 @@ } -
  • +
  • -
  • - diff --git a/src/layout/Navbar/StatusBanner.svelte b/src/layout/Navbar/StatusBanner.svelte new file mode 100644 index 0000000..ccc90e7 --- /dev/null +++ b/src/layout/Navbar/StatusBanner.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..1dcaa65 --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,134 @@ +import { browser } from '$app/environment'; +import { build_url } from '$data/api'; + +export type AuthToken = { + token: string; + expires: number; +}; + +type JwtPayload = { + exp: number; + iss: string; + iat: number; +}; + +export class UnauthenticatedError extends Error { + constructor() { + super('Unauthenticated. Cannot perform admin operations.'); + } +} + +// Get access token. +export function get_access_token(): AuthToken | null { + if (!browser) return null; + const data = localStorage.getItem('revanced_api_access_token'); + if (data) return JSON.parse(data) as AuthToken; + return null; +} + +// (Re)set access token. +export function set_access_token(token?: AuthToken) { + if (!token) localStorage.removeItem('revanced_api_access_token'); + else localStorage.setItem('revanced_api_access_token', JSON.stringify(token)); +} + +// Parse a JWT token +export function parseJwt(token: string): JwtPayload { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent( + atob(base64) + .split('') + .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) + .join('') + ); + return JSON.parse(jsonPayload) as JwtPayload; +} + +// Check if the admin is authenticated +export function is_logged_in(): boolean { + const token = get_access_token(); + if (!token) return false; + return Date.now() < token.expires; +} + +async function digest_fetch( + url: string, + username: string, + password: string, + options: RequestInit = {} +): Promise { + // Helper function to convert ArrayBuffer to Hex string + function bufferToHex(buffer: ArrayBuffer): string { + return Array.from(new Uint8Array(buffer)) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + } + + // Generate SHA-256 digest + async function sha256(message: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(message); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + return bufferToHex(hashBuffer); + } + + // Perform an initial request to get the `WWW-Authenticate` header + const initialResponse = await fetch(url, { + method: options.method || 'GET', + headers: options.headers || {} + }); + + if (!initialResponse.ok && initialResponse.status !== 401) + throw new Error(`Initial request failed with status: ${initialResponse.status}`); + + if (initialResponse.ok && initialResponse.status === 200) return initialResponse; + + const authHeader = initialResponse.headers.get('Www-Authenticate'); + if (!authHeader || !authHeader.startsWith('Digest ')) + throw new Error('No Digest authentication header found'); + + // Parse the `WWW-Authenticate` header to extract the fields + const authParams = authHeader + .replace('Digest ', '') + .split(',') + .reduce((acc: Record, item) => { + const [key, value] = item.trim().split('='); + acc[key] = value.replace(/"/g, ''); + return acc; + }, {}); + + const { realm, nonce, algorithm } = authParams; + const method = options.method || 'GET'; + const uri = new URL(url).pathname; + + // https://ktor.io/docs/server-digest-auth.html#flow + const HA1 = await sha256(`${username}:${realm}:${password}`); + const HA2 = await sha256(`${method}:${uri}`); + + const responseHash = await sha256(`${HA1}:${nonce}:${HA2}`); + + // Build the Authorization header + const authHeaderDigest = `Digest username="${username}", realm="${realm}", nonce="${nonce}", uri="${uri}", algorithm=${algorithm}, response="${responseHash}"`; + + // Perform the final request with the Authorization header + const finalResponse = await fetch(url, { + ...options, + headers: { + ...options.headers, + Authorization: authHeaderDigest + } + }); + + return finalResponse; +} + +export async function login(username: string, password: string) { + const res = await digest_fetch(build_url('token'), username, password); + if (!res.ok) return false; + + const data = await res.json(); + const payload = parseJwt(data.token); + set_access_token({ token: data.token, expires: payload.exp * 1000 }); + return true; +} diff --git a/src/lib/components/Banner.svelte b/src/lib/components/Banner.svelte index d841da5..f87bd1a 100644 --- a/src/lib/components/Banner.svelte +++ b/src/lib/components/Banner.svelte @@ -1,160 +1,105 @@ -