Skip to content
Merged
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
112 changes: 111 additions & 1 deletion src/lib/header/Header.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
<script>
<script lang="ts">
import { page } from '$app/stores';
import { user } from '$lib/services/auth';
import { signInWithGithub, signOut } from '$lib/services/auth';

async function handleLogin() {
try {
await signInWithGithub();
} catch (error) {
console.error('Login error:', error);
}
}

async function handleLogout() {
try {
await signOut();
} catch (error) {
console.error('Logout error:', error);
}
}
</script>

<header>
Expand All @@ -18,7 +36,24 @@
<li class:active={$page.url.pathname === '/about'}>
<a href="/about">About</a>
</li>
{#if $user}
<li class:active={$page.url.pathname === '/submit'}>
<a href="/submit">Submit Problem</a>
</li>
{/if}
</ul>
<div class="auth-buttons">
{#if $user}
<div class="user-info">
<span class="user-name">{$user.email?.split('@')[0] || 'User'}</span>
<button class="logout-button" on:click={handleLogout}>Logout</button>
</div>
{:else}
<button class="login-button" on:click={handleLogin} title="Login with Google">
<span class="login-text">Sign in</span>
</button>
{/if}
</div>
</nav>
</div>
</header>
Expand Down Expand Up @@ -59,6 +94,7 @@
nav {
display: flex;
align-items: center;
gap: 1.5rem;
}

ul {
Expand Down Expand Up @@ -98,13 +134,87 @@
color: var(--accent-color);
}

.auth-buttons {
display: flex;
align-items: center;
}

.login-button,
.logout-button {
border: none;
border-radius: 4px;
padding: 0.4rem 0.75rem;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.4rem;
}

.login-button {
background-color: #4285f4;
color: white;
border: 1px solid #4285f4;
border-radius: 4px;
padding: 0.4rem 0.75rem;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}

.login-button:hover {
background-color: #3367d6;
border-color: #3367d6;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}

.logout-button {
background-color: transparent;
color: var(--text-color);
border: 1px solid var(--border-color);
}

.logout-button:hover {
background-color: rgba(0, 0, 0, 0.05);
color: var(--text-color);
}

.user-info {
display: flex;
align-items: center;
gap: 0.75rem;
}

.user-name {
font-weight: 600;
font-size: 0.875rem;
}

@media (max-width: 768px) {
.container {
padding: 0 1rem;
}

nav {
flex-direction: column;
align-items: flex-end;
gap: 0.75rem;
}

.logo span {
display: none;
}

.user-name {
display: none;
}

.login-button {
padding: 0.4rem 0.75rem;
}
}
</style>
69 changes: 69 additions & 0 deletions src/lib/services/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { supabase } from './database';
import { writable } from 'svelte/store';
import type { Session, User } from '@supabase/supabase-js';

// Create a store for the user
export const user = writable<User | null>(null);
export const session = writable<Session | null>(null);

// Initialize the auth state
export async function initAuth() {
// Get the initial session
const { data } = await supabase.auth.getSession();
session.set(data.session);
user.set(data.session?.user || null);

// Listen for auth changes
const { data: authListener } = supabase.auth.onAuthStateChange((event, newSession) => {
session.set(newSession);
user.set(newSession?.user || null);
});

return authListener;
}

// Sign in with Github
export async function signInWithGithub() {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});

if (error) {
console.error('Error signing in with Github:', error);
throw error;
}
}

// Sign out
export async function signOut() {
const { error } = await supabase.auth.signOut();

if (error) {
console.error('Error signing out:', error);
throw error;
}
}

// Check if user has admin role
export async function isAdmin(userId: string): Promise<boolean> {
try {
const { data, error } = await supabase
.from('user_roles')
.select('role')
.eq('user_id', userId)
.single();

if (error) {
console.error('Error checking admin status:', error);
return false;
}

return data?.role === 'admin';
} catch (err) {
console.error('Failed to check admin status:', err);
return false;
}
}
18 changes: 17 additions & 1 deletion src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
<script>
<script lang="ts">
import Header from '$lib/header/Header.svelte';
import Footer from '$lib/footer/Footer.svelte';
import '../app.css';
import { onMount } from 'svelte';
import { initAuth } from '$lib/services/auth';

let authSubscription: { subscription: { unsubscribe: () => void } } | null = null;

onMount(async () => {
// Initialize authentication
authSubscription = await initAuth();

// Clean up on component unmount
return () => {
if (authSubscription?.subscription) {
authSubscription.subscription.unsubscribe();
}
};
});
</script>

<div class="app">
Expand Down
22 changes: 22 additions & 0 deletions src/routes/auth/callback/+server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { redirect } from '@sveltejs/kit';

export const GET = async (event) => {
const {
url,
locals: { supabase }
} = event;
const code = url.searchParams.get('code');
const next = url.searchParams.get('next') ?? '/';

if (code) {
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
throw redirect(303, `/${next.slice(1)}`);
}
}

// return the user to an error page with instructions
throw redirect(303, '/auth/auth-code-error');
};
Loading