refactor: cleanup skeleton, setup supabase properly
This commit is contained in:
22
package-lock.json
generated
22
package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "drp-48",
|
"name": "drp-48",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@supabase/ssr": "^0.6.1",
|
||||||
"@supabase/supabase-js": "^2.49.8"
|
"@supabase/supabase-js": "^2.49.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1459,6 +1460,27 @@
|
|||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@supabase/ssr": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-QtQgEMvaDzr77Mk3vZ3jWg2/y+D8tExYF7vcJT+wQ8ysuvOeGGjYbZlvj5bHYsj/SpC0bihcisnwPrM4Gp5G4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@supabase/supabase-js": "^2.43.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/ssr/node_modules/cookie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@supabase/storage-js": {
|
"node_modules/@supabase/storage-js": {
|
||||||
"version": "2.7.1",
|
"version": "2.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz",
|
||||||
|
|||||||
@@ -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
14
src/app.d.ts
vendored
@@ -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
81
src/hooks.server.ts
Normal 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
12
src/lib/database.d.ts
vendored
@@ -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: []
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
9
src/routes/+layout.server.ts
Normal file
9
src/routes/+layout.server.ts
Normal 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
44
src/routes/+layout.ts
Normal 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 };
|
||||||
|
};
|
||||||
@@ -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(*)");
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|
||||||
|
const { data: imgUpload, error: imageError } = await supabase.storage
|
||||||
|
.from("files_bucket")
|
||||||
|
.upload(`public/${studySpaceInsert.id}-${imageFile.name}`, imageFile, {
|
||||||
|
contentType: imageFile.type
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (imageError) return alert(`Error uploading image: ${imageError.message}`);
|
||||||
alert("Study space uploaded successfully!");
|
|
||||||
await invalidate("db:study_spaces");
|
const { error: imageInsertError } = await supabase
|
||||||
} else {
|
.from("study_space_images")
|
||||||
alert("Failed to upload study space.");
|
.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>
|
||||||
|
|||||||
@@ -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 });
|
|
||||||
};
|
|
||||||
@@ -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);
|
|
||||||
@@ -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;
|
||||||
@@ -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);
|
|
||||||
Reference in New Issue
Block a user