Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit: - Blog-System mit 100+ Artikeln - Supabase-Integration - Responsive Design mit Tailwind CSS - TypeScript-Konfiguration - Testing-Setup mit Vitest und Playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
# How to Apply the Rate Limits Migration
|
||||
|
||||
## Quick Start (Manual Application)
|
||||
|
||||
Since the Supabase CLI is not configured for this project, please follow these steps to apply the migration manually through the Supabase dashboard:
|
||||
|
||||
### Step 1: Access the SQL Editor
|
||||
|
||||
Open the Supabase SQL Editor in your browser:
|
||||
```
|
||||
https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/sql
|
||||
```
|
||||
|
||||
### Step 2: Copy the Migration SQL
|
||||
|
||||
Open the migration file:
|
||||
```
|
||||
supabase/migrations/20260125_create_rate_limits_table.sql
|
||||
```
|
||||
|
||||
Copy the entire contents (approximately 42 lines of SQL).
|
||||
|
||||
### Step 3: Execute the Migration
|
||||
|
||||
1. In the SQL Editor, paste the copied SQL
|
||||
2. Click the **"RUN"** button (or press `Ctrl+Enter` / `Cmd+Enter`)
|
||||
3. Wait for the success message: "Success. No rows returned"
|
||||
|
||||
### Step 4: Verify the Table Was Created
|
||||
|
||||
**Option A: Using Table Editor**
|
||||
1. Navigate to: https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/editor
|
||||
2. Look for **"rate_limits"** in the table list (left sidebar)
|
||||
3. Click on it to see the table structure
|
||||
|
||||
**Option B: Using SQL Query**
|
||||
Run this query in the SQL Editor:
|
||||
```sql
|
||||
SELECT
|
||||
table_name,
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'rate_limits'
|
||||
ORDER BY ordinal_position;
|
||||
```
|
||||
|
||||
You should see 5 rows (one for each column):
|
||||
- identifier (text)
|
||||
- count (integer)
|
||||
- window_start (timestamp with time zone)
|
||||
- created_at (timestamp with time zone)
|
||||
- updated_at (timestamp with time zone)
|
||||
|
||||
**Option C: Check Indexes**
|
||||
Run this query to verify indexes were created:
|
||||
```sql
|
||||
SELECT
|
||||
indexname,
|
||||
indexdef
|
||||
FROM pg_indexes
|
||||
WHERE tablename = 'rate_limits';
|
||||
```
|
||||
|
||||
You should see:
|
||||
- Primary key on (identifier, window_start)
|
||||
- idx_rate_limits_identifier_window
|
||||
- idx_rate_limits_window_start
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "relation 'rate_limits' already exists"
|
||||
|
||||
The table is already created! No action needed. Verify it exists using the verification steps above.
|
||||
|
||||
### Error: "permission denied"
|
||||
|
||||
Make sure you're:
|
||||
1. Logged into the correct Supabase account
|
||||
2. Have admin/owner permissions on the project
|
||||
3. Using the correct project URL
|
||||
|
||||
### Table not visible in Table Editor
|
||||
|
||||
1. Refresh the page (F5)
|
||||
2. Check the SQL output for error messages
|
||||
3. Try the SQL verification query in Option B above
|
||||
|
||||
### Need to Revert/Drop the Table?
|
||||
|
||||
If you need to start over, run this SQL:
|
||||
```sql
|
||||
DROP TABLE IF EXISTS rate_limits CASCADE;
|
||||
```
|
||||
|
||||
Then re-apply the migration from Step 1.
|
||||
|
||||
## Alternative: Automated Scripts
|
||||
|
||||
We've provided scripts for automated migration, but they require additional setup:
|
||||
|
||||
### Using apply-migration.js (requires service role key)
|
||||
|
||||
1. Add `SUPABASE_SERVICE_ROLE_KEY` to `.env.local`
|
||||
2. Run: `node scripts/apply-migration.js`
|
||||
|
||||
### Using Supabase CLI (if installed)
|
||||
|
||||
```bash
|
||||
supabase migration up
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
After the migration is applied successfully:
|
||||
1. ✅ Verify the table exists (see Step 4 above)
|
||||
2. ✅ Mark this subtask as complete
|
||||
3. ✅ Proceed to Phase 2: Building the rate limiting utility
|
||||
@@ -0,0 +1,85 @@
|
||||
# Supabase Migration Instructions
|
||||
|
||||
## Apply Migration: Create rate_limits Table
|
||||
|
||||
### Quick Steps
|
||||
|
||||
1. **Open Supabase SQL Editor**
|
||||
- Go to: https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/sql
|
||||
|
||||
2. **Copy Migration SQL**
|
||||
- Open: `supabase/migrations/20260125_create_rate_limits_table.sql`
|
||||
- Copy all contents (Ctrl+A, Ctrl+C)
|
||||
|
||||
3. **Execute Migration**
|
||||
- Paste the SQL into the Supabase SQL editor
|
||||
- Click the "Run" button (or press Ctrl+Enter)
|
||||
- Wait for "Success. No rows returned" message
|
||||
|
||||
4. **Verify Table Creation**
|
||||
- Go to Table Editor: https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/editor
|
||||
- Look for "rate_limits" table in the list
|
||||
- Click on it to verify the schema
|
||||
|
||||
### Expected Table Schema
|
||||
|
||||
The `rate_limits` table should have:
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| identifier | TEXT | Client identifier (IP address) |
|
||||
| count | INTEGER | Number of requests in window |
|
||||
| window_start | TIMESTAMPTZ | Start of rate limit window |
|
||||
| created_at | TIMESTAMPTZ | When record was created |
|
||||
| updated_at | TIMESTAMPTZ | When record was last updated |
|
||||
|
||||
**Primary Key:** (identifier, window_start)
|
||||
|
||||
**Indexes:**
|
||||
- `idx_rate_limits_identifier_window` on (identifier, window_start DESC)
|
||||
- `idx_rate_limits_window_start` on (window_start)
|
||||
|
||||
### Alternative: Using Supabase CLI (if available)
|
||||
|
||||
If you have Supabase CLI installed:
|
||||
|
||||
```bash
|
||||
supabase db push
|
||||
```
|
||||
|
||||
Or apply the specific migration:
|
||||
|
||||
```bash
|
||||
supabase migration up
|
||||
```
|
||||
|
||||
### Verification Query
|
||||
|
||||
Run this query in the SQL editor to verify the table exists:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
table_name,
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'rate_limits'
|
||||
ORDER BY ordinal_position;
|
||||
```
|
||||
|
||||
You should see all 5 columns listed.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Error: "relation already exists"**
|
||||
- The table already exists. You can verify by checking Table Editor.
|
||||
|
||||
**Error: "permission denied"**
|
||||
- Make sure you're logged into the correct Supabase project.
|
||||
- Verify you have admin/owner access to the project.
|
||||
|
||||
**Table not appearing in Table Editor**
|
||||
- Refresh the page (F5)
|
||||
- Check the SQL editor for any error messages
|
||||
- Verify the SQL was executed successfully
|
||||
@@ -0,0 +1,155 @@
|
||||
-- Chat Tables Migration
|
||||
-- Creates tables for chatbot functionality with visitor sessions and message history
|
||||
-- Version: 001
|
||||
-- Date: 2026-01-25
|
||||
|
||||
-- ============================================================================
|
||||
-- Table: chat_sessions
|
||||
-- Stores chatbot session information for visitors
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS chat_sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
||||
visitor_id TEXT NOT NULL,
|
||||
metadata JSONB DEFAULT '{}'::jsonb,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
-- Index for quick lookup by visitor_id (used for session continuity)
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_sessions_visitor_id ON chat_sessions(visitor_id);
|
||||
|
||||
-- Index for ordering sessions by creation time
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_sessions_created_at ON chat_sessions(created_at DESC);
|
||||
|
||||
-- Comment on table
|
||||
COMMENT ON TABLE chat_sessions IS 'Stores chatbot sessions for website visitors';
|
||||
COMMENT ON COLUMN chat_sessions.visitor_id IS 'Unique identifier for the visitor (generated client-side or from fingerprint)';
|
||||
COMMENT ON COLUMN chat_sessions.metadata IS 'Additional session data (locale, user_agent, referrer, etc.)';
|
||||
|
||||
-- ============================================================================
|
||||
-- Table: chat_messages
|
||||
-- Stores individual chat messages within a session
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS chat_messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
session_id UUID NOT NULL REFERENCES chat_sessions(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
-- Index for fetching messages by session (primary query pattern)
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_messages_session_id ON chat_messages(session_id);
|
||||
|
||||
-- Composite index for fetching messages in order within a session
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_messages_session_created ON chat_messages(session_id, created_at ASC);
|
||||
|
||||
-- Comment on table
|
||||
COMMENT ON TABLE chat_messages IS 'Stores individual messages in chatbot conversations';
|
||||
COMMENT ON COLUMN chat_messages.role IS 'Message sender role: user, assistant (AI), or system (instructions)';
|
||||
COMMENT ON COLUMN chat_messages.content IS 'The message text content';
|
||||
|
||||
-- ============================================================================
|
||||
-- Updated At Trigger for chat_sessions
|
||||
-- Automatically updates the updated_at column when a session is modified
|
||||
-- ============================================================================
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER update_chat_sessions_updated_at
|
||||
BEFORE UPDATE ON chat_sessions
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- ============================================================================
|
||||
-- Row Level Security (RLS)
|
||||
-- Enabled for production security - server uses service role key to bypass
|
||||
-- ============================================================================
|
||||
|
||||
-- Enable RLS on chat_sessions
|
||||
ALTER TABLE chat_sessions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Enable RLS on chat_messages
|
||||
ALTER TABLE chat_messages ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ============================================================================
|
||||
-- RLS Policies for chat_sessions
|
||||
-- ============================================================================
|
||||
|
||||
-- Policy: Allow service role full access (for server-side API routes)
|
||||
-- Note: Service role key automatically bypasses RLS, but this is explicit
|
||||
CREATE POLICY "Service role has full access to chat_sessions"
|
||||
ON chat_sessions
|
||||
FOR ALL
|
||||
TO service_role
|
||||
USING (true)
|
||||
WITH CHECK (true);
|
||||
|
||||
-- Policy: Allow visitors to read their own sessions (for potential future client-side access)
|
||||
CREATE POLICY "Visitors can read their own sessions"
|
||||
ON chat_sessions
|
||||
FOR SELECT
|
||||
TO anon, authenticated
|
||||
USING (visitor_id = current_setting('request.headers')::json->>'x-visitor-id');
|
||||
|
||||
-- Policy: Allow visitors to create sessions (for potential future client-side access)
|
||||
CREATE POLICY "Visitors can create sessions"
|
||||
ON chat_sessions
|
||||
FOR INSERT
|
||||
TO anon, authenticated
|
||||
WITH CHECK (visitor_id = current_setting('request.headers')::json->>'x-visitor-id');
|
||||
|
||||
-- ============================================================================
|
||||
-- RLS Policies for chat_messages
|
||||
-- ============================================================================
|
||||
|
||||
-- Policy: Allow service role full access (for server-side API routes)
|
||||
CREATE POLICY "Service role has full access to chat_messages"
|
||||
ON chat_messages
|
||||
FOR ALL
|
||||
TO service_role
|
||||
USING (true)
|
||||
WITH CHECK (true);
|
||||
|
||||
-- Policy: Allow visitors to read messages from their sessions
|
||||
CREATE POLICY "Visitors can read messages from their sessions"
|
||||
ON chat_messages
|
||||
FOR SELECT
|
||||
TO anon, authenticated
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM chat_sessions
|
||||
WHERE chat_sessions.id = chat_messages.session_id
|
||||
AND chat_sessions.visitor_id = current_setting('request.headers')::json->>'x-visitor-id'
|
||||
)
|
||||
);
|
||||
|
||||
-- Policy: Allow visitors to insert messages into their sessions
|
||||
CREATE POLICY "Visitors can insert messages into their sessions"
|
||||
ON chat_messages
|
||||
FOR INSERT
|
||||
TO anon, authenticated
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM chat_sessions
|
||||
WHERE chat_sessions.id = chat_messages.session_id
|
||||
AND chat_sessions.visitor_id = current_setting('request.headers')::json->>'x-visitor-id'
|
||||
)
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- Grants
|
||||
-- ============================================================================
|
||||
|
||||
-- Grant usage on tables to authenticated and anon roles
|
||||
GRANT SELECT, INSERT ON chat_sessions TO anon, authenticated;
|
||||
GRANT SELECT, INSERT ON chat_messages TO anon, authenticated;
|
||||
|
||||
-- Grant all to service role (for server-side operations)
|
||||
GRANT ALL ON chat_sessions TO service_role;
|
||||
GRANT ALL ON chat_messages TO service_role;
|
||||
@@ -0,0 +1,41 @@
|
||||
-- Create rate_limits table for persistent rate limiting
|
||||
-- This replaces the in-memory Map-based rate limiter
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rate_limits (
|
||||
-- Unique identifier (typically IP address)
|
||||
identifier TEXT NOT NULL,
|
||||
|
||||
-- Number of requests in the current time window
|
||||
count INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
-- Start of the rate limit window (for sliding window algorithm)
|
||||
window_start TIMESTAMPTZ NOT NULL,
|
||||
|
||||
-- Timestamp when the record was created
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Timestamp when the record was last updated
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Primary key ensures one record per identifier per time window
|
||||
PRIMARY KEY (identifier, window_start)
|
||||
);
|
||||
|
||||
-- Index for fast lookups by identifier and window_start
|
||||
-- This is critical for rate limiting performance
|
||||
CREATE INDEX IF NOT EXISTS idx_rate_limits_identifier_window
|
||||
ON rate_limits(identifier, window_start DESC);
|
||||
|
||||
-- Index for cleanup queries (to remove old rate limit records)
|
||||
CREATE INDEX IF NOT EXISTS idx_rate_limits_window_start
|
||||
ON rate_limits(window_start);
|
||||
|
||||
-- Add a comment to the table
|
||||
COMMENT ON TABLE rate_limits IS 'Stores rate limiting data for API endpoints. Each record tracks request counts within a time window for a specific identifier (typically IP address).';
|
||||
|
||||
-- Add comments to columns for documentation
|
||||
COMMENT ON COLUMN rate_limits.identifier IS 'Unique identifier for the client (typically IP address)';
|
||||
COMMENT ON COLUMN rate_limits.count IS 'Number of requests made in the current time window';
|
||||
COMMENT ON COLUMN rate_limits.window_start IS 'Start timestamp of the rate limit window';
|
||||
COMMENT ON COLUMN rate_limits.created_at IS 'Timestamp when this record was first created';
|
||||
COMMENT ON COLUMN rate_limits.updated_at IS 'Timestamp when this record was last updated';
|
||||
Reference in New Issue
Block a user