feat: initial commit

This commit is contained in:
2025-06-10 21:09:58 +02:00
commit 1fb753469e
84 changed files with 3912 additions and 0 deletions

View File

@ -0,0 +1,152 @@
package setup
import (
"context"
"crypto/rand"
"fmt"
"forge.cadoles.com/wpetit/kouiz/internal/config"
"forge.cadoles.com/wpetit/kouiz/internal/http/handler/webui/auth"
"github.com/gorilla/sessions"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/gitea"
"github.com/markbates/goth/providers/github"
"github.com/markbates/goth/providers/google"
"github.com/markbates/goth/providers/openidConnect"
"github.com/pkg/errors"
)
func NewAuthHandlerFromConfig(ctx context.Context, conf *config.Config) (*auth.Handler, error) {
// Configure sessions store
keyPairs := make([][]byte, 0)
if len(conf.HTTP.Session.Keys) == 0 {
key, err := getRandomBytes(32)
if err != nil {
return nil, errors.Wrap(err, "could not generate cookie signing key")
}
keyPairs = append(keyPairs, key)
} else {
for _, k := range conf.HTTP.Session.Keys {
keyPairs = append(keyPairs, []byte(k))
}
}
sessionStore := sessions.NewCookieStore(keyPairs...)
sessionStore.MaxAge(int(conf.HTTP.Session.Cookie.MaxAge))
sessionStore.Options.Path = conf.HTTP.Session.Cookie.Path
sessionStore.Options.HttpOnly = conf.HTTP.Session.Cookie.HTTPOnly
sessionStore.Options.Secure = conf.HTTP.Session.Cookie.Secure
// Configure providers
gothProviders := make([]goth.Provider, 0)
providers := make([]auth.Provider, 0)
if conf.Auth.Providers.Google.Key != "" && conf.Auth.Providers.Google.Secret != "" {
googleProvider := google.New(
conf.Auth.Providers.Google.Key,
conf.Auth.Providers.Google.Secret,
fmt.Sprintf("%s/auth/providers/google/callback", conf.HTTP.BaseURL),
conf.Auth.Providers.Google.Scopes...,
)
gothProviders = append(gothProviders, googleProvider)
providers = append(providers, auth.Provider{
ID: googleProvider.Name(),
Label: "Google",
Icon: "fa-google",
})
}
if conf.Auth.Providers.Github.Key != "" && conf.Auth.Providers.Github.Secret != "" {
githubProvider := github.New(
conf.Auth.Providers.Github.Key,
conf.Auth.Providers.Github.Secret,
fmt.Sprintf("%s/auth/providers/github/callback", conf.HTTP.BaseURL),
conf.Auth.Providers.Github.Scopes...,
)
gothProviders = append(gothProviders, githubProvider)
providers = append(providers, auth.Provider{
ID: githubProvider.Name(),
Label: "Github",
Icon: "fa-github",
})
}
if conf.Auth.Providers.Gitea.Key != "" && conf.Auth.Providers.Gitea.Secret != "" {
giteaProvider := gitea.NewCustomisedURL(
conf.Auth.Providers.Gitea.Key,
conf.Auth.Providers.Gitea.Secret,
fmt.Sprintf("%s/auth/providers/gitea/callback", conf.HTTP.BaseURL),
conf.Auth.Providers.Gitea.AuthURL,
conf.Auth.Providers.Gitea.TokenURL,
conf.Auth.Providers.Gitea.ProfileURL,
conf.Auth.Providers.Gitea.Scopes...,
)
gothProviders = append(gothProviders, giteaProvider)
providers = append(providers, auth.Provider{
ID: giteaProvider.Name(),
Label: conf.Auth.Providers.Gitea.Label,
Icon: "fa-git-alt",
})
}
if conf.Auth.Providers.OIDC.Key != "" && conf.Auth.Providers.OIDC.Secret != "" {
oidcProvider, err := openidConnect.New(
conf.Auth.Providers.OIDC.Key,
conf.Auth.Providers.OIDC.Secret,
fmt.Sprintf("%s/auth/providers/openid-connect/callback", conf.HTTP.BaseURL),
conf.Auth.Providers.OIDC.DiscoveryURL,
conf.Auth.Providers.OIDC.Scopes...,
)
if err != nil {
return nil, errors.Wrap(err, "could not configure oidc provider")
}
gothProviders = append(gothProviders, oidcProvider)
providers = append(providers, auth.Provider{
ID: oidcProvider.Name(),
Label: conf.Auth.Providers.OIDC.Label,
Icon: conf.Auth.Providers.OIDC.Icon,
})
}
goth.UseProviders(gothProviders...)
gothic.Store = sessionStore
opts := []auth.OptionFunc{
auth.WithProviders(providers...),
}
auth := auth.NewHandler(
sessionStore,
opts...,
)
return auth, nil
}
func getRandomBytes(n int) ([]byte, error) {
data := make([]byte, n)
read, err := rand.Read(data)
if err != nil {
return nil, errors.WithStack(err)
}
if read != n {
return nil, errors.Errorf("could not read %d bytes", n)
}
return data, nil
}

