diff --git a/app/api/companies/[slug]/members/[userId]/route.ts b/app/api/companies/[slug]/members/[userId]/route.ts
index d9957c0d..145b47a1 100644
--- a/app/api/companies/[slug]/members/[userId]/route.ts
+++ b/app/api/companies/[slug]/members/[userId]/route.ts
@@ -11,8 +11,8 @@ export const runtime = 'nodejs'
// Validation schema for role update
const updateRoleSchema = z.object({
- role: z.enum(['owner', 'admin', 'editor', 'member'], {
- errorMap: () => ({ message: 'Role must be owner, admin, editor, or member' }),
+ role: z.enum(['owner', 'admin', 'editor', 'viewer'], {
+ errorMap: () => ({ message: 'Role must be owner, admin, editor, or viewer' }),
}),
})
diff --git a/app/api/companies/[slug]/members/invite/route.ts b/app/api/companies/[slug]/members/invite/route.ts
index 1a532189..eca82817 100644
--- a/app/api/companies/[slug]/members/invite/route.ts
+++ b/app/api/companies/[slug]/members/invite/route.ts
@@ -13,8 +13,8 @@ export const runtime = 'nodejs'
// Validation schema for invite request
const inviteSchema = z.object({
email: z.string().email('Invalid email address'),
- role: z.enum(['admin', 'editor', 'member'], {
- errorMap: () => ({ message: 'Role must be admin, editor, or member' }),
+ role: z.enum(['admin', 'editor', 'viewer'], {
+ errorMap: () => ({ message: 'Role must be admin, editor, or viewer' }),
}),
})
diff --git a/app/dashboard/company/[slug]/accept-invitation/page.tsx b/app/dashboard/company/[slug]/accept-invitation/page.tsx
index 685cb706..f341dff6 100644
--- a/app/dashboard/company/[slug]/accept-invitation/page.tsx
+++ b/app/dashboard/company/[slug]/accept-invitation/page.tsx
@@ -199,7 +199,7 @@ export default function AcceptInvitationPage() {
<>
- You've been invited to join {companyName} on CodeUnia.
+ You've been invited to join {companyName} on Codeunia.
By accepting this invitation, you'll be able to collaborate with the team and manage events.
diff --git a/app/dashboard/company/[slug]/analytics/page.tsx b/app/dashboard/company/[slug]/analytics/page.tsx
index 9b888c48..034af72f 100644
--- a/app/dashboard/company/[slug]/analytics/page.tsx
+++ b/app/dashboard/company/[slug]/analytics/page.tsx
@@ -9,6 +9,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
import { Skeleton } from '@/components/ui/skeleton'
import { AnalyticsCharts } from '@/components/dashboard/AnalyticsCharts'
import { useCompanyContext } from '@/contexts/CompanyContext'
+import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
import { CompanyAnalytics } from '@/types/company'
import { format, subDays, startOfMonth, endOfMonth, subMonths } from 'date-fns'
import { CalendarIcon, AlertCircle, TrendingUp } from 'lucide-react'
@@ -25,6 +26,7 @@ export default function AnalyticsPage() {
const params = useParams()
const companySlug = params?.slug as string
const { currentCompany, loading: companyLoading } = useCompanyContext()
+ const isPendingInvitation = usePendingInvitationRedirect()
const [analytics, setAnalytics] = useState([])
const [loading, setLoading] = useState(true)
@@ -67,6 +69,14 @@ export default function AnalyticsPage() {
}
}, [currentCompany, fetchAnalytics])
+ if (companyLoading || isPendingInvitation) {
+ return (
+
+ )
+ }
+
const handlePresetChange = (preset: PresetRange) => {
setSelectedPreset(preset)
const today = new Date()
diff --git a/app/dashboard/company/[slug]/events/page.tsx b/app/dashboard/company/[slug]/events/page.tsx
index e46252cf..886d8bca 100644
--- a/app/dashboard/company/[slug]/events/page.tsx
+++ b/app/dashboard/company/[slug]/events/page.tsx
@@ -2,6 +2,7 @@
import { useState, useEffect, useCallback } from 'react'
import { useCompanyContext } from '@/contexts/CompanyContext'
+import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
@@ -25,6 +26,7 @@ import {
export default function CompanyEventsPage() {
const { currentCompany, loading: companyLoading } = useCompanyContext()
+ const isPendingInvitation = usePendingInvitationRedirect()
const [events, setEvents] = useState([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('')
@@ -58,6 +60,14 @@ export default function CompanyEventsPage() {
}
}, [currentCompany, fetchEvents])
+ if (companyLoading || isPendingInvitation) {
+ return (
+
+ )
+ }
+
const filteredEvents = events.filter(event =>
event.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
event.category.toLowerCase().includes(searchTerm.toLowerCase())
diff --git a/app/dashboard/company/[slug]/hackathons/page.tsx b/app/dashboard/company/[slug]/hackathons/page.tsx
index 80f862e7..9da9f887 100644
--- a/app/dashboard/company/[slug]/hackathons/page.tsx
+++ b/app/dashboard/company/[slug]/hackathons/page.tsx
@@ -2,6 +2,7 @@
import { useState, useEffect, useCallback } from 'react'
import { useCompanyContext } from '@/contexts/CompanyContext'
+import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
@@ -29,6 +30,7 @@ interface Hackathon {
export default function CompanyHackathonsPage() {
const { currentCompany, loading: companyLoading } = useCompanyContext()
+ const isPendingInvitation = usePendingInvitationRedirect()
const [hackathons, setHackathons] = useState([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('')
@@ -61,6 +63,14 @@ export default function CompanyHackathonsPage() {
}
}, [currentCompany, fetchHackathons])
+ if (companyLoading || isPendingInvitation) {
+ return (
+
+ )
+ }
+
const filteredHackathons = hackathons.filter(hackathon =>
hackathon.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
hackathon.category?.toLowerCase().includes(searchTerm.toLowerCase())
diff --git a/app/dashboard/company/[slug]/page.tsx b/app/dashboard/company/[slug]/page.tsx
index a37526ab..de1c6c03 100644
--- a/app/dashboard/company/[slug]/page.tsx
+++ b/app/dashboard/company/[slug]/page.tsx
@@ -2,6 +2,7 @@
import React from 'react'
import { useCompanyContext } from '@/contexts/CompanyContext'
+import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import Link from 'next/link'
@@ -20,8 +21,9 @@ import { useSubscription } from '@/hooks/useSubscription'
export default function CompanySlugDashboardPage() {
const { currentCompany, userRole, loading, error } = useCompanyContext()
const { usage } = useSubscription(currentCompany?.slug)
+ const isPendingInvitation = usePendingInvitationRedirect()
- if (loading) {
+ if (loading || isPendingInvitation) {
return (
diff --git a/app/dashboard/company/[slug]/settings/page.tsx b/app/dashboard/company/[slug]/settings/page.tsx
index b8409c7b..5800333e 100644
--- a/app/dashboard/company/[slug]/settings/page.tsx
+++ b/app/dashboard/company/[slug]/settings/page.tsx
@@ -2,6 +2,7 @@
import React, { useState } from 'react'
import { useCompanyContext } from '@/contexts/CompanyContext'
+import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
@@ -34,6 +35,7 @@ import { useSubscription } from '@/hooks/useSubscription'
export default function CompanySettingsPage() {
const { currentCompany, userRole, loading: contextLoading, refreshCompany } = useCompanyContext()
+ const isPendingInvitation = usePendingInvitationRedirect()
const { usage } = useSubscription(currentCompany?.slug)
const { toast } = useToast()
const [loading, setLoading] = useState(false)
@@ -103,6 +105,14 @@ export default function CompanySettingsPage() {
}
}, [currentCompany])
+ if (contextLoading || isPendingInvitation) {
+ return (
+
+ )
+ }
+
const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }))
}
diff --git a/app/dashboard/company/[slug]/subscription/page.tsx b/app/dashboard/company/[slug]/subscription/page.tsx
index 0f384771..9af77795 100644
--- a/app/dashboard/company/[slug]/subscription/page.tsx
+++ b/app/dashboard/company/[slug]/subscription/page.tsx
@@ -42,13 +42,23 @@ export default async function SubscriptionPage({ params }: PageProps) {
.select('role, status')
.eq('company_id', company.id)
.eq('user_id', user.id)
- .eq('status', 'active')
.single()
+ // Redirect if no membership found
if (!membership) {
redirect('/dashboard/company')
}
+ // Redirect pending invitations to accept page
+ if (membership.status === 'pending') {
+ redirect(`/dashboard/company/${slug}/accept-invitation`)
+ }
+
+ // Only active members can access subscription
+ if (membership.status !== 'active') {
+ redirect('/dashboard/company')
+ }
+
// Only owners and admins can manage subscription
if (!['owner', 'admin'].includes(membership.role)) {
redirect(`/dashboard/company/${slug}`)
diff --git a/app/dashboard/company/[slug]/team/page.tsx b/app/dashboard/company/[slug]/team/page.tsx
index df4ad246..77d6ac57 100644
--- a/app/dashboard/company/[slug]/team/page.tsx
+++ b/app/dashboard/company/[slug]/team/page.tsx
@@ -4,6 +4,7 @@ import React from 'react'
import { useParams } from 'next/navigation'
import { TeamManagement } from '@/components/dashboard/TeamManagement'
import { useCompanyContext } from '@/contexts/CompanyContext'
+import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
import { Skeleton } from '@/components/ui/skeleton'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { AlertCircle } from 'lucide-react'
@@ -12,8 +13,9 @@ export default function TeamPage() {
const params = useParams()
const companySlug = params?.slug as string
const { currentCompany, userRole, loading, error } = useCompanyContext()
+ const isPendingInvitation = usePendingInvitationRedirect()
- if (loading) {
+ if (loading || isPendingInvitation) {
return (
diff --git a/app/dashboard/company/layout.tsx b/app/dashboard/company/layout.tsx
index 690a6e6a..844a781d 100644
--- a/app/dashboard/company/layout.tsx
+++ b/app/dashboard/company/layout.tsx
@@ -124,7 +124,7 @@ function CompanyDashboardContent({
}) {
const params = useParams()
const companySlug = params?.slug as string
- const { currentCompany, loading, error } = useCompanyContext()
+ const { currentCompany, userCompanies, loading, error } = useCompanyContext()
// Show loading while company context is loading
if (loading) {
@@ -140,6 +140,17 @@ function CompanyDashboardContent({
return
{children}
}
+ // Check if user has pending invitation (not yet accepted)
+ const membership = userCompanies.find(
+ (uc) => uc.company.slug === companySlug
+ )
+ const isPendingInvitation = membership?.status === 'pending'
+
+ // If user has pending invitation, don't show sidebar (only show invitation page)
+ if (isPendingInvitation) {
+ return
{children}
+ }
+
// Generate sidebar items with dynamic company slug
const sidebarItems: SidebarGroupType[] = [
{
diff --git a/components/dashboard/TeamManagement.tsx b/components/dashboard/TeamManagement.tsx
index 6acffe7e..39912dc3 100644
--- a/components/dashboard/TeamManagement.tsx
+++ b/components/dashboard/TeamManagement.tsx
@@ -83,8 +83,8 @@ export function TeamManagement({
const [removeDialogOpen, setRemoveDialogOpen] = useState(false)
const [selectedMember, setSelectedMember] = useState
(null)
const [inviteEmail, setInviteEmail] = useState('')
- const [inviteRole, setInviteRole] = useState<'admin' | 'editor' | 'member'>('member')
- const [newRole, setNewRole] = useState<'owner' | 'admin' | 'editor' | 'member'>('member')
+ const [inviteRole, setInviteRole] = useState<'admin' | 'editor' | 'viewer'>('viewer')
+ const [newRole, setNewRole] = useState<'owner' | 'admin' | 'editor' | 'viewer'>('viewer')
const [submitting, setSubmitting] = useState(false)
const { toast } = useToast()
@@ -160,7 +160,7 @@ export function TeamManagement({
setInviteDialogOpen(false)
setInviteEmail('')
- setInviteRole('member')
+ setInviteRole('viewer')
fetchMembers()
} catch (error) {
console.error('Error inviting member:', error)
@@ -257,7 +257,7 @@ export function TeamManagement({
// Open role dialog
const openRoleDialog = (member: CompanyMember) => {
setSelectedMember(member)
- setNewRole(member.role as 'owner' | 'admin' | 'editor' | 'member')
+ setNewRole(member.role as 'owner' | 'admin' | 'editor' | 'viewer')
setRoleDialogOpen(true)
}
@@ -291,11 +291,11 @@ export function TeamManagement({
Active
)
- case 'invited':
+ case 'pending':
return (
- Invited
+ Pending
)
case 'suspended':
@@ -494,13 +494,13 @@ export function TeamManagement({
- Member - View only
+ Viewer - View only
Editor - Create drafts
Admin - Full event management
- {inviteRole === 'member' && 'Can view company events and analytics'}
+ {inviteRole === 'viewer' && 'Can view company events and analytics'}
{inviteRole === 'editor' && 'Can create draft events'}
{inviteRole === 'admin' && 'Can create, edit, and manage events'}
@@ -543,7 +543,7 @@ export function TeamManagement({
Owner - Full control
Admin - Full event management
Editor - Create drafts
- Member - View only
+ Viewer - View only
diff --git a/components/help/CompanyFAQ.tsx b/components/help/CompanyFAQ.tsx
index 7986cb4f..624e08b2 100644
--- a/components/help/CompanyFAQ.tsx
+++ b/components/help/CompanyFAQ.tsx
@@ -91,7 +91,7 @@ const faqData: FAQItem[] = [
{
category: 'Team Management',
question: 'Can team members see each other\'s activity?',
- answer: 'Owners and Admins can see all team activity. Editors and Members see limited activity relevant to their role.',
+ answer: 'Owners and Admins can see all team activity. Editors and Viewers see limited activity relevant to their role.',
},
{
category: 'Team Management',
diff --git a/lib/email/templates/team-invitation.tsx b/lib/email/templates/team-invitation.tsx
index 7c077f90..452fbab2 100644
--- a/lib/email/templates/team-invitation.tsx
+++ b/lib/email/templates/team-invitation.tsx
@@ -140,7 +140,7 @@ export const getTeamInvitationEmail = (params: TeamInvitationParams) => {
`
return {
- subject: `You've been invited to join ${params.companyName} on CodeUnia`,
+ subject: `You've been invited to join ${params.companyName} on Codeunia`,
html: getEmailTemplate(content),
}
}
diff --git a/lib/hooks/usePendingInvitationRedirect.ts b/lib/hooks/usePendingInvitationRedirect.ts
new file mode 100644
index 00000000..809cb00e
--- /dev/null
+++ b/lib/hooks/usePendingInvitationRedirect.ts
@@ -0,0 +1,30 @@
+import { useEffect } from 'react'
+import { useRouter } from 'next/navigation'
+import { useCompanyContext } from '@/contexts/CompanyContext'
+
+/**
+ * Hook to redirect users with pending invitations to the accept-invitation page
+ * This prevents unauthorized access to company pages before accepting the invitation
+ */
+export function usePendingInvitationRedirect() {
+ const router = useRouter()
+ const { currentCompany, userCompanies, loading } = useCompanyContext()
+
+ useEffect(() => {
+ if (loading || !currentCompany) return
+
+ const membership = userCompanies.find(
+ (uc) => uc.company.slug === currentCompany.slug
+ )
+
+ if (membership?.status === 'pending') {
+ router.push(`/dashboard/company/${currentCompany.slug}/accept-invitation`)
+ }
+ }, [currentCompany, userCompanies, loading, router])
+
+ // Return whether the user has a pending invitation
+ const membership = userCompanies.find(
+ (uc) => uc.company?.slug === currentCompany?.slug
+ )
+ return membership?.status === 'pending'
+}
diff --git a/lib/services/company-member-service.ts b/lib/services/company-member-service.ts
index 6295a263..92c3acc5 100644
--- a/lib/services/company-member-service.ts
+++ b/lib/services/company-member-service.ts
@@ -44,15 +44,15 @@ class CompanyMemberService {
async inviteMember(
companyId: string,
email: string,
- role: 'admin' | 'editor' | 'member',
+ role: 'admin' | 'editor' | 'viewer',
invitedBy: string
): Promise
{
const supabase = await createClient()
// Validate role
- if (!['admin', 'editor', 'member'].includes(role)) {
+ if (!['admin', 'editor', 'viewer'].includes(role)) {
throw new CompanyError(
- 'Invalid role. Must be admin, editor, or member',
+ 'Invalid role. Must be admin, editor, or viewer',
CompanyErrorCodes.UNAUTHORIZED,
400
)
@@ -233,14 +233,14 @@ class CompanyMemberService {
*/
async updateMemberRole(
memberId: string,
- role: 'owner' | 'admin' | 'editor' | 'member'
+ role: 'owner' | 'admin' | 'editor' | 'viewer'
): Promise {
const supabase = await createClient()
// Validate role
- if (!['owner', 'admin', 'editor', 'member'].includes(role)) {
+ if (!['owner', 'admin', 'editor', 'viewer'].includes(role)) {
throw new CompanyError(
- 'Invalid role. Must be owner, admin, editor, or member',
+ 'Invalid role. Must be owner, admin, editor, or viewer',
CompanyErrorCodes.UNAUTHORIZED,
400
)
@@ -585,7 +585,7 @@ class CompanyMemberService {
view_analytics: true,
edit_company: false,
},
- member: {
+ viewer: {
manage_events: false,
manage_team: false,
view_analytics: true,
@@ -593,7 +593,7 @@ class CompanyMemberService {
},
}
- const permissions = rolePermissions[member.role] || rolePermissions.member
+ const permissions = rolePermissions[member.role] || rolePermissions.viewer
return permissions[permission] || false
}
@@ -643,7 +643,7 @@ class CompanyMemberService {
await sendEmail({
to: email,
- subject: `You've been invited to join ${companyName} on CodeUnia`,
+ subject: `You've been invited to join ${companyName} on Codeunia`,
html: emailContent,
})
}
@@ -691,7 +691,7 @@ class CompanyMemberService {
- You've been invited to join ${params.companyName} on CodeUnia as a ${params.role}.
+ You've been invited to join ${params.companyName} on Codeunia as a ${params.role}.
diff --git a/types/company.ts b/types/company.ts
index b28b4210..4ddaea5b 100644
--- a/types/company.ts
+++ b/types/company.ts
@@ -63,9 +63,9 @@ export interface CompanyMember {
id: string
company_id: string
user_id: string
- role: 'owner' | 'admin' | 'editor' | 'member'
+ role: 'owner' | 'admin' | 'editor' | 'viewer'
permissions?: Record
- status: 'active' | 'invited' | 'suspended'
+ status: 'active' | 'pending' | 'suspended'
invited_by?: string
invited_at?: string
joined_at: string