refactor: use a boostrap service to bootstrap the app

This commit is contained in:
Stavros
2025-08-25 22:03:06 +03:00
parent dbadb096b4
commit 659d3561e0
9 changed files with 369 additions and 273 deletions

View File

@@ -0,0 +1,246 @@
package bootstrap
import (
"fmt"
"strings"
"tinyauth/internal/config"
"tinyauth/internal/controller"
"tinyauth/internal/middleware"
"tinyauth/internal/service"
"tinyauth/internal/utils"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
)
type Controller interface {
SetupRoutes()
}
type Middleware interface {
Middleware() gin.HandlerFunc
Init() error
Name() string
}
type Service interface {
Init() error
}
type BootstrapApp struct {
Config config.Config
}
func NewBootstrapApp(config config.Config) *BootstrapApp {
return &BootstrapApp{
Config: config,
}
}
func (app *BootstrapApp) Setup() error {
// Parse users
users, err := utils.GetUsers(app.Config.Users, app.Config.UsersFile)
if err != nil {
return err
}
// Get domain
domain, err := utils.GetUpperDomain(app.Config.AppURL)
if err != nil {
return err
}
// Cookie names
cookieId := utils.GenerateIdentifier(strings.Split(domain, ".")[0])
sessionCookieName := fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId)
csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId)
redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
// Secrets
encryptionSecret, err := utils.DeriveKey(app.Config.Secret, "encryption")
if err != nil {
return err
}
hmacSecret, err := utils.DeriveKey(app.Config.Secret, "hmac")
if err != nil {
return err
}
// Create configs
authConfig := service.AuthServiceConfig{
Users: users,
OauthWhitelist: app.Config.OAuthWhitelist,
SessionExpiry: app.Config.SessionExpiry,
SecureCookie: app.Config.SecureCookie,
Domain: domain,
LoginTimeout: app.Config.LoginTimeout,
LoginMaxRetries: app.Config.LoginMaxRetries,
SessionCookieName: sessionCookieName,
HMACSecret: hmacSecret,
EncryptionSecret: encryptionSecret,
}
// Setup services
var ldapService *service.LdapService
if app.Config.LdapAddress != "" {
ldapConfig := service.LdapServiceConfig{
Address: app.Config.LdapAddress,
BindDN: app.Config.LdapBindDN,
BindPassword: app.Config.LdapBindPassword,
BaseDN: app.Config.LdapBaseDN,
Insecure: app.Config.LdapInsecure,
SearchFilter: app.Config.LdapSearchFilter,
}
ldapService = service.NewLdapService(ldapConfig)
err := ldapService.Init()
if err != nil {
ldapService = nil
}
}
dockerService := service.NewDockerService()
authService := service.NewAuthService(authConfig, dockerService, ldapService)
oauthBrokerService := service.NewOAuthBrokerService(app.getOAuthBrokerConfig())
// Initialize services
services := []Service{
dockerService,
authService,
oauthBrokerService,
}
for _, svc := range services {
if svc != nil {
err := svc.Init()
if err != nil {
return err
}
}
}
// Configured providers
var configuredProviders []string
if authService.UserAuthConfigured() || ldapService != nil {
configuredProviders = append(configuredProviders, "username")
}
configuredProviders = append(configuredProviders, oauthBrokerService.GetConfiguredServices()...)
if len(configuredProviders) == 0 {
return fmt.Errorf("no authentication providers configured")
}
// Create engine
engine := gin.New()
router := engine.Group("/api")
// Create middlewares
var middlewares []Middleware
contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{
Domain: domain,
}, authService, oauthBrokerService)
uiMiddleware := middleware.NewUIMiddleware()
zerologMiddleware := middleware.NewZerologMiddleware()
middlewares = append(middlewares, contextMiddleware, uiMiddleware, zerologMiddleware)
for _, middleware := range middlewares {
log.Debug().Str("middleware", middleware.Name()).Msg("Initializing middleware")
err := middleware.Init()
if err != nil {
return fmt.Errorf("failed to initialize %s middleware: %w", middleware.Name(), err)
}
router.Use(middleware.Middleware())
}
// Create controllers
contextController := controller.NewContextController(controller.ContextControllerConfig{
ConfiguredProviders: configuredProviders,
DisableContinue: app.Config.DisableContinue,
Title: app.Config.Title,
GenericName: app.Config.GenericName,
Domain: domain,
ForgotPasswordMessage: app.Config.FogotPasswordMessage,
BackgroundImage: app.Config.BackgroundImage,
OAuthAutoRedirect: app.Config.OAuthAutoRedirect,
}, router)
oauthController := controller.NewOAuthController(controller.OAuthControllerConfig{
AppURL: app.Config.AppURL,
SecureCookie: app.Config.SecureCookie,
CSRFCookieName: csrfCookieName,
RedirectCookieName: redirectCookieName,
}, router, authService, oauthBrokerService)
proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
AppURL: app.Config.AppURL,
}, router, dockerService, authService)
userController := controller.NewUserController(controller.UserControllerConfig{
Domain: domain,
}, router, authService)
healthController := controller.NewHealthController(router)
// Setup routes
controller := []Controller{
contextController,
oauthController,
proxyController,
userController,
healthController,
}
for _, ctrl := range controller {
log.Debug().Msgf("Setting up %T routes", ctrl)
ctrl.SetupRoutes()
}
// Start server
address := fmt.Sprintf("%s:%d", app.Config.Address, app.Config.Port)
log.Info().Msgf("Starting server on %s", address)
if err := engine.Run(address); err != nil {
log.Fatal().Err(err).Msg("Failed to start server")
}
return nil
}
// Temporary
func (app *BootstrapApp) getOAuthBrokerConfig() map[string]config.OAuthServiceConfig {
return map[string]config.OAuthServiceConfig{
"google": {
ClientID: app.Config.GoogleClientId,
ClientSecret: app.Config.GoogleClientSecret,
RedirectURL: fmt.Sprintf("%s/api/oauth/callback/google", app.Config.AppURL),
},
"github": {
ClientID: app.Config.GithubClientId,
ClientSecret: app.Config.GithubClientSecret,
RedirectURL: fmt.Sprintf("%s/api/oauth/callback/github", app.Config.AppURL),
},
"generic": {
ClientID: app.Config.GenericClientId,
ClientSecret: app.Config.GenericClientSecret,
RedirectURL: fmt.Sprintf("%s/api/oauth/callback/generic", app.Config.AppURL),
Scopes: strings.Split(app.Config.GenericScopes, ","),
AuthURL: app.Config.GenericAuthURL,
TokenURL: app.Config.GenericTokenURL,
UserinfoURL: app.Config.GenericUserURL,
InsecureSkipVerify: app.Config.GenericSkipSSL,
},
}
}