View File

@ -0,0 +1,27 @@
package setup
import (
"context"
"forge.cadoles.com/wpetit/kouiz/internal/config"
"forge.cadoles.com/wpetit/kouiz/internal/http"
"github.com/pkg/errors"
)
func NewHTTPServerFromConfig(ctx context.Context, conf *config.Config) (*http.Server, error) {
// Configure Web UI handler
webui, err := NewWebUIHandlerFromConfig(ctx, conf)
if err != nil {
return nil, errors.Wrap(err, "could not configure webui handler from config")
}
// Create HTTP server
server := http.NewServer(
webui,
http.WithAddress(conf.HTTP.Address),
http.WithBaseURL(conf.HTTP.BaseURL),
)
return server, nil
}

View File

@ -0,0 +1,26 @@
package setup
import (
"context"
"log"
"forge.cadoles.com/wpetit/kouiz/internal/config"
"forge.cadoles.com/wpetit/kouiz/internal/http/handler/webui/quiz"
"forge.cadoles.com/wpetit/kouiz/internal/store"
"github.com/glebarez/sqlite"
"github.com/pkg/errors"
"gorm.io/gorm"
)
func NewQuizHandlerFromConfig(ctx context.Context, conf *config.Config) (*quiz.Handler, error) {
db, err := gorm.Open(sqlite.Open(string(conf.Store.DSN)), &gorm.Config{})
if err != nil {
log.Fatalf("[FATAL] %+v", errors.Wrap(err, "could not open store"))
}
if conf.Store.Debug {
db = db.Debug()
}
return quiz.NewHandler(store.New(db)), nil
}

View File

@ -0,0 +1,44 @@
package setup
import (
"net/url"
"github.com/pkg/errors"
)
var ErrNotRegistered = errors.New("not registered")
type Factory[T any] func(u *url.URL) (T, error)
type Registry[T any] struct {
mappings map[string]Factory[T]
}
func (r *Registry[T]) Register(scheme string, factory Factory[T]) {
r.mappings[scheme] = factory
}
func (r *Registry[T]) From(rawURL string) (T, error) {
u, err := url.Parse(rawURL)
if err != nil {
return *new(T), errors.WithStack(err)
}
factory, exists := r.mappings[u.Scheme]
if !exists {
return *new(T), errors.Wrapf(ErrNotRegistered, "scheme '%s' not found", u.Scheme)
}
value, err := factory(u)
if err != nil {
return *new(T), errors.WithStack(err)
}
return value, nil
}
func NewRegistry[T any]() *Registry[T] {
return &Registry[T]{
mappings: make(map[string]Factory[T]),
}
}

View File

@ -0,0 +1,54 @@
package setup
import (
"context"
"net/http"
"forge.cadoles.com/wpetit/kouiz/internal/config"
"forge.cadoles.com/wpetit/kouiz/internal/http/handler/webui"
"forge.cadoles.com/wpetit/kouiz/internal/http/handler/webui/common"
"github.com/pkg/errors"
)
func NewWebUIHandlerFromConfig(ctx context.Context, conf *config.Config) (*webui.Handler, error) {
opts := make([]webui.OptionFunc, 0)
// Configure auth handler
authHandler, err := NewAuthHandlerFromConfig(ctx, conf)
if err != nil {
return nil, errors.Wrap(err, "could not configure auth handler from config")
}
authMiddleware := authHandler.Middleware()
opts = append(opts, webui.WithMount("/auth/", authHandler))
// Configure index redirect
opts = append(opts, webui.WithMount("/", authMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/quiz", http.StatusTemporaryRedirect)
}))))
// Configure quiz handler
quizHandler, err := NewQuizHandlerFromConfig(ctx, conf)
if err != nil {
return nil, errors.Wrap(err, "could not configure issue handler from config")
}
opts = append(opts, webui.WithMount("/quiz/", authMiddleware(quizHandler)))
// Configure common handler
commonHandler, err := NewCommonHandlerFromConfig(ctx, conf)
if err != nil {
return nil, errors.Wrap(err, "could not configure common handler from config")
}
opts = append(opts, webui.WithMount("/assets/", commonHandler))
handler := webui.NewHandler(opts...)
return handler, nil
}
func NewCommonHandlerFromConfig(ctx context.Context, conf *config.Config) (*common.Handler, error) {
return common.NewHandler(), nil
}