diff --git a/.cursor/rules/directory-naming-conventions.mdc b/.cursor/rules/directory-naming-conventions.mdc
index b93c988b..b6ecb6aa 100644
--- a/.cursor/rules/directory-naming-conventions.mdc
+++ b/.cursor/rules/directory-naming-conventions.mdc
@@ -1,5 +1,5 @@
---
-description:
-globs:
-alwaysApply: false
+description:
+globs:
+alwaysApply: true
---
diff --git a/app/api/organizations/[id]/members/[userId]/route.ts b/app/api/organizations/[id]/members/[userId]/route.ts
new file mode 100644
index 00000000..eef39f25
--- /dev/null
+++ b/app/api/organizations/[id]/members/[userId]/route.ts
@@ -0,0 +1,162 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { organizationService } from '@/lib/organization-service';
+import { OrganizationUser } from '@/types/organization';
+
+// Update a member's role
+export async function PATCH(
+ request: NextRequest,
+ { params }: { params: { id: string; userId: string } }
+) {
+ try {
+ const { id: organizationId, userId } = params;
+ const { role } = await request.json();
+ const currentUser = await organizationService.getCurrentUser();
+
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: 'Unauthorized' },
+ { status: 401 }
+ );
+ }
+
+ // Check if current user has permission (admin or owner)
+ const currentUserRole = await organizationService.getUserOrganizationRole(
+ currentUser.id,
+ organizationId
+ );
+
+ if (!currentUserRole || !['admin', 'owner'].includes(currentUserRole)) {
+ return NextResponse.json(
+ { error: 'You do not have permission to update member roles' },
+ { status: 403 }
+ );
+ }
+
+ // Only owners can update admin roles
+ if (role === 'owner' && currentUserRole !== 'owner') {
+ return NextResponse.json(
+ { error: 'Only owners can promote members to owners' },
+ { status: 403 }
+ );
+ }
+
+ // Check the current role of the target user
+ const targetUserRole = await organizationService.getUserOrganizationRole(
+ userId,
+ organizationId
+ );
+
+ // Prevent demoting the last owner
+ if (targetUserRole === 'owner') {
+ const members = await organizationService.getOrganizationMembers(organizationId);
+ const ownerCount = members.filter((m: OrganizationUser) => m.role === 'owner').length;
+
+ if (ownerCount === 1 && role !== 'owner') {
+ return NextResponse.json(
+ { error: 'Cannot demote the last owner of the organization' },
+ { status: 400 }
+ );
+ }
+ }
+
+ const updatedMember = await organizationService.updateMemberRole(
+ organizationId,
+ userId,
+ role
+ );
+
+ return NextResponse.json(updatedMember);
+ } catch (error) {
+ console.error('Error updating member role:', error);
+ return NextResponse.json(
+ { error: 'Failed to update member role' },
+ { status: 500 }
+ );
+ }
+}
+
+// Remove a member from the organization
+export async function DELETE(
+ request: NextRequest,
+ { params }: { params: { id: string; userId: string } }
+) {
+ try {
+ const { id: organizationId, userId } = params;
+ const currentUser = await organizationService.getCurrentUser();
+
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: 'Unauthorized' },
+ { status: 401 }
+ );
+ }
+
+ // Check if current user has permission (admin or owner)
+ const currentUserRole = await organizationService.getUserOrganizationRole(
+ currentUser.id,
+ organizationId
+ );
+
+ if (!currentUserRole || !['admin', 'owner'].includes(currentUserRole)) {
+ return NextResponse.json(
+ { error: 'You do not have permission to remove members' },
+ { status: 403 }
+ );
+ }
+
+ // Check the current role of the target user
+ const targetUserRole = await organizationService.getUserOrganizationRole(
+ userId,
+ organizationId
+ );
+
+ // Admins can't remove owners
+ if (targetUserRole === 'owner' && currentUserRole !== 'owner') {
+ return NextResponse.json(
+ { error: 'Only owners can remove other owners' },
+ { status: 403 }
+ );
+ }
+
+ // Prevent removing the last owner
+ if (targetUserRole === 'owner') {
+ const members = await organizationService.getOrganizationMembers(organizationId);
+ const ownerCount = members.filter((m: OrganizationUser) => m.role === 'owner').length;
+
+ if (ownerCount === 1) {
+ return NextResponse.json(
+ { error: 'Cannot remove the last owner of the organization' },
+ { status: 400 }
+ );
+ }
+ }
+
+ // Users can leave organizations (remove themselves)
+ const isSelf = userId === currentUser.id;
+
+ if (isSelf) {
+ // If this is the last owner, prevent leaving
+ if (targetUserRole === 'owner') {
+ const members = await organizationService.getOrganizationMembers(organizationId);
+ const ownerCount = members.filter((m: OrganizationUser) => m.role === 'owner').length;
+
+ if (ownerCount === 1) {
+ return NextResponse.json(
+ { error: 'As the last owner, you cannot leave the organization. Transfer ownership first.' },
+ { status: 400 }
+ );
+ }
+ }
+ }
+
+ await organizationService.removeMember(organizationId, userId);
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error('Error removing member:', error);
+ return NextResponse.json(
+ { error: 'Failed to remove member' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/organizations/[id]/members/route.ts b/app/api/organizations/[id]/members/route.ts
new file mode 100644
index 00000000..cd6994b6
--- /dev/null
+++ b/app/api/organizations/[id]/members/route.ts
@@ -0,0 +1,103 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { organizationService } from '@/lib/organization-service';
+
+// Get organization members
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { id: string } }
+) {
+ try {
+ const organizationId = params.id;
+ const currentUser = await organizationService.getCurrentUser();
+
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: 'Unauthorized' },
+ { status: 401 }
+ );
+ }
+
+ // Check if user is a member of this organization
+ const isMember = await organizationService.isUserOrganizationMember(
+ currentUser.id,
+ organizationId
+ );
+
+ if (!isMember) {
+ return NextResponse.json(
+ { error: 'You do not have access to this organization' },
+ { status: 403 }
+ );
+ }
+
+ const members = await organizationService.getOrganizationMembers(organizationId);
+
+ return NextResponse.json(members);
+ } catch (error) {
+ console.error('Error fetching organization members:', error);
+ return NextResponse.json(
+ { error: 'Failed to fetch organization members' },
+ { status: 500 }
+ );
+ }
+}
+
+// Add a new member to the organization
+export async function POST(
+ request: NextRequest,
+ { params }: { params: { id: string } }
+) {
+ try {
+ const organizationId = params.id;
+ const { email, role } = await request.json();
+ const currentUser = await organizationService.getCurrentUser();
+
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: 'Unauthorized' },
+ { status: 401 }
+ );
+ }
+
+ // Check if user is an admin or owner
+ const userRole = await organizationService.getUserOrganizationRole(
+ currentUser.id,
+ organizationId
+ );
+
+ if (!userRole || !['admin', 'owner'].includes(userRole)) {
+ return NextResponse.json(
+ { error: 'You do not have permission to add members' },
+ { status: 403 }
+ );
+ }
+
+ if (!email) {
+ return NextResponse.json(
+ { error: 'Email is required' },
+ { status: 400 }
+ );
+ }
+
+ try {
+ const newMember = await organizationService.addUserToOrganization(
+ organizationId,
+ email,
+ role || 'member'
+ );
+
+ return NextResponse.json(newMember);
+ } catch (err) {
+ return NextResponse.json(
+ { error: 'Failed to add member: ' + (err instanceof Error ? err.message : 'Unknown error') },
+ { status: 400 }
+ );
+ }
+ } catch (error) {
+ console.error('Error adding organization member:', error);
+ return NextResponse.json(
+ { error: 'Failed to add organization member' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/organizations/[id]/route.ts b/app/api/organizations/[id]/route.ts
new file mode 100644
index 00000000..58c8232c
--- /dev/null
+++ b/app/api/organizations/[id]/route.ts
@@ -0,0 +1,141 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { organizationService } from '@/lib/organization-service';
+
+// Get organization by ID
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { id: string } }
+) {
+ try {
+ const organizationId = params.id;
+ const currentUser = await organizationService.getCurrentUser();
+
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: 'Unauthorized' },
+ { status: 401 }
+ );
+ }
+
+ // Check if user is a member of this organization
+ const isMember = await organizationService.isUserOrganizationMember(
+ currentUser.id,
+ organizationId
+ );
+
+ if (!isMember) {
+ return NextResponse.json(
+ { error: 'You do not have access to this organization' },
+ { status: 403 }
+ );
+ }
+
+ const organization = await organizationService.getOrganization(organizationId);
+
+ if (!organization) {
+ return NextResponse.json(
+ { error: 'Organization not found' },
+ { status: 404 }
+ );
+ }
+
+ return NextResponse.json(organization);
+ } catch (error) {
+ console.error('Error fetching organization:', error);
+ return NextResponse.json(
+ { error: 'Failed to fetch organization' },
+ { status: 500 }
+ );
+ }
+}
+
+// Update organization
+export async function PATCH(
+ request: NextRequest,
+ { params }: { params: { id: string } }
+) {
+ try {
+ const organizationId = params.id;
+ const { name, description } = await request.json();
+ const currentUser = await organizationService.getCurrentUser();
+
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: 'Unauthorized' },
+ { status: 401 }
+ );
+ }
+
+ // Check if user is an admin or owner of this organization
+ const userRole = await organizationService.getUserOrganizationRole(
+ currentUser.id,
+ organizationId
+ );
+
+ if (!userRole || !['admin', 'owner'].includes(userRole)) {
+ return NextResponse.json(
+ { error: 'You do not have permission to update this organization' },
+ { status: 403 }
+ );
+ }
+
+ const updateData: { name?: string; description?: string } = {};
+
+ if (name) updateData.name = name;
+ if (description !== undefined) updateData.description = description;
+
+ const organization = await organizationService.updateOrganization(
+ organizationId,
+ updateData
+ );
+
+ return NextResponse.json(organization);
+ } catch (error) {
+ console.error('Error updating organization:', error);
+ return NextResponse.json(
+ { error: 'Failed to update organization' },
+ { status: 500 }
+ );
+ }
+}
+
+// Delete organization
+export async function DELETE(
+ request: NextRequest,
+ { params }: { params: { id: string } }
+) {
+ try {
+ const organizationId = params.id;
+ const currentUser = await organizationService.getCurrentUser();
+
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: 'Unauthorized' },
+ { status: 401 }
+ );
+ }
+
+ // Only owners can delete organizations
+ const userRole = await organizationService.getUserOrganizationRole(
+ currentUser.id,
+ organizationId
+ );
+
+ if (userRole !== 'owner') {
+ return NextResponse.json(
+ { error: 'Only organization owners can delete organizations' },
+ { status: 403 }
+ );
+ }
+
+ await organizationService.deleteOrganization(organizationId);
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error('Error deleting organization:', error);
+ return NextResponse.json(
+ { error: 'Failed to delete organization' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/organizations/by-slug/[slug]/route.ts b/app/api/organizations/by-slug/[slug]/route.ts
new file mode 100644
index 00000000..280eead4
--- /dev/null
+++ b/app/api/organizations/by-slug/[slug]/route.ts
@@ -0,0 +1,50 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { organizationService } from '@/lib/organization-service';
+
+// Get organization by slug
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { slug: string } }
+) {
+ try {
+ const slug = params.slug;
+ const currentUser = await organizationService.getCurrentUser();
+
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: 'Unauthorized' },
+ { status: 401 }
+ );
+ }
+
+ const organization = await organizationService.getOrganizationBySlug(slug);
+
+ if (!organization) {
+ return NextResponse.json(
+ { error: 'Organization not found' },
+ { status: 404 }
+ );
+ }
+
+ // Check if user is a member of this organization
+ const isMember = await organizationService.isUserOrganizationMember(
+ currentUser.id,
+ organization.id
+ );
+
+ if (!isMember) {
+ return NextResponse.json(
+ { error: 'You do not have access to this organization' },
+ { status: 403 }
+ );
+ }
+
+ return NextResponse.json(organization);
+ } catch (error) {
+ console.error('Error fetching organization by slug:', error);
+ return NextResponse.json(
+ { error: 'Failed to fetch organization' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/organizations/route.ts b/app/api/organizations/route.ts
new file mode 100644
index 00000000..0a190776
--- /dev/null
+++ b/app/api/organizations/route.ts
@@ -0,0 +1,61 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { organizationService } from '@/lib/organization-service';
+
+// Get all organizations for the current user
+export async function GET() {
+ try {
+ const currentUser = await organizationService.getCurrentUser();
+
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: 'Unauthorized' },
+ { status: 401 }
+ );
+ }
+
+ const organizations = await organizationService.getUserOrganizations(currentUser.id);
+ return NextResponse.json(organizations);
+ } catch (error) {
+ console.error('Error fetching organizations:', error);
+ return NextResponse.json(
+ { error: 'Failed to fetch organizations' },
+ { status: 500 }
+ );
+ }
+}
+
+// Create a new organization
+export async function POST(request: NextRequest) {
+ try {
+ const { name, description } = await request.json();
+ const currentUser = await organizationService.getCurrentUser();
+
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: 'Unauthorized' },
+ { status: 401 }
+ );
+ }
+
+ if (!name) {
+ return NextResponse.json(
+ { error: 'Organization name is required' },
+ { status: 400 }
+ );
+ }
+
+ const organization = await organizationService.createOrganization(
+ name,
+ description || null,
+ currentUser.id
+ );
+
+ return NextResponse.json(organization);
+ } catch (error) {
+ console.error('Error creating organization:', error);
+ return NextResponse.json(
+ { error: 'Failed to create organization' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/projects/[projectId]/route.ts b/app/api/projects/[projectId]/route.ts
index a17f42ce..83c7d11b 100644
--- a/app/api/projects/[projectId]/route.ts
+++ b/app/api/projects/[projectId]/route.ts
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { projectService } from '@/lib/project-service';
+import { organizationService } from '@/lib/organization-service';
export async function GET(
request: NextRequest,
@@ -7,6 +8,14 @@ export async function GET(
) {
try {
const projectId = (await params).projectId;
+ const currentUser = await organizationService.getCurrentUser();
+
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: 'Unauthorized' },
+ { status: 401 }
+ );
+ }
if (!projectId) {
return NextResponse.json(
@@ -24,6 +33,19 @@ export async function GET(
);
}
+ // Check if user is a member of the project's organization
+ const isMember = await organizationService.isUserOrganizationMember(
+ currentUser.id,
+ project.organizationId
+ );
+
+ if (!isMember) {
+ return NextResponse.json(
+ { error: 'You do not have access to this project' },
+ { status: 403 }
+ );
+ }
+
return NextResponse.json(project);
} catch (error) {
console.error('Error fetching project:', error);
@@ -32,4 +54,105 @@ export async function GET(
{ status: 500 }
);
}
+}
+
+// Update a project
+export async function PATCH(
+ request: NextRequest,
+ { params }: { params: Promise<{ projectId: string }> }
+) {
+ try {
+ const projectId = (await params).projectId;
+ const { name, description } = await request.json();
+ const currentUser = await organizationService.getCurrentUser();
+
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: 'Unauthorized' },
+ { status: 401 }
+ );
+ }
+
+ const project = await projectService.getProject(projectId);
+
+ if (!project) {
+ return NextResponse.json(
+ { error: 'Project not found' },
+ { status: 404 }
+ );
+ }
+
+ // Check if user is a member of the project's organization with proper permissions
+ const userRole = await organizationService.getUserOrganizationRole(
+ currentUser.id,
+ project.organizationId
+ );
+
+ if (!userRole || !['admin', 'owner'].includes(userRole)) {
+ return NextResponse.json(
+ { error: 'You do not have permission to update this project' },
+ { status: 403 }
+ );
+ }
+
+ const updatedProject = await projectService.updateProject(projectId, { name, description });
+
+ return NextResponse.json(updatedProject);
+ } catch (error) {
+ console.error('Error updating project:', error);
+ return NextResponse.json(
+ { error: 'Failed to update project' },
+ { status: 500 }
+ );
+ }
+}
+
+// Delete a project
+export async function DELETE(
+ request: NextRequest,
+ { params }: { params: Promise<{ projectId: string }> }
+) {
+ try {
+ const projectId = (await params).projectId;
+ const currentUser = await organizationService.getCurrentUser();
+
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: 'Unauthorized' },
+ { status: 401 }
+ );
+ }
+
+ const project = await projectService.getProject(projectId);
+
+ if (!project) {
+ return NextResponse.json(
+ { error: 'Project not found' },
+ { status: 404 }
+ );
+ }
+
+ // Check if user is a member of the project's organization with proper permissions
+ const userRole = await organizationService.getUserOrganizationRole(
+ currentUser.id,
+ project.organizationId
+ );
+
+ if (!userRole || !['admin', 'owner'].includes(userRole)) {
+ return NextResponse.json(
+ { error: 'You do not have permission to delete this project' },
+ { status: 403 }
+ );
+ }
+
+ await projectService.deleteProject(projectId);
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error('Error deleting project:', error);
+ return NextResponse.json(
+ { error: 'Failed to delete project' },
+ { status: 500 }
+ );
+ }
}
\ No newline at end of file
diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts
index 164d84ad..0ab89535 100644
--- a/app/api/projects/route.ts
+++ b/app/api/projects/route.ts
@@ -1,10 +1,56 @@
import { NextRequest, NextResponse } from 'next/server';
import { projectService } from '@/lib/project-service';
+import { organizationService } from '@/lib/organization-service';
+import { Organization } from '@/types/organization';
-// Get all projects
-export async function GET() {
+// Get all projects for the current user's organizations
+export async function GET(request: NextRequest) {
try {
- const projects = await projectService.getProjects();
+ const currentUser = await organizationService.getCurrentUser();
+
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: 'Unauthorized' },
+ { status: 401 }
+ );
+ }
+
+ // Get the organization ID from the query params, if provided
+ const { searchParams } = new URL(request.url);
+ const organizationId = searchParams.get('organizationId');
+
+ // If organization ID is provided, get projects for that organization
+ // Otherwise, get projects for all organizations the user is a member of
+ let projects;
+
+ if (organizationId) {
+ // Check if user is a member of this organization
+ const isMember = await organizationService.isUserOrganizationMember(
+ currentUser.id,
+ organizationId
+ );
+
+ if (!isMember) {
+ return NextResponse.json(
+ { error: 'You do not have access to this organization' },
+ { status: 403 }
+ );
+ }
+
+ projects = await projectService.getProjects(organizationId);
+ } else {
+ // Get all organizations the user is a member of
+ const organizations = await organizationService.getUserOrganizations(currentUser.id);
+
+ // Get projects for each organization
+ const projectPromises = organizations.map((org: Organization) =>
+ projectService.getProjects(org.id)
+ );
+
+ const orgProjects = await Promise.all(projectPromises);
+ projects = orgProjects.flat();
+ }
+
return NextResponse.json(projects);
} catch (error) {
console.error('Error fetching projects:', error);
@@ -18,7 +64,15 @@ export async function GET() {
// Create a new project
export async function POST(request: NextRequest) {
try {
- const { name, description } = await request.json();
+ const { name, description, organizationId } = await request.json();
+ const currentUser = await organizationService.getCurrentUser();
+
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: 'Unauthorized' },
+ { status: 401 }
+ );
+ }
if (!name) {
return NextResponse.json(
@@ -26,8 +80,28 @@ export async function POST(request: NextRequest) {
{ status: 400 }
);
}
+
+ if (!organizationId) {
+ return NextResponse.json(
+ { error: 'Organization ID is required' },
+ { status: 400 }
+ );
+ }
+
+ // Check if user is a member of this organization
+ const isMember = await organizationService.isUserOrganizationMember(
+ currentUser.id,
+ organizationId
+ );
+
+ if (!isMember) {
+ return NextResponse.json(
+ { error: 'You do not have access to this organization' },
+ { status: 403 }
+ );
+ }
- const project = await projectService.createProject(name, description);
+ const project = await projectService.createProject(name, organizationId, description);
return NextResponse.json(project);
} catch (error) {
console.error('Error creating project:', error);
diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts
new file mode 100644
index 00000000..94bb6c84
--- /dev/null
+++ b/app/auth/callback/route.ts
@@ -0,0 +1,25 @@
+import { type NextRequest } from 'next/server'
+import { createClient } from '@/lib/utils/supabase/server'
+import { redirect } from 'next/navigation'
+
+export async function GET(request: NextRequest) {
+ const { searchParams } = new URL(request.url)
+ const code = searchParams.get('code')
+
+ if (!code) {
+ return redirect('/login')
+ }
+
+ const supabase = await createClient()
+
+ // Exchange the code for a session
+ const { error } = await supabase.auth.exchangeCodeForSession(code)
+
+ if (error) {
+ console.error('Error exchanging code for session:', error)
+ return redirect('/error')
+ }
+
+ // Successful authentication, redirect to home
+ return redirect('/')
+}
\ No newline at end of file
diff --git a/app/auth/confirm/route.ts b/app/auth/confirm/route.ts
new file mode 100644
index 00000000..3c40a5c5
--- /dev/null
+++ b/app/auth/confirm/route.ts
@@ -0,0 +1,28 @@
+import { type EmailOtpType } from '@supabase/supabase-js'
+import { type NextRequest } from 'next/server'
+
+import { createClient } from '@/lib/utils/supabase/server'
+import { redirect } from 'next/navigation'
+
+export async function GET(request: NextRequest) {
+ const { searchParams } = new URL(request.url)
+ const token_hash = searchParams.get('token_hash')
+ const type = searchParams.get('type') as EmailOtpType | null
+ const next = searchParams.get('next') ?? '/'
+
+ if (token_hash && type) {
+ const supabase = await createClient()
+
+ const { error } = await supabase.auth.verifyOtp({
+ type,
+ token_hash,
+ })
+ if (!error) {
+ // redirect user to specified redirect URL or root of app
+ redirect(next)
+ }
+ }
+
+ // redirect the user to an error page with some instructions
+ redirect('/error')
+}
\ No newline at end of file
diff --git a/app/components/LogoutButton.tsx b/app/components/LogoutButton.tsx
new file mode 100644
index 00000000..aed5da36
--- /dev/null
+++ b/app/components/LogoutButton.tsx
@@ -0,0 +1,29 @@
+'use client'
+
+import Link from 'next/link'
+
+export default function LogoutButton() {
+ return (
+
+
+ Log out
+
+ )
+}
\ No newline at end of file
diff --git a/app/error/page.tsx b/app/error/page.tsx
new file mode 100644
index 00000000..3c170be4
--- /dev/null
+++ b/app/error/page.tsx
@@ -0,0 +1,5 @@
+'use client'
+
+export default function ErrorPage() {
+ return
Sorry, something went wrong
+}
\ No newline at end of file
diff --git a/app/login/actions.ts b/app/login/actions.ts
new file mode 100644
index 00000000..7d2f882b
--- /dev/null
+++ b/app/login/actions.ts
@@ -0,0 +1,55 @@
+'use server'
+
+import { revalidatePath } from 'next/cache'
+import { redirect } from 'next/navigation'
+
+import { createClient } from '@/lib/utils/supabase/server'
+
+export async function signInWithMagicLink(formData: FormData) {
+ const supabase = await createClient()
+
+ // Get email from form data
+ const email = formData.get('email') as string
+
+ // Validate email
+ if (!email || !email.includes('@')) {
+ // In a real app, you'd want to return an error message
+ redirect('/error')
+ }
+
+ // Get the origin for creating the full redirect URL
+ // In production, you should set NEXT_PUBLIC_APP_URL in your environment variables
+ const origin = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
+
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ emailRedirectTo: `${origin}/auth/callback`, // Redirect to our auth callback handler
+ },
+ })
+
+ if (error) {
+ redirect('/error')
+ }
+
+ // Redirect to a confirmation page
+ redirect('/login/confirmation')
+}
+
+// Keep this for backward compatibility if needed, but it won't be used in the new flow
+export async function login(formData: FormData) {
+ redirect('/login/confirmation')
+}
+
+// Keep this for backward compatibility if needed, but it won't be used in the new flow
+export async function signup(formData: FormData) {
+ redirect('/login/confirmation')
+}
+
+export async function logout() {
+ const supabase = await createClient()
+ await supabase.auth.signOut()
+
+ revalidatePath('/', 'layout')
+ redirect('/login')
+}
\ No newline at end of file
diff --git a/app/login/confirmation/page.tsx b/app/login/confirmation/page.tsx
new file mode 100644
index 00000000..1a07b097
--- /dev/null
+++ b/app/login/confirmation/page.tsx
@@ -0,0 +1,15 @@
+export default function ConfirmationPage() {
+ return (
+
+
Check your email
+
+
+ We've sent you a magic link to your email address. Click the link in the email to sign in.
+
+
+ If you don't see the email, check your spam folder. The link will expire after 24 hours.
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/app/login/page.tsx b/app/login/page.tsx
new file mode 100644
index 00000000..7f6b3185
--- /dev/null
+++ b/app/login/page.tsx
@@ -0,0 +1,33 @@
+import { signInWithMagicLink } from '@/app/login/actions'
+
+export default function LoginPage() {
+ return (
+
+
Sign in to your account
+
+
+ )
+}
\ No newline at end of file
diff --git a/app/logout/page.tsx b/app/logout/page.tsx
new file mode 100644
index 00000000..ddf89222
--- /dev/null
+++ b/app/logout/page.tsx
@@ -0,0 +1,18 @@
+import { logout } from '@/app/login/actions'
+
+export default function LogoutPage() {
+ return (
+
+
Log out
+
Are you sure you want to log out?
+
+
+ )
+}
\ No newline at end of file
diff --git a/app/organizations/[slug]/page.tsx b/app/organizations/[slug]/page.tsx
new file mode 100644
index 00000000..c4b43f4f
--- /dev/null
+++ b/app/organizations/[slug]/page.tsx
@@ -0,0 +1,132 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useParams } from 'next/navigation';
+import { Organization } from '@/types/organization';
+import { ProjectGrid } from '@/components/projects/ProjectGrid';
+import { CreateProjectDialog } from '@/components/projects/CreateProjectDialog';
+import { Button } from '@/components/ui/button';
+import { PlusCircle, Users } from 'lucide-react';
+import Link from 'next/link';
+import { useToast } from '@/components/ui/use-toast';
+
+export default function OrganizationPage() {
+ const params = useParams();
+ const slug = params.slug as string;
+ const [organization, setOrganization] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isCreateProjectOpen, setIsCreateProjectOpen] = useState(false);
+ const { toast } = useToast();
+
+ useEffect(() => {
+ const fetchOrganization = async () => {
+ try {
+ setIsLoading(true);
+
+ // First fetch the organization by slug
+ const orgResponse = await fetch(`/api/organizations/by-slug/${slug}`);
+
+ if (!orgResponse.ok) {
+ throw new Error('Failed to fetch organization');
+ }
+
+ const orgData = await orgResponse.json();
+ setOrganization(orgData);
+ } catch (error) {
+ console.error('Error fetching organization:', error);
+ toast({
+ title: 'Error',
+ description: 'Failed to load organization data',
+ variant: 'destructive',
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ if (slug) {
+ fetchOrganization();
+ }
+ }, [slug, toast]);
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ if (!organization) {
+ return (
+
+
Organization not found
+
The organization you're looking for doesn't exist or you don't have access to it.