feat: initial commit
This commit is contained in:
34
internal/http/handler/webui/auth/component/login_page.templ
Normal file
34
internal/http/handler/webui/auth/component/login_page.templ
Normal file
@ -0,0 +1,34 @@
|
||||
package component
|
||||
|
||||
import common "forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common/component"
|
||||
|
||||
type LoginPageVModel struct {
|
||||
Providers []ProviderVModel
|
||||
}
|
||||
|
||||
type ProviderVModel struct {
|
||||
ID string
|
||||
Label string
|
||||
Icon string
|
||||
}
|
||||
|
||||
templ LoginPage(vmodel LoginPageVModel) {
|
||||
@common.Page() {
|
||||
<div class="is-flex is-justify-content-center is-align-items-center is-fullheight">
|
||||
<nav class="panel is-link" style="min-width: 33%">
|
||||
<p class="panel-heading">
|
||||
<div class="title">ClearCase</div>
|
||||
<span> - choose your provider</span>
|
||||
</p>
|
||||
for _, provider := range vmodel.Providers {
|
||||
<a class="panel-block" href={ templ.URL("/auth/providers/" + provider.ID) } hx-boost="false">
|
||||
<span class="panel-icon is-size-3">
|
||||
<i class={ "fab", provider.Icon } aria-hidden="true"></i>
|
||||
</span>
|
||||
{ provider.Label }
|
||||
</a>
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
}
|
124
internal/http/handler/webui/auth/component/login_page_templ.go
Normal file
124
internal/http/handler/webui/auth/component/login_page_templ.go
Normal file
@ -0,0 +1,124 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package component
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import common "forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common/component"
|
||||
|
||||
type LoginPageVModel struct {
|
||||
Providers []ProviderVModel
|
||||
}
|
||||
|
||||
type ProviderVModel struct {
|
||||
ID string
|
||||
Label string
|
||||
Icon string
|
||||
}
|
||||
|
||||
func LoginPage(vmodel LoginPageVModel) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"is-flex is-justify-content-center is-align-items-center is-fullheight\"><nav class=\"panel is-link\" style=\"min-width: 33%\"><p class=\"panel-heading\"><div class=\"title\">ClearCase</div><span> - choose your provider</span></p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, provider := range vmodel.Providers {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a class=\"panel-block\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 templ.SafeURL = templ.URL("/auth/providers/" + provider.ID)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var3)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" hx-boost=\"false\"><span class=\"panel-icon is-size-3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 = []any{"fab", provider.Icon}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<i class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var4).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/auth/component/login_page.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" aria-hidden=\"true\"></i></span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(provider.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/auth/component/login_page.templ`, Line: 28, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</nav></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = common.Page().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
55
internal/http/handler/webui/auth/handler.go
Normal file
55
internal/http/handler/webui/auth/handler.go
Normal file
@ -0,0 +1,55 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/auth/component"
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
type Provider = component.ProviderVModel
|
||||
|
||||
type Handler struct {
|
||||
mux *http.ServeMux
|
||||
sessionStore sessions.Store
|
||||
sessionName string
|
||||
providers []Provider
|
||||
defaultAdmin *DefaultAdmin
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func NewHandler(sessionStore sessions.Store, funcs ...OptionFunc) *Handler {
|
||||
opts := NewOptions(funcs...)
|
||||
h := &Handler{
|
||||
mux: http.NewServeMux(),
|
||||
sessionStore: sessionStore,
|
||||
sessionName: opts.SessionName,
|
||||
providers: opts.Providers,
|
||||
defaultAdmin: opts.DefaultAdmin,
|
||||
}
|
||||
|
||||
h.mux.HandleFunc("GET /login", h.getLoginPage)
|
||||
h.mux.Handle("GET /providers/{provider}", withContextProvider(http.HandlerFunc(h.handleProvider)))
|
||||
h.mux.Handle("GET /providers/{provider}/callback", withContextProvider(http.HandlerFunc(h.handleProviderCallback)))
|
||||
h.mux.HandleFunc("GET /logout", h.handleLogout)
|
||||
h.mux.Handle("GET /providers/{provider}/logout", withContextProvider(http.HandlerFunc(h.handleProviderLogout)))
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
var _ http.Handler = &Handler{}
|
||||
|
||||
func withContextProvider(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
provider := r.PathValue("provider")
|
||||
r = r.WithContext(context.WithValue(r.Context(), "provider", provider))
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
16
internal/http/handler/webui/auth/login_page.go
Normal file
16
internal/http/handler/webui/auth/login_page.go
Normal file
@ -0,0 +1,16 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/auth/component"
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
func (h *Handler) getLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
vmodel := component.LoginPageVModel{
|
||||
Providers: h.providers,
|
||||
}
|
||||
login := component.LoginPage(vmodel)
|
||||
templ.Handler(login).ServeHTTP(w, r)
|
||||
}
|
33
internal/http/handler/webui/auth/middleware.go
Normal file
33
internal/http/handler/webui/auth/middleware.go
Normal file
@ -0,0 +1,33 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http/context"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrUserNotFound = errors.New("user not found")
|
||||
|
||||
func (h *Handler) Middleware() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := h.retrieveSessionUser(r)
|
||||
if err != nil {
|
||||
slog.ErrorContext(r.Context(), "could not retrieve user from session", slog.Any("error", errors.WithStack(err)))
|
||||
http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.SetUser(ctx, user)
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
45
internal/http/handler/webui/auth/options.go
Normal file
45
internal/http/handler/webui/auth/options.go
Normal file
@ -0,0 +1,45 @@
|
||||
package auth
|
||||
|
||||
type DefaultAdmin struct {
|
||||
Provider string
|
||||
Email string
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Providers []Provider
|
||||
DefaultAdmin *DefaultAdmin
|
||||
SessionName string
|
||||
}
|
||||
|
||||
type OptionFunc func(opts *Options)
|
||||
|
||||
func NewOptions(funcs ...OptionFunc) *Options {
|
||||
opts := &Options{
|
||||
Providers: make([]Provider, 0),
|
||||
SessionName: "rkvst_auth",
|
||||
}
|
||||
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func WithProviders(providers ...Provider) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.Providers = providers
|
||||
}
|
||||
}
|
||||
|
||||
func WithDefaultAdmin(defaultAdmin DefaultAdmin) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.DefaultAdmin = &defaultAdmin
|
||||
}
|
||||
}
|
||||
|
||||
func WithSessionName(sessionName string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.SessionName = sessionName
|
||||
}
|
||||
}
|
71
internal/http/handler/webui/auth/provider.go
Normal file
71
internal/http/handler/webui/auth/provider.go
Normal file
@ -0,0 +1,71 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (h *Handler) handleProvider(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := gothic.CompleteUserAuth(w, r); err == nil {
|
||||
http.Redirect(w, r, "/auth/logout", http.StatusTemporaryRedirect)
|
||||
} else {
|
||||
gothic.BeginAuthHandler(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleProviderCallback(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := gothic.CompleteUserAuth(w, r)
|
||||
if err != nil {
|
||||
slog.ErrorContext(r.Context(), "could not complete user auth", slog.Any("error", errors.WithStack(err)))
|
||||
http.Redirect(w, r, "/auth/logout", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
slog.DebugContext(ctx, "authenticated user", slog.Any("user", user))
|
||||
|
||||
if err := h.storeSessionUser(w, r, &user); err != nil {
|
||||
slog.ErrorContext(r.Context(), "could not store session user", slog.Any("error", errors.WithStack(err)))
|
||||
http.Redirect(w, r, "/auth/logout", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (h *Handler) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := h.retrieveSessionUser(r)
|
||||
if err != nil && !errors.Is(err, errSessionNotFound) {
|
||||
common.HandleError(w, r, errors.WithStack(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.clearSession(w, r); err != nil && !errors.Is(err, errSessionNotFound) {
|
||||
common.HandleError(w, r, errors.WithStack(err))
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL := fmt.Sprintf("/auth/providers/%s/logout", user.Provider)
|
||||
|
||||
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (h *Handler) handleProviderLogout(w http.ResponseWriter, r *http.Request) {
|
||||
if err := gothic.Logout(w, r); err != nil {
|
||||
common.HandleError(w, r, errors.WithStack(err))
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
}
|
73
internal/http/handler/webui/auth/session.go
Normal file
73
internal/http/handler/webui/auth/session.go
Normal file
@ -0,0 +1,73 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/markbates/goth"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const userAttr = "u"
|
||||
const tenantIDAttr = "t"
|
||||
|
||||
var errSessionNotFound = errors.New("session not found")
|
||||
|
||||
func (h *Handler) storeSessionUser(w http.ResponseWriter, r *http.Request, user *goth.User) error {
|
||||
sess, err := h.getSession(r)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
sess.Values[userAttr] = user
|
||||
|
||||
if err := sess.Save(r, w); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) retrieveSessionUser(r *http.Request) (*goth.User, error) {
|
||||
sess, err := h.getSession(r)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
user, ok := sess.Values[userAttr].(*goth.User)
|
||||
if !ok {
|
||||
return nil, errors.WithStack(errSessionNotFound)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (h *Handler) getSession(r *http.Request) (*sessions.Session, error) {
|
||||
sess, err := h.sessionStore.Get(r, h.sessionName)
|
||||
if err != nil {
|
||||
slog.ErrorContext(r.Context(), "could not retrieve session from store", slog.Any("error", errors.WithStack(err)))
|
||||
return sess, errors.WithStack(errSessionNotFound)
|
||||
}
|
||||
|
||||
return sess, nil
|
||||
}
|
||||
|
||||
func (h *Handler) clearSession(w http.ResponseWriter, r *http.Request) error {
|
||||
sess, err := h.getSession(r)
|
||||
if err != nil && !errors.Is(err, errSessionNotFound) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if sess == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
sess.Options.MaxAge = -1
|
||||
|
||||
if err := sess.Save(r, w); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user