Move config struct to model, avoid dependancy to storage

This commit is contained in:
nemunaire 2025-05-11 23:50:37 +02:00
parent 34f1404adb
commit ee2f033b0d
33 changed files with 377 additions and 228 deletions

View file

@ -67,7 +67,7 @@ func main() {
color.NoColor = true color.NoColor = true
// Load and parse options // Load and parse options
var opts *config.Options var opts *happydns.Options
if opts, err = config.ConsolidateConfig(); err != nil { if opts, err = config.ConsolidateConfig(); err != nil {
log.Fatal(err) log.Fatal(err)
} }

View file

@ -29,18 +29,17 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/internal/storage" "git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/internal/usecase" "git.happydns.org/happyDomain/internal/usecase"
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
) )
type BackupController struct { type BackupController struct {
config *config.Options config *happydns.Options
store storage.Storage store storage.Storage
} }
func NewBackupController(cfg *config.Options, store storage.Storage) *BackupController { func NewBackupController(cfg *happydns.Options, store storage.Storage) *BackupController {
return &BackupController{ return &BackupController{
config: cfg, config: cfg,
store: store, store: store,

View file

@ -26,17 +26,16 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/internal/usecase/session" "git.happydns.org/happyDomain/internal/usecase/session"
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
) )
type SessionController struct { type SessionController struct {
config *config.Options config *happydns.Options
store session.SessionStorage store session.SessionStorage
} }
func NewSessionController(cfg *config.Options, store session.SessionStorage) *SessionController { func NewSessionController(cfg *happydns.Options, store session.SessionStorage) *SessionController {
return &SessionController{ return &SessionController{
config: cfg, config: cfg,
store: store, store: store,

View file

@ -25,11 +25,11 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"git.happydns.org/happyDomain/internal/api-admin/controller" "git.happydns.org/happyDomain/internal/api-admin/controller"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/internal/storage" "git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/model"
) )
func declareBackupRoutes(cfg *config.Options, router *gin.RouterGroup, store storage.Storage) { func declareBackupRoutes(cfg *happydns.Options, router *gin.RouterGroup, store storage.Storage) {
bc := controller.NewBackupController(cfg, store) bc := controller.NewBackupController(cfg, store)
router.POST("/backup.json", bc.BackupJSON) router.POST("/backup.json", bc.BackupJSON)

View file

@ -25,12 +25,11 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
api "git.happydns.org/happyDomain/internal/api/route" api "git.happydns.org/happyDomain/internal/api/route"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/internal/storage" "git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
) )
func DeclareRoutes(cfg *config.Options, router *gin.Engine, s storage.Storage, dependancies happydns.UsecaseDependancies) { func DeclareRoutes(cfg *happydns.Options, router *gin.Engine, s storage.Storage, dependancies happydns.UsecaseDependancies) {
apiRoutes := router.Group("/api") apiRoutes := router.Group("/api")
declareBackupRoutes(cfg, apiRoutes, s) declareBackupRoutes(cfg, apiRoutes, s)

View file

@ -25,11 +25,11 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"git.happydns.org/happyDomain/internal/api-admin/controller" "git.happydns.org/happyDomain/internal/api-admin/controller"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/internal/storage" "git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/model"
) )
func declareSessionsRoutes(cfg *config.Options, router *gin.RouterGroup, store storage.Storage) { func declareSessionsRoutes(cfg *happydns.Options, router *gin.RouterGroup, store storage.Storage) {
sc := controller.NewSessionController(cfg, store) sc := controller.NewSessionController(cfg, store)
router.DELETE("/sessions", sc.DeleteSessions) router.DELETE("/sessions", sc.DeleteSessions)

View file

@ -31,6 +31,7 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
@ -39,7 +40,6 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"git.happydns.org/happyDomain/internal/api/middleware" "git.happydns.org/happyDomain/internal/api/middleware"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
) )
@ -48,23 +48,39 @@ const (
) )
type OIDCProvider struct { type OIDCProvider struct {
config *config.Options config *happydns.Options
authService happydns.AuthenticationUsecase authService happydns.AuthenticationUsecase
oauth2config *oauth2.Config oauth2config *oauth2.Config
oidcVerifier *oidc.IDTokenVerifier oidcVerifier *oidc.IDTokenVerifier
} }
func NewOIDCProvider(cfg *config.Options, authService happydns.AuthenticationUsecase) *OIDCProvider { func GetOIDCProvider(o *happydns.Options, ctx context.Context) (*oidc.Provider, error) {
return oidc.NewProvider(ctx, strings.TrimSuffix(o.OIDCClients[0].ProviderURL.String(), "/.well-known/openid-configuration"))
}
func GetOAuth2Config(o *happydns.Options, provider *oidc.Provider) *oauth2.Config {
oauth2Config := oauth2.Config{
ClientID: o.OIDCClients[0].ClientID,
ClientSecret: o.OIDCClients[0].ClientSecret,
RedirectURL: o.GetAuthURL().String(),
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
return &oauth2Config
}
func NewOIDCProvider(cfg *happydns.Options, authService happydns.AuthenticationUsecase) *OIDCProvider {
// Initialize OIDC // Initialize OIDC
provider, err := cfg.GetOIDCProvider(context.Background()) provider, err := GetOIDCProvider(cfg, context.Background())
if err != nil { if err != nil {
log.Fatal("Unable to instantiate OIDC Provider:", err) log.Fatal("Unable to instantiate OIDC Provider:", err)
} }
oauth2Config := cfg.GetOAuth2Config(provider) oauth2Config := GetOAuth2Config(cfg, provider)
oidcVerifier := provider.Verifier(&oidc.Config{ oidcVerifier := provider.Verifier(&oidc.Config{
ClientID: config.OIDCClientID, ClientID: cfg.OIDCClients[0].ClientID,
}) })
return &OIDCProvider{ return &OIDCProvider{

View file

@ -29,22 +29,21 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"git.happydns.org/happyDomain/internal/api/controller" "git.happydns.org/happyDomain/internal/api/controller"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
) )
func DeclareAuthenticationRoutes(cfg *config.Options, baserouter, apirouter *gin.RouterGroup, dependancies happydns.UsecaseDependancies) *controller.LoginController { func DeclareAuthenticationRoutes(cfg *happydns.Options, baserouter, apirouter *gin.RouterGroup, dependancies happydns.UsecaseDependancies) *controller.LoginController {
lc := controller.NewLoginController(dependancies.AuthenticationUsecase()) lc := controller.NewLoginController(dependancies.AuthenticationUsecase())
apirouter.POST("/auth", lc.Login) apirouter.POST("/auth", lc.Login)
apirouter.POST("/auth/logout", lc.Logout) apirouter.POST("/auth/logout", lc.Logout)
if cfg.GetOIDCProviderURL() != "" { if len(cfg.OIDCClients) > 0 {
oidcp := controller.NewOIDCProvider(cfg, dependancies.AuthenticationUsecase()) oidcp := controller.NewOIDCProvider(cfg, dependancies.AuthenticationUsecase())
authRoutes := baserouter.Group("/auth") authRoutes := baserouter.Group("/auth")
providerurl, _ := url.Parse(cfg.GetOIDCProviderURL()) providerurl, _ := url.Parse(cfg.OIDCClients[0].ProviderURL.String())
authRoutes.GET("has_oidc", func(c *gin.Context) { authRoutes.GET("has_oidc", func(c *gin.Context) {
parts := strings.Split(strings.TrimSuffix(providerurl.Host, "."), ".") parts := strings.Split(strings.TrimSuffix(providerurl.Host, "."), ".")
if len(parts) > 2 { if len(parts) > 2 {

View file

@ -25,7 +25,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"git.happydns.org/happyDomain/internal/api/middleware" "git.happydns.org/happyDomain/internal/api/middleware"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
) )
@ -49,7 +48,7 @@ import (
// @name Authorization // @name Authorization
// @description Description for what is this security definition being used // @description Description for what is this security definition being used
func DeclareRoutes(cfg *config.Options, router *gin.Engine, dependancies happydns.UsecaseDependancies) { func DeclareRoutes(cfg *happydns.Options, router *gin.Engine, dependancies happydns.UsecaseDependancies) {
// Declare routes // Declare routes
baseRoutes := router.Group("") baseRoutes := router.Group("")

View file

@ -33,13 +33,13 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
admin "git.happydns.org/happyDomain/internal/api-admin/route" admin "git.happydns.org/happyDomain/internal/api-admin/route"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/internal/usecase" "git.happydns.org/happyDomain/internal/usecase"
"git.happydns.org/happyDomain/model"
) )
type Admin struct { type Admin struct {
router *gin.Engine router *gin.Engine
cfg *config.Options cfg *happydns.Options
srv *http.Server srv *http.Server
} }

View file

@ -31,7 +31,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
api "git.happydns.org/happyDomain/internal/api/route" api "git.happydns.org/happyDomain/internal/api/route"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/internal/mailer" "git.happydns.org/happyDomain/internal/mailer"
"git.happydns.org/happyDomain/internal/newsletter" "git.happydns.org/happyDomain/internal/newsletter"
"git.happydns.org/happyDomain/internal/session" "git.happydns.org/happyDomain/internal/session"
@ -59,7 +58,7 @@ type Usecases struct {
} }
type App struct { type App struct {
cfg *config.Options cfg *happydns.Options
mailer *mailer.Mailer mailer *mailer.Mailer
newsletter happydns.NewsletterSubscriptor newsletter happydns.NewsletterSubscriptor
router *gin.Engine router *gin.Engine
@ -125,7 +124,7 @@ func (a *App) ZoneUsecase() happydns.ZoneUsecase {
return a.usecases.zone return a.usecases.zone
} }
func NewApp(cfg *config.Options) *App { func NewApp(cfg *happydns.Options) *App {
app := &App{ app := &App{
cfg: cfg, cfg: cfg,
} }
@ -140,7 +139,7 @@ func NewApp(cfg *config.Options) *App {
return app return app
} }
func NewAppWithStorage(cfg *config.Options, store storage.Storage) *App { func NewAppWithStorage(cfg *happydns.Options, store storage.Storage) *App {
app := &App{ app := &App{
cfg: cfg, cfg: cfg,
store: store, store: store,
@ -191,9 +190,9 @@ func (app *App) initStorageEngine() {
} }
func (app *App) initNewsletter() { func (app *App) initNewsletter() {
if app.cfg.ListmonkURL.URL != nil { if app.cfg.ListmonkURL.String() != "" {
app.newsletter = &newsletter.ListmonkNewsletterSubscription{ app.newsletter = &newsletter.ListmonkNewsletterSubscription{
ListmonkURL: app.cfg.ListmonkURL.URL, ListmonkURL: &app.cfg.ListmonkURL,
ListmonkId: app.cfg.ListmonkId, ListmonkId: app.cfg.ListmonkId,
} }
} else { } else {

View file

@ -34,7 +34,6 @@ import (
"time" "time"
"git.happydns.org/happyDomain/internal/api/controller" "git.happydns.org/happyDomain/internal/api/controller"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/internal/storage" "git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
) )
@ -45,7 +44,7 @@ const (
) )
type insightsCollector struct { type insightsCollector struct {
cfg *config.Options cfg *happydns.Options
store storage.Storage store storage.Storage
stop chan bool stop chan bool
} }
@ -117,11 +116,12 @@ func (c *insightsCollector) collect() (*happydns.Insights, error) {
data.Config.DisableEmbeddedLogin = c.cfg.DisableEmbeddedLogin data.Config.DisableEmbeddedLogin = c.cfg.DisableEmbeddedLogin
data.Config.DisableProviders = c.cfg.DisableProviders data.Config.DisableProviders = c.cfg.DisableProviders
data.Config.DisableRegistration = c.cfg.DisableRegistration data.Config.DisableRegistration = c.cfg.DisableRegistration
data.Config.HasBaseURL = c.cfg.GetBasePath() != "" data.Config.HasBaseURL = c.cfg.BasePath != ""
data.Config.HasDevProxy = c.cfg.DevProxy != "" data.Config.HasDevProxy = c.cfg.DevProxy != ""
data.Config.HasExternalAuth = c.cfg.ExternalAuth.String() != "" data.Config.HasExternalAuth = c.cfg.ExternalAuth.String() != ""
data.Config.HasListmonkURL = c.cfg.ListmonkURL.String() != "" data.Config.HasListmonkURL = c.cfg.ListmonkURL.String() != ""
data.Config.LocalBind = strings.HasPrefix(c.cfg.Bind, "127.0.0.1:") || strings.HasPrefix(c.cfg.Bind, "[::1]:") data.Config.LocalBind = strings.HasPrefix(c.cfg.Bind, "127.0.0.1:") || strings.HasPrefix(c.cfg.Bind, "[::1]:")
data.Config.NbOidcProviders = len(c.cfg.OIDCClients)
data.Config.NoAuthActive = c.cfg.NoAuth data.Config.NoAuthActive = c.cfg.NoAuth
data.Config.NoMail = c.cfg.NoMail data.Config.NoMail = c.cfg.NoMail
data.Config.NonUnixAdminBind = strings.Contains(c.cfg.AdminBind, ":") data.Config.NonUnixAdminBind = strings.Contains(c.cfg.AdminBind, ":")

View file

@ -26,26 +26,27 @@ import (
"fmt" "fmt"
"git.happydns.org/happyDomain/internal/storage" "git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/model"
) )
// declareFlags registers flags for the structure Options. // declareFlags registers flags for the structure Options.
func (o *Options) declareFlags() { func declareFlags(o *happydns.Options) {
flag.StringVar(&o.DevProxy, "dev", o.DevProxy, "Proxify traffic to this host for static assets") flag.StringVar(&o.DevProxy, "dev", o.DevProxy, "Proxify traffic to this host for static assets")
flag.StringVar(&o.AdminBind, "admin-bind", o.AdminBind, "Bind port/socket for administration interface") flag.StringVar(&o.AdminBind, "admin-bind", o.AdminBind, "Bind port/socket for administration interface")
flag.StringVar(&o.Bind, "bind", ":8081", "Bind port/socket") flag.StringVar(&o.Bind, "bind", ":8081", "Bind port/socket")
flag.BoolVar(&o.DisableProviders, "disable-providers-edit", o.DisableProviders, "Disallow all actions on provider (add/edit/delete)") flag.BoolVar(&o.DisableProviders, "disable-providers-edit", o.DisableProviders, "Disallow all actions on provider (add/edit/delete)")
flag.BoolVar(&o.DisableRegistration, "disable-registration", o.DisableRegistration, "Forbids new account creation through public form/API (still allow registration from external services)") flag.BoolVar(&o.DisableRegistration, "disable-registration", o.DisableRegistration, "Forbids new account creation through public form/API (still allow registration from external services)")
flag.BoolVar(&o.DisableEmbeddedLogin, "disable-embedded-login", o.DisableEmbeddedLogin, "Disables the internal user/password login in favor of external-auth or OIDC") flag.BoolVar(&o.DisableEmbeddedLogin, "disable-embedded-login", o.DisableEmbeddedLogin, "Disables the internal user/password login in favor of external-auth or OIDC")
flag.Var(&o.ExternalURL, "externalurl", "Begining of the URL, before the base, that should be used eg. in mails") flag.Var(&URL{&o.ExternalURL}, "externalurl", "Begining of the URL, before the base, that should be used eg. in mails")
flag.StringVar(&o.baseURL, "baseurl", o.baseURL, "URL prepended to each URL") flag.StringVar(&o.BasePath, "baseurl", o.BasePath, "URL prepended to each URL")
flag.StringVar(&o.DefaultNameServer, "default-ns", o.DefaultNameServer, "Adress to the default name server") flag.StringVar(&o.DefaultNameServer, "default-ns", o.DefaultNameServer, "Adress to the default name server")
flag.Var(&o.StorageEngine, "storage-engine", fmt.Sprintf("Select the storage engine between %v", storage.GetStorageEngines())) flag.StringVar(&o.StorageEngine, "storage-engine", o.StorageEngine, fmt.Sprintf("Select the storage engine between %v", storage.GetStorageEngines()))
flag.BoolVar(&o.NoAuth, "no-auth", false, "Disable user access control, use default account") flag.BoolVar(&o.NoAuth, "no-auth", false, "Disable user access control, use default account")
flag.Var(&o.JWTSecretKey, "jwt-secret-key", "Secret key used to verify JWT authentication tokens (a random secret is used if undefined)") flag.Var(&JWTSecretKey{&o.JWTSecretKey}, "jwt-secret-key", "Secret key used to verify JWT authentication tokens (a random secret is used if undefined)")
flag.Var(&o.ExternalAuth, "external-auth", "Base URL to use for login and registration (use embedded forms if left empty)") flag.Var(&URL{&o.ExternalAuth}, "external-auth", "Base URL to use for login and registration (use embedded forms if left empty)")
flag.BoolVar(&o.OptOutInsights, "opt-out-insights", false, "Disable the anonymous usage statistics report. If you care about this project and don't participate in discussions, don't opt-out.") flag.BoolVar(&o.OptOutInsights, "opt-out-insights", false, "Disable the anonymous usage statistics report. If you care about this project and don't participate in discussions, don't opt-out.")
flag.Var(&o.ListmonkURL, "newsletter-server-url", "Base URL of the listmonk newsletter server") flag.Var(&URL{&o.ListmonkURL}, "newsletter-server-url", "Base URL of the listmonk newsletter server")
flag.IntVar(&o.ListmonkId, "newsletter-id", 1, "Listmonk identifier of the list receiving the new user") flag.IntVar(&o.ListmonkId, "newsletter-id", 1, "Listmonk identifier of the list receiving the new user")
flag.BoolVar(&o.NoMail, "no-mail", o.NoMail, "Disable all automatic mails, skip email verification at registration") flag.BoolVar(&o.NoMail, "no-mail", o.NoMail, "Disable all automatic mails, skip email verification at registration")
@ -60,11 +61,11 @@ func (o *Options) declareFlags() {
} }
// parseCLI parse the flags and treats extra args as configuration filename. // parseCLI parse the flags and treats extra args as configuration filename.
func (o *Options) parseCLI() error { func parseCLI(o *happydns.Options) error {
flag.Parse() flag.Parse()
for _, conf := range flag.Args() { for _, conf := range flag.Args() {
err := o.parseFile(conf) err := parseFile(o, conf)
if err != nil { if err != nil {
return err return err
} }

View file

@ -32,104 +32,30 @@ import (
"path" "path"
"strings" "strings"
"git.happydns.org/happyDomain/internal/storage" "git.happydns.org/happyDomain/model"
) )
// Options stores the configuration of the software.
type Options struct {
// AdminBind is the address:port or unix socket used to serve the admin
// API.
AdminBind string
// Bind is the address:port used to bind the main interface with API.
Bind string
// BaseURL is the relative path where begins the root of the app.
baseURL string
// DevProxy is the URL that override static assets.
DevProxy string
// DefaultNameServer is the NS server suggested by default.
DefaultNameServer string
// DisableProviders should disallow all actions on provider (add/edit/delete) through public API.
DisableProviders bool
// DisableRegistration forbids all new registration using the public form/API.
DisableRegistration bool
// DisableEmbeddedLogin disables the internal user/password login in favor of ExternalAuth or OIDC.
DisableEmbeddedLogin bool
// ExternalAuth is the URL of the login form to use instead of the embedded one.
ExternalAuth URL
// ExternalURL keeps the URL used in communications (such as email,
// ...), when it needs to use complete URL, not only relative parts.
ExternalURL URL
// JWTSecretKey stores the private key to sign and verify JWT tokens.
JWTSecretKey JWTSecretKey
// JWTSigningMethod is the signing method to check token signature.
JWTSigningMethod string
// NoAuth controls if there is user access control or not.
NoAuth bool
// OptOutInsights disable the anonymous usage statistics report.
OptOutInsights bool
// StorageEngine points to the storage engine used.
StorageEngine storage.StorageEngine
ListmonkURL URL
ListmonkId int
// MailFrom holds the content of the From field for all e-mails that
// will be send.
MailFrom mail.Address
NoMail bool
MailSMTPHost string
MailSMTPPort uint
MailSMTPUsername string
MailSMTPPassword string
MailSMTPTLSSNoVerify bool
}
// GetBaseURL returns the full url to the absolute ExternalURL, including BaseURL.
func (o *Options) GetBaseURL() string {
return fmt.Sprintf("%s%s", o.ExternalURL.URL.String(), o.baseURL)
}
// GetBasePath returns the baseURL.
func (o *Options) GetBasePath() string {
return o.baseURL
}
// ConsolidateConfig fills an Options struct by reading configuration from // ConsolidateConfig fills an Options struct by reading configuration from
// config files, environment, then command line. // config files, environment, then command line.
// //
// Should be called only one time. // Should be called only one time.
func ConsolidateConfig() (opts *Options, err error) { func ConsolidateConfig() (opts *happydns.Options, err error) {
u, _ := url.Parse("http://localhost:8081") u, _ := url.Parse("http://localhost:8081")
// Define defaults options // Define defaults options
opts = &Options{ opts = &happydns.Options{
AdminBind: "./happydomain.sock", AdminBind: "./happydomain.sock",
baseURL: "/", BasePath: "/",
Bind: ":8081", Bind: ":8081",
DefaultNameServer: "127.0.0.1:53", DefaultNameServer: "127.0.0.1:53",
ExternalURL: URL{URL: u}, ExternalURL: *u,
JWTSigningMethod: "HS512", JWTSigningMethod: "HS512",
MailFrom: mail.Address{Name: "happyDomain", Address: "happydomain@localhost"}, MailFrom: mail.Address{Name: "happyDomain", Address: "happydomain@localhost"},
MailSMTPPort: 587, MailSMTPPort: 587,
StorageEngine: storage.StorageEngine("leveldb"), StorageEngine: "leveldb",
} }
opts.declareFlags() declareFlags(opts)
// Establish a list of possible configuration file locations // Establish a list of possible configuration file locations
configLocations := []string{ configLocations := []string{
@ -146,7 +72,7 @@ func ConsolidateConfig() (opts *Options, err error) {
for _, filename := range configLocations { for _, filename := range configLocations {
if _, e := os.Stat(filename); !os.IsNotExist(e) { if _, e := os.Stat(filename); !os.IsNotExist(e) {
log.Printf("Loading configuration from %s\n", filename) log.Printf("Loading configuration from %s\n", filename)
err = opts.parseFile(filename) err = parseFile(opts, filename)
if err != nil { if err != nil {
return return
} }
@ -155,22 +81,22 @@ func ConsolidateConfig() (opts *Options, err error) {
} }
// Then, overwrite that by what is present in the environment // Then, overwrite that by what is present in the environment
err = opts.parseEnvironmentVariables() err = parseEnvironmentVariables(opts)
if err != nil { if err != nil {
return return
} }
// Finaly, command line takes precedence // Finaly, command line takes precedence
err = opts.parseCLI() err = parseCLI(opts)
if err != nil { if err != nil {
return return
} }
// Sanitize options // Sanitize options
if opts.baseURL != "/" { if opts.BasePath != "/" {
opts.baseURL = path.Clean(opts.baseURL) opts.BasePath = path.Clean(opts.BasePath)
} else { } else {
opts.baseURL = "" opts.BasePath = ""
} }
if opts.NoMail && opts.MailSMTPHost != "" { if opts.NoMail && opts.MailSMTPHost != "" {
@ -178,26 +104,26 @@ func ConsolidateConfig() (opts *Options, err error) {
return return
} }
if opts.ExternalURL.URL.Host == "" || opts.ExternalURL.URL.Scheme == "" { if opts.ExternalURL.Host == "" || opts.ExternalURL.Scheme == "" {
u, err2 := url.Parse("http://" + opts.ExternalURL.URL.String()) u, err2 := url.Parse("http://" + opts.ExternalURL.String())
if err2 == nil { if err2 == nil {
opts.ExternalURL.URL = u opts.ExternalURL = *u
} else { } else {
err = fmt.Errorf("You defined an external URL without a scheme. The expected value is eg. http://localhost:8081") err = fmt.Errorf("You defined an external URL without a scheme. The expected value is eg. http://localhost:8081")
return return
} }
} }
if len(opts.ExternalURL.URL.Path) > 1 { if len(opts.ExternalURL.Path) > 1 {
if opts.baseURL != "" && opts.baseURL != opts.ExternalURL.URL.Path { if opts.BasePath != "" && opts.BasePath != opts.ExternalURL.Path {
err = fmt.Errorf("You defined both baseurl and a path to externalurl that are different. Define only one of those.") err = fmt.Errorf("You defined both baseurl and a path to externalurl that are different. Define only one of those.")
return return
} }
opts.baseURL = path.Clean(opts.ExternalURL.URL.Path) opts.BasePath = path.Clean(opts.ExternalURL.Path)
} }
opts.ExternalURL.URL.Path = "" opts.ExternalURL.Path = ""
opts.ExternalURL.URL.Fragment = "" opts.ExternalURL.Fragment = ""
opts.ExternalURL.URL.RawQuery = "" opts.ExternalURL.RawQuery = ""
if len(opts.JWTSecretKey) == 0 { if len(opts.JWTSecretKey) == 0 {
opts.JWTSecretKey = make([]byte, 32) opts.JWTSecretKey = make([]byte, 32)
@ -207,12 +133,17 @@ func ConsolidateConfig() (opts *Options, err error) {
} }
} }
err = ExtendsConfigWithOIDC(opts)
if err != nil {
return
}
return return
} }
// parseLine treats a config line and place the read value in the variable // parseLine treats a config line and place the read value in the variable
// declared to the corresponding flag. // declared to the corresponding flag.
func (o *Options) parseLine(line string) (err error) { func parseLine(o *happydns.Options, line string) (err error) {
fields := strings.SplitN(line, "=", 2) fields := strings.SplitN(line, "=", 2)
orig_key := strings.TrimSpace(fields[0]) orig_key := strings.TrimSpace(fields[0])
value := strings.TrimSpace(fields[1]) value := strings.TrimSpace(fields[1])

View file

@ -19,18 +19,20 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
package config // import "git.happydns.org/happyDomain/config" package config // import "git.happydns.org/happyDomain/internal/config"
import ( import (
"net/url" "net/url"
"testing" "testing"
"git.happydns.org/happyDomain/model"
) )
func TestParseLine(t *testing.T) { func TestParseLine(t *testing.T) {
cfg := Options{} cfg := &happydns.Options{}
cfg.declareFlags() declareFlags(cfg)
err := cfg.parseLine("HAPPYDOMAIN_BIND=:8080") err := parseLine(cfg, "HAPPYDOMAIN_BIND=:8080")
if err != nil { if err != nil {
t.Fatalf(`parseLine("BIND=:8080") => %v`, err.Error()) t.Fatalf(`parseLine("BIND=:8080") => %v`, err.Error())
} }
@ -38,30 +40,30 @@ func TestParseLine(t *testing.T) {
t.Fatalf(`parseLine("BIND=:8080") = %q, want ":8080"`, cfg.Bind) t.Fatalf(`parseLine("BIND=:8080") = %q, want ":8080"`, cfg.Bind)
} }
err = cfg.parseLine("BASEURL=/base") err = parseLine(cfg, "BASEURL=/base")
if err != nil { if err != nil {
t.Fatalf(`parseLine("BASEURL=/base") => %v`, err.Error()) t.Fatalf(`parseLine("BASEURL=/base") => %v`, err.Error())
} }
if cfg.baseURL != "/base" { if cfg.BasePath != "/base" {
t.Fatalf(`parseLine("BASEURL=/base") = %q, want "/base"`, cfg.baseURL) t.Fatalf(`parseLine("BASEURL=/base") = %q, want "/base"`, cfg.BasePath)
} }
cfg.parseLine("EXTERNALURL=https://happydomain.org") parseLine(cfg, "EXTERNALURL=https://happydomain.org")
if cfg.ExternalURL.String() != "https://happydomain.org" { if cfg.ExternalURL.String() != "https://happydomain.org" {
t.Fatalf(`parseLine("EXTERNAL_URL=https://happydomain.org") = %q, want "https://happydomain.org"`, cfg.ExternalURL) t.Fatalf(`parseLine("EXTERNAL_URL=https://happydomain.org") = %q, want "https://happydomain.org"`, cfg.ExternalURL.String())
} }
cfg.parseLine("DEFAULT-NS=42.42.42.42:5353") parseLine(cfg, "DEFAULT-NS=42.42.42.42:5353")
if cfg.DefaultNameServer != "42.42.42.42:5353" { if cfg.DefaultNameServer != "42.42.42.42:5353" {
t.Fatalf(`parseLine("DEFAULT-NS=42.42.42.42:5353") = %q, want "42.42.42.42:5353"`, cfg.DefaultNameServer) t.Fatalf(`parseLine("DEFAULT-NS=42.42.42.42:5353") = %q, want "42.42.42.42:5353"`, cfg.DefaultNameServer)
} }
cfg.parseLine("DEFAULT_NS=42.42.42.42:3535") parseLine(cfg, "DEFAULT_NS=42.42.42.42:3535")
if cfg.DefaultNameServer != "42.42.42.42:3535" { if cfg.DefaultNameServer != "42.42.42.42:3535" {
t.Fatalf(`parseLine("DEFAULT_NS=42.42.42.42:3535") = %q, want "42.42.42.42:3535"`, cfg.DefaultNameServer) t.Fatalf(`parseLine("DEFAULT_NS=42.42.42.42:3535") = %q, want "42.42.42.42:3535"`, cfg.DefaultNameServer)
} }
err = cfg.parseLine("NO_AUTH=true") err = parseLine(cfg, "NO_AUTH=true")
if err != nil { if err != nil {
t.Fatalf(`parseLine("NO_AUTH=true") => %v`, err.Error()) t.Fatalf(`parseLine("NO_AUTH=true") => %v`, err.Error())
} }
@ -73,8 +75,8 @@ func TestParseLine(t *testing.T) {
func TestGetBaseURL(t *testing.T) { func TestGetBaseURL(t *testing.T) {
u, _ := url.Parse("http://localhost:8081") u, _ := url.Parse("http://localhost:8081")
cfg := Options{ cfg := &happydns.Options{
ExternalURL: URL{URL: u}, ExternalURL: *u,
} }
builded_url := cfg.GetBaseURL() builded_url := cfg.GetBaseURL()
@ -82,7 +84,7 @@ func TestGetBaseURL(t *testing.T) {
t.Fatalf(`GetBaseURL() = %q, want "http://localhost:8081"`, builded_url) t.Fatalf(`GetBaseURL() = %q, want "http://localhost:8081"`, builded_url)
} }
cfg.baseURL = "/base" cfg.BasePath = "/base"
builded_url = cfg.GetBaseURL() builded_url = cfg.GetBaseURL()
if builded_url != "http://localhost:8081/base" { if builded_url != "http://localhost:8081/base" {

View file

@ -27,10 +27,12 @@ import (
"net/url" "net/url"
) )
type JWTSecretKey []byte type JWTSecretKey struct {
Secret *[]byte
}
func (i *JWTSecretKey) String() string { func (i *JWTSecretKey) String() string {
return base64.StdEncoding.EncodeToString(*i) return base64.StdEncoding.EncodeToString(*i.Secret)
} }
func (i *JWTSecretKey) Set(value string) error { func (i *JWTSecretKey) Set(value string) error {
@ -39,7 +41,7 @@ func (i *JWTSecretKey) Set(value string) error {
return err return err
} }
*i = z *i.Secret = z
return nil return nil
} }
@ -83,6 +85,6 @@ func (i *URL) Set(value string) error {
return err return err
} }
i.URL = u *i.URL = *u
return nil return nil
} }

View file

@ -25,14 +25,16 @@ import (
"fmt" "fmt"
"os" "os"
"strings" "strings"
"git.happydns.org/happyDomain/model"
) )
// parseEnvironmentVariables analyzes all the environment variables to find // parseEnvironmentVariables analyzes all the environment variables to find
// each one starting by HAPPYDOMAIN_ // each one starting by HAPPYDOMAIN_
func (o *Options) parseEnvironmentVariables() (err error) { func parseEnvironmentVariables(o *happydns.Options) (err error) {
for _, line := range os.Environ() { for _, line := range os.Environ() {
if strings.HasPrefix(line, "HAPPYDOMAIN_") || strings.HasPrefix(line, "HAPPYDNS_") { if strings.HasPrefix(line, "HAPPYDOMAIN_") || strings.HasPrefix(line, "HAPPYDNS_") {
err := o.parseLine(line) err := parseLine(o, line)
if err != nil { if err != nil {
return fmt.Errorf("error in environment (%q): %w", line, err) return fmt.Errorf("error in environment (%q): %w", line, err)
} }

View file

@ -26,11 +26,13 @@ import (
"fmt" "fmt"
"os" "os"
"strings" "strings"
"git.happydns.org/happyDomain/model"
) )
// parseFile opens the file at the given filename path, then treat each line // parseFile opens the file at the given filename path, then treat each line
// not starting with '#' as a configuration statement. // not starting with '#' as a configuration statement.
func (o *Options) parseFile(filename string) error { func parseFile(o *happydns.Options, filename string) error {
fp, err := os.Open(filename) fp, err := os.Open(filename)
if err != nil { if err != nil {
return err return err
@ -43,7 +45,7 @@ func (o *Options) parseFile(filename string) error {
n += 1 n += 1
line := strings.TrimSpace(scanner.Text()) line := strings.TrimSpace(scanner.Text())
if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 { if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
err := o.parseLine(line) err := parseLine(o, line)
if err != nil { if err != nil {
return fmt.Errorf("%v:%d: error in configuration: %w", filename, n, err) return fmt.Errorf("%v:%d: error in configuration: %w", filename, n, err)
} }

View file

@ -23,6 +23,10 @@
package config package config
func (o *Options) GetOIDCProviderURL() string { import (
return "" "git.happydns.org/happyDomain/model"
)
func ExtendsConfigWithOIDC(o *happydns.Options) error {
return nil
} }

View file

@ -24,50 +24,32 @@
package config package config
import ( import (
"context"
"flag" "flag"
"net/url" "net/url"
"path"
"strings"
"github.com/coreos/go-oidc/v3/oidc" "git.happydns.org/happyDomain/model"
"golang.org/x/oauth2"
) )
var ( var (
OIDCClientID string oidcClientID string
oidcClientSecret string oidcClientSecret string
OIDCProviderURL string oidcProviderURL url.URL
) )
func init() { func init() {
flag.StringVar(&OIDCClientID, "oidc-client-id", OIDCClientID, "ClientID for OIDC") flag.StringVar(&oidcClientID, "oidc-client-id", oidcClientID, "ClientID for OIDC")
flag.StringVar(&oidcClientSecret, "oidc-client-secret", oidcClientSecret, "Secret for OIDC") flag.StringVar(&oidcClientSecret, "oidc-client-secret", oidcClientSecret, "Secret for OIDC")
flag.StringVar(&OIDCProviderURL, "oidc-provider-url", OIDCProviderURL, "Base URL of the OpenId Connect service") flag.Var(&URL{&oidcProviderURL}, "oidc-provider-url", "Base URL of the OpenId Connect service")
} }
func (o *Options) GetAuthURL() *url.URL { func ExtendsConfigWithOIDC(o *happydns.Options) error {
redirecturl := *o.ExternalURL.URL if oidcProviderURL.String() != "" {
redirecturl.Path = path.Join(redirecturl.Path, o.baseURL, "auth", "callback") o.OIDCClients = append(o.OIDCClients, happydns.OIDCSettings{
return &redirecturl ClientID: oidcClientID,
}
func (o *Options) GetOIDCProvider(ctx context.Context) (*oidc.Provider, error) {
return oidc.NewProvider(ctx, strings.TrimSuffix(OIDCProviderURL, "/.well-known/openid-configuration"))
}
func (o *Options) GetOIDCProviderURL() string {
return OIDCProviderURL
}
func (o *Options) GetOAuth2Config(provider *oidc.Provider) *oauth2.Config {
oauth2Config := oauth2.Config{
ClientID: OIDCClientID,
ClientSecret: oidcClientSecret, ClientSecret: oidcClientSecret,
RedirectURL: o.GetAuthURL().String(), ProviderURL: oidcProviderURL,
Endpoint: provider.Endpoint(), })
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
} }
return &oauth2Config return nil
} }

View file

@ -0,0 +1,82 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build !nooidc
package config // import "git.happydns.org/happyDomain/internal/config"
import (
"testing"
"git.happydns.org/happyDomain/model"
)
func TestOIDCConfig(t *testing.T) {
cfg := &happydns.Options{}
err := parseLine(cfg, "HAPPYDOMAIN_OIDC_CLIENT_ID=test-oidc-1")
if err != nil {
t.Fatalf(`parseLine("HAPPYDOMAIN_OIDC_CLIENT_ID=test-oidc-1") => %v`, err.Error())
}
if oidcClientID != "test-oidc-1" {
t.Fatalf(`parseLine("HAPPYDOMAIN_OIDC_CLIENT_ID=test-oidc-1") = %q, want "test-oidc-1"`, oidcClientID)
}
err = parseLine(cfg, "HAPPYDOMAIN_OIDC_CLIENT_SECRET=s3cret$")
if err != nil {
t.Fatalf(`parseLine("HAPPYDOMAIN_OIDC_CLIENT_SECRET=s3cret$") => %v`, err.Error())
}
if oidcClientSecret != "s3cret$" {
t.Fatalf(`parseLine("HAPPYDOMAIN_OIDC_CLIENT_SECRET=s3cret$") = %q, want "s3cret$"`, oidcClientSecret)
}
if oidcProviderURL.String() != "" {
t.Fatalf(`before parseLine("HAPPYDOMAIN_OIDC_PROVIDER_URL") = %q, want ""`, oidcProviderURL.String())
}
err = parseLine(cfg, "HAPPYDOMAIN_OIDC_PROVIDER_URL=https://localhost:12345/secret")
if err != nil {
t.Fatalf(`parseLine("HAPPYDOMAIN_OIDC_PROVIDER_URL=https://localhost:12345/secret") => %v`, err.Error())
}
if oidcProviderURL.String() != "https://localhost:12345/secret" {
t.Fatalf(`parseLine("HAPPYDOMAIN_OIDC_PROVIDER_URL=https://localhost:12345/secret") = %q, want "https://localhost:12345/secret"`, cfg.Bind)
}
// Test extended config
err = ExtendsConfigWithOIDC(cfg)
if err != nil {
t.Fatalf(`ExtendsConfigWithOIDC(cfg) => %v`, err.Error())
}
if len(cfg.OIDCClients) != 1 {
t.Fatalf(`len(cfg.OIDCClients) == %d, should be 1`, len(cfg.OIDCClients))
}
if cfg.OIDCClients[0].ClientID != "test-oidc-1" {
t.Fatalf(`cfg.OIDCClients[0].ClientID == %q, should be test-oidc-1`, cfg.OIDCClients[0].ClientID)
}
if cfg.OIDCClients[0].ClientSecret != "s3cret$" {
t.Fatalf(`cfg.OIDCClients[0].ClientSecret == %q, should be test-oidc-1`, cfg.OIDCClients[0].ClientSecret)
}
if cfg.OIDCClients[0].ProviderURL.String() != "https://localhost:12345/secret" {
t.Fatalf(`cfg.OIDCClients[0].ProviderURL == %q, should be https://localhost:12345/secret`, cfg.OIDCClients[0].ProviderURL.String())
}
}

View file

@ -33,7 +33,6 @@ import (
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/mileusna/useragent" "github.com/mileusna/useragent"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/internal/storage" "git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
) )
@ -47,13 +46,13 @@ type SessionStore struct {
storage storage.Storage storage storage.Storage
} }
func NewSessionStore(opts *config.Options, storage storage.Storage, keyPairs ...[]byte) *SessionStore { func NewSessionStore(opts *happydns.Options, storage storage.Storage, keyPairs ...[]byte) *SessionStore {
store := &SessionStore{ store := &SessionStore{
Codecs: securecookie.CodecsFromPairs(keyPairs...), Codecs: securecookie.CodecsFromPairs(keyPairs...),
options: &sessions.Options{ options: &sessions.Options{
Path: opts.GetBasePath() + "/", Path: opts.BasePath + "/",
MaxAge: 86400 * 30, MaxAge: 86400 * 30,
Secure: opts.DevProxy == "" && opts.ExternalURL.URL.Scheme != "http", Secure: opts.DevProxy == "" && opts.ExternalURL.Scheme != "http",
HttpOnly: true, HttpOnly: true,
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
}, },

View file

@ -34,11 +34,10 @@ func (i *StorageEngine) String() string {
} }
func (i *StorageEngine) Set(value string) (err error) { func (i *StorageEngine) Set(value string) (err error) {
se := StorageEngine(value) if _, ok := StorageEngines[value]; !ok {
if _, ok := StorageEngines[se]; !ok {
return fmt.Errorf("Unexistant storage engine: please select one between: %v", GetStorageEngines()) return fmt.Errorf("Unexistant storage engine: please select one between: %v", GetStorageEngines())
} }
*i = se *i = StorageEngine(value)
return nil return nil
} }
@ -48,7 +47,7 @@ type StorageInstanciation func() (Storage, error)
// StorageEngines lists all Storage implementations declared, with a // StorageEngines lists all Storage implementations declared, with a
// way to instanciate automatically each. // way to instanciate automatically each.
var StorageEngines = map[StorageEngine]StorageInstanciation{} var StorageEngines = map[string]StorageInstanciation{}
// GetStorageEngines returns all declared Storage implementation. // GetStorageEngines returns all declared Storage implementation.
func GetStorageEngines() (se []string) { func GetStorageEngines() (se []string) {

View file

@ -27,7 +27,6 @@ import (
"net/mail" "net/mail"
"strings" "strings"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/internal/helpers" "git.happydns.org/happyDomain/internal/helpers"
"git.happydns.org/happyDomain/internal/mailer" "git.happydns.org/happyDomain/internal/mailer"
"git.happydns.org/happyDomain/internal/storage" "git.happydns.org/happyDomain/internal/storage"
@ -35,12 +34,12 @@ import (
) )
type authUserUsecase struct { type authUserUsecase struct {
config *config.Options config *happydns.Options
mailer *mailer.Mailer mailer *mailer.Mailer
store storage.AuthUserAndSessionStorage store storage.AuthUserAndSessionStorage
} }
func NewAuthUserUsecase(cfg *config.Options, m *mailer.Mailer, store storage.AuthUserAndSessionStorage) happydns.AuthUserUsecase { func NewAuthUserUsecase(cfg *happydns.Options, m *mailer.Mailer, store storage.AuthUserAndSessionStorage) happydns.AuthUserUsecase {
return &authUserUsecase{ return &authUserUsecase{
config: cfg, config: cfg,
mailer: m, mailer: m,

View file

@ -25,18 +25,17 @@ import (
"fmt" "fmt"
"time" "time"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/internal/storage" "git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
) )
type loginUsecase struct { type loginUsecase struct {
config *config.Options config *happydns.Options
store storage.AuthenticationStorage store storage.AuthenticationStorage
userService happydns.UserUsecase userService happydns.UserUsecase
} }
func NewAuthenticationUsecase(cfg *config.Options, store storage.AuthenticationStorage, userService happydns.UserUsecase) happydns.AuthenticationUsecase { func NewAuthenticationUsecase(cfg *happydns.Options, store storage.AuthenticationStorage, userService happydns.UserUsecase) happydns.AuthenticationUsecase {
return &loginUsecase{ return &loginUsecase{
config: cfg, config: cfg,
store: store, store: store,

View file

@ -22,15 +22,14 @@
package usecase package usecase
import ( import (
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
) )
type formUsecase struct { type formUsecase struct {
config *config.Options config *happydns.Options
} }
func NewFormUsecase(cfg *config.Options) happydns.FormUsecase { func NewFormUsecase(cfg *happydns.Options) happydns.FormUsecase {
return &formUsecase{ return &formUsecase{
config: cfg, config: cfg,
} }

View file

@ -24,19 +24,18 @@ package usecase
import ( import (
"fmt" "fmt"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/internal/forms" "git.happydns.org/happyDomain/internal/forms"
"git.happydns.org/happyDomain/internal/usecase/provider" "git.happydns.org/happyDomain/internal/usecase/provider"
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
) )
type providerSettingsUsecase struct { type providerSettingsUsecase struct {
config *config.Options config *happydns.Options
providerService happydns.ProviderUsecase providerService happydns.ProviderUsecase
store provider.ProviderStorage store provider.ProviderStorage
} }
func NewProviderSettingsUsecase(cfg *config.Options, ps happydns.ProviderUsecase, store provider.ProviderStorage) happydns.ProviderSettingsUsecase { func NewProviderSettingsUsecase(cfg *happydns.Options, ps happydns.ProviderUsecase, store provider.ProviderStorage) happydns.ProviderSettingsUsecase {
return &providerSettingsUsecase{ return &providerSettingsUsecase{
config: cfg, config: cfg,
providerService: ps, providerService: ps,

View file

@ -25,7 +25,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/internal/storage" "git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
"git.happydns.org/happyDomain/providers" "git.happydns.org/happyDomain/providers"
@ -33,10 +32,10 @@ import (
type providerUsecase struct { type providerUsecase struct {
happydns.ProviderUsecase happydns.ProviderUsecase
config *config.Options config *happydns.Options
} }
func NewProviderUsecase(cfg *config.Options, store storage.ProviderAndDomainStorage) happydns.ProviderUsecase { func NewProviderUsecase(cfg *happydns.Options, store storage.ProviderAndDomainStorage) happydns.ProviderUsecase {
return &providerUsecase{ return &providerUsecase{
ProviderUsecase: NewAdminProviderUsecase(store), ProviderUsecase: NewAdminProviderUsecase(store),
config: cfg, config: cfg,

View file

@ -30,7 +30,6 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
"git.happydns.org/happyDomain/internal/config"
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
) )
@ -39,10 +38,10 @@ var (
) )
type resolverUsecase struct { type resolverUsecase struct {
config *config.Options config *happydns.Options
} }
func NewResolverUsecase(cfg *config.Options) happydns.ResolverUsecase { func NewResolverUsecase(cfg *happydns.Options) happydns.ResolverUsecase {
return &resolverUsecase{ return &resolverUsecase{
config: cfg, config: cfg,
} }

106
model/config.go Normal file
View file

@ -0,0 +1,106 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package happydns
import (
"fmt"
"net/mail"
"net/url"
"path"
)
// Options stores the configuration of the software.
type Options struct {
// AdminBind is the address:port or unix socket used to serve the admin
// API.
AdminBind string
// Bind is the address:port used to bind the main interface with API.
Bind string
// BasePath is the relative path where begins the root of the app.
BasePath string
// DevProxy is the URL that override static assets.
DevProxy string
// DefaultNameServer is the NS server suggested by default.
DefaultNameServer string
// DisableProviders should disallow all actions on provider (add/edit/delete) through public API.
DisableProviders bool
// DisableRegistration forbids all new registration using the public form/API.
DisableRegistration bool
// DisableEmbeddedLogin disables the internal user/password login in favor of ExternalAuth or OIDC.
DisableEmbeddedLogin bool
// ExternalAuth is the URL of the login form to use instead of the embedded one.
ExternalAuth url.URL
// ExternalURL keeps the URL used in communications (such as email,
// ...), when it needs to use complete URL, not only relative parts.
ExternalURL url.URL
// JWTSecretKey stores the private key to sign and verify JWT tokens.
JWTSecretKey []byte
// JWTSigningMethod is the signing method to check token signature.
JWTSigningMethod string
// NoAuth controls if there is user access control or not.
NoAuth bool
// OptOutInsights disable the anonymous usage statistics report.
OptOutInsights bool
// StorageEngine points to the storage engine used.
StorageEngine string
ListmonkURL url.URL
ListmonkId int
// MailFrom holds the content of the From field for all e-mails that
// will be send.
MailFrom mail.Address
NoMail bool
MailSMTPHost string
MailSMTPPort uint
MailSMTPUsername string
MailSMTPPassword string
MailSMTPTLSSNoVerify bool
OIDCClients []OIDCSettings
}
// GetBaseURL returns the full url to the absolute ExternalURL, including BaseURL.
func (o *Options) GetBaseURL() string {
return fmt.Sprintf("%s%s", o.ExternalURL.String(), o.BasePath)
}
func (o *Options) GetAuthURL() *url.URL {
redirecturl := o.ExternalURL
redirecturl.Path = path.Join(redirecturl.Path, o.BasePath, "auth", "callback")
return &redirecturl
}

View file

@ -43,6 +43,7 @@ type Insights struct {
HasExternalAuth bool `json:"hasExternalAuth,omitempty"` HasExternalAuth bool `json:"hasExternalAuth,omitempty"`
HasListmonkURL bool `json:"hasListmonkURL,omitempty"` HasListmonkURL bool `json:"hasListmonkURL,omitempty"`
LocalBind bool `json:"localBind,omitempty"` LocalBind bool `json:"localBind,omitempty"`
NbOidcProviders int `json:"nbOidcProviders,omitempty"`
NoAuthActive bool `json:"noAuthActive,omitempty"` NoAuthActive bool `json:"noAuthActive,omitempty"`
NoMail bool `json:"noMail,omitempty"` NoMail bool `json:"noMail,omitempty"`
NonUnixAdminBind bool `json:"nonUnixAdminBind,omitempty"` NonUnixAdminBind bool `json:"nonUnixAdminBind,omitempty"`

32
model/oidc.go Normal file
View file

@ -0,0 +1,32 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2024 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package happydns
import (
"net/url"
)
type OIDCSettings struct {
ClientID string
ClientSecret string
ProviderURL url.URL
}

View file

@ -35,7 +35,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"git.happydns.org/happyDomain/internal/config" "git.happydns.org/happyDomain/model"
) )
var ( var (
@ -55,7 +55,7 @@ func init() {
flag.StringVar(&MsgHeaderColor, "msg-header-color", MsgHeaderColor, "Background color of the banner added at the top of the app") flag.StringVar(&MsgHeaderColor, "msg-header-color", MsgHeaderColor, "Background color of the banner added at the top of the app")
} }
func DeclareRoutes(cfg *config.Options, router *gin.Engine) { func DeclareRoutes(cfg *happydns.Options, router *gin.Engine) {
if cfg.DisableProviders { if cfg.DisableProviders {
CustomHeadHTML += `<script type="text/javascript">window.disable_providers = true;</script>` CustomHeadHTML += `<script type="text/javascript">window.disable_providers = true;</script>`
} }
@ -68,7 +68,7 @@ func DeclareRoutes(cfg *config.Options, router *gin.Engine) {
CustomHeadHTML += `<script type="text/javascript">window.disable_embedded_login = true;</script>` CustomHeadHTML += `<script type="text/javascript">window.disable_embedded_login = true;</script>`
} }
if config.OIDCProviderURL != "" { if len(cfg.OIDCClients) > 0 {
CustomHeadHTML += `<script type="text/javascript">window.oidc_configured = true;</script>` CustomHeadHTML += `<script type="text/javascript">window.oidc_configured = true;</script>`
} }
@ -129,7 +129,7 @@ func DeclareRoutes(cfg *config.Options, router *gin.Engine) {
}) })
} }
func serveOrReverse(forced_url string, cfg *config.Options) gin.HandlerFunc { func serveOrReverse(forced_url string, cfg *happydns.Options) gin.HandlerFunc {
if cfg.DevProxy != "" { if cfg.DevProxy != "" {
// Forward to the Vue dev proxy // Forward to the Vue dev proxy
return func(c *gin.Context) { return func(c *gin.Context) {