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:
2025-10-16 21:23:17 +02:00
parent bbf728d110
commit 87b78a4a69
11 changed files with 1095 additions and 218 deletions

View File

@@ -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
View 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
View 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.*

View File

@@ -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
})
}) })
} }

View File

@@ -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"`
} }

View File

@@ -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) {

View File

@@ -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")

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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;