Files
gems/opal-task/cmd/server.go
T
joakim d51c6da18d feat: replace mock mode with real backend dev mode
Add --dev flag to `opal server start` that disables auth (injects
userID=1 for all requests) and exposes a /auth/dev-session endpoint,
so the frontend can develop against a real backend without OAuth
config. Remove VITE_MOCK_MODE and all mock data/branches from the
frontend stores. Add scripts/dev.sh to start both services locally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 17:07:34 +01:00

213 lines
6.7 KiB
Go

package cmd
import (
"fmt"
"os"
"strings"
"git.jnss.me/joakim/opal/internal/api"
"git.jnss.me/joakim/opal/internal/engine"
"github.com/spf13/cobra"
)
// validateServerConfig checks that all required environment variables are set for server mode
func validateServerConfig() error {
// Check required OAuth/JWT environment variables
required := map[string]string{
"OAUTH_CLIENT_ID": os.Getenv("OAUTH_CLIENT_ID"),
"OAUTH_CLIENT_SECRET": os.Getenv("OAUTH_CLIENT_SECRET"),
"OAUTH_ISSUER": os.Getenv("OAUTH_ISSUER"),
"OAUTH_REDIRECT_URI": os.Getenv("OAUTH_REDIRECT_URI"),
"JWT_SECRET": os.Getenv("JWT_SECRET"),
}
missing := []string{}
for key, value := range required {
if value == "" {
missing = append(missing, key)
}
}
if len(missing) > 0 {
return fmt.Errorf("missing required environment variables for server mode:\n %s\n\nPlease set these variables before starting the server.", strings.Join(missing, "\n "))
}
// Validate data directory is writable
dataDir, err := engine.GetDataDir()
if err != nil {
return fmt.Errorf("cannot resolve data directory: %w", err)
}
// Check if directory exists and is writable
info, err := os.Stat(dataDir)
if err != nil {
// Directory doesn't exist yet, check parent
parent := dataDir
for parent != "/" && parent != "." {
parent = strings.TrimSuffix(parent, "/")
idx := strings.LastIndex(parent, "/")
if idx <= 0 {
parent = "/"
break
}
parent = parent[:idx]
if parent == "" {
parent = "/"
}
if pInfo, pErr := os.Stat(parent); pErr == nil {
if !pInfo.IsDir() {
return fmt.Errorf("parent path is not a directory: %s", parent)
}
// Check write permission by trying to create data dir
if err := os.MkdirAll(dataDir, 0755); err != nil {
return fmt.Errorf("data directory not writable: %s (error: %v)", dataDir, err)
}
break
}
}
} else if !info.IsDir() {
return fmt.Errorf("data directory path exists but is not a directory: %s", dataDir)
}
return nil
}
var serverCmd = &cobra.Command{
Use: "server",
Short: "Server management commands",
Long: `Commands for running and managing the opal API server`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// Override root's PersistentPreRun - server handles its own initialization
// But still apply directory overrides from flags
if configDirFlag != "" {
engine.SetConfigDirOverride(configDirFlag)
}
if dataDirFlag != "" {
engine.SetDataDirOverride(dataDirFlag)
}
},
}
var serverStartCmd = &cobra.Command{
Use: "start",
Short: "Start the opal API server",
Long: `Starts the opal-task REST API server for remote access.
Examples:
opal server start
opal server start --addr :8080
opal server start --dev
opal server start --db /var/lib/opal/opal.db`,
Run: func(cmd *cobra.Command, args []string) {
addr, _ := cmd.Flags().GetString("addr")
dbPath, _ := cmd.Flags().GetString("db")
devMode, _ := cmd.Flags().GetBool("dev")
// Override DB path if specified
if dbPath != "" {
os.Setenv("OPAL_DB_PATH", dbPath)
}
// In dev mode, skip OAuth config validation
if devMode {
fmt.Println("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
fmt.Println("┃ ⚠ DEV MODE ENABLED ⚠ ┃")
fmt.Println("┃ Auth disabled — all requests use uid 1 ┃")
fmt.Println("┃ Do NOT use in production! ┃")
fmt.Println("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
} else {
if err := validateServerConfig(); err != nil {
fmt.Fprintf(os.Stderr, "Server configuration validation failed:\n%v\n", err)
os.Exit(1)
}
}
// Load config (read-only — uses defaults if no opal.yml exists)
if _, err := engine.LoadConfig(); err != nil {
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
os.Exit(1)
}
// Initialize database
if err := engine.InitDB(); err != nil {
fmt.Fprintf(os.Stderr, "Error initializing database: %v\n", err)
os.Exit(1)
}
defer engine.CloseDB()
// Create and start server
server := api.NewServer(addr, devMode)
if err := server.Start(); err != nil {
fmt.Fprintf(os.Stderr, "Error starting server: %v\n", err)
os.Exit(1)
}
},
}
var keygenCmd = &cobra.Command{
Use: "keygen",
Short: "Generate API key for server authentication",
Long: `Generate a new API key for authenticating with the opal server.
This command should be run on the server with direct database access.
The generated key will be displayed once and cannot be retrieved again.
Examples:
opal server keygen --name "My Phone"
opal server keygen --name "Laptop" --db /var/lib/opal/opal.db`,
Run: func(cmd *cobra.Command, args []string) {
name, _ := cmd.Flags().GetString("name")
dbPath, _ := cmd.Flags().GetString("db")
if name == "" {
fmt.Fprintf(os.Stderr, "Error: --name is required\n")
os.Exit(1)
}
// Override DB path if specified
if dbPath != "" {
os.Setenv("OPAL_DB_PATH", dbPath)
}
if err := engine.InitDB(); err != nil {
fmt.Fprintf(os.Stderr, "Error initializing database: %v\n", err)
os.Exit(1)
}
defer engine.CloseDB()
key, err := engine.GenerateAPIKey(name)
if err != nil {
fmt.Fprintf(os.Stderr, "Error generating API key: %v\n", err)
os.Exit(1)
}
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
fmt.Println("API Key Generated Successfully")
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
fmt.Printf("Name: %s\n", name)
fmt.Printf("Key: %s\n", key)
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
fmt.Println("")
fmt.Println("⚠️ IMPORTANT: Save this key securely!")
fmt.Println(" It will not be displayed again.")
fmt.Println("")
fmt.Println("To configure a client:")
fmt.Printf(" opal sync init --url https://opal.yourdomain.com --key %s\n", key)
},
}
func init() {
rootCmd.AddCommand(serverCmd)
serverCmd.AddCommand(serverStartCmd)
serverCmd.AddCommand(keygenCmd)
serverStartCmd.Flags().StringP("addr", "a", ":8080", "Server address")
serverStartCmd.Flags().StringP("db", "d", "", "Database path (default: config directory)")
serverStartCmd.Flags().Bool("dev", false, "Enable dev mode (no auth, no OAuth env vars required)")
keygenCmd.Flags().StringP("name", "n", "", "Name for this API key (e.g., device name)")
keygenCmd.Flags().StringP("db", "d", "", "Database path (default: config directory)")
keygenCmd.MarkFlagRequired("name")
}