diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d676ff4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,71 @@ +# Dependencies +node_modules +npm-debug.log* + +# Next.js +.next/ +out/ + +# Production build +dist + +# Environment variables +.env*.local +.env + +# Log files +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# macOS +.DS_Store + +# IDE files +.vscode/ +.idea/ + +# Git +.git +.gitignore + +# Documentation +README.md +CONTRIBUTING.md +LICENSE \ No newline at end of file diff --git a/.env.example b/.env.example index 19790e1..55b633d 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,32 @@ # Clerk Authentication -NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key_here -CLERK_SECRET_KEY=your_clerk_secret_key_here +NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxx +CLERK_SECRET_KEY=sk_test_xxxxx -# Supabase -NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url_here -NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key_here +# Supabase Database +NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=xxxxx -# OpenAI API (for Vercel AI SDK) -OPENAI_API_KEY=your_openai_api_key_here +# Redis/Upstash (for session caching and rate limiting) +UPSTASH_REDIS_REST_URL=https://xxxxx.upstash.io +UPSTASH_REDIS_REST_TOKEN=xxxxx -# Anthropic API (optional, for Claude models) -ANTHROPIC_API_KEY=your_anthropic_api_key_here \ No newline at end of file +# Alternative: Local Redis +# REDIS_URL=redis://localhost:6379 +# REDIS_TOKEN=xxxxx + +# AI Services +OPENAI_API_KEY=sk-xxxxx +ANTHROPIC_API_KEY=sk-ant-xxxxx + +# Security (Generate a random 256-bit key) +ENCRYPTION_KEY=xxxxx + +# AWS S3 (for file exports - optional) +AWS_ACCESS_KEY_ID=xxxxx +AWS_SECRET_ACCESS_KEY=xxxxx +AWS_REGION=us-east-1 +AWS_S3_BUCKET=codeguide-exports + +# Application Settings +NODE_ENV=development +NEXT_PUBLIC_APP_URL=http://localhost:3000 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 2f1c205..bbe261e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,416 +1,71 @@ -# CLAUDE.md - CodeGuide Starter Kit +# Claude Code Task Management Guide -This file contains essential context about the project structure, technologies, and conventions to help Claude understand and work effectively within this codebase. +## Documentation Available -## Project Overview +📚 **Project Documentation**: Check the documentation files in this directory for project-specific setup instructions and guides. +**Project Tasks**: Check the tasks directory in documentation/tasks for the list of tasks to be completed. Use the CLI commands below to interact with them. -**CodeGuide Starter Kit** is a modern Next.js starter template featuring authentication, database integration, AI capabilities, and a comprehensive UI component system. +## MANDATORY Task Management Workflow -### Core Technologies - -- **Framework**: Next.js 15 with App Router (`/src/app` directory structure) -- **Language**: TypeScript with strict mode enabled -- **Styling**: TailwindCSS v4 with CSS custom properties -- **UI Components**: shadcn/ui (New York style) with Lucide icons -- **Authentication**: Clerk with middleware protection -- **Database**: Supabase with third-party auth integration -- **AI Integration**: Vercel AI SDK with support for Anthropic Claude and OpenAI -- **Theme System**: next-themes with dark mode support - -## Project Structure - -``` -src/ -├── app/ # Next.js App Router -│ ├── api/ # API routes -│ │ └── chat/ # AI chat endpoint -│ ├── globals.css # Global styles with dark mode -│ ├── layout.tsx # Root layout with providers -│ └── page.tsx # Home page with status dashboard -├── components/ -│ ├── ui/ # shadcn/ui components (40+ components) -│ ├── chat.tsx # AI chat interface -│ ├── setup-guide.tsx # Configuration guide -│ ├── theme-provider.tsx # Theme context provider -│ └── theme-toggle.tsx # Dark mode toggle components -├── lib/ -│ ├── utils.ts # Utility functions (cn, etc.) -│ ├── supabase.ts # Supabase client configurations -│ ├── user.ts # User utilities using Clerk -│ └── env-check.ts # Environment validation -└── middleware.ts # Clerk authentication middleware -``` - -## Key Configuration Files - -- **package.json**: Dependencies and scripts -- **components.json**: shadcn/ui configuration (New York style, neutral colors) -- **tsconfig.json**: TypeScript configuration with path aliases (`@/`) -- **.env.example**: Environment variables template -- **SUPABASE_CLERK_SETUP.md**: Integration setup guide - -## Authentication & Database - -### Clerk Integration -- Middleware protects `/dashboard(.*)` and `/profile(.*)` routes -- Components: `SignInButton`, `SignedIn`, `SignedOut`, `UserButton` -- User utilities in `src/lib/user.ts` use `currentUser()` from Clerk - -### Supabase Integration -- **Client**: `createSupabaseServerClient()` for server-side with Clerk tokens -- **RLS**: Row Level Security uses `auth.jwt() ->> 'sub'` for Clerk user IDs -- **Example Migration**: `supabase/migrations/001_example_tables_with_rls.sql` - -#### Supabase Client Usage Patterns - -**Server-side (Recommended for data fetching):** -```typescript -import { createSupabaseServerClient } from "@/lib/supabase" - -export async function getServerData() { - const supabase = await createSupabaseServerClient() - - const { data, error } = await supabase - .from('posts') - .select('*') - .order('created_at', { ascending: false }) - - if (error) { - console.error('Database error:', error) - return null - } - - return data -} -``` - -**Client-side (For interactive operations):** -```typescript -"use client" - -import { supabase } from "@/lib/supabase" -import { useAuth } from "@clerk/nextjs" - -function ClientComponent() { - const { getToken } = useAuth() - - const fetchData = async () => { - const token = await getToken() - - // Pass token manually for client-side operations - const { data, error } = await supabase - .from('posts') - .select('*') - .auth(token) - - return data - } -} -``` - -## UI & Styling - -### TailwindCSS Setup -- **Version**: TailwindCSS v4 with PostCSS -- **Custom Properties**: CSS variables for theming -- **Dark Mode**: Class-based with `next-themes` -- **Animations**: `tw-animate-css` package included - -### shadcn/ui Components -- **Style**: New York variant -- **Theme**: Neutral base color with CSS variables -- **Icons**: Lucide React -- **Components Available**: 40+ UI components (Button, Card, Dialog, etc.) - -### Theme System -- **Provider**: `ThemeProvider` in layout with system detection -- **Toggle Components**: `ThemeToggle` (dropdown) and `SimpleThemeToggle` (button) -- **Persistence**: Automatic theme persistence across sessions - -## AI Integration - -### Vercel AI SDK -- **Endpoint**: `/api/chat/route.ts` -- **Providers**: Anthropic Claude and OpenAI support -- **Chat Component**: Real-time streaming chat interface -- **Authentication**: Requires Clerk authentication - -## Development Conventions - -### File Organization -- **Components**: Use PascalCase, place in appropriate directories -- **Utilities**: Place reusable functions in `src/lib/` -- **Types**: Define alongside components or in dedicated files -- **API Routes**: Follow Next.js App Router conventions - -### Import Patterns -```typescript -// Path aliases (configured in tsconfig.json) -import { Button } from "@/components/ui/button" -import { getCurrentUser } from "@/lib/user" -import { supabase } from "@/lib/supabase" - -// External libraries -import { useTheme } from "next-themes" -import { SignedIn, useAuth } from "@clerk/nextjs" -``` - -### Component Patterns -```typescript -// Client components (when using hooks/state) -"use client" - -// Server components (default, for data fetching) -export default async function ServerComponent() { - const user = await getCurrentUser() - // ... -} -``` - -## Environment Variables - -Required for full functionality: +🚨 **YOU MUST FOLLOW THIS EXACT WORKFLOW - NO EXCEPTIONS** 🚨 +### **STEP 1: DISCOVER TASKS (MANDATORY)** +You MUST start by running this command to see all available tasks: ```bash -# Clerk Authentication -NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_... -CLERK_SECRET_KEY=sk_test_... - -# Supabase Database -NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... - -# AI Integration (optional) -OPENAI_API_KEY=sk-... -ANTHROPIC_API_KEY=sk-ant-... -``` - -## Common Patterns - -### Row Level Security (RLS) Policies - -All database tables should use RLS policies that reference Clerk user IDs via `auth.jwt() ->> 'sub'`. - -**Basic User-Owned Data Pattern:** -```sql --- Enable RLS on table -ALTER TABLE posts ENABLE ROW LEVEL SECURITY; - --- Users can read all posts (public) -CREATE POLICY "Anyone can read posts" ON posts - FOR SELECT USING (true); - --- Users can only insert posts as themselves -CREATE POLICY "Users can insert own posts" ON posts - FOR INSERT WITH CHECK (auth.jwt() ->> 'sub' = user_id); - --- Users can only update their own posts -CREATE POLICY "Users can update own posts" ON posts - FOR UPDATE USING (auth.jwt() ->> 'sub' = user_id); - --- Users can only delete their own posts -CREATE POLICY "Users can delete own posts" ON posts - FOR DELETE USING (auth.jwt() ->> 'sub' = user_id); -``` - -**Private Data Pattern:** -```sql --- Completely private to each user -CREATE POLICY "Users can only access own data" ON private_notes - FOR ALL USING (auth.jwt() ->> 'sub' = user_id); -``` - -**Conditional Visibility Pattern:** -```sql --- Public profiles or own profile -CREATE POLICY "Users can read public profiles or own profile" ON profiles - FOR SELECT USING ( - is_public = true OR auth.jwt() ->> 'sub' = user_id - ); +task-manager list-tasks ``` -**Collaboration Pattern:** -```sql --- Owner and collaborators can access -CREATE POLICY "Owners and collaborators can read" ON collaborations - FOR SELECT USING ( - auth.jwt() ->> 'sub' = owner_id OR - auth.jwt() ->> 'sub' = ANY(collaborators) - ); +### **STEP 2: START EACH TASK (MANDATORY)** +Before working on any task, you MUST mark it as started: +```bash +task-manager start-task ``` -### Database Operations with Supabase - -**Complete CRUD Example:** -```typescript -import { createSupabaseServerClient } from "@/lib/supabase" -import { getCurrentUser } from "@/lib/user" - -// CREATE - Insert new record -export async function createPost(title: string, content: string) { - const user = await getCurrentUser() - if (!user) return null - - const supabase = await createSupabaseServerClient() - - const { data, error } = await supabase - .from('posts') - .insert({ - title, - content, - user_id: user.id, // Clerk user ID - }) - .select() - .single() - - if (error) { - console.error('Error creating post:', error) - return null - } - - return data -} - -// READ - Fetch user's posts -export async function getUserPosts() { - const supabase = await createSupabaseServerClient() - - const { data, error } = await supabase - .from('posts') - .select(` - id, - title, - content, - created_at, - user_id - `) - .order('created_at', { ascending: false }) - - if (error) { - console.error('Error fetching posts:', error) - return [] - } - - return data -} - -// UPDATE - Modify existing record -export async function updatePost(postId: string, updates: { title?: string; content?: string }) { - const supabase = await createSupabaseServerClient() - - const { data, error } = await supabase - .from('posts') - .update(updates) - .eq('id', postId) - .select() - .single() - - if (error) { - console.error('Error updating post:', error) - return null - } - - return data -} - -// DELETE - Remove record -export async function deletePost(postId: string) { - const supabase = await createSupabaseServerClient() - - const { error } = await supabase - .from('posts') - .delete() - .eq('id', postId) - - if (error) { - console.error('Error deleting post:', error) - return false - } - - return true -} +### **STEP 3: COMPLETE EACH TASK (MANDATORY)** +After finishing implementation, you MUST mark the task as completed: +```bash +task-manager complete-task "Brief description of what was implemented" ``` -**Real-time Subscriptions:** -```typescript -"use client" - -import { useEffect, useState } from "react" -import { supabase } from "@/lib/supabase" -import { useAuth } from "@clerk/nextjs" +## Task Files Location -function useRealtimePosts() { - const [posts, setPosts] = useState([]) - const { getToken } = useAuth() +📁 **Task Data**: Your tasks are organized in the `documentation/tasks/` directory: +- Task JSON files contain complete task information +- Use ONLY the `task-manager` commands listed above +- Follow the mandatory workflow sequence for each task - useEffect(() => { - const fetchPosts = async () => { - const token = await getToken() - - const { data } = await supabase - .from('posts') - .select('*') - .auth(token) - - setPosts(data || []) - } +## MANDATORY Task Workflow Sequence - fetchPosts() +🔄 **For EACH individual task, you MUST follow this sequence:** - // Subscribe to changes - const subscription = supabase - .channel('posts-channel') - .on('postgres_changes', - { event: '*', schema: 'public', table: 'posts' }, - (payload) => { - fetchPosts() // Refetch on any change - } - ) - .subscribe() +1. 📋 **DISCOVER**: `task-manager list-tasks` (first time only) +2. 🚀 **START**: `task-manager start-task ` (mark as in progress) +3. 💻 **IMPLEMENT**: Do the actual coding/implementation work +4. ✅ **COMPLETE**: `task-manager complete-task "What was done"` +5. 🔁 **REPEAT**: Go to next task (start from step 2) - return () => { - subscription.unsubscribe() - } - }, [getToken]) +## Task Status Options - return posts -} -``` - -### Protected Routes -Routes matching `/dashboard(.*)` and `/profile(.*)` are automatically protected by Clerk middleware. - -### Theme-Aware Components -```typescript -// Automatic dark mode support via CSS custom properties -
- -
-``` - -## Development Commands - -```bash -npm run dev # Start development server with Turbopack -npm run build # Build for production -npm run start # Start production server -npm run lint # Run ESLint -``` +- `pending` - Ready to work on +- `in_progress` - Currently being worked on +- `completed` - Successfully finished +- `blocked` - Cannot proceed (waiting for dependencies) +- `cancelled` - No longer needed -## Best Practices +## CRITICAL WORKFLOW RULES -1. **Authentication**: Always check user state with Clerk hooks/utilities -2. **Database**: Use RLS policies with Clerk user IDs for security -3. **UI**: Leverage existing shadcn/ui components before creating custom ones -4. **Styling**: Use TailwindCSS classes and CSS custom properties for theming -5. **Types**: Maintain strong TypeScript typing throughout -6. **Performance**: Use server components by default, client components only when needed +❌ **NEVER skip** the `task-manager start-task` command +❌ **NEVER skip** the `task-manager complete-task` command +❌ **NEVER work on multiple tasks simultaneously** +✅ **ALWAYS complete one task fully before starting the next** +✅ **ALWAYS provide completion details in the complete command** +✅ **ALWAYS follow the exact 3-step sequence: list → start → complete** -## Integration Notes +## Final Requirements -- **Clerk + Supabase**: Uses modern third-party auth (not deprecated JWT templates) -- **AI Chat**: Requires authentication and environment variables -- **Dark Mode**: Automatically applied to all shadcn components -- **Mobile**: Responsive design with TailwindCSS breakpoints +🚨 **CRITICAL**: Your work is not complete until you have: +1. ✅ Completed ALL tasks using the mandatory workflow +2. ✅ Committed all changes with comprehensive commit messages +3. ✅ Created a pull request with proper description -This starter kit provides a solid foundation for building modern web applications with authentication, database integration, AI capabilities, and polished UI components. \ No newline at end of file +Remember: The task management workflow is MANDATORY, not optional! diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1409c2d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,56 @@ +# Use the official Node.js runtime as the base image +FROM node:20-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json package-lock.json* ./ +RUN npm ci + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry during the build. +# ENV NEXT_TELEMETRY_DISABLED 1 + +RUN npm run build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production +# Uncomment the following line in case you want to disable telemetry during runtime. +# ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 +ENV HOSTNAME "0.0.0.0" + +CMD ["node", "server.js"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..076fa24 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,69 @@ +version: '3.8' + +services: + # Next.js Application + app: + build: . + ports: + - "3000:3000" + environment: + - NODE_ENV=production + depends_on: + - postgres + - redis + env_file: + - .env.local + volumes: + - .:/app + - /app/node_modules + command: npm run dev + + # PostgreSQL Database + postgres: + image: postgres:15-alpine + environment: + POSTGRES_DB: codeguide + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./supabase/migrations:/docker-entrypoint-initdb.d/ + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 30s + timeout: 10s + retries: 3 + + # Redis for session caching and rate limiting + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 3 + + # Development database management (optional) + adminer: + image: adminer:4-standalone + ports: + - "8080:8080" + environment: + ADMINER_DEFAULT_SERVER: postgres + depends_on: + - postgres + +volumes: + postgres_data: + redis_data: + +networks: + default: + name: codeguide-network \ No newline at end of file diff --git a/documentation/app_flowchart.md b/documentation/app_flowchart.md new file mode 100644 index 0000000..bfc4976 --- /dev/null +++ b/documentation/app_flowchart.md @@ -0,0 +1,12 @@ +flowchart TD + Start[Start] + Start --> Input[User Input] + Input --> Outline[Automated Outline Generation] + Outline --> Analysis[Contextual Analysis and Summary Composition] + Analysis --> Decision{Need Template Customization} + Decision -->|Yes| Template[Customizable Template Framework] + Decision -->|No| Collaboration[Collaboration and Version Control] + Template --> Collaboration + Collaboration --> Export[Multi-Format Export and Integration] + Export --> UX[Intuitive User Experience] + UX --> End[Finish] \ No newline at end of file diff --git a/documentation/backend_structure_document.md b/documentation/backend_structure_document.md new file mode 100644 index 0000000..41560af --- /dev/null +++ b/documentation/backend_structure_document.md @@ -0,0 +1,191 @@ +# Backend Structure Document + +This document outlines the design and setup of the backend system for our intelligent outline and summary generation tool. It covers architecture, databases, APIs, hosting, infrastructure, security, and maintenance. The goal is to give a clear, step-by-step view of how the backend works and how it’s hosted, without assuming deep technical knowledge. + +## 1. Backend Architecture + +Overview: +- We use a service-oriented approach, breaking the backend into focused components (services) that handle specific tasks. +- A central API Gateway routes requests from the frontend or other services to the right place. + +Key Components: +- **API Gateway (Node.js + Express)**: Receives all incoming calls, handles authentication, and sends requests to the appropriate microservice. +- **NLP Service (Python + Flask)**: Processes text inputs, runs natural language analysis, and returns summaries or outlines. +- **Template Service (Node.js)**: Manages outline templates, applying user settings to generate custom outputs. +- **Collaboration Service (Node.js)**: Tracks edits, comments, and versions for each project. +- **Export Service (Node.js)**: Converts final documents into PDF, DOCX, or Markdown. +- **Authentication Service**: Manages user accounts, tokens, and permissions. + +How It Supports: +- **Scalability**: Each service can be scaled independently. If the NLP processor needs more power, we add more instances without touching other services. +- **Maintainability**: Clear boundaries mean teams can work on one service without affecting others. +- **Performance**: Services communicate over fast internal networks; heavy tasks (like NLP) run in optimized environments. + +## 2. Database Management + +We use a combination of relational and non-relational databases to store different kinds of data: + +- **PostgreSQL (SQL)** + - User accounts, project metadata, version histories, and access controls. +- **MongoDB (NoSQL)** + - Flexible storage of raw input texts, generated outlines, logs, and audit trails. +- **Redis (In-Memory Cache)** + - Caches frequent lookups (user sessions, template data) to speed up responses. + +Data Practices: +- Regular backups of PostgreSQL and MongoDB with automated snapshots. +- Read-replicas for PostgreSQL to handle high read loads. +- Data retention policies to archive or purge old logs. + +## 3. Database Schema + +### PostgreSQL Schema (Human-Readable) +- **Users**: Stores user profiles and credentials. + - ID, Email, PasswordHash, Name, CreatedAt +- **Projects**: Holds information about each outline project. + - ID, UserID, Title, Description, CreatedAt, UpdatedAt +- **Templates**: Defines the structure and settings for outlines. + - ID, UserID, Name, JSONDefinition, CreatedAt +- **Versions**: Tracks changes to each project’s outline. + - ID, ProjectID, TemplateID, VersionNumber, ChangeNotes, CreatedAt +- **Comments**: Collaboration notes on specific sections. + - ID, ProjectID, UserID, SectionReference, Text, CreatedAt + +### PostgreSQL Schema (SQL) +```sql +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + name VARCHAR(100), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE projects ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id), + title VARCHAR(255) NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE templates ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id), + name VARCHAR(100) NOT NULL, + json_definition JSONB NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE versions ( + id SERIAL PRIMARY KEY, + project_id INTEGER REFERENCES projects(id), + template_id INTEGER REFERENCES templates(id), + version_number INTEGER NOT NULL, + change_notes TEXT, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE comments ( + id SERIAL PRIMARY KEY, + project_id INTEGER REFERENCES projects(id), + user_id INTEGER REFERENCES users(id), + section_reference VARCHAR(255), + text TEXT NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +); +``` + +### MongoDB Schema (NoSQL) +- **Outlines Collection**: + - _id, projectId, rawInput, generatedOutline (array of sections), createdAt +- **Logs Collection**: + - _id, serviceName, level, message, timestamp + +## 4. API Design and Endpoints + +We follow a RESTful style so that each endpoint represents a resource and uses standard HTTP methods. + +Key Endpoints: + +- **Authentication** + - `POST /auth/register` – Create a new user account + - `POST /auth/login` – Authenticate and return a token +- **Projects** + - `GET /projects` – List all projects for the user + - `POST /projects` – Create a new project + - `GET /projects/{id}` – Retrieve project details + - `PUT /projects/{id}` – Update project metadata + - `DELETE /projects/{id}` – Remove a project +- **Outlines** + - `POST /projects/{id}/outline` – Generate an outline from user input + - `GET /projects/{id}/outline` – Fetch the latest outline +- **Templates** + - `GET /templates` – List user’s templates + - `POST /templates` – Create a template + - `PUT /templates/{id}` – Update a template +- **Collaboration** + - `GET /projects/{id}/comments` – List comments + - `POST /projects/{id}/comments` – Add a comment +- **Exports** + - `POST /projects/{id}/export?format=pdf|docx|md` – Generate a downloadable file + +Authentication tokens are sent in the `Authorization` header as a Bearer token. + +## 5. Hosting Solutions + +We host in the cloud for reliability and easy scaling: +- **Provider**: Amazon Web Services (AWS) +- **Compute**: Containers managed by Elastic Container Service (ECS) or Elastic Kubernetes Service (EKS) +- **Databases**: + - PostgreSQL on Amazon RDS with multi-AZ deployment + - MongoDB Atlas for managed NoSQL hosting + - Redis on Amazon ElastiCache +- **File Storage**: Amazon S3 for exported documents and backups +- **Benefits**: + - High uptime with multi-zone failover + - Pay-as-you-go keeps costs aligned with usage + - Built-in monitoring and security tools + +## 6. Infrastructure Components + +- **Load Balancer**: AWS Application Load Balancer distributes incoming traffic across service instances. +- **Caching**: Redis stores session data and template lookups for sub-millisecond response times. +- **CDN**: Amazon CloudFront serves static assets (front-end bundle, exports) from edge locations. +- **Containerization**: Docker images for each service, ensuring consistent environments. +- **Orchestration**: ECS/EKS auto-scales containers based on CPU and memory usage. +- **Service Discovery**: Internal DNS for services to find and communicate with each other securely. + +## 7. Security Measures + +- **Encryption**: + - TLS everywhere for in-transit data protection + - AES-256 encryption at rest for databases and S3 buckets +- **Authentication & Authorization**: + - JWT-based tokens with short lifetimes and refresh tokens + - Role-based access control (RBAC) ensures users only see their own projects +- **Network Security**: + - Private subnets for databases and internal services + - Public subnets only for load balancers +- **Data Validation**: + - Input sanitization to prevent injection attacks +- **Compliance**: + - Regular security audits and vulnerability scanning + +## 8. Monitoring and Maintenance + +- **Monitoring Tools**: + - AWS CloudWatch for metrics and logs + - ELK (Elasticsearch, Logstash, Kibana) stack for centralized log analysis + - Prometheus + Grafana for service health dashboards +- **Alerts**: + - Automated alerts on CPU/memory spikes, error rates, or high latency +- **Backups & Updates**: + - Daily automated database snapshots, weekly full backups + - Rolling updates to containers with zero downtime deployments + - Dependency updates tracked by a CI/CD pipeline (GitHub Actions) + +## 9. Conclusion and Overall Backend Summary + +Our backend is a collection of specialized services working together through a simple API gateway. It uses reliable, managed databases and cloud infrastructure to ensure the system can grow as demand increases. Strong security and monitoring practices protect user data and guarantee high availability. These choices align with our goals of providing a fast, scalable, and dependable outline-generation tool that users can trust. \ No newline at end of file diff --git a/documentation/frontend_guidelines_document.md b/documentation/frontend_guidelines_document.md new file mode 100644 index 0000000..24e4f93 --- /dev/null +++ b/documentation/frontend_guidelines_document.md @@ -0,0 +1,120 @@ +# Frontend Guideline Document + +This document outlines the frontend architecture, design principles, and technologies powering our Outline Generation and Summary Composition tool. It’s written in everyday language so everyone—technical or non-technical—can understand how the frontend is set up and why. + +## 1. Frontend Architecture + +### Overview +We use a component-based setup built on React with TypeScript. Our build tool is Vite, chosen for fast startup and hot-module reloading. + +### Key Libraries and Frameworks +- **React**: For building reusable UI components. +- **TypeScript**: Adds type safety and better code clarity. +- **Vite**: Modern build tool for quick development feedback. +- **React Router**: Manages navigation between different pages. + +### Scalability, Maintainability, Performance +- **Modular Components**: Each feature lives in its own folder, keeping code organized as the app grows. +- **Lazy Loading**: We split code so that each section loads only when needed, speeding up initial page loads. +- **Clear Type Definitions**: TypeScript interfaces describe data shapes, reducing bugs and making future changes easier. + +## 2. Design Principles + +We follow three main principles: + +### Usability +- **Simple Flows**: Every screen guides users step by step—no surprises. +- **Clear Labels**: Buttons and headings use everyday words (e.g., “Generate Outline,” “Download PDF”). + +### Accessibility +- **Keyboard Navigation**: All interactive elements can be reached by tabbing. +- **ARIA Labels**: Screen-reader friendly attributes on custom controls. +- **Color Contrast**: Meets WCAG AA standards for text readability. + +### Responsiveness +- **Mobile-First**: We design for phones first, then scale up to tablets and desktops. +- **Flexible Layouts**: CSS Grid and Flexbox adapt to different screen sizes. + +## 3. Styling and Theming + +### Styling Approach +- **Tailwind CSS**: Utility-first framework for quick and consistent styling. +- **BEM Naming**: When writing custom CSS modules, we follow Block-Element-Modifier conventions. + +### Theming +We keep a central theme file (`theme.ts`) defining colors, spacing, and typography. This ensures consistent branding. + +### Visual Style +- **Style**: Modern flat design with subtle glassmorphism touches on modal backgrounds. +- **Color Palette**: + - Primary: #4F46E5 (indigo) + - Secondary: #10B981 (emerald) + - Accent: #F59E0B (amber) + - Background: #F3F4F6 (light gray) + - Text: #111827 (dark gray) + +### Typography +We use the **Inter** font (free from Google Fonts) for its clean, modern look. Headings are slightly heavier to create visual hierarchy. + +## 4. Component Structure + +We organize components by feature (also known as “feature folders”): + +- `src/components`: Shared UI elements like Button, Modal, Input. +- `src/features/outline`: Components and hooks specific to outline generation (e.g., `OutlineForm`, `OutlinePreview`). +- `src/features/summary`: Components for summary composition. + +### Reusability +- **Atomic Design**: Atoms (Button, Input), Molecules (FormGroup), Organisms (OutlineForm). +- **Single Responsibility**: Each component handles one piece of the UI, making maintenance straightforward. + +## 5. State Management + +We use React’s Context API with useReducer for global state: + +- **Context**: Stores user input, generated outline, and export options. +- **Reducer**: Defines actions like `SET_INPUT`, `GENERATE_OUTLINE`, `RESET_DATA`. +- **Local State**: Minor UI states (like modal open/closed) live in individual components via useState. + +This approach avoids over-complexity while keeping data flow clear. + +## 6. Routing and Navigation + +We use **React Router v6**: + +- `/`: Home page with tool description. +- `/outline`: Outline generation interface. +- `/summary`: Summary composition interface. +- `/settings`: Theme and export preferences. + +Navigation is handled by a top-level `` component. Links update the URL without a full page reload. + +## 7. Performance Optimization + +### Strategies +- **Code Splitting**: Each route’s code is loaded only when the user visits it. +- **Lazy Loading Images**: Thumbnails and illustrations load as they scroll into view. +- **Tree Shaking**: We rely on Vite’s optimized bundling to remove unused code. +- **Minified Assets**: CSS and JS files are minified in production. + +These measures ensure fast load times and a snappy experience. + +## 8. Testing and Quality Assurance + +### Unit Tests +- **Jest** + **React Testing Library** for testing components in isolation. +- We aim for at least 80% coverage on core logic. + +### Integration Tests +- Combine multiple components to test flows (e.g., entering input and seeing an outline preview). + +### End-to-End Tests +- **Cypress**: Simulates user interactions—filling forms, clicking buttons, downloading files. + +### Linting and Formatting +- **ESLint** + **Prettier** enforce code style. +- **Husky** + **lint-staged** run checks before every commit. + +## 9. Conclusion and Overall Frontend Summary + +Our frontend is a modern, scalable React app that balances simplicity with performance. By following clear design principles, a component-based structure, and thorough testing strategies, we ensure a reliable and user-friendly experience. The consistent theming and responsive layouts keep the interface approachable on any device. This setup makes it easy to add new features—like alternative export formats or collaboration tools—without disrupting the core user experience. \ No newline at end of file diff --git a/documentation/project_requirements_document.md b/documentation/project_requirements_document.md new file mode 100644 index 0000000..5d68ec7 --- /dev/null +++ b/documentation/project_requirements_document.md @@ -0,0 +1,86 @@ +# Project Requirements Document (PRD) + +## 1. Project Overview + +This project, **codeguide-starter-lite**, is an intelligent outline and summary generation tool. It transforms a user’s rough description or prompt into a structured project outline, complete with objectives, deliverables, and milestones. By combining predefined templates with adaptive logic, it removes the manual effort of rearranging sections and editing for consistency. + +Built on natural language processing, the engine also extracts context and synthesizes concise summaries, so stakeholders can quickly grasp the scope and rationale behind any project. The tool supports customizable templates, version control for collaboration, and multi-format export, making it easy to integrate generated documents into existing workflows. + +**Key Objectives** +- Automate creation of clear, consistent project outlines and summaries. +- Provide a flexible template framework that adapts to branding or organizational standards. +- Enable real-time collaboration with version history and comment tracking. +- Offer one-click export to PDF, DOCX, and Markdown for seamless integration. + +**Success Criteria** +- Users can generate a full outline and summary from a prompt in under 30 seconds. +- Template customization covers at least 5 common section structures. +- Collaboration features record and display a history of edits reliably. +- Exported documents retain layout fidelity across supported formats. + +## 2. In-Scope vs. Out-of-Scope + +### In-Scope (Version 1) +- Automated outline generation using templates and adaptive logic. +- Contextual analysis and summary composition via NLP. +- Customizable template system (rename sections, reorder, inject custom text). +- Basic collaboration tools: comments on sections and simple version history. +- Multi-format export: PDF, DOCX, and Markdown. +- Interactive user interface with real-time preview and contextual tooltips. + +### Out-of-Scope (Planned for Later Phases) +- Deep AI-based editing recommendations (tone, readability scoring). +- Mobile-native apps (iOS or Android). +- Third-party project management integrations (e.g., Jira, Asana API). +- Rich text collaboration (track changes inside exports). +- Advanced analytics dashboard on usage and document quality metrics. +- On-premises deployment (initial release will be cloud-only). + +## 3. User Flow + +A new user lands on the welcome page and signs up with email/password or OAuth. After logging in, they see a dashboard listing previous projects and a prominent “Create New Outline” button. Clicking that button opens an input modal where they paste or type a project description. The user then chooses from a list of templates (basic, technical, marketing), or starts from a blank template. + +Next, the user configures template settings—renaming sections, setting order, or injecting custom placeholders. They hit “Generate,” and within seconds the engine displays a live preview of the outline and summary side by side. The user can add comments in the sidebar, switch between versions in the history panel, and once satisfied, click “Export” to download a PDF, DOCX, or Markdown file. + +## 4. Core Features + +- **Automated Outline Generation**: Parse user prompts to build section headers, bullet points, and milestones. +- **Contextual Summary Composition**: Use NLP to extract background, goals, and rationale into a concise paragraph. +- **Customizable Template Framework**: Allow users to rename sections, reorder content, and add custom text fields. +- **Collaboration & Version Control**: Enable comments on specific sections, record each save as a version, and view a timeline of changes. +- **Multi-Format Export**: Export finalized outlines to PDF, DOCX, or Markdown with consistent styling. +- **Real-Time Preview & Tooltips**: Show live updates as users type or adjust settings, plus inline help tips. + +## 5. Tech Stack & Tools + +- **Frontend**: Next.js (React + TypeScript), Tailwind CSS for styling, React Query for data fetching, MDX renderer for preview. +- **Backend**: Node.js with Express or NestJS (TypeScript), OpenAI GPT-4 API for NLP tasks, WebSocket (Socket.io) for real-time collaboration. +- **Database**: PostgreSQL (version history, templates, user data), Redis (session caching, rate limiting). +- **Storage & File Conversion**: AWS S3 for exports, Puppeteer or PDFKit for PDF generation, mammoth.js for DOCX. +- **Authentication**: JWT-based, OAuth 2.0 support (Google, GitHub). +- **Dev & IDE Tools**: VS Code with ESLint, Prettier; GitHub Actions for CI/CD; Docker for local environment. + +## 6. Non-Functional Requirements + +- **Performance**: 95th percentile response time under 2 seconds for outline generation (excluding model latency). Preview updates in under 200ms. +- **Scalability**: Support up to 1,000 concurrent users with horizontal backend scaling. +- **Security & Compliance**: HTTPS everywhere, JWT tokens stored securely, data encrypted at rest (AES-256) and in transit (TLS 1.2+). GDPR-friendly data handling. +- **Usability**: WCAG 2.1 AA accessible UI, mobile-responsive design, clear error messaging. +- **Reliability**: 99.9% uptime SLA, automated backups daily, failover database replica. + +## 7. Constraints & Assumptions + +- **GPT-4 API Availability**: Relies on OpenAI’s uptime and rate limits; assume quota can be increased as needed. +- **Template Definitions**: Stored in JSON files; assume users upload or choose only valid JSON schemas. +- **Cloud-Only Deployment**: No on-premises option in initial release. +- **Browser Support**: Latest versions of Chrome, Firefox, Safari, and Edge. + +## 8. Known Issues & Potential Pitfalls + +- **API Rate Limits**: Hitting OpenAI limits could delay generation. Mitigation: queue requests and fallback to simpler templates if overloaded. +- **Version Conflicts**: Simultaneous edits may overwrite each other. Mitigation: lock sections during editing or implement optimistic concurrency control. +- **Formatting Drifts**: Exported DOCX/PDF may differ slightly from preview. Mitigation: include a style guide, run automated visual regression tests. +- **Prompt Ambiguity**: Vague user descriptions yield poor outlines. Mitigation: add guided prompt helper and sample inputs. + +--- +_End of PRD for codeguide-starter-lite._ \ No newline at end of file diff --git a/documentation/security_guideline_document.md b/documentation/security_guideline_document.md new file mode 100644 index 0000000..7efc062 --- /dev/null +++ b/documentation/security_guideline_document.md @@ -0,0 +1,125 @@ +# Security Guidelines for the Automated Outline & Summary Generation Tool + +## 1. Introduction +This document outlines the security requirements, principles, and controls for the Automated Outline & Summary Generation Tool. It ensures the solution is designed, implemented, and operated with security and privacy in mind, from end to end. + +## 2. Scope +Applies to all components and environments supporting: +- Intelligent outline generation +- Contextual analysis and summary composition +- Customizable template framework +- Collaboration and version control +- Multi-format export (PDF, DOCX, Markdown) +- Interactive web interface and APIs + +## 3. Core Security Principles +- **Security by Design**: Integrate security reviews during design, development, testing, and deployment. +- **Least Privilege**: Grant minimal permissions to services, users, and processes. +- **Defense in Depth**: Layer controls (network, application, data) to mitigate single points of failure. +- **Fail Securely**: Default to denial of access on errors; avoid exposing stack traces or sensitive data. +- **Secure Defaults**: Ship with hardened configurations; require explicit opt-in for less-secure features. + +## 4. Authentication & Access Control +1. **User Authentication**: + - Enforce strong password policies (minimum 12 characters, complexity rules, rotation). + - Store passwords using Argon2id or bcrypt with unique salts. + - Offer Multi-Factor Authentication (MFA) for administrators and power users. +2. **Session Management**: + - Issue cryptographically strong, unpredictable session IDs. + - Set idle and absolute session timeouts. + - Secure cookies with `HttpOnly`, `Secure`, and `SameSite=Strict` attributes. +3. **Role-Based Access Control (RBAC)**: + - Define roles (e.g., Reader, Editor, Admin). + - Enforce server-side permission checks on every endpoint. + - Restrict document creation, editing, export, and version history based on roles. +4. **API Security**: + - Require authentication (JWT or API tokens) on all APIs. + - Validate token signature, expiration (`exp`), and issuer claims. + - Rotate and revoke tokens securely. + +## 5. Input Handling & Template Safety +1. **Input Validation**: + - Validate all user inputs (text prompts, template parameters) server-side. + - Enforce length limits and allowable character sets. +2. **Template Sanitization**: + - Use a sandboxed templating engine to prevent server-side code execution. + - Escape or whitelist variables when injecting into HTML or document templates. + - Disallow arbitrary file inclusion or dynamic import in templates. +3. **Prevent Injection Attacks**: + - Use parameterized queries or ORM for metadata storage. + - Escape or encode user inputs before writing to PDF, DOCX, or HTML. +4. **Redirect & Forward Validation**: + - Maintain an allow-list of internal URLs or resource identifiers. + +## 6. Data Protection & Privacy +1. **Encryption in Transit & at Rest**: + - Enforce TLS 1.2+ (HTTPS) for all web and API traffic. + - Encrypt stored documents and metadata (AES-256). +2. **Sensitive Data Handling**: + - Do not log raw user content; mask or hash identifiers in logs. + - Comply with GDPR/CCPA for any PII in templates or user profiles. +3. **Secrets Management**: + - Store API keys, database credentials, and TLS certificates in a vault (e.g., AWS Secrets Manager). + - Avoid hard-coding secrets in code or config files. + +## 7. Collaboration & Version Control Security +1. **Access Auditing**: + - Log document access, edits, and version events with user ID and timestamp. +2. **Change Control**: + - Require review/approval workflows for template library updates. +3. **Data Integrity**: + - Calculate and store checksums (SHA-256) for each version to detect tampering. + +## 8. Multi-Format Export Hygiene +1. **Output Encoding**: + - Ensure PDF/DOCX generation libraries are patched and sandboxed. + - Strip or encode any HTML/CSS injected by users. +2. **File Permissions & Storage**: + - Store exported files outside the web root. + - Assign restrictive permissions; expire or clean up temporary files. + +## 9. Web Application Security Controls +- **CSRF Protection**: Implement anti-CSRF tokens on all state-changing forms and AJAX calls. +- **Security Headers**: + - Content-Security-Policy: restrict script sources and inline usage. + - X-Frame-Options: SAMEORIGIN. + - X-Content-Type-Options: nosniff. + - Referrer-Policy: no-referrer-when-downgrade. + - Strict-Transport-Security: max-age=31536000; includeSubDomains. +- **CORS**: Allow only trusted origins; restrict allowed methods and headers. +- **Clickjacking Protection**: Use frame-ancestors directive in CSP. + +## 10. Infrastructure & Configuration +- **Hardened Hosts**: Disable unused services; apply OS-level security benchmarks (CIS). +- **Network Controls**: Limit inbound ports; use firewalls and network segmentation. +- **TLS Configuration**: Disable SSLv3/TLS1.0-1.1; prefer strong cipher suites (ECDHE). +- **Configuration Management**: Store configurations in version control; encrypt secrets. +- **Patch Management**: Automate OS and dependency updates; monitor for critical CVEs. + +## 11. Dependency & Supply Chain Security +- **Secure Dependencies**: + - Vet libraries before inclusion; prefer actively maintained packages. + - Use lockfiles (package-lock.json, Pipfile.lock) to guarantee reproducible builds. +- **Vulnerability Scanning**: + - Integrate SCA tools in CI/CD to detect known CVEs. +- **Minimal Footprint**: + - Remove unused features and dev-only dependencies in production builds. + +## 12. Monitoring, Logging & Incident Response +- **Centralized Logging**: Aggregate logs in a secure SIEM; protect log integrity. +- **Alerting & Monitoring**: + - Detect anomalous behavior (e.g., brute-force attempts, unusual export volume). +- **Incident Playbook**: + - Define roles, communication, and containment steps in case of a breach. + +## 13. Secure CI/CD Practices +- **Pipeline Hardening**: + - Enforce least privilege for build agents. + - Sign build artifacts; verify signatures before deployment. +- **Automated Tests**: + - Include static analysis (SAST) and dynamic security tests (DAST). +- **Secrets in Pipelines**: + - Inject secrets at runtime from a vault; do not store in CI logs. + +--- +By adhering to these guidelines, we ensure the Automated Outline & Summary Generation Tool remains robust, resilient, and trustworthy across its entire lifecycle. Continuous review and adjustment of these controls are mandatory as the threat landscape and feature set evolve. \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index e9ffa30..cd72703 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,45 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + // Enable standalone output for Docker + output: "standalone", + + // Security headers + async headers() { + return [ + { + source: "/(.*)", + headers: [ + { + key: "X-Frame-Options", + value: "DENY", + }, + { + key: "X-Content-Type-Options", + value: "nosniff", + }, + { + key: "X-XSS-Protection", + value: "1; mode=block", + }, + { + key: "Strict-Transport-Security", + value: "max-age=31536000; includeSubDomains", + }, + { + key: "Referrer-Policy", + value: "strict-origin-when-cross-origin", + }, + ], + }, + ]; + }, + + // Enable experimental features for better performance + experimental: { + optimizeCss: true, + serverComponentsExternalPackages: ["@supabase/supabase-js"], + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index e5475ea..a1d2fce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", "@supabase/supabase-js": "^2.53.0", + "@upstash/redis": "^1.35.3", "ai": "^4.3.19", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -46,15 +47,18 @@ "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", + "ioredis": "^5.7.0", "lucide-react": "^0.532.0", "next": "15.4.4", "next-themes": "^0.4.6", + "rate-limiter-flexible": "^7.2.0", "react": "19.1.0", "react-day-picker": "^9.8.1", "react-dom": "19.1.0", "react-hook-form": "^7.61.1", "react-resizable-panels": "^3.0.3", "recharts": "^2.15.4", + "redis": "^5.8.0", "sonner": "^2.0.6", "svix": "^1.69.0", "tailwind-merge": "^3.3.1", @@ -71,7 +75,7 @@ "eslint-config-next": "15.4.4", "tailwindcss": "^4", "tw-animate-css": "^1.3.6", - "typescript": "^5" + "typescript": "5.9.2" } }, "node_modules/@ai-sdk/anthropic": { @@ -1025,6 +1029,12 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@ioredis/commands": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.0.tgz", + "integrity": "sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==", + "license": "MIT" + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -2611,6 +2621,66 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@redis/bloom": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.0.tgz", + "integrity": "sha512-kpKZzAAjGiGYn88Bqq6+ozxPg6kGYWRZH9vnOwGcoSCbrW14SZpZVMYMFSio8FH9ZJUdUcmT/RLGlA1W1t0UWQ==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.8.0" + } + }, + "node_modules/@redis/client": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.0.tgz", + "integrity": "sha512-ywZjKGoSSAECGYOd9bJpws6d4867SN686obUWT/sRmo1c/Q8V+jWyInvlqwKa0BOvTHHwYeB2WFUEvd6PADeOQ==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@redis/json": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.0.tgz", + "integrity": "sha512-xPBpwY6aKoRzMSu67MpwrBiSliON9bfHo9Y/pSPBjW8/KoOm1MzGqwJUO20qdjXpFoKJsDWwxIE1LHdBNzcImw==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.8.0" + } + }, + "node_modules/@redis/search": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.8.0.tgz", + "integrity": "sha512-lF9pNv9vySmirm1EzCH6YeRjhvH6lLT7tvebYHEM7WTkEQ/7kZWb4athliKESHpxzTQ36U9UbzuedSywHV6OhQ==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.8.0" + } + }, + "node_modules/@redis/time-series": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.8.0.tgz", + "integrity": "sha512-kPTlW2ACXokjQNXjCD8Pw9mHDoB94AHUlHFahyjxz9lUJUVwiva2Dgfrd2k3JxHhSBqyY2PREIj9YwIUSTSSqQ==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.8.0" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -3143,17 +3213,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", - "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", + "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/type-utils": "8.38.0", - "@typescript-eslint/utils": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/type-utils": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -3167,9 +3237,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.38.0", + "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -3183,16 +3253,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", - "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", + "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "engines": { @@ -3204,18 +3274,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", - "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", + "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.38.0", - "@typescript-eslint/types": "^8.38.0", + "@typescript-eslint/tsconfig-utils": "^8.39.0", + "@typescript-eslint/types": "^8.39.0", "debug": "^4.3.4" }, "engines": { @@ -3226,18 +3296,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", - "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", + "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0" + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3248,9 +3318,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", - "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", + "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", "dev": true, "license": "MIT", "engines": { @@ -3261,19 +3331,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", - "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", + "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -3286,13 +3356,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", - "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", + "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", "dev": true, "license": "MIT", "engines": { @@ -3304,16 +3374,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", - "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", + "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.38.0", - "@typescript-eslint/tsconfig-utils": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", + "@typescript-eslint/project-service": "8.39.0", + "@typescript-eslint/tsconfig-utils": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3329,7 +3399,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -3389,16 +3459,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", - "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", + "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0" + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3409,17 +3479,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", - "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", + "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/types": "8.39.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -3699,6 +3769,15 @@ "win32" ] }, + "node_modules/@upstash/redis": { + "version": "1.35.3", + "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.35.3.tgz", + "integrity": "sha512-hSjv66NOuahW3MisRGlSgoszU2uONAY2l5Qo3Sae8OT3/Tng9K+2/cBRuyPBX8egwEGcNNCF9+r0V6grNnhL+w==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -4194,6 +4273,15 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/cmdk": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", @@ -4494,7 +4582,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4557,6 +4644,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -5820,6 +5916,30 @@ "node": ">=12" } }, + "node_modules/ioredis": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz", + "integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.3.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -6703,6 +6823,18 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6853,7 +6985,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -7353,6 +7484,12 @@ ], "license": "MIT" }, + "node_modules/rate-limiter-flexible": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-7.2.0.tgz", + "integrity": "sha512-hrf0vIS/WOBegnHg+uPXxsXhuQYlNGfZiCmK5Wgudb12xlZUhpv9yD23yp/EW6BKQosshqnIQRQV+r3jyfIGQg==", + "license": "ISC" + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -7565,6 +7702,43 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/redis": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-5.8.0.tgz", + "integrity": "sha512-re0MHm1KHbiVIUPDGoUM3jldvjH5EM/wGZ3A33gyUYoC/UnVNKNnZHM5hcJVry7L2O2eJU3nflSXTliv10BTKg==", + "license": "MIT", + "dependencies": { + "@redis/bloom": "5.8.0", + "@redis/client": "5.8.0", + "@redis/json": "5.8.0", + "@redis/search": "5.8.0", + "@redis/time-series": "5.8.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -8018,6 +8192,12 @@ "dev": true, "license": "MIT" }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/standardwebhooks": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", @@ -8553,9 +8733,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -8585,6 +8765,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", diff --git a/package.json b/package.json index 10313d1..6be85b0 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,18 @@ "dev": "next dev --turbopack", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "type-check": "tsc --noEmit", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "docker:build": "docker build -t codeguide-app .", + "docker:run": "docker-compose up", + "docker:down": "docker-compose down", + "security:audit": "npm audit", + "security:scan": "npx audit-ci --moderate", + "db:migrate": "supabase db push", + "db:reset": "supabase db reset" }, "dependencies": { "@ai-sdk/anthropic": "^1.2.12", @@ -40,6 +51,7 @@ "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", "@supabase/supabase-js": "^2.53.0", + "@upstash/redis": "^1.35.3", "ai": "^4.3.19", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -47,15 +59,18 @@ "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", + "ioredis": "^5.7.0", "lucide-react": "^0.532.0", "next": "15.4.4", "next-themes": "^0.4.6", + "rate-limiter-flexible": "^7.2.0", "react": "19.1.0", "react-day-picker": "^9.8.1", "react-dom": "19.1.0", "react-hook-form": "^7.61.1", "react-resizable-panels": "^3.0.3", "recharts": "^2.15.4", + "redis": "^5.8.0", "sonner": "^2.0.6", "svix": "^1.69.0", "tailwind-merge": "^3.3.1", @@ -72,6 +87,6 @@ "eslint-config-next": "15.4.4", "tailwindcss": "^4", "tw-animate-css": "^1.3.6", - "typescript": "^5" + "typescript": "5.9.2" } } diff --git a/src/app/api/generate-outline/route.ts b/src/app/api/generate-outline/route.ts new file mode 100644 index 0000000..b34618f --- /dev/null +++ b/src/app/api/generate-outline/route.ts @@ -0,0 +1,237 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; +import { withRateLimit } from "@/lib/rate-limit"; +import { AIOutlineGenerator, AIRateLimit } from "@/lib/ai-service"; +import { createSupabaseServerClient } from "@/lib/supabase"; +import { isValidUUID } from "@/lib/security"; +import { z } from "zod"; + +const generateOutlineSchema = z.object({ + prompt: z.string().min(10).max(2000), + template_id: z.string().uuid().optional(), + project_id: z.string().uuid().optional(), + options: z.object({ + model: z.enum(["gpt-4", "gpt-4-turbo", "claude-3-5-sonnet"]).optional(), + temperature: z.number().min(0).max(2).optional(), + maxTokens: z.number().min(100).max(8000).optional(), + }).optional(), +}); + +async function generateOutlineHandler(req: NextRequest) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Check rate limiting for AI requests + const canMakeRequest = await AIRateLimit.checkRateLimit(userId); + if (!canMakeRequest) { + return NextResponse.json( + { + error: "Rate limit exceeded", + message: "Too many AI requests. Please wait before making another request." + }, + { status: 429 } + ); + } + + const body = await req.json(); + const validation = generateOutlineSchema.safeParse(body); + + if (!validation.success) { + return NextResponse.json( + { error: "Invalid input", details: validation.error.issues }, + { status: 400 } + ); + } + + const { prompt, template_id, project_id, options } = validation.data; + + const supabase = await createSupabaseServerClient(); + let template = null; + + // Get template if provided + if (template_id) { + const { data: templateData, error: templateError } = await supabase + .from("templates") + .select("*") + .eq("id", template_id) + .single(); + + if (templateError || !templateData) { + return NextResponse.json( + { error: "Template not found" }, + { status: 404 } + ); + } + + // Check if user can access this template + if (!templateData.is_public && templateData.created_by !== userId) { + return NextResponse.json( + { error: "Access denied to template" }, + { status: 403 } + ); + } + + template = templateData; + } + + // Verify project access if project_id is provided + if (project_id) { + const { data: projectData, error: projectError } = await supabase + .from("projects") + .select("owner_id, collaborators") + .eq("id", project_id) + .single(); + + if (projectError || !projectData) { + return NextResponse.json( + { error: "Project not found" }, + { status: 404 } + ); + } + + // Check if user has access to the project + const hasAccess = projectData.owner_id === userId || + projectData.collaborators.includes(userId); + + if (!hasAccess) { + return NextResponse.json( + { error: "Access denied to project" }, + { status: 403 } + ); + } + } + + try { + // Generate the outline using AI + const outline = await AIOutlineGenerator.generateOutline( + prompt, + template || undefined, + { + ...options, + userId, + } + ); + + // If project_id is provided, update the project with the generated outline + if (project_id) { + const { error: updateError } = await supabase + .from("projects") + .update({ + outline_data: outline, + status: "in_progress" + }) + .eq("id", project_id); + + if (updateError) { + console.error("Error updating project with outline:", updateError); + // Don't fail the request if we can't update the project + } else { + // Create a new version for the project + const { data: versionData } = await supabase + .from("project_versions") + .select("version_number") + .eq("project_id", project_id) + .order("version_number", { ascending: false }) + .limit(1); + + const nextVersion = versionData && versionData.length > 0 + ? versionData[0].version_number + 1 + : 1; + + await supabase + .from("project_versions") + .insert({ + project_id, + version_number: nextVersion, + outline_data: outline, + changes_summary: "AI-generated outline", + created_by: userId, + }); + } + } + + return NextResponse.json({ + outline, + message: "Outline generated successfully", + template_used: template?.name || null + }); + + } catch (aiError) { + console.error("AI generation error:", aiError); + + // Return a more user-friendly error message + const errorMessage = aiError instanceof Error + ? aiError.message + : "Failed to generate outline"; + + return NextResponse.json( + { + error: "AI service error", + message: errorMessage, + fallback_available: true + }, + { status: 503 } + ); + } + + } catch (error) { + console.error("Error in generate outline:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +async function getGenerationHistoryHandler(req: NextRequest) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { searchParams } = new URL(req.url); + const limit = Math.min(parseInt(searchParams.get("limit") || "10"), 50); + const offset = parseInt(searchParams.get("offset") || "0"); + + const supabase = await createSupabaseServerClient(); + + const { data: history, error } = await supabase + .from("ai_generation_logs") + .select(` + id, + prompt, + model, + success, + response_time_ms, + created_at, + project:projects(id, title) + `) + .eq("created_by", userId) + .order("created_at", { ascending: false }) + .range(offset, offset + limit - 1); + + if (error) { + console.error("Error fetching generation history:", error); + return NextResponse.json( + { error: "Failed to fetch history" }, + { status: 500 } + ); + } + + return NextResponse.json({ history }); + } catch (error) { + console.error("Error in get generation history:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +// Apply rate limiting +export const POST = withRateLimit(generateOutlineHandler); +export const GET = withRateLimit(getGenerationHistoryHandler); \ No newline at end of file diff --git a/src/app/api/projects/[id]/outline/route.ts b/src/app/api/projects/[id]/outline/route.ts new file mode 100644 index 0000000..b4ab4d7 --- /dev/null +++ b/src/app/api/projects/[id]/outline/route.ts @@ -0,0 +1,207 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; +import { createSupabaseServerClient } from "@/lib/supabase"; +import { withRateLimit } from "@/lib/rate-limit"; +import { isValidUUID } from "@/lib/security"; +import { z } from "zod"; + +const updateOutlineSchema = z.object({ + outline_data: z.record(z.any()), // Flexible outline structure + changes_summary: z.string().optional(), +}); + +interface RouteContext { + params: { + id: string; + }; +} + +async function getOutlineHandler(req: NextRequest, context: RouteContext) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = context.params; + + if (!isValidUUID(id)) { + return NextResponse.json( + { error: "Invalid project ID" }, + { status: 400 } + ); + } + + const supabase = await createSupabaseServerClient(); + + const { data: project, error } = await supabase + .from("projects") + .select(` + id, + title, + outline_data, + updated_at, + template:templates(name, category, sections) + `) + .eq("id", id) + .single(); + + if (error || !project) { + return NextResponse.json( + { error: "Project not found" }, + { status: 404 } + ); + } + + // Check if user has access to this project + const { data: accessCheck } = await supabase + .from("projects") + .select("owner_id, collaborators") + .eq("id", id) + .single(); + + const hasAccess = accessCheck?.owner_id === userId || + accessCheck?.collaborators.includes(userId); + + if (!hasAccess) { + return NextResponse.json( + { error: "Access denied" }, + { status: 403 } + ); + } + + return NextResponse.json({ + project: { + id: project.id, + title: project.title, + outline_data: project.outline_data, + updated_at: project.updated_at, + template: project.template, + } + }); + } catch (error) { + console.error("Error in get outline:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +async function updateOutlineHandler(req: NextRequest, context: RouteContext) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = context.params; + + if (!isValidUUID(id)) { + return NextResponse.json( + { error: "Invalid project ID" }, + { status: 400 } + ); + } + + const body = await req.json(); + const validation = updateOutlineSchema.safeParse(body); + + if (!validation.success) { + return NextResponse.json( + { error: "Invalid input", details: validation.error.issues }, + { status: 400 } + ); + } + + const { outline_data, changes_summary } = validation.data; + + const supabase = await createSupabaseServerClient(); + + // Check if project exists and user has access + const { data: project, error: fetchError } = await supabase + .from("projects") + .select("owner_id, collaborators") + .eq("id", id) + .single(); + + if (fetchError || !project) { + return NextResponse.json( + { error: "Project not found" }, + { status: 404 } + ); + } + + const hasAccess = project.owner_id === userId || + project.collaborators.includes(userId); + + if (!hasAccess) { + return NextResponse.json( + { error: "Access denied" }, + { status: 403 } + ); + } + + // Update the project outline + const { data: updatedProject, error: updateError } = await supabase + .from("projects") + .update({ + outline_data, + status: "in_progress" + }) + .eq("id", id) + .select(` + id, + title, + outline_data, + updated_at, + template:templates(name, category) + `) + .single(); + + if (updateError) { + console.error("Error updating project outline:", updateError); + return NextResponse.json( + { error: "Failed to update outline" }, + { status: 500 } + ); + } + + // Create a new version + const { data: versionData } = await supabase + .from("project_versions") + .select("version_number") + .eq("project_id", id) + .order("version_number", { ascending: false }) + .limit(1); + + const nextVersion = versionData && versionData.length > 0 + ? versionData[0].version_number + 1 + : 1; + + await supabase + .from("project_versions") + .insert({ + project_id: id, + version_number: nextVersion, + outline_data, + changes_summary: changes_summary || "Manual outline update", + created_by: userId, + }); + + return NextResponse.json({ + project: updatedProject, + message: "Outline updated successfully" + }); + } catch (error) { + console.error("Error in update outline:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +// Apply rate limiting +export const GET = withRateLimit(getOutlineHandler); +export const PUT = withRateLimit(updateOutlineHandler); \ No newline at end of file diff --git a/src/app/api/projects/[id]/versions/route.ts b/src/app/api/projects/[id]/versions/route.ts new file mode 100644 index 0000000..2e93e15 --- /dev/null +++ b/src/app/api/projects/[id]/versions/route.ts @@ -0,0 +1,224 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; +import { createSupabaseServerClient } from "@/lib/supabase"; +import { withRateLimit } from "@/lib/rate-limit"; +import { isValidUUID } from "@/lib/security"; + +interface RouteContext { + params: { + id: string; + }; +} + +async function getVersionsHandler(req: NextRequest, context: RouteContext) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = context.params; + + if (!isValidUUID(id)) { + return NextResponse.json( + { error: "Invalid project ID" }, + { status: 400 } + ); + } + + const { searchParams } = new URL(req.url); + const limit = Math.min(parseInt(searchParams.get("limit") || "10"), 50); + const offset = parseInt(searchParams.get("offset") || "0"); + + const supabase = await createSupabaseServerClient(); + + // Check if user has access to this project + const { data: project, error: projectError } = await supabase + .from("projects") + .select("owner_id, collaborators") + .eq("id", id) + .single(); + + if (projectError || !project) { + return NextResponse.json( + { error: "Project not found" }, + { status: 404 } + ); + } + + const hasAccess = project.owner_id === userId || + project.collaborators.includes(userId); + + if (!hasAccess) { + return NextResponse.json( + { error: "Access denied" }, + { status: 403 } + ); + } + + // Get project versions + const { data: versions, error } = await supabase + .from("project_versions") + .select(` + id, + version_number, + changes_summary, + created_at, + created_by, + outline_data + `) + .eq("project_id", id) + .order("version_number", { ascending: false }) + .range(offset, offset + limit - 1); + + if (error) { + console.error("Error fetching versions:", error); + return NextResponse.json( + { error: "Failed to fetch versions" }, + { status: 500 } + ); + } + + // Get total count + const { count } = await supabase + .from("project_versions") + .select("*", { count: "exact", head: true }) + .eq("project_id", id); + + return NextResponse.json({ + versions, + total: count || 0, + limit, + offset + }); + } catch (error) { + console.error("Error in get versions:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +async function restoreVersionHandler(req: NextRequest, context: RouteContext) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = context.params; + + if (!isValidUUID(id)) { + return NextResponse.json( + { error: "Invalid project ID" }, + { status: 400 } + ); + } + + const body = await req.json(); + const { version_number } = body; + + if (!version_number || typeof version_number !== "number") { + return NextResponse.json( + { error: "Valid version number is required" }, + { status: 400 } + ); + } + + const supabase = await createSupabaseServerClient(); + + // Check if user has access to this project + const { data: project, error: projectError } = await supabase + .from("projects") + .select("owner_id, collaborators") + .eq("id", id) + .single(); + + if (projectError || !project) { + return NextResponse.json( + { error: "Project not found" }, + { status: 404 } + ); + } + + const hasAccess = project.owner_id === userId || + project.collaborators.includes(userId); + + if (!hasAccess) { + return NextResponse.json( + { error: "Access denied" }, + { status: 403 } + ); + } + + // Get the version to restore + const { data: version, error: versionError } = await supabase + .from("project_versions") + .select("outline_data") + .eq("project_id", id) + .eq("version_number", version_number) + .single(); + + if (versionError || !version) { + return NextResponse.json( + { error: "Version not found" }, + { status: 404 } + ); + } + + // Update the project with the restored outline + const { error: updateError } = await supabase + .from("projects") + .update({ + outline_data: version.outline_data, + status: "in_progress" + }) + .eq("id", id); + + if (updateError) { + console.error("Error restoring version:", updateError); + return NextResponse.json( + { error: "Failed to restore version" }, + { status: 500 } + ); + } + + // Create a new version entry for the restoration + const { data: versionData } = await supabase + .from("project_versions") + .select("version_number") + .eq("project_id", id) + .order("version_number", { ascending: false }) + .limit(1); + + const nextVersion = versionData && versionData.length > 0 + ? versionData[0].version_number + 1 + : 1; + + await supabase + .from("project_versions") + .insert({ + project_id: id, + version_number: nextVersion, + outline_data: version.outline_data, + changes_summary: `Restored from version ${version_number}`, + created_by: userId, + }); + + return NextResponse.json({ + message: `Successfully restored to version ${version_number}`, + new_version: nextVersion + }); + } catch (error) { + console.error("Error in restore version:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +// Apply rate limiting +export const GET = withRateLimit(getVersionsHandler); +export const POST = withRateLimit(restoreVersionHandler); \ No newline at end of file diff --git a/src/app/api/projects/route.ts b/src/app/api/projects/route.ts new file mode 100644 index 0000000..60dd4ce --- /dev/null +++ b/src/app/api/projects/route.ts @@ -0,0 +1,132 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; +import { createSupabaseServerClient } from "@/lib/supabase"; +import { withRateLimit } from "@/lib/rate-limit"; +import { z } from "zod"; + +const createProjectSchema = z.object({ + title: z.string().min(1).max(200), + description: z.string().optional(), + template_id: z.string().uuid().optional(), +}); + +async function getProjectsHandler(req: NextRequest) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const supabase = await createSupabaseServerClient(); + + const { data: projects, error } = await supabase + .from("projects") + .select(` + *, + template:templates(name, category) + `) + .or(`owner_id.eq.${userId},collaborators.cs.{${userId}}`) + .order("created_at", { ascending: false }); + + if (error) { + console.error("Error fetching projects:", error); + return NextResponse.json( + { error: "Failed to fetch projects" }, + { status: 500 } + ); + } + + return NextResponse.json({ projects }); + } catch (error) { + console.error("Error in get projects:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +async function createProjectHandler(req: NextRequest) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const body = await req.json(); + const validation = createProjectSchema.safeParse(body); + + if (!validation.success) { + return NextResponse.json( + { error: "Invalid input", details: validation.error.issues }, + { status: 400 } + ); + } + + const { title, description, template_id } = validation.data; + + const supabase = await createSupabaseServerClient(); + + // If template_id is provided, verify it exists + if (template_id) { + const { data: template, error: templateError } = await supabase + .from("templates") + .select("id") + .eq("id", template_id) + .single(); + + if (templateError || !template) { + return NextResponse.json( + { error: "Invalid template ID" }, + { status: 400 } + ); + } + } + + const { data: project, error } = await supabase + .from("projects") + .insert({ + title, + description, + template_id, + owner_id: userId, + outline_data: {}, + }) + .select(` + *, + template:templates(name, category) + `) + .single(); + + if (error) { + console.error("Error creating project:", error); + return NextResponse.json( + { error: "Failed to create project" }, + { status: 500 } + ); + } + + // Create initial version + await supabase + .from("project_versions") + .insert({ + project_id: project.id, + version_number: 1, + outline_data: {}, + changes_summary: "Initial version", + created_by: userId, + }); + + return NextResponse.json({ project }, { status: 201 }); + } catch (error) { + console.error("Error in create project:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +// Apply rate limiting to both handlers +export const GET = withRateLimit(getProjectsHandler); +export const POST = withRateLimit(createProjectHandler); \ No newline at end of file diff --git a/src/app/api/templates/[id]/route.ts b/src/app/api/templates/[id]/route.ts new file mode 100644 index 0000000..d0f04e5 --- /dev/null +++ b/src/app/api/templates/[id]/route.ts @@ -0,0 +1,212 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; +import { createSupabaseServerClient } from "@/lib/supabase"; +import { withRateLimit } from "@/lib/rate-limit"; +import { isValidUUID } from "@/lib/security"; +import { z } from "zod"; + +const updateTemplateSchema = z.object({ + name: z.string().min(1).max(200).optional(), + description: z.string().optional(), + category: z.string().min(1).max(50).optional(), + sections: z.array( + z.object({ + title: z.string().min(1).max(200), + description: z.string(), + required: z.boolean(), + }) + ).optional(), + is_public: z.boolean().optional(), +}); + +interface RouteContext { + params: { + id: string; + }; +} + +async function getTemplateHandler(req: NextRequest, context: RouteContext) { + try { + const { id } = context.params; + + if (!isValidUUID(id)) { + return NextResponse.json( + { error: "Invalid template ID" }, + { status: 400 } + ); + } + + const supabase = await createSupabaseServerClient(); + + const { data: template, error } = await supabase + .from("templates") + .select("*") + .eq("id", id) + .single(); + + if (error || !template) { + return NextResponse.json( + { error: "Template not found" }, + { status: 404 } + ); + } + + // Check if user can access this template + const { userId } = await auth(); + if (!template.is_public && template.created_by !== userId) { + return NextResponse.json( + { error: "Access denied" }, + { status: 403 } + ); + } + + return NextResponse.json({ template }); + } catch (error) { + console.error("Error in get template:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +async function updateTemplateHandler(req: NextRequest, context: RouteContext) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = context.params; + + if (!isValidUUID(id)) { + return NextResponse.json( + { error: "Invalid template ID" }, + { status: 400 } + ); + } + + const body = await req.json(); + const validation = updateTemplateSchema.safeParse(body); + + if (!validation.success) { + return NextResponse.json( + { error: "Invalid input", details: validation.error.issues }, + { status: 400 } + ); + } + + const supabase = await createSupabaseServerClient(); + + // Check if template exists and user owns it + const { data: existingTemplate, error: fetchError } = await supabase + .from("templates") + .select("created_by") + .eq("id", id) + .single(); + + if (fetchError || !existingTemplate) { + return NextResponse.json( + { error: "Template not found" }, + { status: 404 } + ); + } + + if (existingTemplate.created_by !== userId) { + return NextResponse.json( + { error: "Access denied" }, + { status: 403 } + ); + } + + const { data: template, error } = await supabase + .from("templates") + .update(validation.data) + .eq("id", id) + .select("*") + .single(); + + if (error) { + console.error("Error updating template:", error); + return NextResponse.json( + { error: "Failed to update template" }, + { status: 500 } + ); + } + + return NextResponse.json({ template }); + } catch (error) { + console.error("Error in update template:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +async function deleteTemplateHandler(req: NextRequest, context: RouteContext) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = context.params; + + if (!isValidUUID(id)) { + return NextResponse.json( + { error: "Invalid template ID" }, + { status: 400 } + ); + } + + const supabase = await createSupabaseServerClient(); + + // Check if template exists and user owns it + const { data: existingTemplate, error: fetchError } = await supabase + .from("templates") + .select("created_by") + .eq("id", id) + .single(); + + if (fetchError || !existingTemplate) { + return NextResponse.json( + { error: "Template not found" }, + { status: 404 } + ); + } + + if (existingTemplate.created_by !== userId) { + return NextResponse.json( + { error: "Access denied" }, + { status: 403 } + ); + } + + const { error } = await supabase + .from("templates") + .delete() + .eq("id", id); + + if (error) { + console.error("Error deleting template:", error); + return NextResponse.json( + { error: "Failed to delete template" }, + { status: 500 } + ); + } + + return NextResponse.json({ message: "Template deleted successfully" }); + } catch (error) { + console.error("Error in delete template:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +// Apply rate limiting to all handlers +export const GET = withRateLimit(getTemplateHandler); +export const PUT = withRateLimit(updateTemplateHandler); +export const DELETE = withRateLimit(deleteTemplateHandler); \ No newline at end of file diff --git a/src/app/api/templates/route.ts b/src/app/api/templates/route.ts new file mode 100644 index 0000000..9d8673f --- /dev/null +++ b/src/app/api/templates/route.ts @@ -0,0 +1,136 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; +import { createSupabaseServerClient } from "@/lib/supabase"; +import { withRateLimit } from "@/lib/rate-limit"; +import { z } from "zod"; + +const createTemplateSchema = z.object({ + name: z.string().min(1).max(200), + description: z.string().optional(), + category: z.string().min(1).max(50), + sections: z.array( + z.object({ + title: z.string().min(1).max(200), + description: z.string(), + required: z.boolean(), + }) + ).min(1), + is_public: z.boolean().default(false), +}); + +async function getTemplatesHandler(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const category = searchParams.get("category"); + const includePrivate = searchParams.get("include_private") === "true"; + + const supabase = await createSupabaseServerClient(); + const { userId } = await auth(); + + let query = supabase + .from("templates") + .select("*") + .order("name"); + + if (includePrivate && userId) { + // Get public templates AND user's private templates + query = query.or(`is_public.eq.true,created_by.eq.${userId}`); + } else { + // Get only public templates + query = query.eq("is_public", true); + } + + if (category) { + query = query.eq("category", category); + } + + const { data: templates, error } = await query; + + if (error) { + console.error("Error fetching templates:", error); + return NextResponse.json( + { error: "Failed to fetch templates" }, + { status: 500 } + ); + } + + return NextResponse.json({ templates }); + } catch (error) { + console.error("Error in get templates:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +async function createTemplateHandler(req: NextRequest) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const body = await req.json(); + const validation = createTemplateSchema.safeParse(body); + + if (!validation.success) { + return NextResponse.json( + { error: "Invalid input", details: validation.error.issues }, + { status: 400 } + ); + } + + const { name, description, category, sections, is_public } = validation.data; + + const supabase = await createSupabaseServerClient(); + + // Check if template with same name already exists for this user + const { data: existingTemplate } = await supabase + .from("templates") + .select("id") + .eq("name", name) + .eq("created_by", userId) + .single(); + + if (existingTemplate) { + return NextResponse.json( + { error: "Template with this name already exists" }, + { status: 409 } + ); + } + + const { data: template, error } = await supabase + .from("templates") + .insert({ + name, + description, + category, + sections, + is_public, + created_by: userId, + }) + .select("*") + .single(); + + if (error) { + console.error("Error creating template:", error); + return NextResponse.json( + { error: "Failed to create template" }, + { status: 500 } + ); + } + + return NextResponse.json({ template }, { status: 201 }); + } catch (error) { + console.error("Error in create template:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +// Apply rate limiting +export const GET = withRateLimit(getTemplatesHandler); +export const POST = withRateLimit(createTemplateHandler); \ No newline at end of file diff --git a/src/lib/ai-service.ts b/src/lib/ai-service.ts new file mode 100644 index 0000000..0187783 --- /dev/null +++ b/src/lib/ai-service.ts @@ -0,0 +1,319 @@ +import { openai } from "@ai-sdk/openai"; +import { anthropic } from "@ai-sdk/anthropic"; +import { generateObject, generateText } from "ai"; +import { z } from "zod"; +import { AppCache } from "./redis"; +import { createSupabaseServerClient } from "./supabase"; + +// Define the outline structure schema +const outlineSchema = z.object({ + title: z.string(), + sections: z.array( + z.object({ + title: z.string(), + description: z.string(), + subsections: z.array( + z.object({ + title: z.string(), + content: z.string(), + }) + ).optional(), + bulletPoints: z.array(z.string()).optional(), + estimatedTime: z.string().optional(), + priority: z.enum(["high", "medium", "low"]).optional(), + }) + ), + summary: z.string(), + keyObjectives: z.array(z.string()), + estimatedDuration: z.string().optional(), + requiredResources: z.array(z.string()).optional(), +}); + +export type GeneratedOutline = z.infer; + +interface GenerationOptions { + model?: "gpt-4" | "gpt-4-turbo" | "claude-3-5-sonnet"; + temperature?: number; + maxTokens?: number; + userId?: string; +} + +interface TemplateSection { + title: string; + description: string; + required: boolean; +} + +interface Template { + id: string; + name: string; + category: string; + sections: TemplateSection[]; +} + +export class AIOutlineGenerator { + private static readonly DEFAULT_MODEL = "gpt-4"; + private static readonly CACHE_TTL = 300; // 5 minutes + private static readonly MAX_RETRIES = 3; + + static async generateOutline( + prompt: string, + template?: Template, + options: GenerationOptions = {} + ): Promise { + const { + model = this.DEFAULT_MODEL, + temperature = 0.7, + maxTokens = 4000, + userId, + } = options; + + // Check cache first + const cacheKey = `outline:${this.hashPrompt(prompt)}:${template?.id || "default"}`; + const cached = await AppCache.get(cacheKey); + if (cached) { + return cached; + } + + // Log the generation attempt + const logData = { + prompt, + model, + start_time: Date.now(), + user_id: userId, + }; + + try { + let result: GeneratedOutline; + + if (model.startsWith("claude")) { + result = await this.generateWithAnthropic(prompt, { + temperature, + maxTokens, + }, template); + } else { + result = await this.generateWithOpenAI(prompt, { + temperature, + maxTokens, + }, template); + } + + // Cache the result + await AppCache.set(cacheKey, result, this.CACHE_TTL); + + // Log successful generation + await this.logGeneration({ + ...logData, + success: true, + tokens_used: this.estimateTokens(JSON.stringify(result)), + response_time_ms: Date.now() - logData.start_time, + generated_content: result, + }); + + return result; + } catch (error) { + // Log failed generation + await this.logGeneration({ + ...logData, + success: false, + error_message: error instanceof Error ? error.message : "Unknown error", + response_time_ms: Date.now() - logData.start_time, + }); + + // Try fallback approach + return this.generateFallbackOutline(prompt, template); + } + } + + private static async generateWithOpenAI( + prompt: string, + options: { temperature: number; maxTokens: number }, + template?: Template + ): Promise { + const systemPrompt = this.buildSystemPrompt(template); + const userPrompt = this.buildUserPrompt(prompt, template); + + const { object } = await generateObject({ + model: openai("gpt-4"), + schema: outlineSchema, + system: systemPrompt, + prompt: userPrompt, + temperature: options.temperature, + maxTokens: options.maxTokens, + }); + + return object; + } + + private static async generateWithAnthropic( + prompt: string, + options: { temperature: number; maxTokens: number }, + template?: Template + ): Promise { + const systemPrompt = this.buildSystemPrompt(template); + const userPrompt = this.buildUserPrompt(prompt, template); + + const { object } = await generateObject({ + model: anthropic("claude-3-5-sonnet-20241022"), + schema: outlineSchema, + system: systemPrompt, + prompt: userPrompt, + temperature: options.temperature, + maxTokens: options.maxTokens, + }); + + return object; + } + + private static buildSystemPrompt(template?: Template): string { + let systemPrompt = `You are an expert project planning assistant. Your task is to generate comprehensive, well-structured outlines based on user descriptions. + +Key requirements: +- Create detailed, actionable sections +- Provide clear descriptions for each section +- Include relevant subsections and bullet points where appropriate +- Estimate time requirements and set priorities +- Ensure the outline is practical and achievable +- Focus on clarity and organization`; + + if (template) { + systemPrompt += `\n\nTemplate Structure: +The user has selected the "${template.name}" template (${template.category} category). +Required sections to include: +${template.sections + .filter((s) => s.required) + .map((s) => `- ${s.title}: ${s.description}`) + .join("\n")} + +Optional sections to consider: +${template.sections + .filter((s) => !s.required) + .map((s) => `- ${s.title}: ${s.description}`) + .join("\n")} + +Adapt these sections to fit the user's specific project needs.`; + } + + return systemPrompt; + } + + private static buildUserPrompt(prompt: string, template?: Template): string { + let userPrompt = `Please create a detailed outline for the following project: + +${prompt}`; + + if (template) { + userPrompt += `\n\nUse the "${template.name}" template as a guide, but adapt it to fit this specific project. Include all required sections and any relevant optional sections.`; + } + + userPrompt += `\n\nProvide a comprehensive outline that includes: +- A clear project title +- Detailed sections with descriptions +- Subsections where appropriate +- Key bullet points for each section +- Estimated timeframes +- Priority levels +- A project summary +- Key objectives +- Required resources + +Make sure the outline is practical, actionable, and well-organized.`; + + return userPrompt; + } + + private static generateFallbackOutline( + prompt: string, + template?: Template + ): GeneratedOutline { + // Generate a basic outline structure as fallback + const sections = template?.sections || [ + { title: "Overview", description: "Project description and goals", required: true }, + { title: "Planning", description: "Detailed project planning", required: true }, + { title: "Implementation", description: "Execution strategy", required: true }, + { title: "Review", description: "Project review and next steps", required: true }, + ]; + + return { + title: `Project Outline: ${prompt.slice(0, 50)}...`, + sections: sections.map((section) => ({ + title: section.title, + description: section.description, + bulletPoints: [ + "Define objectives and scope", + "Identify key requirements", + "Plan implementation approach", + ], + priority: "medium" as const, + estimatedTime: "2-4 hours", + })), + summary: `This is a fallback outline generated for: ${prompt}. Please review and customize as needed.`, + keyObjectives: [ + "Complete project planning", + "Execute implementation", + "Review and iterate", + ], + estimatedDuration: "1-2 weeks", + requiredResources: ["Team members", "Project tools", "Time allocation"], + }; + } + + private static hashPrompt(prompt: string): string { + // Simple hash function for caching + let hash = 0; + for (let i = 0; i < prompt.length; i++) { + const char = prompt.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32-bit integer + } + return Math.abs(hash).toString(36); + } + + private static estimateTokens(text: string): number { + // Rough estimation: ~4 characters per token + return Math.ceil(text.length / 4); + } + + private static async logGeneration(logData: any): Promise { + try { + if (!logData.user_id) return; // Skip logging if no user ID + + const supabase = await createSupabaseServerClient(); + await supabase.from("ai_generation_logs").insert({ + project_id: null, // Will be set later when associated with a project + prompt: logData.prompt, + model: logData.model, + tokens_used: logData.tokens_used, + response_time_ms: logData.response_time_ms, + success: logData.success, + error_message: logData.error_message, + generated_content: logData.generated_content, + created_by: logData.user_id, + }); + } catch (error) { + console.error("Failed to log AI generation:", error); + // Don't throw here - logging failure shouldn't break the main flow + } + } +} + +// Rate limiting wrapper for AI requests +export class AIRateLimit { + private static readonly MAX_REQUESTS_PER_HOUR = 100; + private static readonly MAX_REQUESTS_PER_MINUTE = 20; + + static async checkRateLimit(userId: string): Promise { + const hourKey = `ai_rate_limit:hour:${userId}:${Math.floor(Date.now() / 3600000)}`; + const minuteKey = `ai_rate_limit:minute:${userId}:${Math.floor(Date.now() / 60000)}`; + + const [hourlyCount, minuteCount] = await Promise.all([ + AppCache.increment(hourKey, 1), + AppCache.increment(minuteKey, 1), + ]); + + return ( + hourlyCount <= this.MAX_REQUESTS_PER_HOUR && + minuteCount <= this.MAX_REQUESTS_PER_MINUTE + ); + } +} \ No newline at end of file diff --git a/src/lib/env-check.ts b/src/lib/env-check.ts index 3b9a9c3..b1ae9ec 100644 --- a/src/lib/env-check.ts +++ b/src/lib/env-check.ts @@ -8,6 +8,10 @@ export function checkEnvironmentVariables() { url: process.env.NEXT_PUBLIC_SUPABASE_URL, anonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, }, + redis: { + url: process.env.REDIS_URL || process.env.UPSTASH_REDIS_REST_URL, + token: process.env.REDIS_TOKEN || process.env.UPSTASH_REDIS_REST_TOKEN, + }, ai: { openai: process.env.OPENAI_API_KEY, anthropic: process.env.ANTHROPIC_API_KEY, @@ -21,11 +25,14 @@ export function checkEnvironmentVariables() { supabase: !!( requiredEnvVars.supabase.url && requiredEnvVars.supabase.anonKey ), + redis: !!( + requiredEnvVars.redis.url && requiredEnvVars.redis.token + ), ai: !!(requiredEnvVars.ai.openai || requiredEnvVars.ai.anthropic), allConfigured: false, }; - status.allConfigured = status.clerk && status.supabase && status.ai; + status.allConfigured = status.clerk && status.supabase && status.redis && status.ai; return status; } @@ -49,12 +56,24 @@ export function getSetupInstructions() { "Go to https://supabase.com/dashboard", "Create a new project", "Copy NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY to .env.local", + "Run database migrations: supabase db push", ], envVars: ["NEXT_PUBLIC_SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_ANON_KEY"], }, + { + service: "Redis (Upstash)", + description: "Caching and rate limiting", + steps: [ + "Go to https://console.upstash.com/", + "Create a new Redis database", + "Copy UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN to .env.local", + "Alternative: Use local Redis with REDIS_URL=redis://localhost:6379", + ], + envVars: ["UPSTASH_REDIS_REST_URL", "UPSTASH_REDIS_REST_TOKEN"], + }, { service: "OpenAI", - description: "AI language model for chat functionality", + description: "AI language model for outline generation", steps: [ "Go to https://platform.openai.com/", "Create an API key", diff --git a/src/lib/rate-limit.ts b/src/lib/rate-limit.ts new file mode 100644 index 0000000..7fe6a0c --- /dev/null +++ b/src/lib/rate-limit.ts @@ -0,0 +1,78 @@ +import { NextRequest, NextResponse } from "next/server"; +import { rateLimiter, apiRateLimiter } from "./redis"; + +export async function rateLimit( + req: NextRequest, + identifier?: string, + isApiRoute = false +) { + // Use provided identifier or fall back to IP + const key = identifier || getClientIP(req); + const limiter = isApiRoute ? apiRateLimiter : rateLimiter; + + try { + await limiter.consume(key); + return null; // No rate limit hit + } catch (rejRes: any) { + // Rate limit exceeded + const remainingPoints = rejRes?.remainingPoints || 0; + const msBeforeNext = rejRes?.msBeforeNext || 0; + + const response = NextResponse.json( + { + error: "Rate limit exceeded", + retryAfter: Math.ceil(msBeforeNext / 1000), + }, + { status: 429 } + ); + + // Add rate limit headers + response.headers.set("X-RateLimit-Limit", limiter.points.toString()); + response.headers.set("X-RateLimit-Remaining", remainingPoints.toString()); + response.headers.set( + "X-RateLimit-Reset", + new Date(Date.now() + msBeforeNext).toISOString() + ); + response.headers.set("Retry-After", Math.ceil(msBeforeNext / 1000).toString()); + + return response; + } +} + +// Helper function to get client IP +function getClientIP(req: NextRequest): string { + const forwarded = req.headers.get("x-forwarded-for"); + const realIP = req.headers.get("x-real-ip"); + const remoteAddr = req.headers.get("remote-addr"); + + if (forwarded) { + return forwarded.split(",")[0].trim(); + } + + if (realIP) { + return realIP; + } + + if (remoteAddr) { + return remoteAddr; + } + + return "127.0.0.1"; +} + +// Rate limit decorator for API routes +export function withRateLimit( + handler: (req: NextRequest, ...args: any[]) => Promise, + identifier?: (req: NextRequest) => string +) { + return async (req: NextRequest, ...args: any[]): Promise => { + const key = identifier ? identifier(req) : undefined; + const rateLimitResponse = await rateLimit(req, key, true); + + if (rateLimitResponse) { + return rateLimitResponse; + } + + return handler(req, ...args); + }; +} \ No newline at end of file diff --git a/src/lib/redis.ts b/src/lib/redis.ts new file mode 100644 index 0000000..340d3a6 --- /dev/null +++ b/src/lib/redis.ts @@ -0,0 +1,81 @@ +import { Redis } from "@upstash/redis"; +import { RateLimiterRedis } from "rate-limiter-flexible"; + +// Use Upstash Redis for serverless environments or local Redis for development +const redis = new Redis({ + url: process.env.REDIS_URL || process.env.UPSTASH_REDIS_REST_URL!, + token: process.env.REDIS_TOKEN || process.env.UPSTASH_REDIS_REST_TOKEN!, +}); + +// Rate limiter configuration +export const rateLimiter = new RateLimiterRedis({ + storeClient: redis as any, // Type assertion needed for compatibility + keyPrefix: "rl:", // Rate limiter key prefix + points: 5, // Number of requests + duration: 60, // Per 60 seconds + blockDuration: 300, // Block for 5 minutes if limit exceeded +}); + +// API rate limiter (more restrictive) +export const apiRateLimiter = new RateLimiterRedis({ + storeClient: redis as any, + keyPrefix: "api_rl:", + points: 100, // Number of API requests + duration: 3600, // Per hour + blockDuration: 3600, // Block for 1 hour +}); + +// Export Redis client for other uses +export { redis }; + +// Session cache utilities +export class SessionCache { + private static keyPrefix = "session:"; + + static async set(sessionId: string, data: any, ttlSeconds = 3600): Promise { + await redis.set( + `${this.keyPrefix}${sessionId}`, + JSON.stringify(data), + { ex: ttlSeconds } + ); + } + + static async get(sessionId: string): Promise { + const data = await redis.get(`${this.keyPrefix}${sessionId}`); + return data ? JSON.parse(data as string) : null; + } + + static async delete(sessionId: string): Promise { + await redis.del(`${this.keyPrefix}${sessionId}`); + } + + static async extend(sessionId: string, ttlSeconds = 3600): Promise { + await redis.expire(`${this.keyPrefix}${sessionId}`, ttlSeconds); + } +} + +// Cache utilities for application data +export class AppCache { + private static keyPrefix = "app:"; + + static async set(key: string, data: any, ttlSeconds = 300): Promise { + await redis.set( + `${this.keyPrefix}${key}`, + JSON.stringify(data), + { ex: ttlSeconds } + ); + } + + static async get(key: string): Promise { + const data = await redis.get(`${this.keyPrefix}${key}`); + return data ? JSON.parse(data as string) : null; + } + + static async delete(key: string): Promise { + await redis.del(`${this.keyPrefix}${key}`); + } + + static async increment(key: string, amount = 1): Promise { + return await redis.incrby(`${this.keyPrefix}${key}`, amount); + } +} \ No newline at end of file diff --git a/src/lib/security.ts b/src/lib/security.ts new file mode 100644 index 0000000..dedf04f --- /dev/null +++ b/src/lib/security.ts @@ -0,0 +1,130 @@ +import crypto from "crypto"; + +// AES-256 encryption for sensitive data at rest +const algorithm = "aes-256-gcm"; +const keyLength = 32; // 256 bits + +// Generate or get encryption key from environment +function getEncryptionKey(): Buffer { + const key = process.env.ENCRYPTION_KEY; + if (!key) { + throw new Error("ENCRYPTION_KEY environment variable is required"); + } + + // If key is shorter than required, derive it using PBKDF2 + if (key.length < keyLength * 2) { // *2 because hex encoding + return crypto.pbkdf2Sync(key, "salt", 100000, keyLength, "sha256"); + } + + return Buffer.from(key, "hex"); +} + +export interface EncryptedData { + encryptedText: string; + iv: string; + authTag: string; +} + +export function encrypt(text: string): EncryptedData { + try { + const key = getEncryptionKey(); + const iv = crypto.randomBytes(16); + + const cipher = crypto.createCipher(algorithm, key); + + let encrypted = cipher.update(text, "utf8", "hex"); + encrypted += cipher.final("hex"); + + const authTag = cipher.getAuthTag(); + + return { + encryptedText: encrypted, + iv: iv.toString("hex"), + authTag: authTag.toString("hex"), + }; + } catch (error) { + console.error("Encryption error:", error); + throw new Error("Failed to encrypt data"); + } +} + +export function decrypt(encryptedData: EncryptedData): string { + try { + const key = getEncryptionKey(); + const { encryptedText, iv, authTag } = encryptedData; + + const decipher = crypto.createDecipher(algorithm, key); + decipher.setAuthTag(Buffer.from(authTag, "hex")); + + let decrypted = decipher.update(encryptedText, "hex", "utf8"); + decrypted += decipher.final("utf8"); + + return decrypted; + } catch (error) { + console.error("Decryption error:", error); + throw new Error("Failed to decrypt data"); + } +} + +// Hash function for passwords (though we use Clerk, this is for other sensitive data) +export function hashData(data: string): string { + const salt = crypto.randomBytes(16).toString("hex"); + const hash = crypto.pbkdf2Sync(data, salt, 100000, 64, "sha256").toString("hex"); + return `${salt}:${hash}`; +} + +export function verifyHash(data: string, hashedData: string): boolean { + try { + const [salt, hash] = hashedData.split(":"); + const dataHash = crypto.pbkdf2Sync(data, salt, 100000, 64, "sha256").toString("hex"); + return hash === dataHash; + } catch (error) { + return false; + } +} + +// Generate secure random tokens +export function generateSecureToken(length: number = 32): string { + return crypto.randomBytes(length).toString("hex"); +} + +// Sanitize input to prevent XSS +export function sanitizeInput(input: string): string { + return input + .replace(/[<>]/g, "") // Remove potential HTML tags + .replace(/javascript:/gi, "") // Remove javascript: protocol + .replace(/on\w+\s*=/gi, "") // Remove event handlers + .trim(); +} + +// Validate UUID format +export function isValidUUID(uuid: string): boolean { + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return uuidRegex.test(uuid); +} + +// Content Security Policy headers +export const securityHeaders = { + "Content-Security-Policy": [ + "default-src 'self'", + "script-src 'self' 'unsafe-inline' 'unsafe-eval' *.clerk.accounts.dev *.clerk.com", + "style-src 'self' 'unsafe-inline' fonts.googleapis.com", + "font-src 'self' fonts.gstatic.com", + "img-src 'self' data: *.supabase.co *.clerk.com", + "connect-src 'self' *.supabase.co *.clerk.accounts.dev *.clerk.com wss://*.supabase.co", + "frame-ancestors 'none'", + ].join("; "), + "X-Frame-Options": "DENY", + "X-Content-Type-Options": "nosniff", + "X-XSS-Protection": "1; mode=block", + "Referrer-Policy": "strict-origin-when-cross-origin", + "Permissions-Policy": "camera=(), microphone=(), geolocation=()", +}; + +// Rate limiting configurations +export const rateLimits = { + auth: { points: 5, duration: 900 }, // 5 attempts per 15 minutes + api: { points: 100, duration: 3600 }, // 100 requests per hour + upload: { points: 10, duration: 3600 }, // 10 uploads per hour + export: { points: 5, duration: 3600 }, // 5 exports per hour +}; \ No newline at end of file diff --git a/src/lib/template-utils.ts b/src/lib/template-utils.ts new file mode 100644 index 0000000..e07b743 --- /dev/null +++ b/src/lib/template-utils.ts @@ -0,0 +1,326 @@ +import { z } from "zod"; + +// Template section schema +export const templateSectionSchema = z.object({ + title: z.string().min(1).max(200), + description: z.string(), + required: z.boolean(), + placeholder: z.string().optional(), + type: z.enum(["text", "list", "table", "markdown"]).default("text"), + validation: z.object({ + minLength: z.number().optional(), + maxLength: z.number().optional(), + required: z.boolean().default(false), + }).optional(), +}); + +// Template schema +export const templateSchema = z.object({ + id: z.string().uuid().optional(), + name: z.string().min(1).max(200), + description: z.string().optional(), + category: z.string().min(1).max(50), + sections: z.array(templateSectionSchema).min(1), + is_public: z.boolean().default(false), + created_by: z.string().optional(), + created_at: z.string().optional(), + updated_at: z.string().optional(), +}); + +// Define outline section schema with explicit typing +export const outlineSectionSchema: z.ZodSchema = z.lazy(() => z.object({ + id: z.string().optional(), + title: z.string(), + description: z.string(), + content: z.string().optional(), + subsections: z.array(outlineSectionSchema).optional(), + bulletPoints: z.array(z.string()).optional(), + estimatedTime: z.string().optional(), + priority: z.enum(["high", "medium", "low"]).optional(), + status: z.enum(["not_started", "in_progress", "completed"]).default("not_started"), + assignee: z.string().optional(), + dueDate: z.string().optional(), + tags: z.array(z.string()).optional(), +})); + +// Complete outline schema +export const outlineSchema = z.object({ + title: z.string(), + description: z.string().optional(), + sections: z.array(outlineSectionSchema), + summary: z.string(), + keyObjectives: z.array(z.string()), + estimatedDuration: z.string().optional(), + requiredResources: z.array(z.string()).optional(), + metadata: z.object({ + version: z.string().default("1.0"), + lastModified: z.string().optional(), + template_id: z.string().uuid().optional(), + generated_by: z.enum(["ai", "manual", "template"]).default("manual"), + }).optional(), +}); + +export type TemplateSection = z.infer; +export type Template = z.infer; +export type OutlineSection = z.infer; +export type Outline = z.infer; + +// Template validation utilities +export class TemplateValidator { + static validateTemplate(template: any): { valid: boolean; errors?: string[] } { + try { + templateSchema.parse(template); + return { valid: true }; + } catch (error) { + if (error instanceof z.ZodError) { + return { + valid: false, + errors: error.errors.map(err => `${err.path.join('.')}: ${err.message}`) + }; + } + return { valid: false, errors: ["Unknown validation error"] }; + } + } + + static validateOutline(outline: any): { valid: boolean; errors?: string[] } { + try { + outlineSchema.parse(outline); + return { valid: true }; + } catch (error) { + if (error instanceof z.ZodError) { + return { + valid: false, + errors: error.errors.map(err => `${err.path.join('.')}: ${err.message}`) + }; + } + return { valid: false, errors: ["Unknown validation error"] }; + } + } + + static validateSectionContent( + section: OutlineSection, + templateSection?: TemplateSection + ): { valid: boolean; errors?: string[] } { + const errors: string[] = []; + + if (!section.title || section.title.trim().length === 0) { + errors.push("Section title is required"); + } + + if (!section.description || section.description.trim().length === 0) { + errors.push("Section description is required"); + } + + if (templateSection?.validation) { + const validation = templateSection.validation; + const content = section.content || ""; + + if (validation.required && content.trim().length === 0) { + errors.push(`Content is required for section: ${section.title}`); + } + + if (validation.minLength && content.length < validation.minLength) { + errors.push(`Content must be at least ${validation.minLength} characters`); + } + + if (validation.maxLength && content.length > validation.maxLength) { + errors.push(`Content must not exceed ${validation.maxLength} characters`); + } + } + + return { + valid: errors.length === 0, + errors: errors.length > 0 ? errors : undefined + }; + } +} + +// Template processing utilities +export class TemplateProcessor { + static createOutlineFromTemplate(template: Template, userInput?: Partial): Outline { + const sections: OutlineSection[] = template.sections.map(templateSection => ({ + title: templateSection.title, + description: templateSection.description, + content: templateSection.placeholder || "", + bulletPoints: [], + priority: templateSection.required ? "high" : "medium", + status: "not_started" as const, + })); + + return { + title: userInput?.title || `New ${template.name}`, + description: userInput?.description || template.description, + sections, + summary: userInput?.summary || `Outline based on ${template.name} template`, + keyObjectives: userInput?.keyObjectives || [], + estimatedDuration: userInput?.estimatedDuration, + requiredResources: userInput?.requiredResources || [], + metadata: { + version: "1.0", + lastModified: new Date().toISOString(), + template_id: template.id, + generated_by: "template", + }, + }; + } + + static mergeTemplateWithOutline(template: Template, outline: Outline): Outline { + // Create a map of existing sections by title for quick lookup + const existingSections = new Map( + outline.sections.map(section => [section.title.toLowerCase(), section]) + ); + + // Process template sections + const mergedSections: OutlineSection[] = template.sections.map(templateSection => { + const existingSection = existingSections.get(templateSection.title.toLowerCase()); + + if (existingSection) { + // Update existing section with template metadata + return { + ...existingSection, + description: existingSection.description || templateSection.description, + }; + } else { + // Create new section from template + return { + title: templateSection.title, + description: templateSection.description, + content: templateSection.placeholder || "", + priority: templateSection.required ? "high" : "medium", + status: "not_started" as const, + }; + } + }); + + // Add any existing sections that aren't in the template + outline.sections.forEach(section => { + const isInTemplate = template.sections.some( + ts => ts.title.toLowerCase() === section.title.toLowerCase() + ); + if (!isInTemplate) { + mergedSections.push(section); + } + }); + + return { + ...outline, + sections: mergedSections, + metadata: { + version: outline.metadata?.version || "1.0", + generated_by: outline.metadata?.generated_by || "template", + template_id: template.id, + lastModified: new Date().toISOString(), + }, + }; + } + + static calculateCompletionPercentage(outline: Outline): number { + if (outline.sections.length === 0) return 0; + + const completedSections = outline.sections.filter( + section => section.status === "completed" + ).length; + + return Math.round((completedSections / outline.sections.length) * 100); + } + + static generateSectionId(): string { + return `section_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + static estimateTotalDuration(outline: Outline): string { + const sections = outline.sections; + if (sections.length === 0) return "Unknown"; + + let totalMinutes = 0; + let hasEstimates = false; + + sections.forEach(section => { + if (section.estimatedTime) { + const time = this.parseTimeEstimate(section.estimatedTime); + if (time > 0) { + totalMinutes += time; + hasEstimates = true; + } + } + }); + + if (!hasEstimates) return "Unknown"; + + return this.formatDuration(totalMinutes); + } + + private static parseTimeEstimate(timeStr: string): number { + // Parse various time formats: "2 hours", "30 minutes", "2-4 hours", "1.5 hours" + const lowerStr = timeStr.toLowerCase(); + + // Extract numbers + const numbers = lowerStr.match(/\d+(?:\.\d+)?/g); + if (!numbers) return 0; + + const value = parseFloat(numbers[0]); + + if (lowerStr.includes('hour')) { + return value * 60; // Convert to minutes + } else if (lowerStr.includes('minute')) { + return value; + } else if (lowerStr.includes('day')) { + return value * 480; // 8 hours per day + } else if (lowerStr.includes('week')) { + return value * 2400; // 5 days * 8 hours + } + + return 0; // Default if format not recognized + } + + private static formatDuration(minutes: number): string { + if (minutes < 60) { + return `${minutes} minutes`; + } else if (minutes < 480) { // Less than 8 hours + const hours = Math.round(minutes / 60 * 10) / 10; // Round to 1 decimal + return hours === 1 ? "1 hour" : `${hours} hours`; + } else if (minutes < 2400) { // Less than 5 days + const days = Math.round(minutes / 480 * 10) / 10; + return days === 1 ? "1 day" : `${days} days`; + } else { + const weeks = Math.round(minutes / 2400 * 10) / 10; + return weeks === 1 ? "1 week" : `${weeks} weeks`; + } + } +} + +// Common template categories and their default sections +export const TEMPLATE_CATEGORIES = { + basic: { + name: "Basic", + description: "General purpose templates for common projects", + color: "#6B7280" + }, + technical: { + name: "Technical", + description: "Templates for software development and technical projects", + color: "#3B82F6" + }, + marketing: { + name: "Marketing", + description: "Templates for marketing campaigns and strategies", + color: "#EF4444" + }, + business: { + name: "Business", + description: "Templates for business planning and operations", + color: "#10B981" + }, + academic: { + name: "Academic", + description: "Templates for research and academic projects", + color: "#8B5CF6" + }, + personal: { + name: "Personal", + description: "Templates for personal projects and goals", + color: "#F59E0B" + } +} as const; + +export type TemplateCategoryKey = keyof typeof TEMPLATE_CATEGORIES; \ No newline at end of file diff --git a/supabase/migrations/002_project_outline_tables.sql b/supabase/migrations/002_project_outline_tables.sql new file mode 100644 index 0000000..3ad1da4 --- /dev/null +++ b/supabase/migrations/002_project_outline_tables.sql @@ -0,0 +1,351 @@ +-- Project Outline Generation System Database Schema +-- Creates tables for projects, templates, outlines, and collaboration features + +-- Templates table for storing reusable outline templates +CREATE TABLE IF NOT EXISTS public.templates ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + description TEXT, + category TEXT NOT NULL DEFAULT 'general', -- basic, technical, marketing, business, academic + sections JSONB NOT NULL DEFAULT '[]'::jsonb, -- Array of section objects with title, description, required fields + is_public BOOLEAN DEFAULT true, + created_by TEXT, -- Clerk user ID (null for system templates) + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Projects table for storing user projects +CREATE TABLE IF NOT EXISTS public.projects ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title TEXT NOT NULL, + description TEXT, + template_id UUID REFERENCES public.templates(id), + owner_id TEXT NOT NULL, -- Clerk user ID + collaborators TEXT[] DEFAULT '{}', -- Array of Clerk user IDs + outline_data JSONB NOT NULL DEFAULT '{}'::jsonb, -- Generated outline content + status TEXT DEFAULT 'draft' CHECK (status IN ('draft', 'in_progress', 'completed', 'archived')), + settings JSONB DEFAULT '{}'::jsonb, -- Project-specific settings + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Project versions for version control +CREATE TABLE IF NOT EXISTS public.project_versions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + project_id UUID NOT NULL REFERENCES public.projects(id) ON DELETE CASCADE, + version_number INTEGER NOT NULL, + outline_data JSONB NOT NULL, + changes_summary TEXT, + created_by TEXT NOT NULL, -- Clerk user ID + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Comments for collaboration +CREATE TABLE IF NOT EXISTS public.project_comments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + project_id UUID NOT NULL REFERENCES public.projects(id) ON DELETE CASCADE, + parent_comment_id UUID REFERENCES public.project_comments(id) ON DELETE CASCADE, + section_path TEXT, -- JSON path to the section being commented on + content TEXT NOT NULL, + author_id TEXT NOT NULL, -- Clerk user ID + is_resolved BOOLEAN DEFAULT false, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Export history +CREATE TABLE IF NOT EXISTS public.export_history ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + project_id UUID NOT NULL REFERENCES public.projects(id) ON DELETE CASCADE, + export_format TEXT NOT NULL CHECK (export_format IN ('pdf', 'docx', 'markdown')), + file_url TEXT, -- S3 URL or local path + file_size BIGINT, + exported_by TEXT NOT NULL, -- Clerk user ID + export_settings JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- AI generation logs for debugging and analytics +CREATE TABLE IF NOT EXISTS public.ai_generation_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + project_id UUID REFERENCES public.projects(id) ON DELETE CASCADE, + prompt TEXT NOT NULL, + model TEXT NOT NULL, + tokens_used INTEGER, + response_time_ms INTEGER, + success BOOLEAN DEFAULT true, + error_message TEXT, + generated_content JSONB, + created_by TEXT NOT NULL, -- Clerk user ID + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Enable Row Level Security +ALTER TABLE public.templates ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.project_versions ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.project_comments ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.export_history ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.ai_generation_logs ENABLE ROW LEVEL SECURITY; + +-- RLS Policies for templates +-- Anyone can read public templates +CREATE POLICY "Anyone can read public templates" ON public.templates + FOR SELECT USING (is_public = true); + +-- Users can read their own templates +CREATE POLICY "Users can read own templates" ON public.templates + FOR SELECT USING (auth.jwt() ->> 'sub' = created_by); + +-- Users can create templates +CREATE POLICY "Users can create templates" ON public.templates + FOR INSERT WITH CHECK (auth.jwt() ->> 'sub' = created_by); + +-- Users can update their own templates +CREATE POLICY "Users can update own templates" ON public.templates + FOR UPDATE USING (auth.jwt() ->> 'sub' = created_by); + +-- Users can delete their own templates +CREATE POLICY "Users can delete own templates" ON public.templates + FOR DELETE USING (auth.jwt() ->> 'sub' = created_by); + +-- RLS Policies for projects +-- Project owners and collaborators can read projects +CREATE POLICY "Owners and collaborators can read projects" ON public.projects + FOR SELECT USING ( + auth.jwt() ->> 'sub' = owner_id OR + auth.jwt() ->> 'sub' = ANY(collaborators) + ); + +-- Only project owners can create projects +CREATE POLICY "Owners can create projects" ON public.projects + FOR INSERT WITH CHECK (auth.jwt() ->> 'sub' = owner_id); + +-- Owners and collaborators can update projects +CREATE POLICY "Owners and collaborators can update projects" ON public.projects + FOR UPDATE USING ( + auth.jwt() ->> 'sub' = owner_id OR + auth.jwt() ->> 'sub' = ANY(collaborators) + ); + +-- Only owners can delete projects +CREATE POLICY "Owners can delete projects" ON public.projects + FOR DELETE USING (auth.jwt() ->> 'sub' = owner_id); + +-- RLS Policies for project versions +-- Project owners and collaborators can read versions +CREATE POLICY "Project members can read versions" ON public.project_versions + FOR SELECT USING ( + EXISTS ( + SELECT 1 FROM public.projects p + WHERE p.id = project_id + AND (p.owner_id = auth.jwt() ->> 'sub' OR auth.jwt() ->> 'sub' = ANY(p.collaborators)) + ) + ); + +-- Project members can create versions +CREATE POLICY "Project members can create versions" ON public.project_versions + FOR INSERT WITH CHECK ( + auth.jwt() ->> 'sub' = created_by AND + EXISTS ( + SELECT 1 FROM public.projects p + WHERE p.id = project_id + AND (p.owner_id = auth.jwt() ->> 'sub' OR auth.jwt() ->> 'sub' = ANY(p.collaborators)) + ) + ); + +-- RLS Policies for comments +-- Project members can read comments +CREATE POLICY "Project members can read comments" ON public.project_comments + FOR SELECT USING ( + EXISTS ( + SELECT 1 FROM public.projects p + WHERE p.id = project_id + AND (p.owner_id = auth.jwt() ->> 'sub' OR auth.jwt() ->> 'sub' = ANY(p.collaborators)) + ) + ); + +-- Project members can create comments +CREATE POLICY "Project members can create comments" ON public.project_comments + FOR INSERT WITH CHECK ( + auth.jwt() ->> 'sub' = author_id AND + EXISTS ( + SELECT 1 FROM public.projects p + WHERE p.id = project_id + AND (p.owner_id = auth.jwt() ->> 'sub' OR auth.jwt() ->> 'sub' = ANY(p.collaborators)) + ) + ); + +-- Users can update their own comments +CREATE POLICY "Users can update own comments" ON public.project_comments + FOR UPDATE USING (auth.jwt() ->> 'sub' = author_id); + +-- Users can delete their own comments +CREATE POLICY "Users can delete own comments" ON public.project_comments + FOR DELETE USING (auth.jwt() ->> 'sub' = author_id); + +-- RLS Policies for export history +-- Project members can read export history +CREATE POLICY "Project members can read exports" ON public.export_history + FOR SELECT USING ( + EXISTS ( + SELECT 1 FROM public.projects p + WHERE p.id = project_id + AND (p.owner_id = auth.jwt() ->> 'sub' OR auth.jwt() ->> 'sub' = ANY(p.collaborators)) + ) + ); + +-- Users can create exports for projects they have access to +CREATE POLICY "Project members can create exports" ON public.export_history + FOR INSERT WITH CHECK ( + auth.jwt() ->> 'sub' = exported_by AND + EXISTS ( + SELECT 1 FROM public.projects p + WHERE p.id = project_id + AND (p.owner_id = auth.jwt() ->> 'sub' OR auth.jwt() ->> 'sub' = ANY(p.collaborators)) + ) + ); + +-- RLS Policies for AI generation logs +-- Project members can read AI logs +CREATE POLICY "Project members can read ai logs" ON public.ai_generation_logs + FOR SELECT USING ( + EXISTS ( + SELECT 1 FROM public.projects p + WHERE p.id = project_id + AND (p.owner_id = auth.jwt() ->> 'sub' OR auth.jwt() ->> 'sub' = ANY(p.collaborators)) + ) + ); + +-- Users can create AI logs for projects they have access to +CREATE POLICY "Project members can create ai logs" ON public.ai_generation_logs + FOR INSERT WITH CHECK ( + auth.jwt() ->> 'sub' = created_by AND + EXISTS ( + SELECT 1 FROM public.projects p + WHERE p.id = project_id + AND (p.owner_id = auth.jwt() ->> 'sub' OR auth.jwt() ->> 'sub' = ANY(p.collaborators)) + ) + ); + +-- Create indexes for better performance +CREATE INDEX IF NOT EXISTS idx_templates_category ON public.templates(category); +CREATE INDEX IF NOT EXISTS idx_templates_public ON public.templates(is_public); +CREATE INDEX IF NOT EXISTS idx_templates_created_by ON public.templates(created_by); + +CREATE INDEX IF NOT EXISTS idx_projects_owner_id ON public.projects(owner_id); +CREATE INDEX IF NOT EXISTS idx_projects_status ON public.projects(status); +CREATE INDEX IF NOT EXISTS idx_projects_collaborators ON public.projects USING GIN(collaborators); +CREATE INDEX IF NOT EXISTS idx_projects_created_at ON public.projects(created_at DESC); + +CREATE INDEX IF NOT EXISTS idx_project_versions_project_id ON public.project_versions(project_id); +CREATE INDEX IF NOT EXISTS idx_project_versions_created_at ON public.project_versions(created_at DESC); + +CREATE INDEX IF NOT EXISTS idx_project_comments_project_id ON public.project_comments(project_id); +CREATE INDEX IF NOT EXISTS idx_project_comments_parent ON public.project_comments(parent_comment_id); +CREATE INDEX IF NOT EXISTS idx_project_comments_author ON public.project_comments(author_id); + +CREATE INDEX IF NOT EXISTS idx_export_history_project_id ON public.export_history(project_id); +CREATE INDEX IF NOT EXISTS idx_export_history_created_at ON public.export_history(created_at DESC); + +CREATE INDEX IF NOT EXISTS idx_ai_generation_logs_project_id ON public.ai_generation_logs(project_id); +CREATE INDEX IF NOT EXISTS idx_ai_generation_logs_created_at ON public.ai_generation_logs(created_at DESC); + +-- Create updated_at triggers +CREATE TRIGGER update_templates_updated_at + BEFORE UPDATE ON public.templates + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_projects_updated_at + BEFORE UPDATE ON public.projects + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_project_comments_updated_at + BEFORE UPDATE ON public.project_comments + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- Insert default templates +INSERT INTO public.templates (name, description, category, sections, is_public, created_by) VALUES +( + 'Basic Project Outline', + 'A simple template for general project planning and documentation', + 'basic', + '[ + {"title": "Executive Summary", "description": "High-level overview of the project", "required": true}, + {"title": "Project Goals", "description": "Clear objectives and success criteria", "required": true}, + {"title": "Timeline", "description": "Key milestones and deadlines", "required": true}, + {"title": "Resources", "description": "Required resources and team members", "required": false}, + {"title": "Risk Assessment", "description": "Potential challenges and mitigation strategies", "required": false}, + {"title": "Next Steps", "description": "Immediate action items", "required": true} + ]'::jsonb, + true, + NULL +), +( + 'Technical Documentation', + 'Template for technical projects and software development', + 'technical', + '[ + {"title": "Overview", "description": "Project description and technical summary", "required": true}, + {"title": "Architecture", "description": "System architecture and design patterns", "required": true}, + {"title": "Requirements", "description": "Functional and non-functional requirements", "required": true}, + {"title": "API Documentation", "description": "Endpoint specifications and examples", "required": false}, + {"title": "Database Schema", "description": "Data models and relationships", "required": false}, + {"title": "Testing Strategy", "description": "Unit, integration, and performance testing", "required": true}, + {"title": "Deployment", "description": "Environment setup and deployment process", "required": true} + ]'::jsonb, + true, + NULL +), +( + 'Marketing Campaign', + 'Template for marketing campaign planning and execution', + 'marketing', + '[ + {"title": "Campaign Overview", "description": "Campaign goals and target audience", "required": true}, + {"title": "Market Research", "description": "Audience analysis and competitive landscape", "required": true}, + {"title": "Strategy", "description": "Marketing channels and tactics", "required": true}, + {"title": "Content Plan", "description": "Content calendar and creative assets", "required": true}, + {"title": "Budget", "description": "Budget allocation and cost estimates", "required": true}, + {"title": "Metrics", "description": "KPIs and success measurements", "required": true}, + {"title": "Timeline", "description": "Campaign schedule and key dates", "required": true} + ]'::jsonb, + true, + NULL +), +( + 'Business Plan', + 'Comprehensive business plan template', + 'business', + '[ + {"title": "Executive Summary", "description": "Business overview and key points", "required": true}, + {"title": "Company Description", "description": "Mission, vision, and company background", "required": true}, + {"title": "Market Analysis", "description": "Industry and target market research", "required": true}, + {"title": "Organization & Management", "description": "Team structure and key personnel", "required": true}, + {"title": "Products/Services", "description": "Offerings and value proposition", "required": true}, + {"title": "Marketing & Sales", "description": "Customer acquisition and retention strategy", "required": true}, + {"title": "Financial Projections", "description": "Revenue forecasts and funding requirements", "required": true} + ]'::jsonb, + true, + NULL +), +( + 'Research Paper', + 'Academic research and analysis template', + 'academic', + '[ + {"title": "Abstract", "description": "Research summary and key findings", "required": true}, + {"title": "Introduction", "description": "Problem statement and research questions", "required": true}, + {"title": "Literature Review", "description": "Existing research and theoretical framework", "required": true}, + {"title": "Methodology", "description": "Research methods and data collection", "required": true}, + {"title": "Results", "description": "Findings and data analysis", "required": true}, + {"title": "Discussion", "description": "Interpretation and implications", "required": true}, + {"title": "Conclusion", "description": "Summary and future research directions", "required": true}, + {"title": "References", "description": "Bibliography and citations", "required": true} + ]'::jsonb, + true, + NULL +); \ No newline at end of file