refactor: cleanup skeleton, setup supabase properly

This commit is contained in:
2025-06-04 11:21:54 +01:00
parent 8b09523b21
commit b4f2b60bec
14 changed files with 5545 additions and 5384 deletions

10590
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -42,6 +42,7 @@
"vitest": "^3.0.0" "vitest": "^3.0.0"
}, },
"dependencies": { "dependencies": {
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.49.8" "@supabase/supabase-js": "^2.49.8"
} }
} }

14
src/app.d.ts vendored
View File

@@ -1,10 +1,20 @@
import type { Session, SupabaseClient, User } from "@supabase/supabase-js";
import type { Database } from "./lib/database.d.ts"; // import generated types
// See https://svelte.dev/docs/kit/types#app.d.ts // See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces // for information about these interfaces
declare global { declare global {
namespace App { namespace App {
// interface Error {} // interface Error {}
// interface Locals {} interface Locals {
// interface PageData {} supabase: SupabaseClient<Database>;
safeGetSession: () => Promise<{ session: Session | null; user: User | null }>;
session: Session | null;
user: User | null;
}
interface PageData {
session: Session | null;
}
// interface PageState {} // interface PageState {}
// interface Platform {} // interface Platform {}
} }

81
src/hooks.server.ts Normal file
View File

@@ -0,0 +1,81 @@
import { createServerClient } from "@supabase/ssr";
import { type Handle, redirect } from "@sveltejs/kit";
import { sequence } from "@sveltejs/kit/hooks";
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from "$env/static/public";
const supabase: Handle = async ({ event, resolve }) => {
/**
* Creates a Supabase client specific to this server request.
*
* The Supabase client gets the Auth token from the request cookies.
*/
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
cookies: {
getAll: () => event.cookies.getAll(),
/**
* SvelteKit's cookies API requires `path` to be explicitly set in
* the cookie options. Setting `path` to `/` replicates previous/
* standard behavior.
*/
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) => {
event.cookies.set(name, value, { ...options, path: "/" });
});
}
}
});
/**
* Unlike `supabase.auth.getSession()`, which returns the session _without_
* validating the JWT, this function also calls `getUser()` to validate the
* JWT before returning the session.
*/
event.locals.safeGetSession = async () => {
const {
data: { session }
} = await event.locals.supabase.auth.getSession();
if (!session) {
return { session: null, user: null };
}
const {
data: { user },
error
} = await event.locals.supabase.auth.getUser();
if (error) {
// JWT validation has failed
return { session: null, user: null };
}
return { session, user };
};
return resolve(event, {
filterSerializedResponseHeaders(name) {
/**
* Supabase libraries use the `content-range` and `x-supabase-api-version`
* headers, so we need to tell SvelteKit to pass it through.
*/
return name === "content-range" || name === "x-supabase-api-version";
}
});
};
const authGuard: Handle = async ({ event, resolve }) => {
const { session, user } = await event.locals.safeGetSession();
event.locals.session = session;
event.locals.user = user;
if (!event.locals.session && event.url.pathname.startsWith("/private")) {
redirect(303, "/auth");
}
if (event.locals.session && event.url.pathname === "/auth") {
redirect(303, "/private");
}
return resolve(event);
};
export const handle: Handle = sequence(supabase, authGuard);

12
src/lib/database.d.ts vendored
View File

@@ -65,21 +65,27 @@ export type Database = {
} }
study_spaces: { study_spaces: {
Row: { Row: {
building_address: string | null
created_at: string | null created_at: string | null
description: string | null
id: string id: string
title: string location: string | null
updated_at: string | null updated_at: string | null
} }
Insert: { Insert: {
building_address?: string | null
created_at?: string | null created_at?: string | null
description?: string | null
id?: string id?: string
title: string location?: string | null
updated_at?: string | null updated_at?: string | null
} }
Update: { Update: {
building_address?: string | null
created_at?: string | null created_at?: string | null
description?: string | null
id?: string id?: string
title?: string location?: string | null
updated_at?: string | null updated_at?: string | null
} }
Relationships: [] Relationships: []

View File

@@ -1 +1,5 @@
// place files you want to import through the `$lib` alias in this folder. import type { Database } from "./database.d.ts";
export type Table<T extends keyof Database["public"]["Tables"]> =
Database["public"]["Tables"][T]["Row"];
export type Enum<T extends keyof Database["public"]["Enums"]> = Database["public"]["Enums"][T];

View File

@@ -0,0 +1,9 @@
import type { LayoutServerLoad } from "./$types";
export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => {
const { session } = await safeGetSession();
return {
session,
cookies: cookies.getAll()
};
};

44
src/routes/+layout.ts Normal file
View File

@@ -0,0 +1,44 @@
import { createBrowserClient, createServerClient, isBrowser } from "@supabase/ssr";
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from "$env/static/public";
import type { Database } from "$lib/database";
import type { LayoutLoad } from "./$types";
export const load: LayoutLoad = async ({ data, depends, fetch }) => {
/**
* Declare a dependency so the layout can be invalidated, for example, on
* session refresh.
*/
depends("supabase:auth");
const supabase = isBrowser()
? createBrowserClient<Database>(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch
}
})
: createServerClient<Database>(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch
},
cookies: {
getAll() {
return data.cookies;
}
}
});
/**
* It's fine to use `getSession` here, because on the client, `getSession` is
* safe, and on the server, it reads `session` from the `LayoutData`, which
* safely checked the session using `safeGetSession`.
*/
const {
data: { session }
} = await supabase.auth.getSession();
const {
data: { user }
} = await supabase.auth.getUser();
return { session, supabase, user };
};

