mirror of
https://github.com/run-llama/auto_rfp.git
synced 2026-06-30 21:57:56 -04:00
adding organization
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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('/')
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function LogoutButton() {
|
||||
return (
|
||||
<Link
|
||||
href="/logout"
|
||||
className="flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium hover:bg-gray-100"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
<polyline points="16 17 21 12 16 7" />
|
||||
<line x1="21" y1="12" x2="9" y2="12" />
|
||||
</svg>
|
||||
Log out
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
'use client'
|
||||
|
||||
export default function ErrorPage() {
|
||||
return <p>Sorry, something went wrong</p>
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export default function ConfirmationPage() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center space-y-6 py-12">
|
||||
<h1 className="text-2xl font-bold">Check your email</h1>
|
||||
<div className="max-w-md text-center">
|
||||
<p className="mb-4">
|
||||
We've sent you a magic link to your email address. Click the link in the email to sign in.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
If you don't see the email, check your spam folder. The link will expire after 24 hours.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { signInWithMagicLink } from '@/app/login/actions'
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center space-y-4 py-8">
|
||||
<h1 className="text-2xl font-bold">Sign in to your account</h1>
|
||||
<form className="w-full max-w-md space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="email" className="block text-sm font-medium">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
className="w-full rounded-md border px-3 py-2"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
formAction={signInWithMagicLink}
|
||||
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
||||
>
|
||||
Send Magic Link
|
||||
</button>
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
We'll email you a magic link for a password-free sign in.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { logout } from '@/app/login/actions'
|
||||
|
||||
export default function LogoutPage() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center space-y-6 py-12">
|
||||
<h1 className="text-2xl font-bold">Log out</h1>
|
||||
<p className="text-center">Are you sure you want to log out?</p>
|
||||
<form action={logout}>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-md bg-blue-600 px-6 py-2 text-white hover:bg-blue-700"
|
||||
>
|
||||
Log out
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<Organization | null>(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 (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="animate-pulse flex flex-col gap-4 w-full max-w-4xl">
|
||||
<div className="h-12 bg-gray-200 rounded w-1/3"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||
<div className="h-64 bg-gray-200 rounded w-full mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!organization) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<h1 className="text-2xl font-bold">Organization not found</h1>
|
||||
<p className="text-gray-600 mb-4">The organization you're looking for doesn't exist or you don't have access to it.</p>
|
||||
<Link href="/">
|
||||
<Button>Back to Dashboard</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6 px-4">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">{organization.name}</h1>
|
||||
{organization.description && (
|
||||
<p className="text-gray-600 mt-1">{organization.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Link href={`/organizations/${slug}/members`}>
|
||||
<Button variant="outline">
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
Members
|
||||
</Button>
|
||||
</Link>
|
||||
<Button onClick={() => setIsCreateProjectOpen(true)}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
New Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-medium mb-4">Projects</h2>
|
||||
{organization.projects && organization.projects.length > 0 ? (
|
||||
<ProjectGrid
|
||||
projects={organization.projects}
|
||||
isLoading={false}
|
||||
/>
|
||||
) : (
|
||||
<div className="border rounded-lg p-8 text-center">
|
||||
<h3 className="text-lg font-medium mb-2">No projects yet</h3>
|
||||
<p className="text-gray-600 mb-4">Create your first project to get started</p>
|
||||
<Button onClick={() => setIsCreateProjectOpen(true)}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
Create Project
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Create project dialog */}
|
||||
{organization && (
|
||||
<CreateProjectDialog
|
||||
isOpen={isCreateProjectOpen}
|
||||
onOpenChange={setIsCreateProjectOpen}
|
||||
organizationId={organization.id}
|
||||
onSuccess={() => {
|
||||
// Refresh the organization data to show the new project
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
import { createClient } from '@/lib/utils/supabase/server'
|
||||
|
||||
export default async function PrivatePage() {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { data, error } = await supabase.auth.getUser()
|
||||
if (error || !data?.user) {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
return <p>Hello {data.user.email}</p>
|
||||
}
|
||||
@@ -23,7 +23,8 @@ import {
|
||||
Users,
|
||||
Calendar,
|
||||
MessageSquare,
|
||||
HelpCircle
|
||||
HelpCircle,
|
||||
LogOut
|
||||
} from "lucide-react"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
@@ -39,6 +40,7 @@ function HomeSidebarContent() {
|
||||
const bottomMenuItems = [
|
||||
{ id: "settings", label: "Settings", icon: Settings, path: "/settings" },
|
||||
{ id: "help", label: "Help & Support", icon: HelpCircle, path: "/help" },
|
||||
{ id: "logout", label: "Log out", icon: LogOut, path: "/logout" },
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -127,9 +129,11 @@ function HomeSidebarContent() {
|
||||
<span className="text-xs text-muted-foreground">Admin</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
<Link href="/logout">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" aria-label="Log out">
|
||||
<LogOut className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</SidebarFooter>
|
||||
</>
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
SidebarTrigger,
|
||||
useSidebar
|
||||
} from "@/components/ui/sidebar"
|
||||
import { Calendar, FileText, FolderOpen, HelpCircle, Home, MessageSquare, Settings, Upload, Users } from "lucide-react"
|
||||
import { Calendar, FileText, FolderOpen, HelpCircle, Home, LogOut, MessageSquare, Settings, Upload, Users } from "lucide-react"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
@@ -121,6 +121,7 @@ function SidebarInnerContent() {
|
||||
const bottomMenuItems = [
|
||||
{ id: "settings", label: "Settings", icon: Settings, path: "/settings" },
|
||||
{ id: "help", label: "Help & Support", icon: HelpCircle, path: "/help" },
|
||||
{ id: "logout", label: "Log out", icon: LogOut, path: "/logout" },
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -241,9 +242,11 @@ function SidebarInnerContent() {
|
||||
<span className="text-xs text-muted-foreground">Project Owner</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
<Link href="/logout">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" aria-label="Log out">
|
||||
<LogOut className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</SidebarFooter>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
|
||||
interface CreateOrganizationDialogProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess?: (orgId: string, orgSlug: string) => void;
|
||||
}
|
||||
|
||||
export function CreateOrganizationDialog({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
onSuccess
|
||||
}: CreateOrganizationDialogProps) {
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!name.trim()) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Organization name is required',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
const response = await fetch('/api/organizations', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
description: description.trim() || null,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const organization = await response.json();
|
||||
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: `${organization.name} has been created`,
|
||||
});
|
||||
|
||||
// Reset form
|
||||
setName('');
|
||||
setDescription('');
|
||||
|
||||
// Close dialog
|
||||
onOpenChange(false);
|
||||
|
||||
// Call success callback or redirect
|
||||
if (onSuccess) {
|
||||
onSuccess(organization.id, organization.slug);
|
||||
} else {
|
||||
router.push(`/organizations/${organization.slug}`);
|
||||
}
|
||||
} else {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to create organization');
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to create organization',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create organization</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new organization to manage projects and team members.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Acme Inc."
|
||||
className="col-span-3"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">Description (optional)</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Brief description of your organization"
|
||||
className="col-span-3"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || !name.trim()}
|
||||
>
|
||||
{isSubmitting ? 'Creating...' : 'Create'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
PlusCircle
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Organization } from '@/types/organization';
|
||||
|
||||
export function OrganizationSelector({
|
||||
currentOrganizationId,
|
||||
onCreateNew
|
||||
}: {
|
||||
currentOrganizationId?: string;
|
||||
onCreateNew: () => void;
|
||||
}) {
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOrganizations = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch('/api/organizations');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setOrganizations(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching organizations:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchOrganizations();
|
||||
}, []);
|
||||
|
||||
const handleOrganizationChange = (orgId: string) => {
|
||||
if (orgId !== currentOrganizationId) {
|
||||
const org = organizations.find(o => o.id === orgId);
|
||||
if (org) {
|
||||
router.push(`/organizations/${org.slug}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
disabled={isLoading || organizations.length === 0}
|
||||
value={currentOrganizationId}
|
||||
onValueChange={handleOrganizationChange}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder={isLoading ? 'Loading...' : 'Select organization'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{organizations.map((org) => (
|
||||
<SelectItem key={org.id} value={org.id}>
|
||||
{org.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={onCreateNew}
|
||||
title="Create new organization"
|
||||
>
|
||||
<PlusCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
|
||||
interface CreateProjectDialogProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
organizationId: string;
|
||||
onSuccess?: (projectId: string) => void;
|
||||
}
|
||||
|
||||
export function CreateProjectDialog({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
organizationId,
|
||||
onSuccess
|
||||
}: CreateProjectDialogProps) {
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!name.trim()) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Project name is required',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
const response = await fetch('/api/projects', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
description: description.trim() || null,
|
||||
organizationId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const project = await response.json();
|
||||
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: `${project.name} has been created`,
|
||||
});
|
||||
|
||||
// Reset form
|
||||
setName('');
|
||||
setDescription('');
|
||||
|
||||
// Close dialog
|
||||
onOpenChange(false);
|
||||
|
||||
// Call success callback or redirect
|
||||
if (onSuccess) {
|
||||
onSuccess(project.id);
|
||||
} else {
|
||||
router.push(`/project?projectId=${project.id}`);
|
||||
}
|
||||
} else {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to create project');
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to create project',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new project in your organization.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="My Project"
|
||||
className="col-span-3"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">Description (optional)</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Brief description of your project"
|
||||
className="col-span-3"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || !name.trim()}
|
||||
>
|
||||
{isSubmitting ? 'Creating...' : 'Create'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { SearchBar } from "./SearchBar";
|
||||
import { ProjectGrid } from "./ProjectGrid";
|
||||
import { Organization } from "@/types/organization";
|
||||
import { Project } from "@/types/project";
|
||||
import { OrganizationSelector } from "@/components/organizations/OrganizationSelector";
|
||||
import { CreateOrganizationDialog } from "@/components/organizations/CreateOrganizationDialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PlusCircle } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
export function HomeContent() {
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [isCreateOrgOpen, setIsCreateOrgOpen] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProjects = async () => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/projects");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setProjects(data);
|
||||
setIsLoading(true);
|
||||
|
||||
// Fetch organizations
|
||||
const orgsResponse = await fetch("/api/organizations");
|
||||
if (!orgsResponse.ok) {
|
||||
throw new Error("Failed to fetch organizations");
|
||||
}
|
||||
|
||||
const orgsData = await orgsResponse.json();
|
||||
setOrganizations(orgsData);
|
||||
|
||||
// Fetch projects (across all organizations)
|
||||
const projectsResponse = await fetch("/api/projects");
|
||||
if (projectsResponse.ok) {
|
||||
const projectsData = await projectsResponse.json();
|
||||
setProjects(projectsData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching projects:", error);
|
||||
console.error("Error fetching data:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to load data",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProjects();
|
||||
}, []);
|
||||
fetchData();
|
||||
}, [toast]);
|
||||
|
||||
// Filter projects based on search
|
||||
const filteredProjects = projects.filter(project =>
|
||||
@@ -34,18 +63,58 @@ export function HomeContent() {
|
||||
|
||||
return (
|
||||
<div className="w-full p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold">Dashboard</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<OrganizationSelector
|
||||
onCreateNew={() => setIsCreateOrgOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchBar
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
/>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-medium mb-4">Your Projects</h2>
|
||||
<ProjectGrid
|
||||
projects={filteredProjects}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
{organizations.length === 0 && !isLoading ? (
|
||||
<div className="mt-8 border rounded-lg p-8 text-center">
|
||||
<h2 className="text-xl font-medium mb-2">Welcome to AutoRFP</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Create your first organization to get started
|
||||
</p>
|
||||
<Button onClick={() => setIsCreateOrgOpen(true)}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
Create Organization
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-medium">Your Projects</h2>
|
||||
{organizations.length > 0 && (
|
||||
<Link href={`/organizations/${organizations[0].slug}`}>
|
||||
<Button variant="outline" size="sm">
|
||||
View All
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ProjectGrid
|
||||
projects={filteredProjects}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CreateOrganizationDialog
|
||||
isOpen={isCreateOrgOpen}
|
||||
onOpenChange={setIsCreateOrgOpen}
|
||||
onSuccess={(orgId, orgSlug) => {
|
||||
window.location.href = `/organizations/${orgSlug}`;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
import { db } from './db';
|
||||
import { createClient } from '@/lib/utils/supabase/server';
|
||||
|
||||
export const organizationService = {
|
||||
// Organization operations
|
||||
async createOrganization(name: string, description: string | null = null, userId: string) {
|
||||
// Create a slug from the name (lowercase, replace spaces with dashes, remove special chars)
|
||||
const slug = name
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^\w-]+/g, '')
|
||||
.replace(/--+/g, '-')
|
||||
.substring(0, 50); // Limit length
|
||||
|
||||
// Make the slug unique by adding a random suffix if needed
|
||||
let uniqueSlug = slug;
|
||||
let slugExists = await db.organization.findUnique({
|
||||
where: { slug: uniqueSlug },
|
||||
});
|
||||
|
||||
if (slugExists) {
|
||||
uniqueSlug = `${slug}-${Math.random().toString(36).substring(2, 7)}`;
|
||||
}
|
||||
|
||||
// Create the organization and add the user as an owner in a transaction
|
||||
return db.$transaction(async (tx) => {
|
||||
// Create the organization
|
||||
const organization = await tx.organization.create({
|
||||
data: {
|
||||
name,
|
||||
slug: uniqueSlug,
|
||||
description,
|
||||
},
|
||||
});
|
||||
|
||||
// Add the user as an owner
|
||||
await tx.organizationUser.create({
|
||||
data: {
|
||||
role: 'owner',
|
||||
userId,
|
||||
organizationId: organization.id,
|
||||
},
|
||||
});
|
||||
|
||||
return organization;
|
||||
});
|
||||
},
|
||||
|
||||
async getOrganization(id: string) {
|
||||
return db.organization.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
projects: {
|
||||
orderBy: {
|
||||
updatedAt: 'desc',
|
||||
},
|
||||
},
|
||||
organizationUsers: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async getOrganizationBySlug(slug: string) {
|
||||
return db.organization.findUnique({
|
||||
where: { slug },
|
||||
include: {
|
||||
projects: {
|
||||
orderBy: {
|
||||
updatedAt: 'desc',
|
||||
},
|
||||
},
|
||||
organizationUsers: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async getUserOrganizations(userId: string) {
|
||||
return db.organization.findMany({
|
||||
where: {
|
||||
organizationUsers: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: 'desc',
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async updateOrganization(id: string, data: { name?: string; description?: string }) {
|
||||
return db.organization.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
},
|
||||
|
||||
async deleteOrganization(id: string) {
|
||||
return db.organization.delete({
|
||||
where: { id },
|
||||
});
|
||||
},
|
||||
|
||||
// Organization members operations
|
||||
async addUserToOrganization(organizationId: string, email: string, role: string = 'member') {
|
||||
// First check if the user exists
|
||||
const user = await db.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`User with email ${email} not found`);
|
||||
}
|
||||
|
||||
// Then create the organization user
|
||||
return db.organizationUser.create({
|
||||
data: {
|
||||
role,
|
||||
userId: user.id,
|
||||
organizationId,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async getOrganizationMembers(organizationId: string) {
|
||||
return db.organizationUser.findMany({
|
||||
where: {
|
||||
organizationId,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc',
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async updateMemberRole(organizationId: string, userId: string, role: string) {
|
||||
return db.organizationUser.update({
|
||||
where: {
|
||||
userId_organizationId: {
|
||||
userId,
|
||||
organizationId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
role,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async removeMember(organizationId: string, userId: string) {
|
||||
return db.organizationUser.delete({
|
||||
where: {
|
||||
userId_organizationId: {
|
||||
userId,
|
||||
organizationId,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// User methods
|
||||
async createUserIfNotExists(id: string, email: string, name: string | null = null) {
|
||||
const user = await db.user.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
|
||||
return db.user.create({
|
||||
data: {
|
||||
id,
|
||||
email,
|
||||
name,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async getCurrentUser() {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure user exists in our database
|
||||
const dbUser = await this.createUserIfNotExists(
|
||||
user.id,
|
||||
user.email || '',
|
||||
user.user_metadata?.name || null
|
||||
);
|
||||
|
||||
return dbUser;
|
||||
},
|
||||
|
||||
async getUserOrganizationRole(userId: string, organizationId: string) {
|
||||
const orgUser = await db.organizationUser.findUnique({
|
||||
where: {
|
||||
userId_organizationId: {
|
||||
userId,
|
||||
organizationId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return orgUser?.role || null;
|
||||
},
|
||||
|
||||
async isUserOrganizationMember(userId: string, organizationId: string) {
|
||||
const orgUser = await db.organizationUser.findUnique({
|
||||
where: {
|
||||
userId_organizationId: {
|
||||
userId,
|
||||
organizationId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return !!orgUser;
|
||||
},
|
||||
};
|
||||
+33
-2
@@ -3,20 +3,37 @@ import { RfpDocument, RfpSection, RfpQuestion, AnswerSource } from '@/types/api'
|
||||
|
||||
export const projectService = {
|
||||
// Project operations
|
||||
async createProject(name: string, description?: string) {
|
||||
async createProject(name: string, organizationId: string, description?: string) {
|
||||
return db.project.create({
|
||||
data: {
|
||||
name,
|
||||
description,
|
||||
organizationId,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async getProjects() {
|
||||
async getProjects(organizationId?: string) {
|
||||
// If organizationId is provided, get projects for that organization only
|
||||
if (organizationId) {
|
||||
return db.project.findMany({
|
||||
where: {
|
||||
organizationId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise get all projects (mostly for admin purposes)
|
||||
return db.project.findMany({
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -24,6 +41,7 @@ export const projectService = {
|
||||
return db.project.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
organization: true,
|
||||
questions: {
|
||||
include: {
|
||||
answer: {
|
||||
@@ -37,6 +55,19 @@ export const projectService = {
|
||||
});
|
||||
},
|
||||
|
||||
async updateProject(id: string, data: { name?: string; description?: string }) {
|
||||
return db.project.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
},
|
||||
|
||||
async deleteProject(id: string) {
|
||||
return db.project.delete({
|
||||
where: { id },
|
||||
});
|
||||
},
|
||||
|
||||
// Question operations
|
||||
async saveQuestions(projectId: string, sections: RfpSection[]) {
|
||||
console.log(`Starting to save questions for project ${projectId}. Total sections: ${sections.length}`);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createBrowserClient } from '@supabase/ssr'
|
||||
|
||||
export function createClient() {
|
||||
return createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
|
||||
export async function updateSession(request: NextRequest) {
|
||||
let supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
})
|
||||
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return request.cookies.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
|
||||
supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
})
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
supabaseResponse.cookies.set(name, value, options)
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Do not run code between createServerClient and
|
||||
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
|
||||
// issues with users being randomly logged out.
|
||||
|
||||
// IMPORTANT: DO NOT REMOVE auth.getUser()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (
|
||||
!user &&
|
||||
!request.nextUrl.pathname.startsWith('/login') &&
|
||||
!request.nextUrl.pathname.startsWith('/auth')
|
||||
) {
|
||||
// no user, potentially respond by redirecting the user to the login page
|
||||
const url = request.nextUrl.clone()
|
||||
url.pathname = '/login'
|
||||
return NextResponse.redirect(url)
|
||||
}
|
||||
|
||||
// IMPORTANT: You *must* return the supabaseResponse object as it is.
|
||||
// If you're creating a new response object with NextResponse.next() make sure to:
|
||||
// 1. Pass the request in it, like so:
|
||||
// const myNewResponse = NextResponse.next({ request })
|
||||
// 2. Copy over the cookies, like so:
|
||||
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
|
||||
// 3. Change the myNewResponse object to fit your needs, but avoid changing
|
||||
// the cookies!
|
||||
// 4. Finally:
|
||||
// return myNewResponse
|
||||
// If this is not done, you may be causing the browser and server to go out
|
||||
// of sync and terminate the user's session prematurely!
|
||||
|
||||
return supabaseResponse
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export async function createClient() {
|
||||
const cookieStore = await cookies()
|
||||
|
||||
return createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return cookieStore.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
try {
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
cookieStore.set(name, value, options)
|
||||
)
|
||||
} catch {
|
||||
// The `setAll` method was called from a Server Component.
|
||||
// This can be ignored if you have middleware refreshing
|
||||
// user sessions.
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { type NextRequest } from 'next/server'
|
||||
import { updateSession } from '@/lib/utils/supabase/middleware'
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
return await updateSession(request)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except for the ones starting with:
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
* Feel free to modify this pattern to include more paths.
|
||||
*/
|
||||
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
||||
],
|
||||
}
|
||||
@@ -39,6 +39,8 @@
|
||||
"@radix-ui/react-toggle": "^1.1.6",
|
||||
"@radix-ui/react-toggle-group": "^1.1.7",
|
||||
"@radix-ui/react-tooltip": "^1.2.4",
|
||||
"@supabase/ssr": "^0.6.1",
|
||||
"@supabase/supabase-js": "^2.49.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
|
||||
Generated
+120
-7
@@ -95,6 +95,12 @@ importers:
|
||||
'@radix-ui/react-tooltip':
|
||||
specifier: ^1.2.4
|
||||
version: 1.2.4(@types/react-dom@19.1.3(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@supabase/ssr':
|
||||
specifier: ^0.6.1
|
||||
version: 0.6.1(@supabase/supabase-js@2.49.4)
|
||||
'@supabase/supabase-js':
|
||||
specifier: ^2.49.4
|
||||
version: 2.49.4
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@@ -118,7 +124,7 @@ importers:
|
||||
version: 1.4.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
llamaindex:
|
||||
specifier: ^0.10.3
|
||||
version: 0.10.3(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(zod@3.24.3)
|
||||
version: 0.10.3(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(ws@8.18.2)(zod@3.24.3)
|
||||
lucide-react:
|
||||
specifier: ^0.507.0
|
||||
version: 0.507.0(react@19.1.0)
|
||||
@@ -130,7 +136,7 @@ importers:
|
||||
version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
openai:
|
||||
specifier: ^4.97.0
|
||||
version: 4.97.0(zod@3.24.3)
|
||||
version: 4.97.0(ws@8.18.2)(zod@3.24.3)
|
||||
prisma:
|
||||
specifier: ^6.7.0
|
||||
version: 6.7.0(typescript@5.8.3)
|
||||
@@ -1289,6 +1295,33 @@ packages:
|
||||
'@standard-schema/utils@0.3.0':
|
||||
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||
|
||||
'@supabase/auth-js@2.69.1':
|
||||
resolution: {integrity: sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==}
|
||||
|
||||
'@supabase/functions-js@2.4.4':
|
||||
resolution: {integrity: sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==}
|
||||
|
||||
'@supabase/node-fetch@2.6.15':
|
||||
resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
|
||||
'@supabase/postgrest-js@1.19.4':
|
||||
resolution: {integrity: sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==}
|
||||
|
||||
'@supabase/realtime-js@2.11.2':
|
||||
resolution: {integrity: sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==}
|
||||
|
||||
'@supabase/ssr@0.6.1':
|
||||
resolution: {integrity: sha512-QtQgEMvaDzr77Mk3vZ3jWg2/y+D8tExYF7vcJT+wQ8ysuvOeGGjYbZlvj5bHYsj/SpC0bihcisnwPrM4Gp5G4g==}
|
||||
peerDependencies:
|
||||
'@supabase/supabase-js': ^2.43.4
|
||||
|
||||
'@supabase/storage-js@2.7.1':
|
||||
resolution: {integrity: sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==}
|
||||
|
||||
'@supabase/supabase-js@2.49.4':
|
||||
resolution: {integrity: sha512-jUF0uRUmS8BKt37t01qaZ88H9yV1mbGYnqLeuFWLcdV+x1P4fl0yP9DGtaEhFPZcwSom7u16GkLEH9QJZOqOkw==}
|
||||
|
||||
'@swc/counter@0.1.3':
|
||||
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
||||
|
||||
@@ -1448,6 +1481,9 @@ packages:
|
||||
'@types/node@22.15.3':
|
||||
resolution: {integrity: sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==}
|
||||
|
||||
'@types/phoenix@1.6.6':
|
||||
resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==}
|
||||
|
||||
'@types/react-dom@19.1.3':
|
||||
resolution: {integrity: sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==}
|
||||
peerDependencies:
|
||||
@@ -1465,6 +1501,9 @@ packages:
|
||||
'@types/unist@3.0.3':
|
||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
|
||||
@@ -1555,6 +1594,10 @@ packages:
|
||||
comma-separated-tokens@2.0.3:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
|
||||
cookie@1.0.2:
|
||||
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
cssesc@3.0.0:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2457,6 +2500,18 @@ packages:
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
ws@8.18.2:
|
||||
resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
zod-to-json-schema@3.24.5:
|
||||
resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==}
|
||||
peerDependencies:
|
||||
@@ -2719,11 +2774,11 @@ snapshots:
|
||||
tree-sitter: 0.22.4
|
||||
web-tree-sitter: 0.24.7
|
||||
|
||||
'@llamaindex/openai@0.3.5(zod@3.24.3)':
|
||||
'@llamaindex/openai@0.3.5(ws@8.18.2)(zod@3.24.3)':
|
||||
dependencies:
|
||||
'@llamaindex/core': 0.6.3
|
||||
'@llamaindex/env': 0.1.29
|
||||
openai: 4.97.0(zod@3.24.3)
|
||||
openai: 4.97.0(ws@8.18.2)(zod@3.24.3)
|
||||
transitivePeerDependencies:
|
||||
- '@huggingface/transformers'
|
||||
- encoding
|
||||
@@ -3501,6 +3556,53 @@ snapshots:
|
||||
|
||||
'@standard-schema/utils@0.3.0': {}
|
||||
|
||||
'@supabase/auth-js@2.69.1':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/functions-js@2.4.4':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/node-fetch@2.6.15':
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
'@supabase/postgrest-js@1.19.4':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/realtime-js@2.11.2':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
'@types/phoenix': 1.6.6
|
||||
'@types/ws': 8.18.1
|
||||
ws: 8.18.2
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@supabase/ssr@0.6.1(@supabase/supabase-js@2.49.4)':
|
||||
dependencies:
|
||||
'@supabase/supabase-js': 2.49.4
|
||||
cookie: 1.0.2
|
||||
|
||||
'@supabase/storage-js@2.7.1':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/supabase-js@2.49.4':
|
||||
dependencies:
|
||||
'@supabase/auth-js': 2.69.1
|
||||
'@supabase/functions-js': 2.4.4
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
'@supabase/postgrest-js': 1.19.4
|
||||
'@supabase/realtime-js': 2.11.2
|
||||
'@supabase/storage-js': 2.7.1
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@swc/counter@0.1.3': {}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
@@ -3644,6 +3746,8 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/phoenix@1.6.6': {}
|
||||
|
||||
'@types/react-dom@19.1.3(@types/react@19.1.2)':
|
||||
dependencies:
|
||||
'@types/react': 19.1.2
|
||||
@@ -3658,6 +3762,10 @@ snapshots:
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 20.17.32
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
@@ -3752,6 +3860,8 @@ snapshots:
|
||||
|
||||
comma-separated-tokens@2.0.3: {}
|
||||
|
||||
cookie@1.0.2: {}
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
|
||||
csstype@3.1.3: {}
|
||||
@@ -4116,13 +4226,13 @@ snapshots:
|
||||
lightningcss-win32-arm64-msvc: 1.29.2
|
||||
lightningcss-win32-x64-msvc: 1.29.2
|
||||
|
||||
llamaindex@0.10.3(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(zod@3.24.3):
|
||||
llamaindex@0.10.3(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(ws@8.18.2)(zod@3.24.3):
|
||||
dependencies:
|
||||
'@llamaindex/cloud': 4.0.4(@llamaindex/core@0.6.3)(@llamaindex/env@0.1.29)
|
||||
'@llamaindex/core': 0.6.3
|
||||
'@llamaindex/env': 0.1.29
|
||||
'@llamaindex/node-parser': 2.0.3(@llamaindex/core@0.6.3)(@llamaindex/env@0.1.29)(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)
|
||||
'@llamaindex/openai': 0.3.5(zod@3.24.3)
|
||||
'@llamaindex/openai': 0.3.5(ws@8.18.2)(zod@3.24.3)
|
||||
'@llamaindex/workflow': 1.0.4(@llamaindex/core@0.6.3)(@llamaindex/env@0.1.29)(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)
|
||||
'@types/lodash': 4.17.16
|
||||
'@types/node': 22.15.3
|
||||
@@ -4439,7 +4549,7 @@ snapshots:
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
openai@4.97.0(zod@3.24.3):
|
||||
openai@4.97.0(ws@8.18.2)(zod@3.24.3):
|
||||
dependencies:
|
||||
'@types/node': 18.19.87
|
||||
'@types/node-fetch': 2.6.12
|
||||
@@ -4449,6 +4559,7 @@ snapshots:
|
||||
formdata-node: 4.4.1
|
||||
node-fetch: 2.7.0
|
||||
optionalDependencies:
|
||||
ws: 8.18.2
|
||||
zod: 3.24.3
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
@@ -4843,6 +4954,8 @@ snapshots:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
|
||||
ws@8.18.2: {}
|
||||
|
||||
zod-to-json-schema@3.24.5(zod@3.24.3):
|
||||
dependencies:
|
||||
zod: 3.24.3
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `organizationId` to the `projects` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "projects" ADD COLUMN "organizationId" TEXT NOT NULL;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "organizations" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "organizations_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "organization_users" (
|
||||
"id" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL DEFAULT 'member',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "organization_users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "organizations_slug_key" ON "organizations"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "organization_users_organizationId_idx" ON "organization_users"("organizationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "organization_users_userId_organizationId_key" ON "organization_users"("userId", "organizationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "projects_organizationId_idx" ON "projects"("organizationId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "organization_users" ADD CONSTRAINT "organization_users_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "organization_users" ADD CONSTRAINT "organization_users_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -14,6 +14,53 @@ datasource db {
|
||||
directUrl = env("DIRECT_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id
|
||||
email String @unique
|
||||
name String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relationships
|
||||
organizationUsers OrganizationUser[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model Organization {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
slug String @unique
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relationships
|
||||
projects Project[]
|
||||
organizationUsers OrganizationUser[]
|
||||
|
||||
@@map("organizations")
|
||||
}
|
||||
|
||||
model OrganizationUser {
|
||||
id String @id @default(cuid())
|
||||
role String @default("member") // "owner", "admin", "member"
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Foreign keys
|
||||
userId String
|
||||
organizationId String
|
||||
|
||||
// Relationships
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, organizationId])
|
||||
@@index([organizationId])
|
||||
@@map("organization_users")
|
||||
}
|
||||
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
@@ -22,6 +69,13 @@ model Project {
|
||||
updatedAt DateTime @updatedAt
|
||||
questions Question[]
|
||||
|
||||
// Foreign key
|
||||
organizationId String
|
||||
|
||||
// Relationship
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([organizationId])
|
||||
@@map("projects")
|
||||
}
|
||||
|
||||
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
import { Project } from './project';
|
||||
|
||||
export interface Organization {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
projects?: Project[];
|
||||
organizationUsers?: OrganizationUser[];
|
||||
}
|
||||
|
||||
export interface OrganizationUser {
|
||||
id: string;
|
||||
role: 'owner' | 'admin' | 'member';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
user?: User;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Project } from './project';
|
||||
|
||||
export interface Organization {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
projects?: Project[];
|
||||
organizationUsers?: OrganizationUser[];
|
||||
}
|
||||
|
||||
export interface OrganizationUser {
|
||||
id: string;
|
||||
role: 'owner' | 'admin' | 'member';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
user?: User;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
Reference in New Issue
Block a user