diff --git a/src/lib/header/Header.svelte b/src/lib/header/Header.svelte index f4b76c3..66b49db 100644 --- a/src/lib/header/Header.svelte +++ b/src/lib/header/Header.svelte @@ -1,5 +1,23 @@ -
@@ -18,7 +36,24 @@
  • About
  • + {#if $user} +
  • + Submit Problem +
  • + {/if} +
    + {#if $user} + + {:else} + + {/if} +
    @@ -59,6 +94,7 @@ nav { display: flex; align-items: center; + gap: 1.5rem; } ul { @@ -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; + } } diff --git a/src/lib/services/auth.ts b/src/lib/services/auth.ts new file mode 100644 index 0000000..f50a04b --- /dev/null +++ b/src/lib/services/auth.ts @@ -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(null); +export const session = writable(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 { + 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; + } +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index e440aee..a48ff05 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,7 +1,23 @@ -
    diff --git a/src/routes/auth/callback/+server.js b/src/routes/auth/callback/+server.js new file mode 100644 index 0000000..f54bdaf --- /dev/null +++ b/src/routes/auth/callback/+server.js @@ -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'); +}; diff --git a/src/routes/submit/+page.svelte b/src/routes/submit/+page.svelte new file mode 100644 index 0000000..3e7ae95 --- /dev/null +++ b/src/routes/submit/+page.svelte @@ -0,0 +1,639 @@ + + + + Submit Problems - Probhub + + +
    +
    +

    Submit Problems

    +

    + Share interesting programming problems with the community. Paste one or more Codeforces + problem URLs below, and we'll automatically fetch and add them to the database. +

    + + {#if checkingAdmin} +
    Checking your permissions...
    + {:else if error} +
    {error}
    + {:else if success} +
    Problems processed successfully! See results below.
    + {/if} + + {#if isAdminUser && !checkingAdmin} +
    +
    + + + + Format: https://codeforces.com/contest/1234/problem/A +
    + You can paste multiple URLs - one per line, or separated by spaces. +
    +
    + +
    + + {#if processingResults.length > 0} + + {/if} +
    +
    + + {#if processingResults.length > 0} +
    +

    Processing Results

    +
    + {#each processingResults as result} +
    + +
    + {#if result.status === 'pending'} + Pending + {:else if result.status === 'success'} + ✓ {result.message} + {:else} + ✗ {result.message} + {/if} +
    +
    + {/each} +
    +
    +

    + Total: {processingResults.length} | Success: {processingResults.filter( + (r) => r.status === 'success' + ).length} | Failed: {processingResults.filter((r) => r.status === 'error').length} | Pending: + {processingResults.filter((r) => r.status === 'pending').length} +

    +
    +
    + {/if} + {/if} +
    +
    + + diff --git a/supabase.session.sql b/supabase.session.sql index 723b4bd..b6b3851 100644 --- a/supabase.session.sql +++ b/supabase.session.sql @@ -1,14 +1,88 @@ --- Create problems table -CREATE TABLE problems ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), +-- Create user_roles table for managing admin users +CREATE TABLE IF NOT EXISTS user_roles ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + role TEXT NOT NULL CHECK (role IN ('admin', 'user')), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(user_id) +); +-- Create RLS policies for user_roles table +ALTER TABLE user_roles ENABLE ROW LEVEL SECURITY; +-- Allow users to read their own role +CREATE POLICY "Users can read their own role" ON user_roles FOR +SELECT USING (auth.uid() = user_id); +-- Only allow super admins to insert/update roles +-- Note: The first admin will need to be created manually by a database administrator +CREATE POLICY "Only super admins can insert roles" ON user_roles FOR +INSERT WITH CHECK ( + EXISTS ( + SELECT 1 + FROM user_roles + WHERE user_id = auth.uid() + AND role = 'admin' + ) + ); +CREATE POLICY "Only super admins can update roles" ON user_roles FOR +UPDATE USING ( + EXISTS ( + SELECT 1 + FROM user_roles + WHERE user_id = auth.uid() + AND role = 'admin' + ) + ); +-- Disable deletion of user roles entirely +-- We don't want to allow deletion of user roles at all +-- Create function to automatically assign 'user' role to new users +CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS TRIGGER AS $$ BEGIN +INSERT INTO public.user_roles (user_id, role) +VALUES (NEW.id, 'user'); +RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; +-- Create trigger to call the function when a new user is created +DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; +CREATE TRIGGER on_auth_user_created +AFTER +INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_new_user(); +-- Create problems table if it doesn't exist +CREATE TABLE IF NOT EXISTS problems ( + id TEXT PRIMARY KEY, name TEXT NOT NULL, - tags TEXT [] NOT NULL, + tags TEXT [] NOT NULL DEFAULT '{}', difficulty INTEGER NOT NULL, - url TEXT NOT NULL UNIQUE, - solved INTEGER, - date_added DATE NOT NULL DEFAULT CURRENT_DATE, + url TEXT NOT NULL, + solved INTEGER DEFAULT 0, + date_added TIMESTAMP WITH TIME ZONE DEFAULT NOW(), added_by TEXT NOT NULL, added_by_url TEXT NOT NULL, - likes INTEGER NOT NULL DEFAULT 0, - dislikes INTEGER NOT NULL DEFAULT 0 -); \ No newline at end of file + likes INTEGER DEFAULT 0, + dislikes INTEGER DEFAULT 0 +); +-- Create RLS policies for problems table +ALTER TABLE problems ENABLE ROW LEVEL SECURITY; +-- Allow anyone to read problems +CREATE POLICY "Anyone can read problems" ON problems FOR +SELECT USING (true); +-- Only admins can insert problems +CREATE POLICY "Only admins can insert problems" ON problems FOR +INSERT WITH CHECK ( + EXISTS ( + SELECT 1 + FROM user_roles + WHERE user_id = auth.uid() + AND role = 'admin' + ) + ); +-- Only admins can update problems +CREATE POLICY "Only admins can update problems" ON problems FOR +UPDATE USING ( + EXISTS ( + SELECT 1 + FROM user_roles + WHERE user_id = auth.uid() + AND role = 'admin' + ) + ); +-- Disable deletion of problems entirely +-- We don't want to allow deletion of problems at all \ No newline at end of file