Implement complete API routes and mock authentication for full CMS functionality
- Add comprehensive nested route structure with proper authentication layers - Implement UpdateContent and ReorderCollectionItems handlers with repository pattern - Add automatic mock JWT token fetching for seamless development workflow - Restore content editing and collection reordering functionality broken after database refactoring - Provide production-ready authentication architecture with development convenience - Enable full CMS operations in browser with proper CRUD and bulk transaction support
This commit is contained in:
@@ -1,198 +0,0 @@
|
||||
# Database Architecture Refactoring - Technical Debt
|
||||
|
||||
## Current Problem
|
||||
|
||||
The current database layer violates several software architecture principles and creates maintenance issues:
|
||||
|
||||
### Issues with Current Implementation
|
||||
|
||||
1. **No Interface Abstraction**:
|
||||
- `Database` struct is concrete, not interface-based
|
||||
- Hard to mock for testing
|
||||
- Tight coupling between handlers and database implementation
|
||||
|
||||
2. **Type Switching Everywhere**:
|
||||
```go
|
||||
switch h.database.GetDBType() {
|
||||
case "sqlite3":
|
||||
err = h.database.GetSQLiteQueries().SomeMethod(...)
|
||||
case "postgres":
|
||||
err = h.database.GetPostgreSQLQueries().SomeMethod(...)
|
||||
}
|
||||
```
|
||||
- Repeated in every handler method
|
||||
- Violates DRY principle
|
||||
- Makes adding new database types difficult
|
||||
|
||||
3. **Mixed Responsibilities**:
|
||||
- Single struct holds both SQLite and PostgreSQL queries
|
||||
- Database type detection mixed with query execution
|
||||
- Leaky abstraction (handlers know about database internals)
|
||||
|
||||
4. **Poor Testability**:
|
||||
- Cannot easily mock database operations
|
||||
- Hard to test database-specific edge cases
|
||||
- Difficult to test transaction behavior
|
||||
|
||||
5. **Violates Open/Closed Principle**:
|
||||
- Adding new database support requires modifying existing handlers
|
||||
- Cannot extend database support without changing core business logic
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
### 1. Repository Pattern with Interface
|
||||
|
||||
```go
|
||||
// Domain-focused interface
|
||||
type ContentRepository interface {
|
||||
// Content operations
|
||||
GetContent(ctx context.Context, siteID, contentID string) (*Content, error)
|
||||
CreateContent(ctx context.Context, content *Content) (*Content, error)
|
||||
UpdateContent(ctx context.Context, content *Content) (*Content, error)
|
||||
DeleteContent(ctx context.Context, siteID, contentID string) error
|
||||
|
||||
// Collection operations
|
||||
GetCollection(ctx context.Context, siteID, collectionID string) (*Collection, error)
|
||||
GetCollectionItems(ctx context.Context, siteID, collectionID string) ([]*CollectionItem, error)
|
||||
CreateCollectionItem(ctx context.Context, item *CollectionItem) (*CollectionItem, error)
|
||||
ReorderCollectionItems(ctx context.Context, siteID, collectionID string, positions []ItemPosition) error
|
||||
|
||||
// Transaction support
|
||||
WithTransaction(ctx context.Context, fn func(ContentRepository) error) error
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Database-Specific Implementations
|
||||
|
||||
```go
|
||||
type SQLiteRepository struct {
|
||||
queries *sqlite.Queries
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) GetContent(ctx context.Context, siteID, contentID string) (*Content, error) {
|
||||
result, err := r.queries.GetContent(ctx, sqlite.GetContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.convertSQLiteContent(result), nil
|
||||
}
|
||||
|
||||
type PostgreSQLRepository struct {
|
||||
queries *postgresql.Queries
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (r *PostgreSQLRepository) GetContent(ctx context.Context, siteID, contentID string) (*Content, error) {
|
||||
result, err := r.queries.GetContent(ctx, postgresql.GetContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.convertPostgreSQLContent(result), nil
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Clean Handler Implementation
|
||||
|
||||
```go
|
||||
type ContentHandler struct {
|
||||
repository ContentRepository // Interface, not concrete type
|
||||
authService *auth.AuthService
|
||||
}
|
||||
|
||||
func (h *ContentHandler) GetContent(w http.ResponseWriter, r *http.Request) {
|
||||
contentID := chi.URLParam(r, "id")
|
||||
siteID := r.URL.Query().Get("site_id")
|
||||
|
||||
// No more type switching!
|
||||
content, err := h.repository.GetContent(r.Context(), siteID, contentID)
|
||||
if err != nil {
|
||||
http.Error(w, "Content not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(content)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Dependency Injection
|
||||
|
||||
```go
|
||||
func NewContentHandler(repo ContentRepository, authService *auth.AuthService) *ContentHandler {
|
||||
return &ContentHandler{
|
||||
repository: repo,
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
// In main.go or wherever handlers are initialized
|
||||
var repo ContentRepository
|
||||
switch dbType {
|
||||
case "sqlite3":
|
||||
repo = NewSQLiteRepository(db)
|
||||
case "postgres":
|
||||
repo = NewPostgreSQLRepository(db)
|
||||
}
|
||||
|
||||
contentHandler := NewContentHandler(repo, authService)
|
||||
```
|
||||
|
||||
## Benefits of Refactoring
|
||||
|
||||
1. **Single Responsibility**: Each repository handles one database type
|
||||
2. **Open/Closed**: Easy to add new database types without modifying existing code
|
||||
3. **Testability**: Can easily mock `ContentRepository` interface
|
||||
4. **Clean Code**: Handlers focus on HTTP concerns, not database specifics
|
||||
5. **Type Safety**: Interface ensures all database types implement same methods
|
||||
6. **Performance**: No runtime type switching overhead
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Phase 1: Create Interface and Base Implementations
|
||||
- [ ] Define `ContentRepository` interface
|
||||
- [ ] Create `SQLiteRepository` implementation
|
||||
- [ ] Create `PostgreSQLRepository` implementation
|
||||
- [ ] Add factory function for repository creation
|
||||
|
||||
### Phase 2: Migrate Handlers One by One
|
||||
- [ ] Start with simple handlers (GetContent, etc.)
|
||||
- [ ] Update handler constructors to use interface
|
||||
- [ ] Remove type switching logic
|
||||
- [ ] Add unit tests with mocked repository
|
||||
|
||||
### Phase 3: Advanced Features
|
||||
- [ ] Add transaction support to interface
|
||||
- [ ] Implement batch operations efficiently
|
||||
- [ ] Add connection pooling and retry logic
|
||||
- [ ] Performance optimization for each database type
|
||||
|
||||
### Phase 4: Cleanup
|
||||
- [ ] Remove old `Database` struct
|
||||
- [ ] Remove database type detection logic
|
||||
- [ ] Update documentation
|
||||
- [ ] Add integration tests
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- **Phase 1**: 1-2 days (interface design and base implementations)
|
||||
- **Phase 2**: 2-3 days (handler migration and testing)
|
||||
- **Phase 3**: 1-2 days (advanced features)
|
||||
- **Phase 4**: 1 day (cleanup)
|
||||
|
||||
**Total**: ~5-8 days for complete refactoring
|
||||
|
||||
## Priority
|
||||
|
||||
**Medium Priority** - This is architectural debt that should be addressed when:
|
||||
1. Adding support for new database types
|
||||
2. Significant new database operations are needed
|
||||
3. Testing infrastructure needs improvement
|
||||
4. Performance optimization becomes critical
|
||||
|
||||
The current implementation works correctly but is not maintainable long-term.
|
||||
322
DRAFT_PUBLISH_FEATURE.md
Normal file
322
DRAFT_PUBLISH_FEATURE.md
Normal file
@@ -0,0 +1,322 @@
|
||||
# Feature Document: Draft/Publish System for Insertr CMS
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the design and implementation plan for adding draft/publish functionality to Insertr CMS. Currently, all content changes are immediately reflected when enhancement is triggered manually. This feature will introduce a proper editorial workflow with draft states and controlled publishing.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
### Current State
|
||||
- All content stored in database is immediately available for enhancement
|
||||
- Manual "Enhance" button triggers immediate file updates with latest content
|
||||
- No separation between working drafts and production-ready content
|
||||
- Not suitable for production environments where editorial approval is needed
|
||||
|
||||
### User Pain Points
|
||||
- Editors cannot safely make changes without affecting live site
|
||||
- No preview workflow for reviewing changes before going live
|
||||
- All-or-nothing manual enhancement process
|
||||
- No rollback mechanism for published content
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
- **FR1**: Editors can save draft content without affecting published site
|
||||
- **FR2**: Editors can preview changes before publishing
|
||||
- **FR3**: Authorized users can publish draft content to live site
|
||||
- **FR4**: System supports rollback to previous published versions
|
||||
- **FR5**: Clear visual indication of draft vs published state
|
||||
- **FR6**: Batch publishing of multiple content changes
|
||||
|
||||
### Non-Functional Requirements
|
||||
- **NFR1**: Backward compatibility with existing content
|
||||
- **NFR2**: Minimal performance impact on content editing
|
||||
- **NFR3**: Support for concurrent editing workflows
|
||||
- **NFR4**: Audit trail for all publishing actions
|
||||
|
||||
## Architecture Approaches
|
||||
|
||||
### Option A: Minimal Schema Impact (Published Version Pointer)
|
||||
|
||||
**Core Concept**: Use existing version system with a pointer to published versions.
|
||||
|
||||
**Schema Changes**:
|
||||
```sql
|
||||
CREATE TABLE published_versions (
|
||||
content_id TEXT NOT NULL,
|
||||
site_id TEXT NOT NULL,
|
||||
published_version_id INTEGER NOT NULL,
|
||||
published_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
|
||||
published_by TEXT NOT NULL,
|
||||
PRIMARY KEY (content_id, site_id)
|
||||
);
|
||||
```
|
||||
|
||||
**Pros**:
|
||||
- Minimal database changes
|
||||
- Leverages existing version history
|
||||
- Backward compatible
|
||||
- Simple migration path
|
||||
|
||||
**Cons**:
|
||||
- Less intuitive data model
|
||||
- Enhancement modes add complexity
|
||||
- Auto-save creates unnecessary versions
|
||||
- Limited workflow capabilities
|
||||
|
||||
### Option B: Full Schema Redesign (Recommended)
|
||||
|
||||
**Core Concept**: Explicit draft and published content tables with rich workflow support.
|
||||
|
||||
**Schema Changes**:
|
||||
```sql
|
||||
-- Draft content (working state)
|
||||
CREATE TABLE content_drafts (
|
||||
id TEXT NOT NULL,
|
||||
site_id TEXT NOT NULL,
|
||||
html_content TEXT NOT NULL,
|
||||
original_template TEXT,
|
||||
created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
|
||||
updated_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
|
||||
last_edited_by TEXT DEFAULT 'system' NOT NULL,
|
||||
is_dirty BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
auto_save_at BIGINT,
|
||||
PRIMARY KEY (id, site_id)
|
||||
);
|
||||
|
||||
-- Published content (live state)
|
||||
CREATE TABLE content_published (
|
||||
id TEXT NOT NULL,
|
||||
site_id TEXT NOT NULL,
|
||||
html_content TEXT NOT NULL,
|
||||
original_template TEXT,
|
||||
published_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
|
||||
published_by TEXT DEFAULT 'system' NOT NULL,
|
||||
draft_version_at_publish BIGINT NOT NULL,
|
||||
PRIMARY KEY (id, site_id)
|
||||
);
|
||||
|
||||
-- Enhanced version tracking
|
||||
CREATE TABLE content_versions (
|
||||
version_id SERIAL PRIMARY KEY,
|
||||
content_id TEXT NOT NULL,
|
||||
site_id TEXT NOT NULL,
|
||||
html_content TEXT NOT NULL,
|
||||
original_template TEXT,
|
||||
created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
|
||||
created_by TEXT DEFAULT 'system' NOT NULL,
|
||||
version_type TEXT NOT NULL CHECK (version_type IN ('draft_save', 'publish', 'auto_save')),
|
||||
is_published BOOLEAN DEFAULT FALSE NOT NULL
|
||||
);
|
||||
|
||||
-- Future: Scheduled publishing support
|
||||
CREATE TABLE publish_queue (
|
||||
queue_id SERIAL PRIMARY KEY,
|
||||
site_id TEXT NOT NULL,
|
||||
content_ids TEXT[] NOT NULL,
|
||||
scheduled_at BIGINT,
|
||||
created_by TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed'))
|
||||
);
|
||||
```
|
||||
|
||||
**Pros**:
|
||||
- Clear semantic separation
|
||||
- Rich workflow capabilities
|
||||
- Optimized for different access patterns
|
||||
- Supports advanced features (auto-save, batch publish, scheduling)
|
||||
- Intuitive mental model
|
||||
|
||||
**Cons**:
|
||||
- Significant schema changes
|
||||
- Complex migration required
|
||||
- More storage overhead
|
||||
- Increased development complexity
|
||||
|
||||
## Recommended Solution: Option B (Full Schema Redesign)
|
||||
|
||||
### Content States
|
||||
|
||||
| State | Draft Table | Published Table | Description |
|
||||
|-------|-------------|-----------------|-------------|
|
||||
| `draft_only` | ✅ Exists | ❌ None | New content, never published |
|
||||
| `published` | ✅ Exists | ✅ Exists, Same | Content published, no pending changes |
|
||||
| `modified` | ✅ Exists | ✅ Exists, Different | Published content with unpublished changes |
|
||||
| `scheduled` | ✅ Exists | ✅ Exists | Content queued for future publishing |
|
||||
|
||||
### API Design
|
||||
|
||||
**New Endpoints**:
|
||||
```
|
||||
GET /api/content/{id}?mode=draft|published # Get content in specific state
|
||||
POST /api/content/{id}/save-draft # Save without publishing
|
||||
POST /api/content/{id}/publish # Publish draft content
|
||||
POST /api/content/bulk-publish # Publish multiple items
|
||||
GET /api/content/diff/{id} # Show draft vs published diff
|
||||
POST /api/enhancement/preview # Preview with draft content
|
||||
POST /api/enhancement/publish # Enhance with published content
|
||||
GET /api/status/publishing # Get site publishing status
|
||||
```
|
||||
|
||||
**Enhanced Endpoints**:
|
||||
```
|
||||
PUT /api/content/{id} # Now saves as draft by default
|
||||
```
|
||||
|
||||
### UI/UX Changes
|
||||
|
||||
**Control Panel Updates**:
|
||||
- Replace "🔄 Enhance" with publishing workflow buttons
|
||||
- Add "💾 Save Draft" (auto-saves every 30s)
|
||||
- Add "🚀 Publish Changes" with confirmation dialog
|
||||
- Add "👁️ Preview Changes" for draft enhancement
|
||||
- Add "📊 Publishing Status" indicator
|
||||
|
||||
**Visual States**:
|
||||
- 🟡 **Draft Pending**: Yellow indicator for unpublished changes
|
||||
- 🟢 **Published**: Green indicator when draft matches published
|
||||
- 🔵 **Scheduled**: Blue indicator for queued publishing
|
||||
- 🔴 **Error**: Red indicator for publishing failures
|
||||
|
||||
**New UI Components**:
|
||||
- Diff viewer showing draft vs published changes
|
||||
- Bulk publishing interface for multiple content items
|
||||
- Publishing history and rollback interface
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Core Infrastructure (Week 1-2)
|
||||
- [ ] Create new database schema
|
||||
- [ ] Implement migration scripts
|
||||
- [ ] Update repository interfaces
|
||||
- [ ] Add basic draft/publish operations
|
||||
- [ ] Update content versioning system
|
||||
|
||||
### Phase 2: API Development (Week 3-4)
|
||||
- [ ] Implement new API endpoints
|
||||
- [ ] Update existing endpoints for draft mode
|
||||
- [ ] Add enhancement mode switching
|
||||
- [ ] Implement publishing workflow APIs
|
||||
- [ ] Add content state management
|
||||
|
||||
### Phase 3: UI Integration (Week 5-6)
|
||||
- [ ] Update control panel with new buttons
|
||||
- [ ] Add visual state indicators
|
||||
- [ ] Implement draft auto-save
|
||||
- [ ] Add preview functionality
|
||||
- [ ] Create publishing confirmation dialogs
|
||||
|
||||
### Phase 4: Advanced Features (Week 7-8)
|
||||
- [ ] Implement diff viewer
|
||||
- [ ] Add bulk publishing interface
|
||||
- [ ] Create publishing history view
|
||||
- [ ] Add rollback functionality
|
||||
- [ ] Implement scheduled publishing foundation
|
||||
|
||||
### Phase 5: Testing & Polish (Week 9-10)
|
||||
- [ ] Comprehensive testing across all demo sites
|
||||
- [ ] Performance optimization
|
||||
- [ ] Error handling and edge cases
|
||||
- [ ] Documentation updates
|
||||
- [ ] Migration testing
|
||||
|
||||
## Potential Challenges & Mitigation
|
||||
|
||||
### Challenge 1: Data Migration Complexity
|
||||
**Risk**: Migrating existing content to new schema without data loss
|
||||
**Mitigation**:
|
||||
- Create comprehensive migration scripts with rollback capability
|
||||
- Test migration on demo sites first
|
||||
- Implement gradual migration with dual-write period
|
||||
- Provide data validation and integrity checks
|
||||
|
||||
### Challenge 2: Concurrent Editing Conflicts
|
||||
**Risk**: Multiple editors working on same content simultaneously
|
||||
**Mitigation**:
|
||||
- Implement optimistic locking with version checking
|
||||
- Add conflict detection and resolution UI
|
||||
- Consider edit session management
|
||||
- Provide clear error messages for conflicts
|
||||
|
||||
### Challenge 3: Performance Impact
|
||||
**Risk**: Additional database queries and storage overhead
|
||||
**Mitigation**:
|
||||
- Optimize database indexes for new access patterns
|
||||
- Implement efficient content state queries
|
||||
- Consider caching strategies for published content
|
||||
- Monitor and profile database performance
|
||||
|
||||
### Challenge 4: Backward Compatibility
|
||||
**Risk**: Breaking existing workflows and integrations
|
||||
**Mitigation**:
|
||||
- Maintain existing API compatibility during transition
|
||||
- Provide clear migration path for existing users
|
||||
- Implement feature flags for gradual rollout
|
||||
- Extensive testing with current demo sites
|
||||
|
||||
### Challenge 5: Auto-save vs Version History Noise
|
||||
**Risk**: Too many auto-save versions cluttering history
|
||||
**Mitigation**:
|
||||
- Separate auto-save from manual save operations
|
||||
- Implement version consolidation strategies
|
||||
- Use different version types (auto_save vs draft_save)
|
||||
- Provide version cleanup mechanisms
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- Repository layer draft/publish operations
|
||||
- Content state transition logic
|
||||
- API endpoint functionality
|
||||
- Migration script validation
|
||||
|
||||
### Integration Tests
|
||||
- End-to-end publishing workflow
|
||||
- Enhancement with different content modes
|
||||
- Concurrent editing scenarios
|
||||
- Database migration processes
|
||||
|
||||
### User Acceptance Tests
|
||||
- Editorial workflow testing with demo sites
|
||||
- Performance testing under load
|
||||
- Cross-browser UI compatibility
|
||||
- Mobile device testing
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Functional Metrics
|
||||
- ✅ All existing demo sites work without modification
|
||||
- ✅ Editors can save drafts without affecting live sites
|
||||
- ✅ Publishing workflow completes in <5 seconds
|
||||
- ✅ Zero data loss during migration
|
||||
|
||||
### User Experience Metrics
|
||||
- ✅ Clear visual indication of content states
|
||||
- ✅ Intuitive publishing workflow
|
||||
- ✅ Auto-save prevents content loss
|
||||
- ✅ Preview functionality works accurately
|
||||
|
||||
### Technical Metrics
|
||||
- ✅ API response times remain under 200ms
|
||||
- ✅ Database migration completes in <30 minutes
|
||||
- ✅ Memory usage increase <20%
|
||||
- ✅ 100% test coverage for new functionality
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### Potential Enhancements
|
||||
- **Scheduled Publishing**: Full calendar-based publishing system
|
||||
- **Approval Workflows**: Multi-stage content approval process
|
||||
- **Content Staging**: Multiple environment support (dev/staging/prod)
|
||||
- **Collaborative Editing**: Real-time collaborative editing features
|
||||
- **Content Templates**: Draft templates for consistent content structure
|
||||
|
||||
### Technical Debt
|
||||
- Consider eventual consolidation of version tables
|
||||
- Evaluate long-term storage strategies for large sites
|
||||
- Plan for horizontal scaling of publishing operations
|
||||
- Review and optimize database schema after real-world usage
|
||||
|
||||
---
|
||||
|
||||
*This document will be updated as the feature evolves through design reviews and implementation feedback.*
|
||||
397
INSERTR-CONTENT-FEATURE.md
Normal file
397
INSERTR-CONTENT-FEATURE.md
Normal file
@@ -0,0 +1,397 @@
|
||||
# .insertr-content Feature Specification
|
||||
|
||||
**Status**: Design Phase
|
||||
**Version**: 0.1.0
|
||||
**Last Updated**: October 16, 2025
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The `.insertr-content` feature extends Insertr's CMS capabilities to handle long-form, complex content like blog posts and articles. While `.insertr` handles individual elements and `.insertr-add` manages collections, `.insertr-content` provides a rich text editing experience for larger content blocks that require sophisticated formatting, structure, and content management workflows.
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Existing Insertr Architecture
|
||||
- **StyleAwareEditor**: Rich text editing with automatic style detection
|
||||
- **HTML Preservation Engine**: Perfect fidelity editing maintaining all attributes
|
||||
- **Style Detection Engine**: Converts nested elements to formatting options
|
||||
- **Collection Management**: `.insertr-add` for dynamic content collections
|
||||
- **Static Site Enhancement**: Build-time content injection and file processing
|
||||
|
||||
### Gaps for Blog/Article Content
|
||||
- No unified interface for long-form content editing
|
||||
- Limited content structure management (headings, sections, media)
|
||||
- No publishing workflow (drafts, scheduling, SEO)
|
||||
- Missing blog-specific features (excerpts, metadata, relationships)
|
||||
|
||||
## Feature Requirements
|
||||
|
||||
### Core Functionality
|
||||
|
||||
#### 1. Rich Content Editor
|
||||
```html
|
||||
<!-- Developer Implementation -->
|
||||
<article class="insertr-content blog-post">
|
||||
<h1>Existing Title</h1>
|
||||
<p>Content with <strong class="brand-highlight">custom styling</strong></p>
|
||||
<blockquote class="testimonial">Styled quotes</blockquote>
|
||||
</article>
|
||||
```
|
||||
|
||||
**Capabilities:**
|
||||
- Inline editing mode with contextual toolbar
|
||||
- Style detection and preservation of developer CSS classes
|
||||
- Block-based content management (headings, paragraphs, lists, quotes)
|
||||
- Media insertion and management
|
||||
- Markdown shortcuts for power users
|
||||
|
||||
#### 2. Content Structure Management
|
||||
- Automatic heading hierarchy detection and validation
|
||||
- Drag & drop block reordering
|
||||
- Content outline/table of contents generation
|
||||
- Block templates for common patterns
|
||||
- Live content structure preview
|
||||
|
||||
#### 3. Enhanced Writing Experience
|
||||
- Distraction-free full-screen mode
|
||||
- Auto-save with conflict resolution
|
||||
- Word count and reading time estimation
|
||||
- Typography optimization for readability
|
||||
- Smart formatting (quotes, dashes, spacing)
|
||||
|
||||
### Static Site Integration
|
||||
|
||||
#### 1. Enhanced File Processing
|
||||
```bash
|
||||
# Enhance existing pages
|
||||
insertr enhance blog-post.html --output dist/blog-post.html
|
||||
|
||||
# Generate content-driven pages
|
||||
insertr enhance templates/ --output dist/ --generate-content
|
||||
```
|
||||
|
||||
#### 2. Template-Based Generation
|
||||
```yaml
|
||||
# insertr.yaml
|
||||
content_types:
|
||||
blog_posts:
|
||||
template: "templates/blog-post.html"
|
||||
output_pattern: "blog/{slug}.html"
|
||||
fields:
|
||||
title: text
|
||||
content: insertr-content
|
||||
excerpt: text
|
||||
published_date: date
|
||||
author: text
|
||||
```
|
||||
|
||||
#### 3. Content Storage Strategy
|
||||
```go
|
||||
type BlogPost struct {
|
||||
ID string `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"` // Rich HTML from .insertr-content
|
||||
Excerpt string `json:"excerpt"`
|
||||
Author string `json:"author"`
|
||||
PublishedAt time.Time `json:"published_at"`
|
||||
Status string `json:"status"` // draft, published, archived
|
||||
Template string `json:"template"`
|
||||
Metadata JSON `json:"metadata"` // SEO, social media, etc.
|
||||
}
|
||||
```
|
||||
|
||||
### Blog Management Features
|
||||
|
||||
#### 1. Publishing Workflow
|
||||
- Draft → Review → Published status management
|
||||
- Content scheduling for future publication
|
||||
- Version history with rollback capabilities
|
||||
- Content approval workflow for teams
|
||||
|
||||
#### 2. SEO and Metadata
|
||||
- Meta title and description management
|
||||
- Open Graph and Twitter Card optimization
|
||||
- Structured data (JSON-LD) generation
|
||||
- Reading time and content analysis
|
||||
- Heading structure validation
|
||||
|
||||
#### 3. Content Organization
|
||||
- Categories and tags management
|
||||
- Related content suggestions
|
||||
- Content series/collections
|
||||
- Author management and attribution
|
||||
|
||||
## Technical Architecture
|
||||
|
||||
### Frontend Components
|
||||
|
||||
#### 1. ContentEditor Class
|
||||
```javascript
|
||||
class ContentEditor extends StyleAwareEditor {
|
||||
constructor(element, options) {
|
||||
super(element, {
|
||||
mode: 'content',
|
||||
showOutline: true,
|
||||
enableMarkdownShortcuts: true,
|
||||
autoSave: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
// Content-specific methods
|
||||
insertBlock(type) { /* Insert heading, quote, code block */ }
|
||||
reorderBlocks() { /* Drag & drop reordering */ }
|
||||
generateOutline() { /* TOC generation */ }
|
||||
validateStructure() { /* SEO and accessibility checks */ }
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Enhanced UI Components
|
||||
- **Block Selector**: Visual insertion of headings, quotes, media
|
||||
- **Outline Panel**: Collapsible content structure navigator
|
||||
- **Style Panel**: Context-aware formatting options
|
||||
- **Media Browser**: Integrated asset management
|
||||
- **Metadata Editor**: SEO and social media optimization
|
||||
|
||||
#### 3. Content Structure Engine
|
||||
```javascript
|
||||
class ContentStructureEngine extends StyleDetectionEngine {
|
||||
analyzeContentBlocks(element) {
|
||||
// Detect semantic structure (headings, sections, articles)
|
||||
// Generate content outline and navigation
|
||||
// Identify reusable content patterns
|
||||
}
|
||||
|
||||
validateContentStructure(structure) {
|
||||
// SEO heading hierarchy validation
|
||||
// Accessibility compliance checks
|
||||
// Content readability analysis
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Backend Enhancements
|
||||
|
||||
#### 1. Content Generation Pipeline
|
||||
```go
|
||||
func (e *Enhancer) EnhanceWithContentGeneration(inputDir, outputDir string) error {
|
||||
// 1. Enhance existing HTML files (current behavior)
|
||||
err := e.EnhanceDirectory(inputDir, outputDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. Generate content-driven pages
|
||||
return e.generateContentPages(outputDir)
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Template Processing
|
||||
- Mustache/Handlebars template engine integration
|
||||
- Content type-specific template routing
|
||||
- Dynamic page generation from database content
|
||||
- Static asset optimization and copying
|
||||
|
||||
#### 3. Content API Extensions
|
||||
- Blog post CRUD operations
|
||||
- Content publishing workflow endpoints
|
||||
- SEO metadata management
|
||||
- Media upload and optimization
|
||||
- Version control and history tracking
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Core Rich Text Editor (4-6 weeks)
|
||||
**Deliverables:**
|
||||
- Basic `.insertr-content` class recognition
|
||||
- Inline editing with floating toolbar
|
||||
- Style detection and preservation
|
||||
- Auto-save functionality
|
||||
- Block-based content management
|
||||
|
||||
**Success Criteria:**
|
||||
- Edit long-form content in-place
|
||||
- Preserve all existing CSS styling
|
||||
- Support basic rich text formatting
|
||||
- Maintain HTML structure integrity
|
||||
|
||||
### Phase 2: Content Structure & Management (4-6 weeks)
|
||||
**Deliverables:**
|
||||
- Content outline generation
|
||||
- Drag & drop block reordering
|
||||
- Media insertion and management
|
||||
- Heading hierarchy validation
|
||||
- SEO metadata editing
|
||||
|
||||
**Success Criteria:**
|
||||
- Navigate content via outline
|
||||
- Reorder content blocks visually
|
||||
- Insert and manage images/media
|
||||
- Validate content structure
|
||||
- Edit meta descriptions and titles
|
||||
|
||||
### Phase 3: Static Site Integration (6-8 weeks)
|
||||
**Deliverables:**
|
||||
- Template-based page generation
|
||||
- Content type configuration
|
||||
- Publishing workflow
|
||||
- Build process integration
|
||||
- Documentation and examples
|
||||
|
||||
**Success Criteria:**
|
||||
- Generate blog pages from templates
|
||||
- Publish/unpublish content
|
||||
- Integrate with existing build tools
|
||||
- Complete documentation
|
||||
- Demo implementations
|
||||
|
||||
### Phase 4: Advanced Features (6-8 weeks)
|
||||
**Deliverables:**
|
||||
- Collaborative editing
|
||||
- Advanced SEO tools
|
||||
- Content relationships
|
||||
- Performance optimizations
|
||||
- Third-party integrations
|
||||
|
||||
**Success Criteria:**
|
||||
- Multiple editors simultaneously
|
||||
- Comprehensive SEO analysis
|
||||
- Related content suggestions
|
||||
- Sub-second editor load times
|
||||
- Hugo/Jekyll/Gatsby examples
|
||||
|
||||
## Potential Challenges & Mitigation Strategies
|
||||
|
||||
### Technical Challenges
|
||||
|
||||
#### 1. **Complex Content Structure Preservation**
|
||||
**Challenge**: Maintaining perfect HTML fidelity while providing rich editing
|
||||
**Mitigation**:
|
||||
- Extend existing HTMLPreservationEngine
|
||||
- Comprehensive test suite for edge cases
|
||||
- Gradual rollout with fallback mechanisms
|
||||
|
||||
#### 2. **Performance with Large Content**
|
||||
**Challenge**: Editor performance degrades with very long articles
|
||||
**Mitigation**:
|
||||
- Virtual scrolling for large documents
|
||||
- Lazy loading of editor features
|
||||
- Incremental parsing and rendering
|
||||
- Memory management optimizations
|
||||
|
||||
#### 3. **Style Detection Complexity**
|
||||
**Challenge**: Complex CSS styling may not map well to editing interfaces
|
||||
**Mitigation**:
|
||||
- Configurable style mapping rules
|
||||
- Developer override mechanisms
|
||||
- Graceful degradation to basic formatting
|
||||
- Comprehensive style detection testing
|
||||
|
||||
### User Experience Challenges
|
||||
|
||||
#### 4. **Editor Complexity vs Simplicity**
|
||||
**Challenge**: Power users need advanced features, casual users need simplicity
|
||||
**Mitigation**:
|
||||
- Progressive disclosure of features
|
||||
- Configurable interface complexity
|
||||
- Role-based feature availability
|
||||
- Contextual help and onboarding
|
||||
|
||||
#### 5. **Content Migration**
|
||||
**Challenge**: Moving existing blog content into Insertr system
|
||||
**Mitigation**:
|
||||
- Import tools for common formats (Markdown, HTML, WordPress)
|
||||
- Bulk migration utilities
|
||||
- Content validation and cleanup tools
|
||||
- Migration documentation and tutorials
|
||||
|
||||
### Integration Challenges
|
||||
|
||||
#### 6. **Static Site Generator Compatibility**
|
||||
**Challenge**: Different SSGs have different content conventions
|
||||
**Mitigation**:
|
||||
- Plugin architecture for SSG-specific adaptations
|
||||
- Standard export formats (Markdown, JSON, HTML)
|
||||
- Configuration templates for popular SSGs
|
||||
- Community-driven integration examples
|
||||
|
||||
#### 7. **Build Process Integration**
|
||||
**Challenge**: Fitting into existing development workflows
|
||||
**Mitigation**:
|
||||
- CLI-first approach matching existing tools
|
||||
- CI/CD pipeline integration guides
|
||||
- Watch mode for development
|
||||
- Incremental build optimizations
|
||||
|
||||
### Content Management Challenges
|
||||
|
||||
#### 8. **Version Control and Conflicts**
|
||||
**Challenge**: Managing content changes across multiple editors and builds
|
||||
**Mitigation**:
|
||||
- Operational transformation for real-time collaboration
|
||||
- Clear conflict resolution interfaces
|
||||
- Audit trail for all content changes
|
||||
- Backup and recovery mechanisms
|
||||
|
||||
#### 9. **SEO and Performance Balance**
|
||||
**Challenge**: Rich editing features may impact site performance
|
||||
**Mitigation**:
|
||||
- Minimal runtime overhead for visitors
|
||||
- Conditional loading of editing features
|
||||
- Static generation maintains performance
|
||||
- Performance monitoring and optimization
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### User Adoption
|
||||
- Number of sites using `.insertr-content`
|
||||
- Content creation frequency
|
||||
- User retention and engagement
|
||||
- Community feedback and contributions
|
||||
|
||||
### Technical Performance
|
||||
- Editor load time (target: <2 seconds)
|
||||
- Content save latency (target: <500ms)
|
||||
- Static site build impact (target: <10% increase)
|
||||
- Memory usage optimization
|
||||
|
||||
### Content Quality
|
||||
- Content structure validation pass rate
|
||||
- SEO score improvements
|
||||
- Accessibility compliance metrics
|
||||
- User-reported content issues
|
||||
|
||||
## Open Questions & Decisions Needed
|
||||
|
||||
### Design Decisions
|
||||
1. **Block vs. Inline Editing**: Should we prioritize block-based editing (like Gutenberg) or seamless inline editing?
|
||||
2. **Markdown Support**: How much markdown compatibility should we maintain vs. pure HTML?
|
||||
3. **Template Engine**: Which template engine should we standardize on for content generation?
|
||||
|
||||
### Technical Decisions
|
||||
1. **Database Schema**: How should we structure content types and metadata in the database?
|
||||
2. **API Design**: Should we extend existing APIs or create new content-specific endpoints?
|
||||
3. **Caching Strategy**: How do we handle content caching across the editing and static generation pipeline?
|
||||
|
||||
### Integration Decisions
|
||||
1. **SSG Priority**: Which static site generators should we prioritize for integration?
|
||||
2. **Media Handling**: Should we build media management or integrate with existing solutions?
|
||||
3. **Deployment**: How do we handle automated deployments when content is published?
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Technical Spike** (1 week): Prototype core editing interface with existing StyleAwareEditor
|
||||
2. **Design Review** (1 week): Validate UI/UX approach with user research
|
||||
3. **Architecture Review** (1 week): Finalize technical architecture and database schema
|
||||
4. **Phase 1 Kickoff**: Begin implementation of core rich text editor
|
||||
|
||||
## References
|
||||
|
||||
- [CLASSES.md](./CLASSES.md) - Current class system documentation
|
||||
- [StyleAwareEditor](./lib/src/ui/style-aware-editor.js) - Existing editor implementation
|
||||
- [HTMLPreservationEngine](./lib/src/utils/html-preservation.js) - Current preservation approach
|
||||
- [Content Enhancement Pipeline](./internal/content/enhancer.go) - Static site processing
|
||||
|
||||
---
|
||||
|
||||
*This document is a living specification that will evolve as we learn more about user needs and technical constraints. All stakeholders should contribute to its refinement.*
|
||||
@@ -205,6 +205,162 @@ func (h *ContentHandler) DeleteContent(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Delete operation not yet implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// UpdateContent handles PUT /api/content/{id}
|
||||
func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) {
|
||||
contentID := chi.URLParam(r, "id")
|
||||
siteID := r.URL.Query().Get("site_id")
|
||||
|
||||
if siteID == "" {
|
||||
http.Error(w, "site_id parameter is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
HTMLContent string `json:"html_content"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
userInfo, authErr := h.authService.ExtractUserFromRequest(r)
|
||||
if authErr != nil {
|
||||
http.Error(w, "Authentication required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if content exists
|
||||
existingContent, err := h.repository.GetContent(context.Background(), siteID, contentID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "Content not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if existingContent == nil {
|
||||
http.Error(w, "Content not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Update content using repository
|
||||
updatedContent, err := h.repository.UpdateContent(context.Background(), siteID, contentID, req.HTMLContent, userInfo.ID)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to update content: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(updatedContent)
|
||||
}
|
||||
|
||||
// ReorderCollection handles PUT /api/collections/{id}/reorder
|
||||
func (h *ContentHandler) ReorderCollection(w http.ResponseWriter, r *http.Request) {
|
||||
collectionID := chi.URLParam(r, "id")
|
||||
siteID := r.URL.Query().Get("site_id")
|
||||
|
||||
if siteID == "" {
|
||||
http.Error(w, "site_id parameter is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req ReorderCollectionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Items) == 0 {
|
||||
http.Error(w, "Items array cannot be empty", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
userInfo, authErr := h.authService.ExtractUserFromRequest(r)
|
||||
if authErr != nil {
|
||||
http.Error(w, "Authentication required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Use repository for reordering
|
||||
err := h.repository.ReorderCollectionItems(context.Background(), siteID, collectionID, req.Items, userInfo.ID)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to reorder collection: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"message": fmt.Sprintf("Successfully reordered %d items", len(req.Items)),
|
||||
}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// Stub handlers for remaining endpoints - will implement as needed
|
||||
|
||||
// GetContentVersions handles GET /api/content/{id}/versions
|
||||
func (h *ContentHandler) GetContentVersions(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Content versioning not yet implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// RollbackContent handles POST /api/content/{id}/rollback
|
||||
func (h *ContentHandler) RollbackContent(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Content rollback not yet implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// GetAllCollections handles GET /api/collections
|
||||
func (h *ContentHandler) GetAllCollections(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Get all collections not yet implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// UpdateCollectionItem handles PUT /api/collections/{id}/items/{item_id}
|
||||
func (h *ContentHandler) UpdateCollectionItem(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Update collection item not yet implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// DeleteCollectionItem handles DELETE /api/collections/{id}/items/{item_id}
|
||||
func (h *ContentHandler) DeleteCollectionItem(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Delete collection item not yet implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// EnhanceSite handles POST /api/enhance
|
||||
func (h *ContentHandler) EnhanceSite(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Site enhancement not yet implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// GetAuthToken handles GET /api/auth/token - provides mock tokens in dev mode
|
||||
func (h *ContentHandler) GetAuthToken(w http.ResponseWriter, r *http.Request) {
|
||||
// Only provide mock tokens in development mode
|
||||
if !h.authService.IsDevMode() {
|
||||
http.Error(w, "Mock authentication only available in development mode", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a mock JWT token
|
||||
mockToken, err := h.authService.CreateMockJWT("dev-user", "dev@localhost", "Development User")
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to create mock token: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"token": mockToken,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 86400, // 24 hours
|
||||
"user": map[string]string{
|
||||
"id": "dev-user",
|
||||
"email": "dev@localhost",
|
||||
"name": "Development User",
|
||||
},
|
||||
"dev_mode": true,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// GetCollection handles GET /api/collections/{id}
|
||||
func (h *ContentHandler) GetCollection(w http.ResponseWriter, r *http.Request) {
|
||||
collectionID := chi.URLParam(r, "id")
|
||||
@@ -300,16 +456,62 @@ func (h *ContentHandler) CreateCollectionItem(w http.ResponseWriter, r *http.Req
|
||||
// RegisterRoutes registers all the content API routes
|
||||
func (h *ContentHandler) RegisterRoutes(r chi.Router) {
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
// Content routes
|
||||
r.Get("/content/{id}", h.GetContent)
|
||||
r.Get("/content", h.GetAllContent)
|
||||
r.Post("/content/bulk", h.GetBulkContent)
|
||||
r.Post("/content", h.CreateOrUpdateContent)
|
||||
r.Delete("/content/{id}", h.DeleteContent)
|
||||
// =============================================================================
|
||||
// CONTENT MANAGEMENT - Individual content items
|
||||
// =============================================================================
|
||||
r.Route("/content", func(r chi.Router) {
|
||||
// Public routes (no auth required)
|
||||
r.Get("/bulk", h.GetBulkContent) // GET /api/content/bulk?site_id=X&ids=a,b,c
|
||||
r.Get("/{id}", h.GetContent) // GET /api/content/{id}?site_id=X
|
||||
r.Get("/", h.GetAllContent) // GET /api/content?site_id=X
|
||||
|
||||
// Collection routes
|
||||
r.Get("/collections/{id}", h.GetCollection)
|
||||
r.Get("/collections/{id}/items", h.GetCollectionItems)
|
||||
r.Post("/collections/{id}/items", h.CreateCollectionItem)
|
||||
// Protected routes (require authentication)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(h.authService.RequireAuth)
|
||||
r.Post("/", h.CreateOrUpdateContent) // POST /api/content (upsert)
|
||||
r.Put("/{id}", h.UpdateContent) // PUT /api/content/{id}?site_id=X
|
||||
r.Delete("/{id}", h.DeleteContent) // DELETE /api/content/{id}?site_id=X
|
||||
|
||||
// Version control sub-routes
|
||||
r.Get("/{id}/versions", h.GetContentVersions) // GET /api/content/{id}/versions?site_id=X
|
||||
r.Post("/{id}/rollback", h.RollbackContent) // POST /api/content/{id}/rollback
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// COLLECTION MANAGEMENT - Groups of related content
|
||||
// =============================================================================
|
||||
r.Route("/collections", func(r chi.Router) {
|
||||
// Public routes
|
||||
r.Get("/", h.GetAllCollections) // GET /api/collections?site_id=X
|
||||
r.Get("/{id}", h.GetCollection) // GET /api/collections/{id}?site_id=X
|
||||
r.Get("/{id}/items", h.GetCollectionItems) // GET /api/collections/{id}/items?site_id=X
|
||||
|
||||
// Protected routes
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(h.authService.RequireAuth)
|
||||
r.Post("/{id}/items", h.CreateCollectionItem) // POST /api/collections/{id}/items
|
||||
r.Put("/{id}/items/{item_id}", h.UpdateCollectionItem) // PUT /api/collections/{id}/items/{item_id}
|
||||
r.Delete("/{id}/items/{item_id}", h.DeleteCollectionItem) // DELETE /api/collections/{id}/items/{item_id}?site_id=X
|
||||
|
||||
// Bulk operations
|
||||
r.Put("/{id}/reorder", h.ReorderCollection) // PUT /api/collections/{id}/reorder?site_id=X
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// AUTHENTICATION - Development token endpoint
|
||||
// =============================================================================
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Get("/token", h.GetAuthToken) // GET /api/auth/token (dev mode only)
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// SITE OPERATIONS - Site-level functionality
|
||||
// =============================================================================
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(h.authService.RequireAuth)
|
||||
r.Post("/enhance", h.EnhanceSite) // POST /api/enhance?site_id=X
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package api
|
||||
|
||||
import "github.com/insertr/insertr/internal/db"
|
||||
|
||||
// Use db package types directly for API responses - no duplication needed
|
||||
// Request models are kept below as they're different (input DTOs)
|
||||
|
||||
@@ -59,12 +61,7 @@ type UpdateCollectionItemRequest struct {
|
||||
UpdatedBy string `json:"updated_by,omitempty"`
|
||||
}
|
||||
|
||||
type CollectionItemPosition struct {
|
||||
ItemID string `json:"itemId"`
|
||||
Position int `json:"position"`
|
||||
}
|
||||
|
||||
type ReorderCollectionRequest struct {
|
||||
Items []CollectionItemPosition `json:"items"`
|
||||
UpdatedBy string `json:"updated_by,omitempty"`
|
||||
Items []db.CollectionItemPosition `json:"items"`
|
||||
UpdatedBy string `json:"updated_by,omitempty"`
|
||||
}
|
||||
|
||||
@@ -307,6 +307,11 @@ func (a *AuthService) IsAuthenticated(r *http.Request) bool {
|
||||
return err == nil && userInfo.ID != "anonymous"
|
||||
}
|
||||
|
||||
// IsDevMode returns true if the service is in development mode
|
||||
func (a *AuthService) IsDevMode() bool {
|
||||
return a.config.DevMode
|
||||
}
|
||||
|
||||
// RequireAuth middleware that requires authentication
|
||||
func (a *AuthService) RequireAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -202,6 +202,14 @@ func (c *HTTPClient) CreateCollectionItemAtomic(ctx context.Context, siteID, col
|
||||
return nil, fmt.Errorf("collection operations not implemented in HTTPClient")
|
||||
}
|
||||
|
||||
func (c *HTTPClient) UpdateContent(ctx context.Context, siteID, contentID, htmlContent, lastEditedBy string) (*db.ContentItem, error) {
|
||||
return nil, fmt.Errorf("content update operations not implemented in HTTPClient")
|
||||
}
|
||||
|
||||
func (c *HTTPClient) ReorderCollectionItems(ctx context.Context, siteID, collectionID string, items []db.CollectionItemPosition, lastEditedBy string) error {
|
||||
return fmt.Errorf("collection reordering not implemented in HTTPClient")
|
||||
}
|
||||
|
||||
// WithTransaction executes a function within a transaction (not supported for HTTP client)
|
||||
func (c *HTTPClient) WithTransaction(ctx context.Context, fn func(db.ContentRepository) error) error {
|
||||
return fmt.Errorf("transactions not supported for HTTP client")
|
||||
|
||||
@@ -264,6 +264,54 @@ func (r *PostgreSQLRepository) CreateCollectionItemAtomic(ctx context.Context, s
|
||||
return nil, fmt.Errorf("CreateCollectionItemAtomic not yet implemented for PostgreSQL")
|
||||
}
|
||||
|
||||
// UpdateContent updates an existing content item
|
||||
func (r *PostgreSQLRepository) UpdateContent(ctx context.Context, siteID, contentID, htmlContent, lastEditedBy string) (*ContentItem, error) {
|
||||
content, err := r.queries.UpdateContent(ctx, postgresql.UpdateContentParams{
|
||||
HtmlContent: htmlContent,
|
||||
LastEditedBy: lastEditedBy,
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ContentItem{
|
||||
ID: content.ID,
|
||||
SiteID: content.SiteID,
|
||||
HTMLContent: content.HtmlContent,
|
||||
OriginalTemplate: FromNullString(content.OriginalTemplate),
|
||||
UpdatedAt: fmt.Sprintf("%d", content.UpdatedAt),
|
||||
LastEditedBy: content.LastEditedBy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReorderCollectionItems reorders collection items in bulk
|
||||
func (r *PostgreSQLRepository) ReorderCollectionItems(ctx context.Context, siteID, collectionID string, items []CollectionItemPosition, lastEditedBy string) error {
|
||||
// Use transaction for atomic bulk updates
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
qtx := r.queries.WithTx(tx)
|
||||
for _, item := range items {
|
||||
err = qtx.UpdateCollectionItemPosition(ctx, postgresql.UpdateCollectionItemPositionParams{
|
||||
ItemID: item.ItemID,
|
||||
CollectionID: collectionID,
|
||||
SiteID: siteID,
|
||||
Position: int32(item.Position),
|
||||
LastEditedBy: lastEditedBy,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update position for item %s: %w", item.ItemID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// WithTransaction executes a function within a database transaction
|
||||
func (r *PostgreSQLRepository) WithTransaction(ctx context.Context, fn func(ContentRepository) error) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
|
||||
@@ -11,6 +11,7 @@ type ContentRepository interface {
|
||||
GetBulkContent(ctx context.Context, siteID string, contentIDs []string) (map[string]ContentItem, error)
|
||||
GetAllContent(ctx context.Context, siteID string) (map[string]ContentItem, error)
|
||||
CreateContent(ctx context.Context, siteID, contentID, htmlContent, originalTemplate, lastEditedBy string) (*ContentItem, error)
|
||||
UpdateContent(ctx context.Context, siteID, contentID, htmlContent, lastEditedBy string) (*ContentItem, error)
|
||||
|
||||
// Collection operations
|
||||
GetCollection(ctx context.Context, siteID, collectionID string) (*CollectionItem, error)
|
||||
@@ -20,6 +21,7 @@ type ContentRepository interface {
|
||||
CreateCollectionTemplate(ctx context.Context, siteID, collectionID, name, htmlTemplate string, isDefault bool) (*CollectionTemplateItem, error)
|
||||
CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error)
|
||||
CreateCollectionItemAtomic(ctx context.Context, siteID, collectionID string, templateID int, lastEditedBy string) (*CollectionItemWithTemplate, error)
|
||||
ReorderCollectionItems(ctx context.Context, siteID, collectionID string, items []CollectionItemPosition, lastEditedBy string) error
|
||||
|
||||
// Transaction support
|
||||
WithTransaction(ctx context.Context, fn func(ContentRepository) error) error
|
||||
@@ -77,6 +79,12 @@ type CollectionItemWithTemplate struct {
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
// CollectionItemPosition represents item position for reordering
|
||||
type CollectionItemPosition struct {
|
||||
ItemID string `json:"itemId"`
|
||||
Position int `json:"position"`
|
||||
}
|
||||
|
||||
// Helper function to convert sql.NullString to string
|
||||
func getStringFromNullString(ns sql.NullString) string {
|
||||
if ns.Valid {
|
||||
|
||||
@@ -269,6 +269,54 @@ func (r *SQLiteRepository) CreateCollectionItemAtomic(ctx context.Context, siteI
|
||||
return nil, fmt.Errorf("CreateCollectionItemAtomic not yet implemented for SQLite")
|
||||
}
|
||||
|
||||
// UpdateContent updates an existing content item
|
||||
func (r *SQLiteRepository) UpdateContent(ctx context.Context, siteID, contentID, htmlContent, lastEditedBy string) (*ContentItem, error) {
|
||||
content, err := r.queries.UpdateContent(ctx, sqlite.UpdateContentParams{
|
||||
HtmlContent: htmlContent,
|
||||
LastEditedBy: lastEditedBy,
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ContentItem{
|
||||
ID: content.ID,
|
||||
SiteID: content.SiteID,
|
||||
HTMLContent: content.HtmlContent,
|
||||
OriginalTemplate: FromNullString(content.OriginalTemplate),
|
||||
UpdatedAt: fmt.Sprintf("%d", content.UpdatedAt),
|
||||
LastEditedBy: content.LastEditedBy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReorderCollectionItems reorders collection items in bulk
|
||||
func (r *SQLiteRepository) ReorderCollectionItems(ctx context.Context, siteID, collectionID string, items []CollectionItemPosition, lastEditedBy string) error {
|
||||
// Use transaction for atomic bulk updates
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
qtx := r.queries.WithTx(tx)
|
||||
for _, item := range items {
|
||||
err = qtx.UpdateCollectionItemPosition(ctx, sqlite.UpdateCollectionItemPositionParams{
|
||||
ItemID: item.ItemID,
|
||||
CollectionID: collectionID,
|
||||
SiteID: siteID,
|
||||
Position: int64(item.Position),
|
||||
LastEditedBy: lastEditedBy,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update position for item %s: %w", item.ItemID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// WithTransaction executes a function within a database transaction
|
||||
func (r *SQLiteRepository) WithTransaction(ctx context.Context, fn func(ContentRepository) error) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
|
||||
@@ -382,13 +382,44 @@ export class ApiClient {
|
||||
* @returns {string} Mock JWT token
|
||||
*/
|
||||
getMockToken() {
|
||||
// Create a mock JWT-like token for development
|
||||
// Format: mock-{user}-{timestamp}-{random}
|
||||
const user = 'anonymous';
|
||||
// First check if we have a stored mock JWT token
|
||||
const storedMockToken = localStorage.getItem('insertr_mock_token');
|
||||
if (storedMockToken && !this.isTokenExpired(storedMockToken)) {
|
||||
return storedMockToken;
|
||||
}
|
||||
|
||||
// If no valid stored token, fetch a new one from the API
|
||||
this.fetchMockTokenAsync();
|
||||
|
||||
// Return a temporary token while we fetch the real one
|
||||
const user = 'dev-user';
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substr(2, 9);
|
||||
return `mock-${user}-${timestamp}-${random}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a real mock JWT token from the development API
|
||||
*/
|
||||
async fetchMockTokenAsync() {
|
||||
try {
|
||||
const authUrl = this.baseUrl.replace('/api/content', '/api/auth/token');
|
||||
const response = await fetch(authUrl);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.token) {
|
||||
localStorage.setItem('insertr_mock_token', data.token);
|
||||
localStorage.setItem('insertr_mock_token_expires', Date.now() + (data.expires_in * 1000));
|
||||
console.log('🔐 Mock JWT token fetched successfully');
|
||||
}
|
||||
} else {
|
||||
console.warn('Failed to fetch mock token, using fallback');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error fetching mock token:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JWT token payload
|
||||
@@ -428,6 +459,15 @@ export class ApiClient {
|
||||
*/
|
||||
isTokenExpired(token) {
|
||||
try {
|
||||
// Check localStorage expiration for mock tokens
|
||||
if (token.startsWith('mock-') && token === localStorage.getItem('insertr_mock_token')) {
|
||||
const expires = localStorage.getItem('insertr_mock_token_expires');
|
||||
if (expires && Date.now() > parseInt(expires)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse JWT expiration
|
||||
const payload = this.parseJWT(token);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
return payload.exp && payload.exp < now;
|
||||
|
||||
Reference in New Issue
Block a user