feat: add env-based configuration and token-in-cookie auth
Implement 12-factor configuration via environment variables and token-in-cookie authentication for Gitea API access. - internal/config/config.go: reads GITEA_URL, GITEA_TOKEN, LISTEN_ADDR, SESSION_SECRET from environment with validation - internal/auth/cookie.go: HMAC-signed HTTP-only cookie for storing Gitea API tokens (Secure, SameSite=Strict) - internal/middleware/auth.go: extracts token from cookie, injects into request context, redirects unauthenticated users to /settings - internal/middleware/logging.go: structured JSON request logging - internal/handlers/settings.go: settings page for entering/removing Gitea API token with mobile-first dark UI - cmd/server/main.go: integrated config, auth middleware, and settings Includes unit tests for config loading, cookie signing/verification, and auth middleware bypass/redirect logic. Closes leeworks-agents/gitea-mobile#2 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Config holds application configuration loaded from environment variables.
|
||||
type Config struct {
|
||||
// GiteaURL is the base URL of the Gitea instance.
|
||||
GiteaURL string
|
||||
|
||||
// GiteaToken is the default API token (optional; users can set their own via cookie).
|
||||
GiteaToken string
|
||||
|
||||
// ListenAddr is the server listen address.
|
||||
ListenAddr string
|
||||
|
||||
// SessionSecret is the HMAC key for signing session cookies.
|
||||
SessionSecret string
|
||||
}
|
||||
|
||||
// Load reads configuration from environment variables.
|
||||
// Returns an error if required variables are missing.
|
||||
func Load() (*Config, error) {
|
||||
cfg := &Config{
|
||||
GiteaURL: os.Getenv("GITEA_URL"),
|
||||
GiteaToken: os.Getenv("GITEA_TOKEN"),
|
||||
ListenAddr: os.Getenv("LISTEN_ADDR"),
|
||||
SessionSecret: os.Getenv("SESSION_SECRET"),
|
||||
}
|
||||
|
||||
if cfg.ListenAddr == "" {
|
||||
cfg.ListenAddr = ":8080"
|
||||
}
|
||||
|
||||
if cfg.GiteaURL == "" {
|
||||
return nil, fmt.Errorf("GITEA_URL environment variable is required")
|
||||
}
|
||||
|
||||
if cfg.SessionSecret == "" {
|
||||
return nil, fmt.Errorf("SESSION_SECRET environment variable is required")
|
||||
}
|
||||
|
||||
if len(cfg.SessionSecret) < 32 {
|
||||
return nil, fmt.Errorf("SESSION_SECRET must be at least 32 characters")
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoad_Success(t *testing.T) {
|
||||
os.Setenv("GITEA_URL", "https://gitea.example.com")
|
||||
os.Setenv("SESSION_SECRET", "test-secret-that-is-at-least-32-chars-long")
|
||||
os.Setenv("LISTEN_ADDR", ":9090")
|
||||
os.Setenv("GITEA_TOKEN", "test-token")
|
||||
defer func() {
|
||||
os.Unsetenv("GITEA_URL")
|
||||
os.Unsetenv("SESSION_SECRET")
|
||||
os.Unsetenv("LISTEN_ADDR")
|
||||
os.Unsetenv("GITEA_TOKEN")
|
||||
}()
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.GiteaURL != "https://gitea.example.com" {
|
||||
t.Errorf("GiteaURL = %q, want %q", cfg.GiteaURL, "https://gitea.example.com")
|
||||
}
|
||||
if cfg.ListenAddr != ":9090" {
|
||||
t.Errorf("ListenAddr = %q, want %q", cfg.ListenAddr, ":9090")
|
||||
}
|
||||
if cfg.GiteaToken != "test-token" {
|
||||
t.Errorf("GiteaToken = %q, want %q", cfg.GiteaToken, "test-token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_DefaultListenAddr(t *testing.T) {
|
||||
os.Setenv("GITEA_URL", "https://gitea.example.com")
|
||||
os.Setenv("SESSION_SECRET", "test-secret-that-is-at-least-32-chars-long")
|
||||
os.Unsetenv("LISTEN_ADDR")
|
||||
defer func() {
|
||||
os.Unsetenv("GITEA_URL")
|
||||
os.Unsetenv("SESSION_SECRET")
|
||||
}()
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.ListenAddr != ":8080" {
|
||||
t.Errorf("ListenAddr = %q, want %q", cfg.ListenAddr, ":8080")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_MissingGiteaURL(t *testing.T) {
|
||||
os.Unsetenv("GITEA_URL")
|
||||
os.Setenv("SESSION_SECRET", "test-secret-that-is-at-least-32-chars-long")
|
||||
defer os.Unsetenv("SESSION_SECRET")
|
||||
|
||||
_, err := Load()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing GITEA_URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_MissingSessionSecret(t *testing.T) {
|
||||
os.Setenv("GITEA_URL", "https://gitea.example.com")
|
||||
os.Unsetenv("SESSION_SECRET")
|
||||
defer os.Unsetenv("GITEA_URL")
|
||||
|
||||
_, err := Load()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing SESSION_SECRET")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_ShortSessionSecret(t *testing.T) {
|
||||
os.Setenv("GITEA_URL", "https://gitea.example.com")
|
||||
os.Setenv("SESSION_SECRET", "tooshort")
|
||||
defer func() {
|
||||
os.Unsetenv("GITEA_URL")
|
||||
os.Unsetenv("SESSION_SECRET")
|
||||
}()
|
||||
|
||||
_, err := Load()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for short SESSION_SECRET")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user