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)
|
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}
|
// GetCollection handles GET /api/collections/{id}
|
||||||
func (h *ContentHandler) GetCollection(w http.ResponseWriter, r *http.Request) {
|
func (h *ContentHandler) GetCollection(w http.ResponseWriter, r *http.Request) {
|
||||||
collectionID := chi.URLParam(r, "id")
|
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
|
// RegisterRoutes registers all the content API routes
|
||||||
func (h *ContentHandler) RegisterRoutes(r chi.Router) {
|
func (h *ContentHandler) RegisterRoutes(r chi.Router) {
|
||||||
r.Route("/api", func(r chi.Router) {
|
r.Route("/api", func(r chi.Router) {
|
||||||
// Content routes
|
// =============================================================================
|
||||||
r.Get("/content/{id}", h.GetContent)
|
// CONTENT MANAGEMENT - Individual content items
|
||||||
r.Get("/content", h.GetAllContent)
|
// =============================================================================
|
||||||
r.Post("/content/bulk", h.GetBulkContent)
|
r.Route("/content", func(r chi.Router) {
|
||||||
r.Post("/content", h.CreateOrUpdateContent)
|
// Public routes (no auth required)
|
||||||
r.Delete("/content/{id}", h.DeleteContent)
|
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
|
// Protected routes (require authentication)
|
||||||
r.Get("/collections/{id}", h.GetCollection)
|
r.Group(func(r chi.Router) {
|
||||||
r.Get("/collections/{id}/items", h.GetCollectionItems)
|
r.Use(h.authService.RequireAuth)
|
||||||
r.Post("/collections/{id}/items", h.CreateCollectionItem)
|
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
|
package api
|
||||||
|
|
||||||
|
import "github.com/insertr/insertr/internal/db"
|
||||||
|
|
||||||
// Use db package types directly for API responses - no duplication needed
|
// Use db package types directly for API responses - no duplication needed
|
||||||
// Request models are kept below as they're different (input DTOs)
|
// Request models are kept below as they're different (input DTOs)
|
||||||
|
|
||||||
@@ -59,12 +61,7 @@ type UpdateCollectionItemRequest struct {
|
|||||||
UpdatedBy string `json:"updated_by,omitempty"`
|
UpdatedBy string `json:"updated_by,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CollectionItemPosition struct {
|
|
||||||
ItemID string `json:"itemId"`
|
|
||||||
Position int `json:"position"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReorderCollectionRequest struct {
|
type ReorderCollectionRequest struct {
|
||||||
Items []CollectionItemPosition `json:"items"`
|
Items []db.CollectionItemPosition `json:"items"`
|
||||||
UpdatedBy string `json:"updated_by,omitempty"`
|
UpdatedBy string `json:"updated_by,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,6 +307,11 @@ func (a *AuthService) IsAuthenticated(r *http.Request) bool {
|
|||||||
return err == nil && userInfo.ID != "anonymous"
|
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
|
// RequireAuth middleware that requires authentication
|
||||||
func (a *AuthService) RequireAuth(next http.Handler) http.Handler {
|
func (a *AuthService) RequireAuth(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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")
|
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)
|
// 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 {
|
func (c *HTTPClient) WithTransaction(ctx context.Context, fn func(db.ContentRepository) error) error {
|
||||||
return fmt.Errorf("transactions not supported for HTTP client")
|
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")
|
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
|
// WithTransaction executes a function within a database transaction
|
||||||
func (r *PostgreSQLRepository) WithTransaction(ctx context.Context, fn func(ContentRepository) error) error {
|
func (r *PostgreSQLRepository) WithTransaction(ctx context.Context, fn func(ContentRepository) error) error {
|
||||||
tx, err := r.db.BeginTx(ctx, nil)
|
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)
|
GetBulkContent(ctx context.Context, siteID string, contentIDs []string) (map[string]ContentItem, error)
|
||||||
GetAllContent(ctx context.Context, siteID 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)
|
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
|
// Collection operations
|
||||||
GetCollection(ctx context.Context, siteID, collectionID string) (*CollectionItem, error)
|
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)
|
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)
|
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)
|
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
|
// Transaction support
|
||||||
WithTransaction(ctx context.Context, fn func(ContentRepository) error) error
|
WithTransaction(ctx context.Context, fn func(ContentRepository) error) error
|
||||||
@@ -77,6 +79,12 @@ type CollectionItemWithTemplate struct {
|
|||||||
IsDefault bool `json:"is_default"`
|
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
|
// Helper function to convert sql.NullString to string
|
||||||
func getStringFromNullString(ns sql.NullString) string {
|
func getStringFromNullString(ns sql.NullString) string {
|
||||||
if ns.Valid {
|
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")
|
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
|
// WithTransaction executes a function within a database transaction
|
||||||
func (r *SQLiteRepository) WithTransaction(ctx context.Context, fn func(ContentRepository) error) error {
|
func (r *SQLiteRepository) WithTransaction(ctx context.Context, fn func(ContentRepository) error) error {
|
||||||
tx, err := r.db.BeginTx(ctx, nil)
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
|||||||
@@ -382,13 +382,44 @@ export class ApiClient {
|
|||||||
* @returns {string} Mock JWT token
|
* @returns {string} Mock JWT token
|
||||||
*/
|
*/
|
||||||
getMockToken() {
|
getMockToken() {
|
||||||
// Create a mock JWT-like token for development
|
// First check if we have a stored mock JWT token
|
||||||
// Format: mock-{user}-{timestamp}-{random}
|
const storedMockToken = localStorage.getItem('insertr_mock_token');
|
||||||
const user = 'anonymous';
|
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 timestamp = Date.now();
|
||||||
const random = Math.random().toString(36).substr(2, 9);
|
const random = Math.random().toString(36).substr(2, 9);
|
||||||
return `mock-${user}-${timestamp}-${random}`;
|
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
|
* Parse JWT token payload
|
||||||
@@ -428,6 +459,15 @@ export class ApiClient {
|
|||||||
*/
|
*/
|
||||||
isTokenExpired(token) {
|
isTokenExpired(token) {
|
||||||
try {
|
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 payload = this.parseJWT(token);
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
return payload.exp && payload.exp < now;
|
return payload.exp && payload.exp < now;
|
||||||
|
|||||||
Reference in New Issue
Block a user