diff --git a/.gitignore b/.gitignore index 3c3dc2d..743f763 100644 --- a/.gitignore +++ b/.gitignore @@ -70,4 +70,5 @@ e2e/.env e2e/.env.local # Playwright generated files -.auth/ \ No newline at end of file +.auth/ +notes.md \ No newline at end of file diff --git a/nsc-events-nextjs/app/auth/sign-in/page.tsx b/nsc-events-nextjs/app/auth/sign-in/page.tsx index 14299e1..dc42f01 100644 --- a/nsc-events-nextjs/app/auth/sign-in/page.tsx +++ b/nsc-events-nextjs/app/auth/sign-in/page.tsx @@ -6,9 +6,11 @@ import Image from "next/image"; import { Container, Paper, Box, TextField, Button, Typography, Link as MuiLink, useMediaQuery } from "@mui/material"; import { textFieldStyle } from "@/components/InputFields"; import { useTheme } from "@mui/material"; +import { createClient } from "@/lib/supabaseClient"; +const supabase = createClient(); -const URL = process.env.NSC_EVENTS_PUBLIC_API_URL; -console.log('Sign-in Dev API Address: ', URL); +// const URL = process.env.NSC_EVENTS_PUBLIC_API_URL; +// console.log('Sign-in Dev API Address: ', URL); const Signin = () => { const { palette } = useTheme(); @@ -17,7 +19,7 @@ const Signin = () => { const lightImagePath = '/images/blue_nsc_logo.png'; const imagePath = palette.mode === "dark" ? darkImagePath : lightImagePath; - + const [error, setError] = useState(""); const [userInfo, setUserInfo] = useState({ email: "", @@ -25,7 +27,7 @@ const Signin = () => { }); const router = useRouter(); - + const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isTablet = useMediaQuery(theme.breakpoints.between('sm', 'md')); @@ -40,51 +42,43 @@ const Signin = () => { const handleSubmit: FormEventHandler = async (e) => { e.preventDefault(); - + setError("") try { - if (!URL) { - setError("API URL not configured"); - return; - } - - const loginEndpoint = `${URL}/auth/login`; - - const res = await fetch(loginEndpoint, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - email, - password, - }), + + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password }); - - if (!res.ok) { - const errorText = await res.text(); - setError("Invalid email or password"); + + if (error) { + setError(error.message || "Invalid email or password"); return; } - - const data = await res.json(); - - if (!data.token) { - setError("Login response missing token"); - return; + + if (!data.session) { + setError("Login was sucessful, but no session was returned") } - - const { token } = data; - let userRole; - + + const token = data.session.access_token; + localStorage.setItem("token", token); + window.dispatchEvent(new CustomEvent('auth-change')); + + let userRole: string | undefined; + try { const payload = JSON.parse(atob(token.split(".")[1])); - userRole = payload.role; + userRole = payload.app_metadata?.role; + + if (!userRole) { + userRole = 'user'; + } + localStorage.setItem("role", userRole); + } catch (parseError) { - setError("Invalid token format"); - return; + console.error("Could not parse JWT:", parseError); + userRole = 'user'; } - - localStorage.setItem("token", token); - window.dispatchEvent(new CustomEvent('auth-change')); - + if (userRole === "admin") { router.push("/admin"); } else if (userRole === "creator") { @@ -92,41 +86,42 @@ const Signin = () => { } else { router.push("/"); } - } catch (err) { + } + catch (err) { setError("An error occurred during login"); console.error("Login error:", err); } }; return ( - - - { @@ -11,11 +14,12 @@ const useAuth = () => { const checkAuth = () => { try { + // console.log("local storage in use auth hook", localStorage) const token = localStorage.getItem('token'); - + // Trim whitespace and check if token exists const trimmedToken = token?.trim(); - + if (!trimmedToken) { setIsAuth(false); setUser(null); @@ -26,6 +30,7 @@ const useAuth = () => { // Validate JWT format (must have 3 parts) const parts = trimmedToken.split('.'); + if (parts.length !== 3) { console.error('Invalid JWT format'); setUser(null); @@ -33,10 +38,12 @@ const useAuth = () => { } // Decode token payload - const payload = parts[1]; - const decoded = atob(payload); - const user = JSON.parse(decoded); - + const payload = JSON.parse(atob(trimmedToken.split(".")[1]));; + // const decoded = atob(payload); + const user = payload.app_metadata; + // console.log("payload", payload) + // console.log("decoded", decoded) + // console.log("user", user) setUser(user); } catch (error) { // Handle any decoding or parsing errors gracefully diff --git a/nsc-events-nextjs/lib/supabaseClient.ts b/nsc-events-nextjs/lib/supabaseClient.ts index 79f2054..71c2bdc 100644 --- a/nsc-events-nextjs/lib/supabaseClient.ts +++ b/nsc-events-nextjs/lib/supabaseClient.ts @@ -7,4 +7,4 @@ export const createClient = () => createBrowserClient( supabaseUrl!, supabaseKey!, - ); \ No newline at end of file + ); diff --git a/nsc-events-nextjs/lib/supabaseServer.ts b/nsc-events-nextjs/lib/supabaseServer.ts new file mode 100644 index 0000000..f2df5ea --- /dev/null +++ b/nsc-events-nextjs/lib/supabaseServer.ts @@ -0,0 +1,26 @@ +import { createServerClient } from '@supabase/ssr'; +import { cookies } from 'next/headers'; + +export function createSupabaseServerClient() { + const cookieStore = cookies(); + + return createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return cookieStore.getAll(); + }, + setAll(cookiesToSet) { + try { + cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options)); + } catch { + // Ignore in some SSR contexts + } + }, + }, + } + ); +} +export default createSupabaseServerClient; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9e07688..4c89415 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11892,7 +11892,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -12727,9 +12726,9 @@ } }, "node_modules/supabase": { - "version": "2.76.15", - "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.76.15.tgz", - "integrity": "sha512-m69o1XPAzZaIWfQiEeT+KY/Ci3OSA663RyoH9xECbXSxhr7dsipLCpCqT1E4MCob0mMhHh/7A+Eltx4y1qSwiQ==", + "version": "2.78.1", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.78.1.tgz", + "integrity": "sha512-fCIE/LTTr1IGrrYLqYBI3w89QU1qW+mRVtUi/Dmrtj+oXtDX4E8VgfFlXZpoYsXy86cfE9RZXUJVAGgvdNfTPg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -12737,7 +12736,7 @@ "bin-links": "^6.0.0", "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2", - "tar": "7.5.9" + "tar": "7.5.11" }, "bin": { "supabase": "bin/supabase" @@ -12813,9 +12812,9 @@ } }, "node_modules/supabase/node_modules/tar": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", - "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", + "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -13300,7 +13299,6 @@ "version": "0.3.27", "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.27.tgz", "integrity": "sha512-pNV1bn+1n8qEe8tUNsNdD8ejuPcMAg47u2lUGnbsajiNUr3p2Js1XLKQjBMH0yMRMDfdX8T+fIRejFmIwy9x4A==", - "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^3.17.0", diff --git a/supabase/migrations/20260309032010_create_update_updated_at_function.sql b/supabase/migrations/20260309032010_create_update_updated_at_function.sql new file mode 100644 index 0000000..82f2f94 --- /dev/null +++ b/supabase/migrations/20260309032010_create_update_updated_at_function.sql @@ -0,0 +1,7 @@ +CREATE OR REPLACE FUNCTION public.update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/supabase/migrations/20260309032220_create_users_table.sql b/supabase/migrations/20260309032220_create_users_table.sql new file mode 100644 index 0000000..16466b9 --- /dev/null +++ b/supabase/migrations/20260309032220_create_users_table.sql @@ -0,0 +1,50 @@ +-- Migration: create_users_table +-- Creates users table for NSC Events + +CREATE TYPE public.user_role AS ENUM ('user', 'creator', 'admin'); + +CREATE TABLE IF NOT EXISTS public.users ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + first_name varchar NOT NULL, + last_name varchar NOT NULL, + pronouns varchar, + role user_role NOT NULL DEFAULT 'user', + email varchar NOT NULL UNIQUE, + google_credentials jsonb, + reset_password_token varchar UNIQUE, + reset_password_expires timestamptz, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now() +); + +-- Enable Row Level Security (RLS) +ALTER TABLE public.users ENABLE ROW LEVEL SECURITY; + +-- CREATE POLICY "Admins can read all users" +-- ON public.users +-- FOR SELECT +-- USING ( +-- EXISTS ( +-- SELECT 1 +-- FROM public.users +-- WHERE id = auth.uid() +-- AND role = 'admin' +-- ) +-- ); + +-- users can only read/update their own row +CREATE POLICY "Users can view own profile" + ON public.users + FOR SELECT + USING (auth.uid() = id); + +CREATE POLICY "Users can update own profile" + ON public.users + FOR UPDATE + USING (auth.uid() = id) + WITH CHECK (auth.uid() = id); + +CREATE OR REPLACE TRIGGER update_users_updated_at + BEFORE UPDATE ON public.users + FOR EACH ROW + EXECUTE FUNCTION public.update_updated_at_column(); \ No newline at end of file diff --git a/supabase/migrations/20260309034258_create_media_table.sql b/supabase/migrations/20260309034258_create_media_table.sql new file mode 100644 index 0000000..9ad1299 --- /dev/null +++ b/supabase/migrations/20260309034258_create_media_table.sql @@ -0,0 +1,35 @@ +CREATE TABLE IF NOT EXISTS public.media ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + filename varchar NOT NULL, + original_name varchar NOT NULL, + mime_type varchar, + size bigint NOT NULL, + s3_key varchar, + s3_url varchar, + type varchar, + uploaded_by_user_id uuid NOT NULL REFERENCES public.users(id) ON DELETE SET NULL, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now() +); + +-- Enable Row Level Security (RLS) +ALTER TABLE public.media ENABLE ROW LEVEL SECURITY; + +-- Uploader can view/update/delete their own media +CREATE POLICY "Users can manage own media" + ON public.media + FOR ALL + USING (auth.uid() = uploaded_by_user_id) + WITH CHECK (auth.uid() = uploaded_by_user_id); + +-- public can access to media +CREATE POLICY "Public can view media" + ON public.media + FOR SELECT + USING (true); + +-- Update updated at +CREATE OR REPLACE TRIGGER update_media_updated_at + BEFORE UPDATE ON public.media + FOR EACH ROW + EXECUTE FUNCTION public.update_updated_at_column(); \ No newline at end of file diff --git a/supabase/migrations/20260310014930_create_tags_table.sql b/supabase/migrations/20260310014930_create_tags_table.sql new file mode 100644 index 0000000..cc7f03d --- /dev/null +++ b/supabase/migrations/20260310014930_create_tags_table.sql @@ -0,0 +1,21 @@ +CREATE TABLE IF NOT EXISTS public.tags ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name varchar NOT NULL UNIQUE, + slug varchar NOT NULL UNIQUE, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now() +); + +-- Enable Row Level Security (RLS) +ALTER TABLE public.tags ENABLE ROW LEVEL SECURITY; + +-- everyone can see tags +CREATE POLICY "Public read access to tags" + ON public.tags + FOR SELECT + USING (true); + +CREATE OR REPLACE TRIGGER update_tags_updated_at + BEFORE UPDATE ON public.tags + FOR EACH ROW + EXECUTE FUNCTION public.update_updated_at_column(); \ No newline at end of file diff --git a/supabase/migrations/20260310022009_create_activities_table.sql b/supabase/migrations/20260310022009_create_activities_table.sql new file mode 100644 index 0000000..7e6bd94 --- /dev/null +++ b/supabase/migrations/20260310022009_create_activities_table.sql @@ -0,0 +1,50 @@ +CREATE TABLE IF NOT EXISTS public.activities ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + created_by_user_id uuid NOT NULL REFERENCES public.users(id) ON DELETE SET NULL, + event_title varchar NOT NULL, + event_description text NOT NULL, + start_date timestamptz NOT NULL, + end_date timestamptz NOT NULL, + event_location varchar NOT NULL, + event_host varchar NOT NULL, + event_meeting_url varchar, + event_registration varchar, + event_capacity varchar NOT NULL, + event_schedule varchar, + event_speakers text, + event_prerequisites varchar, + event_cancellation_policy varchar, + event_contact varchar NOT NULL, + event_social_media jsonb, + event_privacy varchar, + event_accessibility varchar, + event_note varchar, + is_hidden boolean DEFAULT false, + is_archived boolean DEFAULT false, + cover_photo_id uuid REFERENCES public.media(id) ON DELETE SET NULL, + document_id uuid REFERENCES public.media(id) ON DELETE SET NULL, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now() +); + +-- Enable Row Level Security (RLS) +ALTER TABLE public.activities ENABLE ROW LEVEL SECURITY; + +-- Creator can view/update/delete their own events +CREATE POLICY "Creators can manage own events" + ON public.activities + FOR ALL + USING (auth.uid() = created_by_user_id) + WITH CHECK (auth.uid() = created_by_user_id); + +-- public have read access to activities +CREATE POLICY "Public can view activity" + ON public.activities + FOR SELECT + USING (true); + +-- Update updated at +CREATE OR REPLACE TRIGGER update_activities_updated_at + BEFORE UPDATE ON public.activities + FOR EACH ROW + EXECUTE FUNCTION public.update_updated_at_column(); \ No newline at end of file diff --git a/supabase/migrations/20260310031659_create_event_registrations_table.sql b/supabase/migrations/20260310031659_create_event_registrations_table.sql new file mode 100644 index 0000000..38ed0e7 --- /dev/null +++ b/supabase/migrations/20260310031659_create_event_registrations_table.sql @@ -0,0 +1,48 @@ +CREATE TABLE IF NOT EXISTS public.event_registrations ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + activity_id uuid NOT NULL REFERENCES public.activities(id) ON DELETE SET NULL, + user_id uuid NOT NULL REFERENCES public.users(id) ON DELETE SET NULL, + college varchar NOT NULL, + year_of_study varchar NOT NULL, + is_attended boolean DEFAULT false, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now() +); + +-- Enable Row Level Security (RLS) +ALTER TABLE public.event_registrations ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Users view own registrations" + ON public.event_registrations + FOR SELECT + USING (auth.uid() = user_id); + +CREATE POLICY "Users can register themselves" + ON public.event_registrations + FOR INSERT + WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "Users can remove own registrations" + ON public.event_registrations + FOR DELETE + USING (auth.uid() = user_id); + +CREATE POLICY "Users can change own registrations" + ON public.event_registrations + FOR UPDATE + USING (auth.uid() = user_id); + +CREATE POLICY "Creators view registrations for their events" + ON public.event_registrations + FOR SELECT + USING ( + EXISTS ( + SELECT 1 FROM public.activities activity + WHERE activity.id = activity_id + AND activity.created_by_user_id = auth.uid()) + ); +-- Update updated at +CREATE OR REPLACE TRIGGER update_event_registrations_updated_at + BEFORE UPDATE ON public.event_registrations + FOR EACH ROW + EXECUTE FUNCTION public.update_updated_at_column(); \ No newline at end of file diff --git a/supabase/migrations/20260310035012_create_activity_tags_table.sql b/supabase/migrations/20260310035012_create_activity_tags_table.sql new file mode 100644 index 0000000..d1c4b45 --- /dev/null +++ b/supabase/migrations/20260310035012_create_activity_tags_table.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS public.activity_tags ( + activity_id uuid NOT NULL REFERENCES public.activities(id) ON DELETE CASCADE, + tag_id uuid NOT NULL REFERENCES public.tags(id) ON DELETE CASCADE, + PRIMARY KEY (activity_id, tag_id) +); + +ALTER TABLE public.activity_tags ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Public can view activity tags" + ON public.activity_tags + FOR SELECT + USING (true); \ No newline at end of file diff --git a/supabase/seed.sql b/supabase/seed.sql new file mode 100644 index 0000000..18f6468 --- /dev/null +++ b/supabase/seed.sql @@ -0,0 +1,67 @@ + +-- 1. Insert into auth.users (Supabase Auth table) +INSERT INTO auth.users ( + instance_id, + id, + aud, + role, + email, + encrypted_password, + email_confirmed_at, + confirmation_token, + email_change, + email_change_token_new, + recovery_token, + raw_app_meta_data, + raw_user_meta_data, + created_at, + updated_at +) +VALUES ( + '00000000-0000-0000-0000-000000000000'::uuid, + gen_random_uuid(), + 'authenticated', + 'authenticated', + 'admin@gmail.com', + crypt('Admin123!', gen_salt('bf')), + now(), + '', + '', + '', + '', + '{"provider":"email","providers":["email"],"role":"admin"}'::jsonb, + '{}'::jsonb, + now(), + now() +); + +-- 2. Insert matching row in public.users +-- Uses the same id as the auth user above +INSERT INTO public.users ( + id, + first_name, + last_name, + pronouns, + role, + email, + created_at, + updated_at +) +SELECT + id, + 'Admin', + 'User', + 'They/Them', + 'admin', + 'admin@gmail.com', + now(), + now() +FROM auth.users +WHERE email = 'admin@gmail.com' +ON CONFLICT (id) DO UPDATE +SET + first_name = EXCLUDED.first_name, + last_name = EXCLUDED.last_name, + pronouns = EXCLUDED.pronouns, + role = EXCLUDED.role, + updated_at = now(); \ No newline at end of file diff --git a/supabase/snippets/Untitled query 790.sql b/supabase/snippets/Untitled query 790.sql new file mode 100644 index 0000000..69e5c6c --- /dev/null +++ b/supabase/snippets/Untitled query 790.sql @@ -0,0 +1 @@ +SELECT * from public.users \ No newline at end of file