diff --git a/.env.example b/.env.example index c1c57e1..dd7a631 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,7 @@ # Supabase Configuration NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key +SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key + +# Site URL (used for redirect URLs in emails) +NEXT_PUBLIC_SITE_URL=http://localhost:3000 diff --git a/src/app/admin/invite/InviteForm.tsx b/src/app/admin/invite/InviteForm.tsx new file mode 100644 index 0000000..169ca46 --- /dev/null +++ b/src/app/admin/invite/InviteForm.tsx @@ -0,0 +1,87 @@ +'use client' + +import { useState } from 'react' +import Link from 'next/link' +import { inviteUser } from './actions' + +export default function InviteForm() { + const [email, setEmail] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(false) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError(null) + setSuccess(false) + setLoading(true) + + const result = await inviteUser(email) + + if (!result.success) { + setError(result.error || 'Failed to send invitation') + setLoading(false) + return + } + + setSuccess(true) + setEmail('') + setLoading(false) + } + + return ( +
+
+ {success && ( +
+

+ Invitation sent successfully! The user will receive an email with instructions to complete their account setup. +

+
+ )} + + {error && ( +
+

{error}

+
+ )} + +
+ + setEmail(e.target.value)} + className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" + placeholder="user@example.com" + /> +
+ +
+ + Back to Dashboard + + +
+
+
+ ) +} diff --git a/src/app/admin/invite/actions.ts b/src/app/admin/invite/actions.ts new file mode 100644 index 0000000..78724d9 --- /dev/null +++ b/src/app/admin/invite/actions.ts @@ -0,0 +1,54 @@ +'use server' + +import { createClient } from '@supabase/supabase-js' +import { createClient as createServerClient } from '@/lib/supabase/server' + +export async function inviteUser(email: string): Promise<{ success: boolean; error?: string }> { + // First, verify the current user is an admin + const supabase = await createServerClient() + + const { data: { user } } = await supabase.auth.getUser() + + if (!user) { + return { success: false, error: 'You must be logged in to invite users' } + } + + // Check if user is admin + const { data: profile, error: profileError } = await supabase + .from('profiles') + .select('is_admin') + .eq('id', user.id) + .single() + + if (profileError || !profile?.is_admin) { + return { success: false, error: 'You do not have permission to invite users' } + } + + // Create admin client with service role key for inviting users + const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY + if (!serviceRoleKey) { + return { success: false, error: 'Server configuration error: missing service role key' } + } + + const adminClient = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + serviceRoleKey, + { + auth: { + autoRefreshToken: false, + persistSession: false, + }, + } + ) + + // Invite the user + const { error: inviteError } = await adminClient.auth.admin.inviteUserByEmail(email, { + redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/signup`, + }) + + if (inviteError) { + return { success: false, error: inviteError.message } + } + + return { success: true } +} diff --git a/src/app/admin/invite/page.tsx b/src/app/admin/invite/page.tsx new file mode 100644 index 0000000..84a9c9f --- /dev/null +++ b/src/app/admin/invite/page.tsx @@ -0,0 +1,41 @@ +import { redirect } from 'next/navigation' +import { createClient } from '@/lib/supabase/server' +import InviteForm from './InviteForm' + +export default async function AdminInvitePage() { + const supabase = await createClient() + + const { data: { user } } = await supabase.auth.getUser() + + if (!user) { + redirect('/login') + } + + // Check if user is admin + const { data: profile } = await supabase + .from('profiles') + .select('is_admin') + .eq('id', user.id) + .single() + + if (!profile?.is_admin) { + redirect('/dashboard') + } + + return ( +
+
+
+

+ Invite New User +

+

+ Send an invitation email to a new user to give them access to WebVNWrite. +

+
+ + +
+
+ ) +} diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 497583f..f9adb93 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -17,9 +17,18 @@ export default async function DashboardLayout({ redirect('/login') } + // Fetch user profile to check admin status + const { data: profile } = await supabase + .from('profiles') + .select('is_admin') + .eq('id', user.id) + .single() + + const isAdmin = profile?.is_admin || false + return (
- +
{children}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 7b5bec8..d7d1a57 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -3,9 +3,10 @@ import LogoutButton from './LogoutButton' interface NavbarProps { userEmail: string + isAdmin?: boolean } -export default function Navbar({ userEmail }: NavbarProps) { +export default function Navbar({ userEmail, isAdmin }: NavbarProps) { return (