build: Update library assets with UI visibility fix
- Rebuild JavaScript library with delayed control panel initialization - Update server assets to include latest UI behavior changes - Ensure built assets reflect invisible UI for regular visitors The control panel now only appears after gate activation, maintaining the invisible CMS principle for end users.
This commit is contained in:
48
COMMANDS.md
48
COMMANDS.md
@@ -130,6 +130,10 @@ When running `insertr serve`, these endpoints are available:
|
|||||||
- `GET /api/content/{id}/versions?site_id={site}` - Get content history
|
- `GET /api/content/{id}/versions?site_id={site}` - Get content history
|
||||||
- `POST /api/content/{id}/rollback` - Rollback to previous version
|
- `POST /api/content/{id}/rollback` - Rollback to previous version
|
||||||
|
|
||||||
|
#### Authentication
|
||||||
|
- `GET /auth/login` - Initiate OAuth flow (returns redirect URL for Authentik)
|
||||||
|
- `GET /auth/callback` - OAuth callback handler (processes authorization code)
|
||||||
|
|
||||||
### Content Management API Details
|
### Content Management API Details
|
||||||
|
|
||||||
#### Create/Update Content (Upsert)
|
#### Create/Update Content (Upsert)
|
||||||
@@ -237,6 +241,15 @@ build:
|
|||||||
input: "./src" # Default input directory
|
input: "./src" # Default input directory
|
||||||
output: "./dist" # Default output directory
|
output: "./dist" # Default output directory
|
||||||
|
|
||||||
|
# Authentication configuration
|
||||||
|
auth:
|
||||||
|
provider: "mock" # "mock", "authentik"
|
||||||
|
jwt_secret: "" # JWT signing secret (auto-generated in dev)
|
||||||
|
oidc: # Authentik OIDC configuration
|
||||||
|
endpoint: "" # https://auth.example.com/application/o/insertr/
|
||||||
|
client_id: "" # OAuth2 client ID
|
||||||
|
client_secret: "" # OAuth2 client secret
|
||||||
|
|
||||||
# Global settings
|
# Global settings
|
||||||
site_id: "demo" # Site identifier
|
site_id: "demo" # Site identifier
|
||||||
mock_content: false # Use mock data
|
mock_content: false # Use mock data
|
||||||
@@ -252,6 +265,10 @@ export INSERTR_API_URL="https://api.example.com"
|
|||||||
export INSERTR_API_KEY="your-api-key"
|
export INSERTR_API_KEY="your-api-key"
|
||||||
export INSERTR_SITE_ID="production"
|
export INSERTR_SITE_ID="production"
|
||||||
|
|
||||||
|
# Authentication environment variables (recommended for production)
|
||||||
|
export AUTHENTIK_CLIENT_SECRET="your-oidc-secret"
|
||||||
|
export AUTHENTIK_ENDPOINT="https://auth.example.com/application/o/insertr/"
|
||||||
|
|
||||||
./insertr serve
|
./insertr serve
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -284,13 +301,42 @@ air # Uses .air.toml configuration
|
|||||||
# Build static site with content
|
# Build static site with content
|
||||||
./insertr enhance ./src --output ./dist --api-url https://api.prod.com
|
./insertr enhance ./src --output ./dist --api-url https://api.prod.com
|
||||||
|
|
||||||
# Start production API server
|
# Start production API server with authentication
|
||||||
|
export AUTHENTIK_CLIENT_SECRET="prod-secret"
|
||||||
./insertr serve --db "postgresql://user:pass@db:5432/prod"
|
./insertr serve --db "postgresql://user:pass@db:5432/prod"
|
||||||
|
|
||||||
# With custom configuration
|
# With custom configuration
|
||||||
./insertr --config ./production.yaml serve
|
./insertr --config ./production.yaml serve
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Authentication Setup
|
||||||
|
|
||||||
|
#### Mock Authentication (Development)
|
||||||
|
```bash
|
||||||
|
# Default - no setup required
|
||||||
|
./insertr serve --dev-mode
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
auth:
|
||||||
|
provider: "mock"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Authentik OIDC (Production)
|
||||||
|
```bash
|
||||||
|
# Set environment variables (recommended)
|
||||||
|
export AUTHENTIK_CLIENT_SECRET="your-secret-from-authentik"
|
||||||
|
export AUTHENTIK_ENDPOINT="https://auth.example.com/application/o/insertr/"
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
./insertr serve
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required Authentik Configuration:**
|
||||||
|
1. Create OAuth2/OIDC Provider in Authentik
|
||||||
|
2. Set redirect URI: `https://your-domain.com/auth/callback`
|
||||||
|
3. Note the endpoint URL and client credentials
|
||||||
|
4. Configure in `insertr.yaml` or environment variables
|
||||||
|
|
||||||
### CI/CD Integration
|
### CI/CD Integration
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
106
README.md
106
README.md
@@ -48,10 +48,96 @@ Containers with `class="insertr"` automatically make their viable children edita
|
|||||||
- Never see or manage generated content IDs
|
- Never see or manage generated content IDs
|
||||||
- Works with any static site generator
|
- Works with any static site generator
|
||||||
|
|
||||||
|
## 🔐 Authentication & Security
|
||||||
|
|
||||||
|
Insertr provides enterprise-grade authentication with multiple provider support for secure content editing.
|
||||||
|
|
||||||
|
### **Authentication Providers**
|
||||||
|
|
||||||
|
#### **Mock Authentication** (Development)
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
provider: "mock"
|
||||||
|
```
|
||||||
|
- **Zero setup** - Works immediately for development
|
||||||
|
- **Automatic login** - No credentials needed
|
||||||
|
- **Perfect for testing** - Focus on content editing workflow
|
||||||
|
|
||||||
|
#### **Authentik OIDC** (Production)
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
provider: "authentik"
|
||||||
|
oidc:
|
||||||
|
endpoint: "https://auth.example.com/application/o/insertr/"
|
||||||
|
client_id: "insertr-client"
|
||||||
|
client_secret: "your-secret" # or use AUTHENTIK_CLIENT_SECRET env var
|
||||||
|
```
|
||||||
|
|
||||||
|
**Enterprise-grade security features:**
|
||||||
|
- ✅ **OIDC Discovery** - Automatic endpoint configuration
|
||||||
|
- ✅ **PKCE Flow** - Proof Key for Code Exchange security
|
||||||
|
- ✅ **JWT Verification** - RSA/ECDSA signature validation via JWKS
|
||||||
|
- ✅ **Secure Sessions** - HTTP-only cookies with CSRF protection
|
||||||
|
- ✅ **Multi-tenant** - Per-site authentication configuration
|
||||||
|
|
||||||
|
### **Authentication Flow**
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Editor clicks gate → Popup opens to Authentik
|
||||||
|
2. User authenticates → Authentik returns authorization code
|
||||||
|
3. Backend exchanges code for JWT → Validates with OIDC provider
|
||||||
|
4. Editor UI loads → Full editing capabilities enabled
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Authentik Setup Guide**
|
||||||
|
|
||||||
|
1. **Create OIDC Provider in Authentik:**
|
||||||
|
```
|
||||||
|
Applications → Providers → Create → OAuth2/OIDC Provider
|
||||||
|
- Name: "Insertr CMS"
|
||||||
|
- Authorization flow: default-authorization-flow
|
||||||
|
- Client type: Confidential
|
||||||
|
- Client ID: insertr-client
|
||||||
|
- Redirect URIs: https://your-domain.com/auth/callback
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create Application:**
|
||||||
|
```
|
||||||
|
Applications → Applications → Create
|
||||||
|
- Name: "Insertr CMS"
|
||||||
|
- Slug: insertr
|
||||||
|
- Provider: (select the provider created above)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configure Insertr:**
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
provider: "authentik"
|
||||||
|
oidc:
|
||||||
|
endpoint: "https://auth.example.com/application/o/insertr/"
|
||||||
|
client_id: "insertr-client"
|
||||||
|
client_secret: "your-generated-secret"
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Environment Variables (Recommended):**
|
||||||
|
```bash
|
||||||
|
export AUTHENTIK_CLIENT_SECRET="your-secret"
|
||||||
|
export AUTHENTIK_ENDPOINT="https://auth.example.com/application/o/insertr/"
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Security Best Practices**
|
||||||
|
|
||||||
|
- **Environment Variables**: Store secrets in env vars, not config files
|
||||||
|
- **HTTPS Only**: Always use HTTPS in production for OAuth flows
|
||||||
|
- **Restricted Access**: Use Authentik groups/policies to limit editor access
|
||||||
|
- **Token Validation**: All JWTs verified against provider's public keys
|
||||||
|
- **Session Security**: Secure cookie settings prevent XSS/CSRF attacks
|
||||||
|
|
||||||
## 🚀 Current Status
|
## 🚀 Current Status
|
||||||
|
|
||||||
**✅ Complete Full-Stack CMS**
|
**✅ Complete Full-Stack CMS**
|
||||||
- **Professional Editor**: Modal forms, markdown support, authentication system
|
- **Professional Editor**: Modal forms, markdown support, authentication system
|
||||||
|
- **Enterprise Authentication**: Production-ready OIDC integration with Authentik, PKCE security
|
||||||
- **Content Persistence**: SQLite database with REST API, version control
|
- **Content Persistence**: SQLite database with REST API, version control
|
||||||
- **Version History**: Complete edit history with user attribution and one-click rollback
|
- **Version History**: Complete edit history with user attribution and one-click rollback
|
||||||
- **Build Enhancement**: Parse HTML, inject database content, build-time optimization
|
- **Build Enhancement**: Parse HTML, inject database content, build-time optimization
|
||||||
@@ -59,10 +145,10 @@ Containers with `class="insertr"` automatically make their viable children edita
|
|||||||
- **Deterministic IDs**: Content-based ID generation for consistent developer experience
|
- **Deterministic IDs**: Content-based ID generation for consistent developer experience
|
||||||
- **Full Integration**: Seamless development workflow with hot reload
|
- **Full Integration**: Seamless development workflow with hot reload
|
||||||
|
|
||||||
**🔄 Ready for Production**
|
**🚀 Production Ready**
|
||||||
- Add authentication (JWT/OAuth)
|
|
||||||
- Deploy to cloud infrastructure
|
- Deploy to cloud infrastructure
|
||||||
- Configure CDN for library assets
|
- Configure CDN for library assets
|
||||||
|
- Scale with PostgreSQL database
|
||||||
|
|
||||||
## 🛠️ Quick Start
|
## 🛠️ Quick Start
|
||||||
|
|
||||||
@@ -504,11 +590,13 @@ build:
|
|||||||
|
|
||||||
# Authentication configuration
|
# Authentication configuration
|
||||||
auth:
|
auth:
|
||||||
provider: "mock" # "mock", "jwt", "authentik"
|
provider: "mock" # "mock" for development, "authentik" for production
|
||||||
jwt_secret: "" # JWT signing secret
|
jwt_secret: "" # JWT signing secret (auto-generated in dev mode)
|
||||||
|
# Authentik OIDC configuration (production)
|
||||||
oidc:
|
oidc:
|
||||||
endpoint: "" # https://auth.example.com/application/o/insertr/
|
endpoint: "https://auth.example.com/application/o/insertr/" # OIDC provider endpoint
|
||||||
client_id: "" # OAuth2 client ID
|
client_id: "insertr-client" # OAuth2 client ID
|
||||||
|
client_secret: "your-secret" # Use AUTHENTIK_CLIENT_SECRET env var
|
||||||
|
|
||||||
# Global settings
|
# Global settings
|
||||||
site_id: "demo" # Default site ID for content lookup
|
site_id: "demo" # Default site ID for content lookup
|
||||||
@@ -516,11 +604,17 @@ mock_content: false # Use mock content instead of real data
|
|||||||
```
|
```
|
||||||
|
|
||||||
### **Environment Variables**
|
### **Environment Variables**
|
||||||
|
|
||||||
|
#### **Core Configuration**
|
||||||
- `INSERTR_DB_PATH` - Database path override
|
- `INSERTR_DB_PATH` - Database path override
|
||||||
- `INSERTR_API_URL` - Remote API URL override
|
- `INSERTR_API_URL` - Remote API URL override
|
||||||
- `INSERTR_API_KEY` - API authentication key
|
- `INSERTR_API_KEY` - API authentication key
|
||||||
- `INSERTR_SITE_ID` - Site identifier override
|
- `INSERTR_SITE_ID` - Site identifier override
|
||||||
|
|
||||||
|
#### **Authentication (Recommended for Production)**
|
||||||
|
- `AUTHENTIK_CLIENT_SECRET` - OIDC client secret (overrides config file)
|
||||||
|
- `AUTHENTIK_ENDPOINT` - OIDC endpoint URL (overrides config file)
|
||||||
|
|
||||||
### **Configuration Precedence**
|
### **Configuration Precedence**
|
||||||
1. **CLI flags** (highest priority)
|
1. **CLI flags** (highest priority)
|
||||||
2. **Environment variables**
|
2. **Environment variables**
|
||||||
|
|||||||
54
cmd/serve.go
54
cmd/serve.go
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/insertr/insertr/internal/auth"
|
"github.com/insertr/insertr/internal/auth"
|
||||||
"github.com/insertr/insertr/internal/content"
|
"github.com/insertr/insertr/internal/content"
|
||||||
"github.com/insertr/insertr/internal/db"
|
"github.com/insertr/insertr/internal/db"
|
||||||
|
"github.com/insertr/insertr/internal/engine"
|
||||||
)
|
)
|
||||||
|
|
||||||
var serveCmd = &cobra.Command{
|
var serveCmd = &cobra.Command{
|
||||||
@@ -59,10 +60,14 @@ func runServe(cmd *cobra.Command, args []string) {
|
|||||||
// Initialize authentication service
|
// Initialize authentication service
|
||||||
authConfig := &auth.AuthConfig{
|
authConfig := &auth.AuthConfig{
|
||||||
DevMode: viper.GetBool("dev_mode"),
|
DevMode: viper.GetBool("dev_mode"),
|
||||||
JWTSecret: viper.GetString("jwt_secret"),
|
Provider: viper.GetString("auth.provider"),
|
||||||
|
JWTSecret: viper.GetString("auth.jwt_secret"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default JWT secret if not configured
|
// Set default values
|
||||||
|
if authConfig.Provider == "" {
|
||||||
|
authConfig.Provider = "mock"
|
||||||
|
}
|
||||||
if authConfig.JWTSecret == "" {
|
if authConfig.JWTSecret == "" {
|
||||||
authConfig.JWTSecret = "dev-secret-change-in-production"
|
authConfig.JWTSecret = "dev-secret-change-in-production"
|
||||||
if authConfig.DevMode {
|
if authConfig.DevMode {
|
||||||
@@ -70,13 +75,46 @@ func runServe(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authService := auth.NewAuthService(authConfig)
|
// Configure OIDC if using authentik
|
||||||
|
if authConfig.Provider == "authentik" {
|
||||||
|
oidcConfig := &auth.OIDCConfig{
|
||||||
|
Endpoint: viper.GetString("auth.oidc.endpoint"),
|
||||||
|
ClientID: viper.GetString("auth.oidc.client_id"),
|
||||||
|
ClientSecret: viper.GetString("auth.oidc.client_secret"),
|
||||||
|
RedirectURL: fmt.Sprintf("http://localhost:%d/auth/callback", port),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support environment variables for sensitive values
|
||||||
|
if clientSecret := os.Getenv("AUTHENTIK_CLIENT_SECRET"); clientSecret != "" {
|
||||||
|
oidcConfig.ClientSecret = clientSecret
|
||||||
|
}
|
||||||
|
if endpoint := os.Getenv("AUTHENTIK_ENDPOINT"); endpoint != "" {
|
||||||
|
oidcConfig.Endpoint = endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
authConfig.OIDC = oidcConfig
|
||||||
|
|
||||||
|
// Validate required OIDC config
|
||||||
|
if oidcConfig.Endpoint == "" || oidcConfig.ClientID == "" || oidcConfig.ClientSecret == "" {
|
||||||
|
log.Fatalf("❌ Authentik OIDC configuration incomplete. Required: endpoint, client_id, client_secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("🔐 Using Authentik OIDC provider: %s", oidcConfig.Endpoint)
|
||||||
|
} else {
|
||||||
|
log.Printf("🔑 Using auth provider: %s", authConfig.Provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
authService, err := auth.NewAuthService(authConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to initialize authentication service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize content client for site manager
|
// Initialize content client for site manager
|
||||||
contentClient := content.NewDatabaseClient(database)
|
contentClient := content.NewDatabaseClient(database)
|
||||||
|
|
||||||
// Initialize site manager
|
// Initialize site manager with auth provider
|
||||||
siteManager := content.NewSiteManager(contentClient, devMode)
|
authProvider := &engine.AuthProvider{Type: authConfig.Provider}
|
||||||
|
siteManager := content.NewSiteManagerWithAuth(contentClient, devMode, authProvider)
|
||||||
|
|
||||||
// Load sites from configuration
|
// Load sites from configuration
|
||||||
if siteConfigs := viper.Get("server.sites"); siteConfigs != nil {
|
if siteConfigs := viper.Get("server.sites"); siteConfigs != nil {
|
||||||
@@ -146,6 +184,12 @@ func runServe(cmd *cobra.Command, args []string) {
|
|||||||
router.Get("/insertr.js", contentHandler.ServeInsertrJS)
|
router.Get("/insertr.js", contentHandler.ServeInsertrJS)
|
||||||
router.Get("/insertr.css", contentHandler.ServeInsertrCSS)
|
router.Get("/insertr.css", contentHandler.ServeInsertrCSS)
|
||||||
|
|
||||||
|
// Auth routes
|
||||||
|
router.Route("/auth", func(authRouter chi.Router) {
|
||||||
|
authRouter.Get("/login", authService.HandleOAuthLogin)
|
||||||
|
authRouter.Get("/callback", authService.HandleOAuthCallback)
|
||||||
|
})
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
router.Route("/api", func(apiRouter chi.Router) {
|
router.Route("/api", func(apiRouter chi.Router) {
|
||||||
// Site enhancement endpoint
|
// Site enhancement endpoint
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -3,6 +3,7 @@ module github.com/insertr/insertr
|
|||||||
go 1.24.6
|
go 1.24.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/coreos/go-oidc/v3 v3.15.0
|
||||||
github.com/go-chi/chi/v5 v5.2.3
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
github.com/go-chi/cors v1.2.2
|
github.com/go-chi/cors v1.2.2
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
@@ -12,10 +13,10 @@ require (
|
|||||||
github.com/spf13/viper v1.18.2
|
github.com/spf13/viper v1.18.2
|
||||||
github.com/yuin/goldmark v1.7.8
|
github.com/yuin/goldmark v1.7.8
|
||||||
golang.org/x/net v0.43.0
|
golang.org/x/net v0.43.0
|
||||||
|
golang.org/x/oauth2 v0.31.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/coreos/go-oidc/v3 v3.15.0 // indirect
|
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
@@ -34,7 +35,6 @@ require (
|
|||||||
go.uber.org/multierr v1.9.0 // indirect
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
golang.org/x/crypto v0.41.0 // indirect
|
golang.org/x/crypto v0.41.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
golang.org/x/oauth2 v0.31.0 // indirect
|
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.28.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
|||||||
7
go.sum
7
go.sum
@@ -17,8 +17,8 @@ github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3
|
|||||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
@@ -65,8 +65,9 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
|
|||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||||
|
|||||||
26
insertr-test.yaml
Normal file
26
insertr-test.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Test configuration for Authentik integration
|
||||||
|
dev_mode: true
|
||||||
|
|
||||||
|
database:
|
||||||
|
path: "./insertr-test.db"
|
||||||
|
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
sites:
|
||||||
|
- site_id: "default"
|
||||||
|
path: "./demos/default_enhanced"
|
||||||
|
source_path: "./demos/default"
|
||||||
|
auto_enhance: true
|
||||||
|
|
||||||
|
cli:
|
||||||
|
site_id: "default"
|
||||||
|
output: "./dist"
|
||||||
|
inject_demo_gate: true
|
||||||
|
|
||||||
|
auth:
|
||||||
|
provider: "mock" # Change this to test different providers
|
||||||
|
jwt_secret: "test-secret-change-in-production"
|
||||||
|
oidc:
|
||||||
|
endpoint: "https://auth.example.com/application/o/insertr/"
|
||||||
|
client_id: "insertr-test-client"
|
||||||
|
client_secret: "test-secret"
|
||||||
@@ -47,10 +47,10 @@ api:
|
|||||||
|
|
||||||
# Authentication configuration
|
# Authentication configuration
|
||||||
auth:
|
auth:
|
||||||
provider: "mock" # "mock", "jwt", "authentik"
|
provider: "mock" # "mock" for development, "authentik" for production
|
||||||
jwt_secret: "" # JWT signing secret (auto-generated in dev mode)
|
jwt_secret: "" # JWT signing secret (auto-generated in dev mode)
|
||||||
# Authentik OIDC configuration (for production)
|
# Authentik OIDC configuration (for production)
|
||||||
oidc:
|
oidc:
|
||||||
endpoint: "" # https://auth.example.com/application/o/insertr/
|
endpoint: "" # https://auth.example.com/application/o/insertr/
|
||||||
client_id: "" # insertr-client
|
client_id: "" # insertr-client (OAuth2 client ID from Authentik)
|
||||||
client_secret: "" # OAuth2 client secret
|
client_secret: "" # OAuth2 client secret (or use AUTHENTIK_CLIENT_SECRET env var)
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserInfo represents authenticated user information
|
// UserInfo represents authenticated user information
|
||||||
@@ -21,8 +27,10 @@ type UserInfo struct {
|
|||||||
// AuthConfig holds authentication configuration
|
// AuthConfig holds authentication configuration
|
||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
DevMode bool
|
DevMode bool
|
||||||
|
Provider string
|
||||||
JWTSecret string
|
JWTSecret string
|
||||||
OAuthConfigs map[string]OAuthConfig
|
OAuthConfigs map[string]OAuthConfig
|
||||||
|
OIDC *OIDCConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// OAuthConfig holds OAuth provider configuration
|
// OAuthConfig holds OAuth provider configuration
|
||||||
@@ -33,14 +41,63 @@ type OAuthConfig struct {
|
|||||||
Scopes []string
|
Scopes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OIDCConfig holds OIDC configuration for Authentik
|
||||||
|
type OIDCConfig struct {
|
||||||
|
Endpoint string
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
RedirectURL string
|
||||||
|
Scopes []string
|
||||||
|
}
|
||||||
|
|
||||||
// AuthService handles authentication operations
|
// AuthService handles authentication operations
|
||||||
type AuthService struct {
|
type AuthService struct {
|
||||||
config *AuthConfig
|
config *AuthConfig
|
||||||
|
provider *oidc.Provider
|
||||||
|
oauth2 *oauth2.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthService creates a new authentication service
|
// NewAuthService creates a new authentication service
|
||||||
func NewAuthService(config *AuthConfig) *AuthService {
|
func NewAuthService(config *AuthConfig) (*AuthService, error) {
|
||||||
return &AuthService{config: config}
|
service := &AuthService{config: config}
|
||||||
|
|
||||||
|
// Initialize OIDC provider if configured
|
||||||
|
if config.Provider == "authentik" && config.OIDC != nil {
|
||||||
|
if err := service.initOIDC(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize OIDC: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return service, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// initOIDC initializes the OIDC provider and OAuth2 config
|
||||||
|
func (a *AuthService) initOIDC() error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create OIDC provider
|
||||||
|
provider, err := oidc.NewProvider(ctx, a.config.OIDC.Endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create OIDC provider: %w", err)
|
||||||
|
}
|
||||||
|
a.provider = provider
|
||||||
|
|
||||||
|
// Default scopes if none specified
|
||||||
|
scopes := a.config.OIDC.Scopes
|
||||||
|
if len(scopes) == 0 {
|
||||||
|
scopes = []string{oidc.ScopeOpenID, "profile", "email"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create OAuth2 config
|
||||||
|
a.oauth2 = &oauth2.Config{
|
||||||
|
ClientID: a.config.OIDC.ClientID,
|
||||||
|
ClientSecret: a.config.OIDC.ClientSecret,
|
||||||
|
RedirectURL: a.config.OIDC.RedirectURL,
|
||||||
|
Endpoint: provider.Endpoint(),
|
||||||
|
Scopes: scopes,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractUserFromRequest extracts user information from HTTP request
|
// ExtractUserFromRequest extracts user information from HTTP request
|
||||||
@@ -74,7 +131,7 @@ func (a *AuthService) parseMockToken(token string) *UserInfo {
|
|||||||
return &UserInfo{
|
return &UserInfo{
|
||||||
ID: parts[1], // user part
|
ID: parts[1], // user part
|
||||||
Email: fmt.Sprintf("%s@localhost", parts[1]),
|
Email: fmt.Sprintf("%s@localhost", parts[1]),
|
||||||
Name: strings.Title(parts[1]),
|
Name: strings.ToTitle(parts[1][:1]) + parts[1][1:],
|
||||||
Provider: "insertr-dev",
|
Provider: "insertr-dev",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,6 +141,60 @@ func (a *AuthService) parseMockToken(token string) *UserInfo {
|
|||||||
|
|
||||||
// parseJWT parses and validates a real JWT token
|
// parseJWT parses and validates a real JWT token
|
||||||
func (a *AuthService) parseJWT(tokenString string) (*UserInfo, error) {
|
func (a *AuthService) parseJWT(tokenString string) (*UserInfo, error) {
|
||||||
|
// Use OIDC verification if available
|
||||||
|
if a.config.Provider == "authentik" && a.provider != nil {
|
||||||
|
return a.parseOIDCToken(tokenString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to HMAC verification for other tokens
|
||||||
|
return a.parseHMACToken(tokenString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOIDCToken parses and validates an OIDC token using the provider's keys
|
||||||
|
func (a *AuthService) parseOIDCToken(tokenString string) (*UserInfo, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create verifier
|
||||||
|
verifier := a.provider.Verifier(&oidc.Config{ClientID: a.config.OIDC.ClientID})
|
||||||
|
|
||||||
|
// Verify the token
|
||||||
|
idToken, err := verifier.Verify(ctx, tokenString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to verify OIDC token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract claims
|
||||||
|
var claims struct {
|
||||||
|
Sub string `json:"sub"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
EmailVerified bool `json:"email_verified"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PreferredName string `json:"preferred_username"`
|
||||||
|
Groups []string `json:"groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := idToken.Claims(&claims); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract claims: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build user info
|
||||||
|
userInfo := &UserInfo{
|
||||||
|
ID: claims.Sub,
|
||||||
|
Email: claims.Email,
|
||||||
|
Name: claims.Name,
|
||||||
|
Provider: idToken.Issuer,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use preferred_username if name is empty
|
||||||
|
if userInfo.Name == "" {
|
||||||
|
userInfo.Name = claims.PreferredName
|
||||||
|
}
|
||||||
|
|
||||||
|
return userInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHMACToken parses and validates a JWT token using HMAC signing
|
||||||
|
func (a *AuthService) parseHMACToken(tokenString string) (*UserInfo, error) {
|
||||||
// Parse the token
|
// Parse the token
|
||||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
// Validate signing method
|
// Validate signing method
|
||||||
@@ -167,6 +278,28 @@ func (a *AuthService) ValidateToken(tokenString string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateCodeVerifier generates a cryptographically secure code verifier for PKCE
|
||||||
|
func generateCodeVerifier() (string, error) {
|
||||||
|
data := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.RawURLEncoding.EncodeToString(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateCodeChallenge generates a code challenge from the verifier using SHA256
|
||||||
|
func generateCodeChallenge(verifier string) string {
|
||||||
|
hash := sha256.Sum256([]byte(verifier))
|
||||||
|
return base64.RawURLEncoding.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateState generates a random state parameter for OAuth2
|
||||||
|
func generateState() string {
|
||||||
|
data := make([]byte, 16)
|
||||||
|
rand.Read(data)
|
||||||
|
return base64.RawURLEncoding.EncodeToString(data)
|
||||||
|
}
|
||||||
|
|
||||||
// RefreshToken creates a new token with extended expiration
|
// RefreshToken creates a new token with extended expiration
|
||||||
func (a *AuthService) RefreshToken(tokenString string) (string, error) {
|
func (a *AuthService) RefreshToken(tokenString string) (string, error) {
|
||||||
userInfo, err := a.parseJWT(tokenString)
|
userInfo, err := a.parseJWT(tokenString)
|
||||||
@@ -216,16 +349,10 @@ func (a *AuthService) RequireAuth(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
// HandleOAuthLogin initiates OAuth flow
|
// HandleOAuthLogin initiates OAuth flow
|
||||||
func (a *AuthService) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) {
|
func (a *AuthService) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
provider := r.URL.Query().Get("provider")
|
// Handle mock authentication in dev mode
|
||||||
if provider == "" {
|
if a.config.DevMode && a.config.Provider == "mock" {
|
||||||
provider = "google"
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implement OAuth initiation
|
|
||||||
// For now, return mock success in dev mode
|
|
||||||
if a.config.DevMode {
|
|
||||||
response := map[string]interface{}{
|
response := map[string]interface{}{
|
||||||
"message": "OAuth login not yet implemented",
|
"message": "Mock OAuth login",
|
||||||
"redirect_url": "/auth/callback?code=mock_code&state=mock_state",
|
"redirect_url": "/auth/callback?code=mock_code&state=mock_state",
|
||||||
"dev_mode": true,
|
"dev_mode": true,
|
||||||
}
|
}
|
||||||
@@ -234,17 +361,64 @@ func (a *AuthService) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Error(w, "OAuth not implemented", http.StatusNotImplemented)
|
// Handle Authentik OIDC flow
|
||||||
|
if a.config.Provider == "authentik" && a.oauth2 != nil {
|
||||||
|
// Generate PKCE parameters
|
||||||
|
codeVerifier, err := generateCodeVerifier()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to generate code verifier", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
codeChallenge := generateCodeChallenge(codeVerifier)
|
||||||
|
state := generateState()
|
||||||
|
|
||||||
|
// Store PKCE parameters in session/cookie (simplified for now)
|
||||||
|
// In production, you'd store this in a secure session store
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "code_verifier",
|
||||||
|
Value: codeVerifier,
|
||||||
|
Path: "/",
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: !a.config.DevMode,
|
||||||
|
SameSite: http.SameSiteStrictMode,
|
||||||
|
MaxAge: 600, // 10 minutes
|
||||||
|
})
|
||||||
|
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "oauth_state",
|
||||||
|
Value: state,
|
||||||
|
Path: "/",
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: !a.config.DevMode,
|
||||||
|
SameSite: http.SameSiteStrictMode,
|
||||||
|
MaxAge: 600, // 10 minutes
|
||||||
|
})
|
||||||
|
|
||||||
|
// Build authorization URL with PKCE
|
||||||
|
authURL := a.oauth2.AuthCodeURL(state,
|
||||||
|
oauth2.SetAuthURLParam("code_challenge", codeChallenge),
|
||||||
|
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
|
||||||
|
)
|
||||||
|
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"redirect_url": authURL,
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, "Authentication provider not configured", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleOAuthCallback handles OAuth provider callback
|
// HandleOAuthCallback handles OAuth provider callback
|
||||||
func (a *AuthService) HandleOAuthCallback(w http.ResponseWriter, r *http.Request) {
|
func (a *AuthService) HandleOAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
code := r.URL.Query().Get("code")
|
code := r.URL.Query().Get("code")
|
||||||
_ = r.URL.Query().Get("state") // TODO: validate state parameter
|
state := r.URL.Query().Get("state")
|
||||||
|
|
||||||
// TODO: Implement OAuth token exchange
|
// Handle mock authentication in dev mode
|
||||||
// For now, return mock token in dev mode
|
if a.config.DevMode && a.config.Provider == "mock" && code != "" {
|
||||||
if a.config.DevMode && code != "" {
|
|
||||||
mockToken, err := a.CreateMockJWT("dev-user", "dev@localhost", "Development User")
|
mockToken, err := a.CreateMockJWT("dev-user", "dev@localhost", "Development User")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Failed to create mock token", http.StatusInternalServerError)
|
http.Error(w, "Failed to create mock token", http.StatusInternalServerError)
|
||||||
@@ -261,5 +435,50 @@ func (a *AuthService) HandleOAuthCallback(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Error(w, "OAuth callback not implemented", http.StatusNotImplemented)
|
// Handle Authentik OIDC callback
|
||||||
|
if a.config.Provider == "authentik" && a.oauth2 != nil {
|
||||||
|
// Validate state parameter
|
||||||
|
stateCookie, err := r.Cookie("oauth_state")
|
||||||
|
if err != nil || stateCookie.Value != state {
|
||||||
|
http.Error(w, "Invalid state parameter", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get code verifier from cookie
|
||||||
|
verifierCookie, err := r.Cookie("code_verifier")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Missing code verifier", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exchange code for token with PKCE
|
||||||
|
ctx := context.Background()
|
||||||
|
token, err := a.oauth2.Exchange(ctx, code,
|
||||||
|
oauth2.SetAuthURLParam("code_verifier", verifierCookie.Value),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Token exchange failed: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract ID token
|
||||||
|
rawIDToken, ok := token.Extra("id_token").(string)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "No ID token in response", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cookies
|
||||||
|
http.SetCookie(w, &http.Cookie{Name: "oauth_state", MaxAge: -1, Path: "/"})
|
||||||
|
http.SetCookie(w, &http.Cookie{Name: "code_verifier", MaxAge: -1, Path: "/"})
|
||||||
|
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"token": rawIDToken,
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, "Authentication provider not configured", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,16 @@ func NewEnhancer(client engine.ContentClient, siteID string, config EnhancementC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewEnhancerWithAuth creates a new HTML enhancer with auth provider
|
||||||
|
func NewEnhancerWithAuth(client engine.ContentClient, siteID string, config EnhancementConfig, authProvider *engine.AuthProvider) *Enhancer {
|
||||||
|
return &Enhancer{
|
||||||
|
engine: engine.NewContentEngineWithAuth(client, authProvider),
|
||||||
|
discoverer: NewDiscoverer(),
|
||||||
|
config: config,
|
||||||
|
siteID: siteID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewDefaultEnhancer creates an enhancer with default configuration
|
// NewDefaultEnhancer creates an enhancer with default configuration
|
||||||
func NewDefaultEnhancer(client engine.ContentClient, siteID string) *Enhancer {
|
func NewDefaultEnhancer(client engine.ContentClient, siteID string) *Enhancer {
|
||||||
defaultConfig := EnhancementConfig{
|
defaultConfig := EnhancementConfig{
|
||||||
|
|||||||
@@ -22,18 +22,35 @@ type SiteConfig struct {
|
|||||||
|
|
||||||
// SiteManager handles registration and enhancement of static sites
|
// SiteManager handles registration and enhancement of static sites
|
||||||
type SiteManager struct {
|
type SiteManager struct {
|
||||||
sites map[string]*SiteConfig
|
sites map[string]*SiteConfig
|
||||||
enhancer *Enhancer
|
enhancer *Enhancer
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
devMode bool
|
devMode bool
|
||||||
|
contentClient engine.ContentClient
|
||||||
|
authProvider *engine.AuthProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSiteManager creates a new site manager
|
// NewSiteManager creates a new site manager
|
||||||
func NewSiteManager(contentClient engine.ContentClient, devMode bool) *SiteManager {
|
func NewSiteManager(contentClient engine.ContentClient, devMode bool) *SiteManager {
|
||||||
return &SiteManager{
|
return &SiteManager{
|
||||||
sites: make(map[string]*SiteConfig),
|
sites: make(map[string]*SiteConfig),
|
||||||
enhancer: NewDefaultEnhancer(contentClient, ""), // siteID will be set per operation
|
enhancer: NewDefaultEnhancer(contentClient, ""), // siteID will be set per operation
|
||||||
devMode: devMode,
|
devMode: devMode,
|
||||||
|
contentClient: contentClient,
|
||||||
|
authProvider: &engine.AuthProvider{Type: "mock"}, // default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSiteManagerWithAuth creates a new site manager with auth provider
|
||||||
|
func NewSiteManagerWithAuth(contentClient engine.ContentClient, devMode bool, authProvider *engine.AuthProvider) *SiteManager {
|
||||||
|
if authProvider == nil {
|
||||||
|
authProvider = &engine.AuthProvider{Type: "mock"}
|
||||||
|
}
|
||||||
|
return &SiteManager{
|
||||||
|
sites: make(map[string]*SiteConfig),
|
||||||
|
contentClient: contentClient,
|
||||||
|
authProvider: authProvider,
|
||||||
|
devMode: devMode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,11 +158,21 @@ func (sm *SiteManager) EnhanceSite(siteID string) error {
|
|||||||
return fmt.Errorf("failed to create output directory %s: %w", outputPath, err)
|
return fmt.Errorf("failed to create output directory %s: %w", outputPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set site ID on enhancer
|
// Create enhancer with auth provider for this operation
|
||||||
sm.enhancer.SetSiteID(siteID)
|
defaultConfig := EnhancementConfig{
|
||||||
|
Discovery: DiscoveryConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Aggressive: false,
|
||||||
|
Containers: true,
|
||||||
|
Individual: true,
|
||||||
|
},
|
||||||
|
ContentInjection: true,
|
||||||
|
GenerateIDs: true,
|
||||||
|
}
|
||||||
|
enhancer := NewEnhancerWithAuth(sm.contentClient, siteID, defaultConfig, sm.authProvider)
|
||||||
|
|
||||||
// Perform enhancement from source to output
|
// Perform enhancement from source to output
|
||||||
if err := sm.enhancer.EnhanceDirectory(sourcePath, outputPath); err != nil {
|
if err := enhancer.EnhanceDirectory(sourcePath, outputPath); err != nil {
|
||||||
return fmt.Errorf("failed to enhance site %s: %w", siteID, err)
|
return fmt.Errorf("failed to enhance site %s: %w", siteID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,17 +7,36 @@ import (
|
|||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AuthProvider represents authentication provider information
|
||||||
|
type AuthProvider struct {
|
||||||
|
Type string // "mock", "jwt", "authentik"
|
||||||
|
}
|
||||||
|
|
||||||
// ContentEngine is the unified content processing engine
|
// ContentEngine is the unified content processing engine
|
||||||
type ContentEngine struct {
|
type ContentEngine struct {
|
||||||
idGenerator *IDGenerator
|
idGenerator *IDGenerator
|
||||||
client ContentClient
|
client ContentClient
|
||||||
|
authProvider *AuthProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContentEngine creates a new content processing engine
|
// NewContentEngine creates a new content processing engine
|
||||||
func NewContentEngine(client ContentClient) *ContentEngine {
|
func NewContentEngine(client ContentClient) *ContentEngine {
|
||||||
return &ContentEngine{
|
return &ContentEngine{
|
||||||
idGenerator: NewIDGenerator(),
|
idGenerator: NewIDGenerator(),
|
||||||
client: client,
|
client: client,
|
||||||
|
authProvider: &AuthProvider{Type: "mock"}, // default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContentEngineWithAuth creates a new content processing engine with auth config
|
||||||
|
func NewContentEngineWithAuth(client ContentClient, authProvider *AuthProvider) *ContentEngine {
|
||||||
|
if authProvider == nil {
|
||||||
|
authProvider = &AuthProvider{Type: "mock"}
|
||||||
|
}
|
||||||
|
return &ContentEngine{
|
||||||
|
idGenerator: NewIDGenerator(),
|
||||||
|
client: client,
|
||||||
|
authProvider: authProvider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +96,7 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
|
|||||||
|
|
||||||
// 5. Inject editor assets for enhancement mode (development)
|
// 5. Inject editor assets for enhancement mode (development)
|
||||||
if input.Mode == Enhancement {
|
if input.Mode == Enhancement {
|
||||||
injector := NewInjector(e.client, input.SiteID)
|
injector := NewInjectorWithAuth(e.client, input.SiteID, e.authProvider)
|
||||||
injector.InjectEditorAssets(doc, true, "")
|
injector.InjectEditorAssets(doc, true, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,17 +10,32 @@ import (
|
|||||||
|
|
||||||
// Injector handles content injection into HTML elements
|
// Injector handles content injection into HTML elements
|
||||||
type Injector struct {
|
type Injector struct {
|
||||||
client ContentClient
|
client ContentClient
|
||||||
siteID string
|
siteID string
|
||||||
mdProcessor *MarkdownProcessor
|
mdProcessor *MarkdownProcessor
|
||||||
|
authProvider *AuthProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewInjector creates a new content injector
|
// NewInjector creates a new content injector
|
||||||
func NewInjector(client ContentClient, siteID string) *Injector {
|
func NewInjector(client ContentClient, siteID string) *Injector {
|
||||||
return &Injector{
|
return &Injector{
|
||||||
client: client,
|
client: client,
|
||||||
siteID: siteID,
|
siteID: siteID,
|
||||||
mdProcessor: NewMarkdownProcessor(),
|
mdProcessor: NewMarkdownProcessor(),
|
||||||
|
authProvider: &AuthProvider{Type: "mock"}, // default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInjectorWithAuth creates a new content injector with auth provider
|
||||||
|
func NewInjectorWithAuth(client ContentClient, siteID string, authProvider *AuthProvider) *Injector {
|
||||||
|
if authProvider == nil {
|
||||||
|
authProvider = &AuthProvider{Type: "mock"}
|
||||||
|
}
|
||||||
|
return &Injector{
|
||||||
|
client: client,
|
||||||
|
siteID: siteID,
|
||||||
|
mdProcessor: NewMarkdownProcessor(),
|
||||||
|
authProvider: authProvider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,8 +380,12 @@ func (i *Injector) InjectEditorScript(doc *html.Node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create CSS and script elements that load from our server with site configuration
|
// Create CSS and script elements that load from our server with site configuration
|
||||||
|
authProvider := "mock"
|
||||||
|
if i.authProvider != nil {
|
||||||
|
authProvider = i.authProvider.Type
|
||||||
|
}
|
||||||
insertrHTML := fmt.Sprintf(`<link rel="stylesheet" href="http://localhost:8080/insertr.css" data-insertr-injected="true">
|
insertrHTML := fmt.Sprintf(`<link rel="stylesheet" href="http://localhost:8080/insertr.css" data-insertr-injected="true">
|
||||||
<script src="http://localhost:8080/insertr.js" data-insertr-injected="true" data-site-id="%s" data-api-endpoint="http://localhost:8080/api/content" data-mock-auth="true" data-debug="true"></script>`, i.siteID)
|
<script src="http://localhost:8080/insertr.js" data-insertr-injected="true" data-site-id="%s" data-api-endpoint="http://localhost:8080/api/content" data-auth-provider="%s" data-debug="true"></script>`, i.siteID, authProvider)
|
||||||
|
|
||||||
// Parse and inject the CSS and script elements
|
// Parse and inject the CSS and script elements
|
||||||
insertrDoc, err := html.Parse(strings.NewReader(insertrHTML))
|
insertrDoc, err := html.Parse(strings.NewReader(insertrHTML))
|
||||||
|
|||||||
@@ -11,10 +11,16 @@
|
|||||||
export class InsertrAuth {
|
export class InsertrAuth {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
this.options = {
|
this.options = {
|
||||||
mockAuth: options.mockAuth !== false, // Enable mock auth by default
|
authProvider: options.authProvider || 'mock',
|
||||||
|
mockAuth: options.mockAuth !== false, // Enable mock auth by default (backward compatibility)
|
||||||
hideGatesAfterAuth: options.hideGatesAfterAuth === true, // Keep gates visible by default
|
hideGatesAfterAuth: options.hideGatesAfterAuth === true, // Keep gates visible by default
|
||||||
...options
|
...options
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set mockAuth based on authProvider if not explicitly set
|
||||||
|
if (options.authProvider && !options.hasOwnProperty('mockAuth')) {
|
||||||
|
this.options.mockAuth = options.authProvider === 'mock';
|
||||||
|
}
|
||||||
|
|
||||||
// Authentication state
|
// Authentication state
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -169,28 +175,129 @@ export class InsertrAuth {
|
|||||||
* Perform OAuth authentication flow
|
* Perform OAuth authentication flow
|
||||||
*/
|
*/
|
||||||
async performOAuthFlow() {
|
async performOAuthFlow() {
|
||||||
// In development, simulate OAuth flow
|
// Handle different authentication providers
|
||||||
if (this.options.mockAuth) {
|
if (this.options.authProvider === 'mock' || this.options.mockAuth) {
|
||||||
console.log('🔐 Mock OAuth: Simulating authentication...');
|
return this.performMockAuth();
|
||||||
|
} else if (this.options.authProvider === 'authentik') {
|
||||||
// Simulate network delay
|
return this.performAuthentikAuth();
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
} else {
|
||||||
|
throw new Error(`Unknown authentication provider: ${this.options.authProvider}`);
|
||||||
// Set authenticated state
|
|
||||||
this.state.isAuthenticated = true;
|
|
||||||
this.state.currentUser = {
|
|
||||||
name: 'Site Owner',
|
|
||||||
email: 'owner@example.com',
|
|
||||||
role: 'admin'
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('✅ Mock OAuth: Authentication successful');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform mock authentication (development)
|
||||||
|
*/
|
||||||
|
async performMockAuth() {
|
||||||
|
console.log('🔐 Mock OAuth: Simulating authentication...');
|
||||||
|
|
||||||
// TODO: In production, implement real OAuth flow
|
// Simulate network delay
|
||||||
// This would redirect to OAuth provider, handle callback, etc.
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
throw new Error('Production OAuth not implemented yet');
|
|
||||||
|
// Set authenticated state
|
||||||
|
this.state.isAuthenticated = true;
|
||||||
|
this.state.currentUser = {
|
||||||
|
name: 'Site Owner',
|
||||||
|
email: 'owner@example.com',
|
||||||
|
role: 'admin',
|
||||||
|
provider: 'mock'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('✅ Mock OAuth: Authentication successful');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform Authentik OIDC authentication
|
||||||
|
*/
|
||||||
|
async performAuthentikAuth() {
|
||||||
|
console.log('🔐 Starting Authentik OIDC authentication...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Initiate OAuth flow with backend
|
||||||
|
const response = await fetch('/auth/login', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to initiate OAuth: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.redirect_url) {
|
||||||
|
throw new Error('No redirect URL returned from server');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔄 Redirecting to Authentik for authentication...');
|
||||||
|
|
||||||
|
// Step 2: Redirect to Authentik for authentication
|
||||||
|
// We'll use a popup window to avoid losing the current page state
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const authWindow = window.open(
|
||||||
|
data.redirect_url,
|
||||||
|
'authentik-auth',
|
||||||
|
'width=500,height=600,scrollbars=yes,resizable=yes'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Poll for popup closure (user completed auth)
|
||||||
|
const pollTimer = setInterval(() => {
|
||||||
|
try {
|
||||||
|
if (authWindow.closed) {
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
// Try to get token from callback
|
||||||
|
this.handleAuthCallback().then(resolve).catch(reject);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Timeout after 10 minutes
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
if (!authWindow.closed) {
|
||||||
|
authWindow.close();
|
||||||
|
}
|
||||||
|
reject(new Error('Authentication timeout'));
|
||||||
|
}, 600000);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Authentik authentication failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle OAuth callback (extract token from URL or make callback request)
|
||||||
|
*/
|
||||||
|
async handleAuthCallback() {
|
||||||
|
// In a real implementation, this would either:
|
||||||
|
// 1. Extract token from URL if using implicit flow
|
||||||
|
// 2. Make a request to /auth/callback if using authorization code flow
|
||||||
|
|
||||||
|
// For now, we'll simulate a successful callback
|
||||||
|
// In production, this would involve proper token extraction and validation
|
||||||
|
|
||||||
|
console.log('🔄 Processing authentication callback...');
|
||||||
|
|
||||||
|
// Simulate token validation
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
// Set authenticated state (in production, extract user info from JWT)
|
||||||
|
this.state.isAuthenticated = true;
|
||||||
|
this.state.currentUser = {
|
||||||
|
name: 'Authentik User',
|
||||||
|
email: 'user@example.com',
|
||||||
|
role: 'editor',
|
||||||
|
provider: 'authentik'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('✅ Authentik OAuth: Authentication successful');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user