Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,5 @@ e2e/.env
e2e/.env.local

# Playwright generated files
.auth/
.auth/
notes.md
121 changes: 58 additions & 63 deletions nsc-events-nextjs/app/auth/sign-in/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -17,15 +19,15 @@ 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: "",
password: "",
});

const router = useRouter();

const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const isTablet = useMediaQuery(theme.breakpoints.between('sm', 'md'));
Expand All @@ -40,93 +42,86 @@ const Signin = () => {

const handleSubmit: FormEventHandler<HTMLFormElement> = 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") {
router.push("/creator");
} else {
router.push("/");
}
} catch (err) {
}
catch (err) {
setError("An error occurred during login");
console.error("Login error:", err);
}
};

return (
<Container
maxWidth="xs"
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
height: '100vh',
<Container
maxWidth="xs"
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
height: '100vh',
justifyContent: 'center',
mt: isMobile ? -8 : isTablet ? -6 : -10
}}
>
<Paper
elevation={6}
sx={{
padding: 4,
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
borderRadius: 2,
mb: 2
<Paper
elevation={6}
sx={{
padding: 4,
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
borderRadius: 2,
mb: 2
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
marginBottom: 2
<Box
sx={{
display: 'flex',
justifyContent: 'center',
marginBottom: 2
}}
>
<Image
Expand Down
23 changes: 15 additions & 8 deletions nsc-events-nextjs/hooks/useAuth.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
{/* Custom React hook to manage and track user authentication state.*/}
{/* Custom React hook to manage and track user authentication state.*/ }
import { useState, useEffect } from 'react';
import { createClient } from "@/lib/supabaseClient";

const supabase = createClient();
type Role = 'user' | 'creator' | 'admin' | null;
interface User {
role: string;
role: string;
}

const useAuth = () => {
Expand All @@ -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);
Expand All @@ -26,17 +30,20 @@ const useAuth = () => {

// Validate JWT format (must have 3 parts)
const parts = trimmedToken.split('.');

if (parts.length !== 3) {
console.error('Invalid JWT format');
setUser(null);
return;
}

// 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
Expand Down
2 changes: 1 addition & 1 deletion nsc-events-nextjs/lib/supabaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ export const createClient = () =>
createBrowserClient(
supabaseUrl!,
supabaseKey!,
);
);
26 changes: 26 additions & 0 deletions nsc-events-nextjs/lib/supabaseServer.ts
Original file line number Diff line number Diff line change
@@ -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;
16 changes: 7 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -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;
50 changes: 50 additions & 0 deletions supabase/migrations/20260309032220_create_users_table.sql
Original file line number Diff line number Diff line change
@@ -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();
Loading
Loading