View File

@@ -1,12 +1,8 @@
import { createClient } from "@supabase/supabase-js";
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from "$env/static/public";
import type { PageServerLoad } from "./$types"; import type { PageServerLoad } from "./$types";
import type { Database } from "$lib/database";
import { error } from "@sveltejs/kit"; import { error } from "@sveltejs/kit";
export const load: PageServerLoad = async ({ depends }) => { export const load: PageServerLoad = async ({ depends, locals: { supabase } }) => {
depends("db:study_spaces"); depends("db:study_spaces");
const supabase = createClient<Database>(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY);
const { data: studySpaces, error: err } = await supabase const { data: studySpaces, error: err } = await supabase
.from("study_spaces") .from("study_spaces")
.select("*, study_space_images(*)"); .select("*, study_space_images(*)");

View File

@@ -1,35 +1,56 @@
<script lang="ts"> <script lang="ts">
import SpaceCard from "$lib/components/SpaceCard.svelte"; import SpaceCard from "$lib/components/SpaceCard.svelte";
import defaultImg from "$lib/assets/study_space.png"; import defaultImg from "$lib/assets/study_space.png";
import { createClient } from "@supabase/supabase-js";
import type { Database } from "$lib/database";
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from "$env/static/public";
import { invalidate } from "$app/navigation"; import { invalidate } from "$app/navigation";
import type { Table } from "$lib";
const supabase = createClient<Database>(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY);
const { data } = $props(); const { data } = $props();
const { studySpaces } = $derived(data); const { studySpaces, supabase } = $derived(data);
let title = $state(""); const blankStudySpace = {
let fileInput = $state<HTMLInputElement | null>(null); description: "",
building_address: "",
location: ""
};
let studySpaceData = $state<Omit<Table<"study_spaces">, "id" | "created_at" | "updated_at">>({
...blankStudySpace
});
let fileInput = $state<HTMLInputElement>();
async function uploadStudySpace() { async function uploadStudySpace() {
const imageFile = fileInput?.files?.[0]; const imageFile = fileInput?.files?.[0];
if (!imageFile) { if (!imageFile) return alert("Please select an image file.");
alert("Please select an image file.");
return; const { data: studySpaceInsert, error: studySpaceError } = await supabase
} .from("study_spaces")
const params = new URLSearchParams({ title, imgTitle: imageFile.name }); .insert(studySpaceData)
const res = await fetch(`/api/study_spaces?${params.toString()}`, { .select()
method: "POST", .single();
body: imageFile if (studySpaceError)
}); return alert(`Error uploading study space: ${studySpaceError.message}`);
if (res.ok) {
alert("Study space uploaded successfully!"); const { data: imgUpload, error: imageError } = await supabase.storage
await invalidate("db:study_spaces"); .from("files_bucket")
} else { .upload(`public/${studySpaceInsert.id}-${imageFile.name}`, imageFile, {
alert("Failed to upload study space."); contentType: imageFile.type
} });
if (imageError) return alert(`Error uploading image: ${imageError.message}`);
const { error: imageInsertError } = await supabase
.from("study_space_images")
.insert({
study_space_id: studySpaceInsert.id,
image_path: imgUpload.path
})
.select()
.single();
if (imageInsertError) return alert(`Error creating image: ${imageInsertError.message}`);
if (fileInput) fileInput.value = ""; // Clear the file input
studySpaceData = { ...blankStudySpace }; // Reset the form data
alert("Thank you for your contribution!");
invalidate("db:study_spaces"); // Invalidate page data so that it refreshes
} }
</script> </script>
@@ -41,9 +62,9 @@
.from("files_bucket") .from("files_bucket")
.getPublicUrl(studySpace.study_space_images[0].image_path).data.publicUrl .getPublicUrl(studySpace.study_space_images[0].image_path).data.publicUrl
: defaultImg} : defaultImg}
<SpaceCard alt="Photo of {studySpace.title}" imgSrc={imgUrl}> <SpaceCard alt="Photo of {studySpace.description}" imgSrc={imgUrl}>
{#snippet description()} {#snippet description()}
<p>{studySpace.title}</p> <p>{studySpace.description}</p>
{/snippet} {/snippet}
</SpaceCard> </SpaceCard>
{/each} {/each}
@@ -55,7 +76,27 @@
uploadStudySpace(); uploadStudySpace();
}} }}
> >
<input type="text" name="title" bind:value={title} placeholder="Study Space Title" required /> <input
type="text"
name="description"
bind:value={studySpaceData.description}
placeholder="Study Space Description"
required
/>
<input
type="text"
name="location"
bind:value={studySpaceData.location}
placeholder="Study Space Location within building"
required
/>
<input
type="text"
name="building_address"
bind:value={studySpaceData.building_address}
placeholder="Building Address"
required
/>
<input type="file" name="image" accept=".png, image/png" required bind:this={fileInput} /> <input type="file" name="image" accept=".png, image/png" required bind:this={fileInput} />
<button type="submit">Upload some Study Space!</button> <button type="submit">Upload some Study Space!</button>
</form> </form>
@@ -70,5 +111,8 @@
} }
form { form {
max-width: 600px; max-width: 600px;
display: flex;
flex-direction: column;
gap: 0.5rem;
} }
</style> </style>