View File

@@ -12,7 +12,7 @@ var CommitHash = "n/a"
var BuildTimestamp = "n/a"
var SessionCookieName = "tinyauth-session"
var CsrfCookieName = "tinyauth-csrf"
var CSRFCookieName = "tinyauth-csrf"
var RedirectCookieName = "tinyauth-redirect"
type Config struct {
@@ -23,7 +23,7 @@ type Config struct {
AppURL string `validate:"required,url" mapstructure:"app-url"`
Users string `mapstructure:"users"`
UsersFile string `mapstructure:"users-file"`
CookieSecure bool `mapstructure:"cookie-secure"`
SecureCookie bool `mapstructure:"secure-cookie"`
GithubClientId string `mapstructure:"github-client-id"`
GithubClientSecret string `mapstructure:"github-client-secret"`
GithubClientSecretFile string `mapstructure:"github-client-secret-file"`
@@ -43,9 +43,8 @@ type Config struct {
OAuthWhitelist string `mapstructure:"oauth-whitelist"`
OAuthAutoRedirect string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"`
SessionExpiry int `mapstructure:"session-expiry"`
LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"`
LogLevel string `mapstructure:"log-level" validate:"oneof=trace debug info warn error fatal panic"`
Title string `mapstructure:"app-title"`
EnvFile string `mapstructure:"env-file"`
LoginTimeout int `mapstructure:"login-timeout"`
LoginMaxRetries int `mapstructure:"login-max-retries"`
FogotPasswordMessage string `mapstructure:"forgot-password-message"`

View File

@@ -25,7 +25,7 @@ type AuthServiceConfig struct {
Users []config.User
OauthWhitelist string
SessionExpiry int
CookieSecure bool
SecureCookie bool
Domain string
LoginTimeout int
LoginMaxRetries int
@@ -57,10 +57,11 @@ func (auth *AuthService) Init() error {
store.Options = &sessions.Options{
Path: "/",
MaxAge: auth.Config.SessionExpiry,
Secure: auth.Config.CookieSecure,
Secure: auth.Config.SecureCookie,
HttpOnly: true,
Domain: fmt.Sprintf(".%s", auth.Config.Domain),
}
auth.Store = store
return nil
}
@@ -70,7 +71,7 @@ func (auth *AuthService) GetSession(c *gin.Context) (*sessions.Session, error) {
// If there was an error getting the session, it might be invalid so let's clear it and retry
if err != nil {
log.Error().Err(err).Msg("Invalid session, clearing cookie and retrying")
c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.CookieSecure, true)
c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true)
session, err = auth.Store.Get(c.Request, auth.Config.SessionCookieName)
if err != nil {
log.Error().Err(err).Msg("Failed to get session")

View File

@@ -11,6 +11,7 @@ import (
"tinyauth/internal/config"
"golang.org/x/oauth2"
"golang.org/x/oauth2/endpoints"
)
var GithubOAuthScopes = []string{"user:email", "read:user"}
@@ -39,6 +40,7 @@ func NewGithubOAuthService(config config.OAuthServiceConfig) *GithubOAuthService
ClientSecret: config.ClientSecret,
RedirectURL: config.RedirectURL,
Scopes: GithubOAuthScopes,
Endpoint: endpoints.GitHub,
},
}
}

View File

@@ -11,6 +11,7 @@ import (
"tinyauth/internal/config"
"golang.org/x/oauth2"
"golang.org/x/oauth2/endpoints"
)
var GoogleOAuthScopes = []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"}
@@ -34,6 +35,7 @@ func NewGoogleOAuthService(config config.OAuthServiceConfig) *GoogleOAuthService
ClientSecret: config.ClientSecret,
RedirectURL: config.RedirectURL,
Scopes: GoogleOAuthScopes,
Endpoint: endpoints.Google,
},
}
}

View File

@@ -18,6 +18,7 @@ import (
"golang.org/x/crypto/hkdf"
"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
@@ -360,3 +361,22 @@ func GetContext(c *gin.Context) (config.UserContext, error) {
return *userContext, nil
}
func GetLogLevel(level string) zerolog.Level {
switch strings.ToLower(level) {
case "debug":
return zerolog.DebugLevel
case "info":
return zerolog.InfoLevel
case "warn":
return zerolog.WarnLevel
case "error":
return zerolog.ErrorLevel
case "fatal":
return zerolog.FatalLevel
case "panic":
return zerolog.PanicLevel
default:
return zerolog.InfoLevel
}
}