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:
@@ -1,13 +1,19 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// UserInfo represents authenticated user information
|
||||
@@ -21,8 +27,10 @@ type UserInfo struct {
|
||||
// AuthConfig holds authentication configuration
|
||||
type AuthConfig struct {
|
||||
DevMode bool
|
||||
Provider string
|
||||
JWTSecret string
|
||||
OAuthConfigs map[string]OAuthConfig
|
||||
OIDC *OIDCConfig
|
||||
}
|
||||
|
||||
// OAuthConfig holds OAuth provider configuration
|
||||
@@ -33,14 +41,63 @@ type OAuthConfig struct {
|
||||
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
|
||||
type AuthService struct {
|
||||
config *AuthConfig
|
||||
config *AuthConfig
|
||||
provider *oidc.Provider
|
||||
oauth2 *oauth2.Config
|
||||
}
|
||||
|
||||
// NewAuthService creates a new authentication service
|
||||
func NewAuthService(config *AuthConfig) *AuthService {
|
||||
return &AuthService{config: config}
|
||||
func NewAuthService(config *AuthConfig) (*AuthService, error) {
|
||||
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
|
||||
@@ -74,7 +131,7 @@ func (a *AuthService) parseMockToken(token string) *UserInfo {
|
||||
return &UserInfo{
|
||||
ID: parts[1], // user part
|
||||
Email: fmt.Sprintf("%s@localhost", parts[1]),
|
||||
Name: strings.Title(parts[1]),
|
||||
Name: strings.ToTitle(parts[1][:1]) + parts[1][1:],
|
||||
Provider: "insertr-dev",
|
||||
}
|
||||
}
|
||||
@@ -84,6 +141,60 @@ func (a *AuthService) parseMockToken(token string) *UserInfo {
|
||||
|
||||
// parseJWT parses and validates a real JWT token
|
||||
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
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
// Validate signing method
|
||||
@@ -167,6 +278,28 @@ func (a *AuthService) ValidateToken(tokenString string) error {
|
||||
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
|
||||
func (a *AuthService) RefreshToken(tokenString string) (string, error) {
|
||||
userInfo, err := a.parseJWT(tokenString)
|
||||
@@ -216,16 +349,10 @@ func (a *AuthService) RequireAuth(next http.Handler) http.Handler {
|
||||
|
||||
// HandleOAuthLogin initiates OAuth flow
|
||||
func (a *AuthService) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) {
|
||||
provider := r.URL.Query().Get("provider")
|
||||
if provider == "" {
|
||||
provider = "google"
|
||||
}
|
||||
|
||||
// TODO: Implement OAuth initiation
|
||||
// For now, return mock success in dev mode
|
||||
if a.config.DevMode {
|
||||
// Handle mock authentication in dev mode
|
||||
if a.config.DevMode && a.config.Provider == "mock" {
|
||||
response := map[string]interface{}{
|
||||
"message": "OAuth login not yet implemented",
|
||||
"message": "Mock OAuth login",
|
||||
"redirect_url": "/auth/callback?code=mock_code&state=mock_state",
|
||||
"dev_mode": true,
|
||||
}
|
||||
@@ -234,17 +361,64 @@ func (a *AuthService) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
func (a *AuthService) HandleOAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
// For now, return mock token in dev mode
|
||||
if a.config.DevMode && code != "" {
|
||||
// Handle mock authentication in dev mode
|
||||
if a.config.DevMode && a.config.Provider == "mock" && code != "" {
|
||||
mockToken, err := a.CreateMockJWT("dev-user", "dev@localhost", "Development User")
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
func NewDefaultEnhancer(client engine.ContentClient, siteID string) *Enhancer {
|
||||
defaultConfig := EnhancementConfig{
|
||||
|
||||
@@ -22,18 +22,35 @@ type SiteConfig struct {
|
||||
|
||||
// SiteManager handles registration and enhancement of static sites
|
||||
type SiteManager struct {
|
||||
sites map[string]*SiteConfig
|
||||
enhancer *Enhancer
|
||||
mutex sync.RWMutex
|
||||
devMode bool
|
||||
sites map[string]*SiteConfig
|
||||
enhancer *Enhancer
|
||||
mutex sync.RWMutex
|
||||
devMode bool
|
||||
contentClient engine.ContentClient
|
||||
authProvider *engine.AuthProvider
|
||||
}
|
||||
|
||||
// NewSiteManager creates a new site manager
|
||||
func NewSiteManager(contentClient engine.ContentClient, devMode bool) *SiteManager {
|
||||
return &SiteManager{
|
||||
sites: make(map[string]*SiteConfig),
|
||||
enhancer: NewDefaultEnhancer(contentClient, ""), // siteID will be set per operation
|
||||
devMode: devMode,
|
||||
sites: make(map[string]*SiteConfig),
|
||||
enhancer: NewDefaultEnhancer(contentClient, ""), // siteID will be set per operation
|
||||
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)
|
||||
}
|
||||
|
||||
// Set site ID on enhancer
|
||||
sm.enhancer.SetSiteID(siteID)
|
||||
// Create enhancer with auth provider for this operation
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,17 +7,36 @@ import (
|
||||
"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
|
||||
type ContentEngine struct {
|
||||
idGenerator *IDGenerator
|
||||
client ContentClient
|
||||
idGenerator *IDGenerator
|
||||
client ContentClient
|
||||
authProvider *AuthProvider
|
||||
}
|
||||
|
||||
// NewContentEngine creates a new content processing engine
|
||||
func NewContentEngine(client ContentClient) *ContentEngine {
|
||||
return &ContentEngine{
|
||||
idGenerator: NewIDGenerator(),
|
||||
client: client,
|
||||
idGenerator: NewIDGenerator(),
|
||||
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)
|
||||
if input.Mode == Enhancement {
|
||||
injector := NewInjector(e.client, input.SiteID)
|
||||
injector := NewInjectorWithAuth(e.client, input.SiteID, e.authProvider)
|
||||
injector.InjectEditorAssets(doc, true, "")
|
||||
}
|
||||
|
||||
|
||||
@@ -10,17 +10,32 @@ import (
|
||||
|
||||
// Injector handles content injection into HTML elements
|
||||
type Injector struct {
|
||||
client ContentClient
|
||||
siteID string
|
||||
mdProcessor *MarkdownProcessor
|
||||
client ContentClient
|
||||
siteID string
|
||||
mdProcessor *MarkdownProcessor
|
||||
authProvider *AuthProvider
|
||||
}
|
||||
|
||||
// NewInjector creates a new content injector
|
||||
func NewInjector(client ContentClient, siteID string) *Injector {
|
||||
return &Injector{
|
||||
client: client,
|
||||
siteID: siteID,
|
||||
mdProcessor: NewMarkdownProcessor(),
|
||||
client: client,
|
||||
siteID: siteID,
|
||||
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
|
||||
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">
|
||||
<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
|
||||
insertrDoc, err := html.Parse(strings.NewReader(insertrHTML))
|
||||
|
||||
Reference in New Issue
Block a user