View File

@@ -1,35 +0,0 @@
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from "$env/static/public";
import type { Database } from "$lib/database";
import { createClient } from "@supabase/supabase-js";
import { error, type RequestHandler } from "@sveltejs/kit";
export const POST: RequestHandler = async ({ request }) => {
const supabase = createClient<Database>(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY);
const body = await request.bytes();
const url = new URL(request.url);
const title = url.searchParams.get("title");
const imgTitle = url.searchParams.get("imgTitle");
if (!title || !imgTitle) error(400, "Missing required fields: title, imgTitle");
const { data: imageData, error: imageError } = await supabase.storage
.from("files_bucket")
.upload(`public/${imgTitle}`, body, {
contentType: request.headers.get("content-type") || "image/png",
upsert: false
});
if (imageError) error(500, `Failed to upload image: ${imageError.message}`);
const { data: studySpaceData, error: studySpaceError } = await supabase
.from("study_spaces")
.insert({ title })
.select()
.single();
if (studySpaceError) error(500, "Failed to create study space");
const { error: imageLinkError } = await supabase
.from("study_space_images")
.insert({ study_space_id: studySpaceData.id, image_path: imageData.path });
if (imageLinkError) error(500, "Failed to link image to study space");
return new Response(JSON.stringify({ id: studySpaceData.id }), { status: 200 });
};

View File

@@ -40,17 +40,3 @@ FOR EACH ROW EXECUTE FUNCTION handle_updated_at();
CREATE TRIGGER study_space_images_updated_at CREATE TRIGGER study_space_images_updated_at
AFTER UPDATE ON study_space_images AFTER UPDATE ON study_space_images
FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); FOR EACH ROW EXECUTE FUNCTION handle_updated_at();
-- Security
-- ALTER TABLE study_spaces ENABLE ROW LEVEL SECURITY;
-- ALTER TABLE study_space_images ENABLE ROW LEVEL SECURITY;
-- CREATE POLICY "Allow all users to view study spaces"
-- ON study_spaces
-- FOR SELECT
-- USING (true);
-- CREATE POLICY "Allow all users to view study space images"
-- ON study_space_images
-- FOR SELECT
-- USING (true);

View File

@@ -0,0 +1,4 @@
ALTER TABLE study_spaces DROP COLUMN title;
ALTER TABLE study_spaces ADD COLUMN building_address text;
ALTER TABLE study_spaces ADD COLUMN description text;
ALTER TABLE study_spaces ADD COLUMN location text;

View File

@@ -8,7 +8,10 @@ CREATE POLICY "Whack"
CREATE TABLE study_spaces ( CREATE TABLE study_spaces (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
title text NOT NULL, description text,
-- Location within building, e.g., "Room 101"
location text,
building_address text,
created_at timestamp with time zone DEFAULT now(), created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now() updated_at timestamp with time zone DEFAULT now()
); );
@@ -29,17 +32,3 @@ FOR EACH ROW EXECUTE FUNCTION handle_updated_at();
CREATE TRIGGER study_space_images_updated_at CREATE TRIGGER study_space_images_updated_at
AFTER UPDATE ON study_space_images AFTER UPDATE ON study_space_images
FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); FOR EACH ROW EXECUTE FUNCTION handle_updated_at();
-- Security
-- ALTER TABLE study_spaces ENABLE ROW LEVEL SECURITY;
-- ALTER TABLE study_space_images ENABLE ROW LEVEL SECURITY;
-- CREATE POLICY "Allow all users to view study spaces"
-- ON study_spaces
-- FOR SELECT
-- USING (true);
-- CREATE POLICY "Allow all users to view study space images"
-- ON study_space_images
-- FOR SELECT
-- USING (true);