Initial commit: Custom Start Page application with authentication and DynamoDB storage

This commit is contained in:
2026-02-18 22:06:43 -05:00
commit 7175ff14ba
47 changed files with 7592 additions and 0 deletions

114
pkg/config/config.go Normal file
View File

@@ -0,0 +1,114 @@
package config
import (
"fmt"
"os"
"strconv"
)
// Config holds all application configuration
type Config struct {
Server ServerConfig
Database DatabaseConfig
OAuth OAuthConfig
Session SessionConfig
}
// ServerConfig holds server-related configuration
type ServerConfig struct {
Port string
Host string
}
// DatabaseConfig holds DynamoDB configuration
type DatabaseConfig struct {
Region string
Endpoint string // For DynamoDB local
TablePrefix string
UseLocalDB bool
}
// OAuthConfig holds OAuth provider configurations
type OAuthConfig struct {
Google GoogleOAuthConfig
}
// GoogleOAuthConfig holds Google OAuth configuration
type GoogleOAuthConfig struct {
ClientID string
ClientSecret string
RedirectURL string
}
// SessionConfig holds session management configuration
type SessionConfig struct {
SecretKey string
MaxAge int // in seconds
}
// Load loads configuration from environment variables
func Load() (*Config, error) {
cfg := &Config{
Server: ServerConfig{
Port: getEnv("PORT", "8080"),
Host: getEnv("HOST", "localhost"),
},
Database: DatabaseConfig{
Region: getEnv("AWS_REGION", "us-east-1"),
Endpoint: getEnv("DYNAMODB_ENDPOINT", "http://localhost:8000"),
TablePrefix: getEnv("TABLE_PREFIX", "startpage_"),
UseLocalDB: getEnvBool("USE_LOCAL_DB", true),
},
OAuth: OAuthConfig{
Google: GoogleOAuthConfig{
ClientID: getEnv("GOOGLE_CLIENT_ID", ""),
ClientSecret: getEnv("GOOGLE_CLIENT_SECRET", ""),
RedirectURL: getEnv("GOOGLE_REDIRECT_URL", "http://localhost:8080/auth/callback/google"),
},
},
Session: SessionConfig{
SecretKey: getEnv("SESSION_SECRET", "change-me-in-production"),
MaxAge: getEnvInt("SESSION_MAX_AGE", 86400*7), // 7 days default
},
}
// Validate required fields
if cfg.OAuth.Google.ClientID == "" {
return nil, fmt.Errorf("GOOGLE_CLIENT_ID is required")
}
if cfg.OAuth.Google.ClientSecret == "" {
return nil, fmt.Errorf("GOOGLE_CLIENT_SECRET is required")
}
return cfg, nil
}
// getEnv gets an environment variable or returns a default value
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// getEnvBool gets a boolean environment variable or returns a default value
func getEnvBool(key string, defaultValue bool) bool {
if value := os.Getenv(key); value != "" {
boolVal, err := strconv.ParseBool(value)
if err == nil {
return boolVal
}
}
return defaultValue
}
// getEnvInt gets an integer environment variable or returns a default value
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
intVal, err := strconv.Atoi(value)
if err == nil {
return intVal
}
}
return defaultValue
}

191
pkg/config/config_test.go Normal file
View File

@@ -0,0 +1,191 @@
package config
import (
"os"
"testing"
)
func TestLoad(t *testing.T) {
// Set required environment variables
os.Setenv("GOOGLE_CLIENT_ID", "test-client-id")
os.Setenv("GOOGLE_CLIENT_SECRET", "test-client-secret")
defer func() {
os.Unsetenv("GOOGLE_CLIENT_ID")
os.Unsetenv("GOOGLE_CLIENT_SECRET")
}()
cfg, err := Load()
if err != nil {
t.Fatalf("Load() failed: %v", err)
}
// Test default values
if cfg.Server.Port != "8080" {
t.Errorf("Expected default port 8080, got %s", cfg.Server.Port)
}
if cfg.Server.Host != "localhost" {
t.Errorf("Expected default host localhost, got %s", cfg.Server.Host)
}
if cfg.Database.Region != "us-east-1" {
t.Errorf("Expected default region us-east-1, got %s", cfg.Database.Region)
}
if cfg.OAuth.Google.ClientID != "test-client-id" {
t.Errorf("Expected client ID test-client-id, got %s", cfg.OAuth.Google.ClientID)
}
}
func TestLoadMissingOAuthCredentials(t *testing.T) {
// Ensure OAuth credentials are not set
os.Unsetenv("GOOGLE_CLIENT_ID")
os.Unsetenv("GOOGLE_CLIENT_SECRET")
_, err := Load()
if err == nil {
t.Error("Expected error when OAuth credentials are missing, got nil")
}
}
func TestGetEnv(t *testing.T) {
tests := []struct {
name string
key string
defaultValue string
envValue string
expected string
}{
{
name: "returns environment variable when set",
key: "TEST_KEY",
defaultValue: "default",
envValue: "custom",
expected: "custom",
},
{
name: "returns default when environment variable not set",
key: "TEST_KEY_NOT_SET",
defaultValue: "default",
envValue: "",
expected: "default",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.envValue != "" {
os.Setenv(tt.key, tt.envValue)
defer os.Unsetenv(tt.key)
}
result := getEnv(tt.key, tt.defaultValue)
if result != tt.expected {
t.Errorf("Expected %s, got %s", tt.expected, result)
}
})
}
}
func TestGetEnvBool(t *testing.T) {
tests := []struct {
name string
key string
defaultValue bool
envValue string
expected bool
}{
{
name: "returns true when set to true",
key: "TEST_BOOL",
defaultValue: false,
envValue: "true",
expected: true,
},
{
name: "returns false when set to false",
key: "TEST_BOOL",
defaultValue: true,
envValue: "false",
expected: false,
},
{
name: "returns default when not set",
key: "TEST_BOOL_NOT_SET",
defaultValue: true,
envValue: "",
expected: true,
},
{
name: "returns default when invalid value",
key: "TEST_BOOL",
defaultValue: true,
envValue: "invalid",
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.envValue != "" {
os.Setenv(tt.key, tt.envValue)
defer os.Unsetenv(tt.key)
} else {
os.Unsetenv(tt.key)
}
result := getEnvBool(tt.key, tt.defaultValue)
if result != tt.expected {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}
func TestGetEnvInt(t *testing.T) {
tests := []struct {
name string
key string
defaultValue int
envValue string
expected int
}{
{
name: "returns integer when valid",
key: "TEST_INT",
defaultValue: 100,
envValue: "200",
expected: 200,
},
{
name: "returns default when not set",
key: "TEST_INT_NOT_SET",
defaultValue: 100,
envValue: "",
expected: 100,
},
{
name: "returns default when invalid value",
key: "TEST_INT",
defaultValue: 100,
envValue: "invalid",
expected: 100,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.envValue != "" {
os.Setenv(tt.key, tt.envValue)
defer os.Unsetenv(tt.key)
} else {
os.Unsetenv(tt.key)
}
result := getEnvInt(tt.key, tt.defaultValue)
if result != tt.expected {
t.Errorf("Expected %d, got %d", tt.expected, result)
}
})
}
}