table done and finished shitty auth
This commit is contained in:
parent
1adf2fb862
commit
a04b54ce99
12 changed files with 513 additions and 10 deletions
33
src/components/input-email.svelte
Normal file
33
src/components/input-email.svelte
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { ClassValue } from 'svelte/elements';
|
||||||
|
|
||||||
|
const {
|
||||||
|
class: className = undefined,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
label = 'Email',
|
||||||
|
required = false,
|
||||||
|
}: {
|
||||||
|
class?: ClassValue;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
required?: boolean;
|
||||||
|
label?: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative {className ?? ''}">
|
||||||
|
<input
|
||||||
|
{id}
|
||||||
|
type="text"
|
||||||
|
{name}
|
||||||
|
class="peer w-full min-w-0 rounded bg-gray-200 px-2 py-1.5 text-black"
|
||||||
|
inputmode="email"
|
||||||
|
placeholder=""
|
||||||
|
{required}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="pointer-events-none absolute -top-6 left-0 text-sm transition-[translate,color,font-size] peer-placeholder-shown:translate-x-2 peer-placeholder-shown:translate-y-7.5 peer-placeholder-shown:text-base peer-placeholder-shown:text-black/50 peer-focus:translate-x-0 peer-focus:translate-y-0 peer-focus:text-sm peer-focus:text-white"
|
||||||
|
for={id}>{label}</label
|
||||||
|
>
|
||||||
|
</div>
|
32
src/components/input-password.svelte
Normal file
32
src/components/input-password.svelte
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { ClassValue } from 'svelte/elements';
|
||||||
|
|
||||||
|
const {
|
||||||
|
class: className = undefined,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
label = 'Password',
|
||||||
|
required = false,
|
||||||
|
}: {
|
||||||
|
class?: ClassValue;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
required?: boolean;
|
||||||
|
label?: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative {className ?? ''}">
|
||||||
|
<input
|
||||||
|
{id}
|
||||||
|
type="password"
|
||||||
|
{name}
|
||||||
|
class="peer w-full min-w-0 rounded bg-gray-200 px-2 py-1.5 text-black"
|
||||||
|
placeholder=""
|
||||||
|
{required}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="pointer-events-none absolute -top-6 left-0 text-sm transition-[translate,color,font-size] peer-placeholder-shown:translate-x-2 peer-placeholder-shown:translate-y-7.5 peer-placeholder-shown:text-base peer-placeholder-shown:text-black/50 peer-focus:translate-x-0 peer-focus:translate-y-0 peer-focus:text-sm peer-focus:text-white"
|
||||||
|
for={id}>{label}</label
|
||||||
|
>
|
||||||
|
</div>
|
|
@ -1,10 +1,46 @@
|
||||||
<script>
|
<script>
|
||||||
function login() {
|
import { userstate } from '../shared.svelte';
|
||||||
globalThis.user = { name: 'John Doe' };
|
import InputEmail from './input-email.svelte';
|
||||||
console.log('User logged in:', globalThis.user);
|
import InputPassword from './input-password.svelte';
|
||||||
|
|
||||||
|
async function handleSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const form = event.target;
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
credentials: 'same-origin',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// window.location.reload();
|
||||||
|
|
||||||
|
userstate.checkIsLoggedIn();
|
||||||
|
} else {
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button onclick={login}> Login </button>
|
<div class="col-[content] flex grow items-center justify-center">
|
||||||
|
<form onsubmit={handleSubmit} class="flex w-full max-w-xs flex-col gap-6 px-2">
|
||||||
<button type="button" onclick={(e) => console.log(e)}> login2 </button>
|
<InputEmail label="Email" id="login-email" name="email" required></InputEmail>
|
||||||
|
<InputPassword class="mt-2" label="Password" id="login-password" name="password" required
|
||||||
|
></InputPassword>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="flex cursor-pointer items-center justify-center rounded bg-pink-700 px-2.5 py-1.5 text-white transition-[background-color] hover:bg-pink-600 active:outline-1 active:outline-white"
|
||||||
|
type="submit">Login</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
|
@ -4,6 +4,6 @@
|
||||||
const { children }: { children?: Snippet } = $props();
|
const { children }: { children?: Snippet } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main id="main-content">
|
<main id="main-content" class="content grid min-h-(--main-min-height) pt-4 pb-20">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</main>
|
</main>
|
||||||
|
|
100
src/components/progress-table.svelte
Normal file
100
src/components/progress-table.svelte
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { fetchEntries, formatter } from '../util';
|
||||||
|
|
||||||
|
let entriesPromise = $state(fetchEntries());
|
||||||
|
|
||||||
|
let interval: Parameters<typeof clearInterval>[0] = undefined;
|
||||||
|
$effect(() => {
|
||||||
|
clearInterval(interval);
|
||||||
|
|
||||||
|
// Pooling interval to fetch entries every 31 seconds
|
||||||
|
interval = setInterval(async () => {
|
||||||
|
const promise = fetchEntries();
|
||||||
|
// wait for the promise to resolve before assigning to prevent flash
|
||||||
|
await promise;
|
||||||
|
entriesPromise = promise;
|
||||||
|
}, 1000 * 31);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="col-[content]">
|
||||||
|
{#await entriesPromise}
|
||||||
|
<p class="text-center">Loading entries...</p>
|
||||||
|
{:then entries}
|
||||||
|
{#if entries.length === 0}
|
||||||
|
<p class="text-center">No entries.</p>
|
||||||
|
{:else}
|
||||||
|
{@render table(entries)}
|
||||||
|
{/if}
|
||||||
|
{:catch error}
|
||||||
|
<p class="mx-auto max-w-md text-center">
|
||||||
|
Something went wrong fetching entries: {error.message}
|
||||||
|
</p>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#snippet table(entries: Awaited<ReturnType<typeof fetchEntries>>)}
|
||||||
|
<div
|
||||||
|
class="@container grid w-full grid-cols-[auto,1fr,auto,auto,auto] gap-1 font-light @lg:gap-3"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="sticky top-(--header-height) col-span-5 grid grid-cols-[subgrid] gap-1 bg-gray-950 py-2 text-gray-400 capitalize"
|
||||||
|
>
|
||||||
|
<div>id</div>
|
||||||
|
<div class="px-1">name</div>
|
||||||
|
<div class="hidden px-1 @3xl:block">done</div>
|
||||||
|
<div class="hidden px-1 @5xl:block">created</div>
|
||||||
|
<div class="hidden px-1 @xl:block">updated</div>
|
||||||
|
</div>
|
||||||
|
{#each entries as entry (entry.id)}
|
||||||
|
<div class="col-span-5 grid grid-cols-[subgrid] gap-1">
|
||||||
|
<div class="py-2 text-xs text-gray-400/50">{entry.id}</div>
|
||||||
|
<a
|
||||||
|
href={entry.href}
|
||||||
|
class="block px-2 py-1 text-gray-200 capitalize transition-colors selection:bg-pink-600 selection:text-gray-200 hover:text-pink-600 hover:underline"
|
||||||
|
target="_blank"
|
||||||
|
referrerpolicy="no-referrer">{entry.name}</a
|
||||||
|
>
|
||||||
|
{@render done(entry.finished)}
|
||||||
|
<div class="hidden max-w-24 px-1 py-1 text-xs text-gray-200 @5xl:block">
|
||||||
|
{formatter.format(entry.created_at)}
|
||||||
|
</div>
|
||||||
|
<div class="hidden max-w-24 px-1 py-1 text-xs text-gray-200 @xl:block">
|
||||||
|
{formatter.format(entry.updated_at)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet done(finished: boolean)}
|
||||||
|
{#if finished}
|
||||||
|
<svg
|
||||||
|
class="hidden text-green-500 @3xl:block"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
class="hidden text-red-500 @3xl:block"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
|
@ -1,4 +1,5 @@
|
||||||
@import 'tailwindcss' source('.');
|
@import 'tailwindcss' source('.');
|
||||||
|
@source './components/main.svelte';
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--breakout-size: calc((var(--breakpoint-xl) - var(--breakpoint-lg)) / 2);
|
--breakout-size: calc((var(--breakpoint-xl) - var(--breakpoint-lg)) / 2);
|
||||||
|
@ -7,6 +8,11 @@
|
||||||
--content-val: min(100% - calc(var(--spacing) * 8), var(--breakpoint-lg));
|
--content-val: min(100% - calc(var(--spacing) * 8), var(--breakpoint-lg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--header-height: calc(var(--spacing) * 16);
|
||||||
|
--main-min-height: calc(100dvh - var(--header-height));
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
|
@ -3,13 +3,15 @@
|
||||||
import Header from './components/header.svelte';
|
import Header from './components/header.svelte';
|
||||||
import Main from './components/main.svelte';
|
import Main from './components/main.svelte';
|
||||||
import Login from './components/login.svelte';
|
import Login from './components/login.svelte';
|
||||||
|
import ProgressTable from './components/progress-table.svelte';
|
||||||
|
import { userstate } from './shared.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Header></Header>
|
<Header></Header>
|
||||||
<Main>
|
<Main>
|
||||||
{#if !globalThis.user}
|
{#if !userstate.isLoggedIn}
|
||||||
<Login></Login>
|
<Login></Login>
|
||||||
{:else}
|
{:else}
|
||||||
{globalThis.user.name}
|
<ProgressTable></ProgressTable>
|
||||||
{/if}
|
{/if}
|
||||||
</Main>
|
</Main>
|
||||||
|
|
|
@ -4,7 +4,6 @@ import './index.css';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var didMount: boolean | undefined;
|
var didMount: boolean | undefined;
|
||||||
var user: { name: string } | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let app: Record<string, any> | undefined;
|
let app: Record<string, any> | undefined;
|
||||||
|
|
102
src/server.ts
102
src/server.ts
|
@ -6,6 +6,18 @@ import sitemapTxt from './static/sitemap.txt';
|
||||||
import icon from './static/favicon.png' with { type: 'file' };
|
import icon from './static/favicon.png' with { type: 'file' };
|
||||||
const favicon = await Bun.file(icon).bytes();
|
const favicon = await Bun.file(icon).bytes();
|
||||||
const development = env.NODE_ENV !== 'production';
|
const development = env.NODE_ENV !== 'production';
|
||||||
|
import { randomUUIDv7 } from 'bun';
|
||||||
|
import ptApi from './server/pt-api';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var loginTokens: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.loginTokens) {
|
||||||
|
globalThis.loginTokens = new Set<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
const authCookie = 'pt-auth';
|
||||||
|
|
||||||
Bun.serve({
|
Bun.serve({
|
||||||
routes: {
|
routes: {
|
||||||
|
@ -20,9 +32,99 @@ Bun.serve({
|
||||||
headers: { 'Content-Type': 'image/png' },
|
headers: { 'Content-Type': 'image/png' },
|
||||||
}),
|
}),
|
||||||
'/health': new Response('OK'),
|
'/health': new Response('OK'),
|
||||||
|
'/api/entries': {
|
||||||
|
async GET(req) {
|
||||||
|
if (!isLoggedIn(req)) return unauthorizedResp();
|
||||||
|
|
||||||
|
const data = await ptApi.query();
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=30' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/auth/logout': {
|
||||||
|
async POST() {
|
||||||
|
return new Response('Logout successful', { headers: logoutHeaders() });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/auth/login': {
|
||||||
|
async POST(req) {
|
||||||
|
const data = await req.json();
|
||||||
|
const email = data.email;
|
||||||
|
const password = data.password;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof email !== 'string' ||
|
||||||
|
typeof password !== 'string' ||
|
||||||
|
email.length < 3 ||
|
||||||
|
password.length === 0
|
||||||
|
) {
|
||||||
|
return new Response('Missing email or password', { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
if (email === env.EMAIL && password === env.PASSWORD) {
|
||||||
|
let token = randomUUIDv7('base64url');
|
||||||
|
while (globalThis.loginTokens.has(token)) {
|
||||||
|
// generate a new token if it already exists
|
||||||
|
// this is unlikely to happen, but just in case
|
||||||
|
token = randomUUIDv7();
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookie = new Bun.Cookie({
|
||||||
|
name: authCookie,
|
||||||
|
value: token,
|
||||||
|
path: '/',
|
||||||
|
maxAge: 60 * 60 * 24 * 31,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'strict',
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = new Headers({ 'Set-Cookie': cookie.toString() });
|
||||||
|
|
||||||
|
globalThis.loginTokens.add(token);
|
||||||
|
|
||||||
|
return new Response('Login successful', { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 900));
|
||||||
|
|
||||||
|
return new Response('Incorrect email or password', {
|
||||||
|
status: 400,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
development,
|
development,
|
||||||
// async fetch(req, server) {
|
// async fetch(req, server) {
|
||||||
// return new Response("Not found", { status: 404 });
|
// return new Response("Not found", { status: 404 });
|
||||||
// },
|
// },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function isLoggedIn(req: Request) {
|
||||||
|
const cookie = new Bun.CookieMap(req.headers.get('cookie') || '');
|
||||||
|
const token = cookie.get(authCookie);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalThis.loginTokens.has(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logoutHeaders() {
|
||||||
|
return new Headers({
|
||||||
|
'Set-Cookie': new Bun.Cookie({
|
||||||
|
name: authCookie,
|
||||||
|
path: '/',
|
||||||
|
maxAge: -1,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'strict',
|
||||||
|
}).toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function unauthorizedResp() {
|
||||||
|
return new Response('Unauthorized', { status: 401, headers: logoutHeaders() });
|
||||||
|
}
|
||||||
|
|
118
src/server/pt-api.ts
Normal file
118
src/server/pt-api.ts
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import { env } from 'bun';
|
||||||
|
|
||||||
|
type SelectBookmark = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
finished: boolean;
|
||||||
|
created_at: number;
|
||||||
|
updated_at: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseUrl = env.API_SERVICE_URL;
|
||||||
|
const bookmarksUrl = `${baseUrl}/bookmarks`;
|
||||||
|
|
||||||
|
const noBodyHeaders = new Headers({
|
||||||
|
Authorization: `Bearer ${env.BEARER_TOKEN}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const bodyHeaders = new Headers({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${env.BEARER_TOKEN}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function query(): Promise<Array<SelectBookmark>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(bookmarksUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
signal: AbortSignal.timeout(50000),
|
||||||
|
headers: noBodyHeaders,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch bookmarks');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
throw new Error('Invalid bookmarks data');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
throw new Error('Failed to fetch bookmarks');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(id: number) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${bookmarksUrl}/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
headers: noBodyHeaders,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to remove bookmark');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
throw new Error('Failed to remove bookmark');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(id: number, body: { name: string; href: string }) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${bookmarksUrl}/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
headers: bodyHeaders,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to update bookmark');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
throw new Error('Failed to update bookmark');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function check(id: number, finished: boolean) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${bookmarksUrl}/${id}/check/${finished}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
headers: noBodyHeaders,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to check bookmark');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
throw new Error('Failed to check bookmark');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create(body: { name: string; href: string }) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${bookmarksUrl}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
headers: bodyHeaders,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to create bookmark');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
throw new Error('Failed to create bookmark');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { query, remove, update, check, create };
|
28
src/shared.svelte.ts
Normal file
28
src/shared.svelte.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
function getCookieMap() {
|
||||||
|
const cookies = document.cookie.split('; ');
|
||||||
|
const cookieMap = new Map<string, string>();
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i];
|
||||||
|
if (!cookie) continue;
|
||||||
|
let [key, value] = cookie.split('=');
|
||||||
|
if (!key || !value) continue;
|
||||||
|
cookieMap.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookieMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserState {
|
||||||
|
isLoggedIn = $state(false);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.checkIsLoggedIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIsLoggedIn() {
|
||||||
|
const cookieVal = getCookieMap().get('pt-auth');
|
||||||
|
this.isLoggedIn = cookieVal !== undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userstate = new UserState();
|
47
src/util.ts
Normal file
47
src/util.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { userstate } from './shared.svelte';
|
||||||
|
|
||||||
|
type SelectBookmark = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
finished: boolean;
|
||||||
|
created_at: number;
|
||||||
|
updated_at: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function fetchEntries(): Promise<SelectBookmark[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/entries', {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
mode: 'same-origin',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
userstate.checkIsLoggedIn();
|
||||||
|
}
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
throw new Error('Invalid data format');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching entries:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatter = new Intl.DateTimeFormat(undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
Loading…
Add table
Reference in a new issue