diff --git a/components/application.templ b/components/application.templ index 2f72b67..9a0efa6 100644 --- a/components/application.templ +++ b/components/application.templ @@ -1,8 +1,6 @@ package components -import ( - "gitlab.unjx.de/flohoss/godash/services" -) +import "gitlab.unjx.de/flohoss/godash/services" templ Application(application services.Application) { diff --git a/components/system.templ b/components/system.templ index 76e1c6a..2600e1e 100644 --- a/components/system.templ +++ b/components/system.templ @@ -1,9 +1,9 @@ package components import ( - "html/template" - "gitlab.unjx.de/flohoss/godash/services" "fmt" + "gitlab.unjx.de/flohoss/godash/services" + "html/template" ) var BarTemplate = template.Must(template.New("bar").Parse("
")) @@ -20,7 +20,7 @@ templ System(icon string, infoPre string, infoPost string, extraInfo string, per
{ extraInfo }
{ infoPre }{ infoPost }
- @templ.FromGoHTML(BarTemplate, Bar{Id:percentageId, Percentage:percentage}) + @templ.FromGoHTML(BarTemplate, Bar{Id: percentageId, Percentage: percentage})
@@ -42,18 +42,18 @@ templ Uptime(extraInfo string, id string, uptime services.Uptime) {
{ fmt.Sprintf("%d",uptime.Days) } days - @templ.FromGoHTML(countDownTemplate, Countdown{Id:"uptimeHours", Value:uptime.Hours}) + @templ.FromGoHTML(countDownTemplate, Countdown{Id: "uptimeHours", Value: uptime.Hours}) hours - @templ.FromGoHTML(countDownTemplate, Countdown{Id:"uptimeMinutes", Value:uptime.Minutes}) + @templ.FromGoHTML(countDownTemplate, Countdown{Id: "uptimeMinutes", Value: uptime.Minutes}) min - @templ.FromGoHTML(countDownTemplate, Countdown{Id:"uptimeSeconds", Value:uptime.Seconds}) + @templ.FromGoHTML(countDownTemplate, Countdown{Id: "uptimeSeconds", Value: uptime.Seconds}) sec
- @templ.FromGoHTML(BarTemplate, Bar{Id:id, Percentage:float64(uptime.Percentage)}) + @templ.FromGoHTML(BarTemplate, Bar{Id: id, Percentage: float64(uptime.Percentage)})
diff --git a/components/user.templ b/components/user.templ new file mode 100644 index 0000000..26ef43d --- /dev/null +++ b/components/user.templ @@ -0,0 +1,11 @@ +package components + +import "gitlab.unjx.de/flohoss/godash/services" + +templ User(user services.User) { +
+
+ +
+
+} diff --git a/components/weather.templ b/components/weather.templ index d2c350b..8234483 100644 --- a/components/weather.templ +++ b/components/weather.templ @@ -1,8 +1,8 @@ package components import ( - "gitlab.unjx.de/flohoss/godash/services" "fmt" + "gitlab.unjx.de/flohoss/godash/services" ) func getIcon(icon string) string { diff --git a/go.mod b/go.mod index cce7c1e..d498b24 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,13 @@ go 1.22 require ( github.com/a-h/templ v0.2.778 + github.com/alexedwards/scs/v2 v2.8.0 github.com/caarlos0/env/v10 v10.0.0 github.com/coreos/go-oidc/v3 v3.11.0 github.com/go-playground/validator/v10 v10.22.1 github.com/r3labs/sse/v2 v2.10.0 github.com/shirou/gopsutil/v4 v4.24.8 + github.com/thanhpk/randstr v1.0.6 golang.org/x/oauth2 v0.23.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index bdeac65..771e896 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM= github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w= +github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= +github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= github.com/caarlos0/env/v10 v10.0.0 h1:yIHUBZGsyqCnpTkbjk8asUlx6RFhhEs+h7TOBdgdzXA= github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9aGxzeN7G18= github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= @@ -56,6 +58,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/thanhpk/randstr v1.0.6 h1:psAOktJFD4vV9NEVb3qkhRSMvYh4ORRaj1+w/hn4B+o= +github.com/thanhpk/randstr v1.0.6/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= diff --git a/handlers/app.handlers.go b/handlers/app.handlers.go index 368ece3..5ca2c05 100644 --- a/handlers/app.handlers.go +++ b/handlers/app.handlers.go @@ -38,10 +38,6 @@ type AppHandler struct { } func (bh *AppHandler) appHandler(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - http.Redirect(w, r, "/", http.StatusFound) - return - } bookmarks := bh.bookmarkService.GetAllBookmarks() staticSystem := bh.systemService.GetStaticInformation() liveSystem := bh.systemService.GetLiveInformation() @@ -49,5 +45,12 @@ func (bh *AppHandler) appHandler(w http.ResponseWriter, r *http.Request) { titlePage := bh.env.Title - home.HomeIndex(titlePage, bh.env.Version, home.Home(titlePage, bookmarks, staticSystem, liveSystem, weather)).Render(r.Context(), w) + user := services.User{ + Name: w.Header().Get("X-User-Name"), + Email: w.Header().Get("X-User-Email"), + } + gravatar := services.NewGravatarFromEmail(user.Email) + user.Gravatar = gravatar.GetURL() + + home.HomeIndex(titlePage, bh.env.Version, home.Home(titlePage, user, bookmarks, staticSystem, liveSystem, weather)).Render(r.Context(), w) } diff --git a/handlers/auth.handlers.go b/handlers/auth.handlers.go index 33944b1..dbf6096 100644 --- a/handlers/auth.handlers.go +++ b/handlers/auth.handlers.go @@ -5,28 +5,21 @@ import ( "crypto/rand" "crypto/sha256" "encoding/base64" - "encoding/json" - "fmt" "io" "log/slog" "net/http" "os" "time" + "github.com/alexedwards/scs/v2" "github.com/coreos/go-oidc/v3/oidc" + "github.com/thanhpk/randstr" "golang.org/x/oauth2" "gitlab.unjx.de/flohoss/godash/internal/env" + "gitlab.unjx.de/flohoss/godash/services" ) -func randString(nByte int) (string, error) { - b := make([]byte, nByte) - if _, err := io.ReadFull(rand.Reader, b); err != nil { - return "", err - } - return base64.RawURLEncoding.EncodeToString(b), nil -} - func setCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) { c := &http.Cookie{ Name: name, @@ -80,15 +73,21 @@ func NewAuthHandler(env *env.Config) *AuthHandler { } codeChallenge := generateCodeChallenge(codeVerifier) authCodeOptions := []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("redirect_uri", env.OIDCRedirectURI), oauth2.SetAuthURLParam("code_challenge", codeChallenge), oauth2.SetAuthURLParam("code_challenge_method", "S256"), + oauth2.SetAuthURLParam("code_verifier", codeVerifier), } + sessionManager := scs.New() + sessionManager.Lifetime = 24 * time.Hour + return &AuthHandler{ ctx: ctx, oidcProvider: oidcProvider, oauth2Config: oauth2Config, authCodeOptions: authCodeOptions, + SessionManager: sessionManager, } } @@ -97,17 +96,7 @@ type AuthHandler struct { oidcProvider *oidc.Provider oauth2Config *oauth2.Config authCodeOptions []oauth2.AuthCodeOption -} - -func (ah *AuthHandler) handleAuth(w http.ResponseWriter, r *http.Request) { - state, err := randString(16) - if err != nil { - http.Error(w, "Internal error", http.StatusInternalServerError) - return - } - setCallbackCookie(w, r, "state", state) - - http.Redirect(w, r, ah.oauth2Config.AuthCodeURL(state, ah.authCodeOptions...), http.StatusFound) + SessionManager *scs.SessionManager } func (ah *AuthHandler) handleCallback(w http.ResponseWriter, r *http.Request) { @@ -121,49 +110,46 @@ func (ah *AuthHandler) handleCallback(w http.ResponseWriter, r *http.Request) { return } - oauth2Token, err := ah.oauth2Config.Exchange(ah.ctx, r.URL.Query().Get("code")) + oauth2Token, err := ah.oauth2Config.Exchange(ah.ctx, r.URL.Query().Get("code"), ah.authCodeOptions...) if err != nil { http.Error(w, "failed to exchange token: "+err.Error(), http.StatusInternalServerError) return } - userInfo, err := ah.oidcProvider.UserInfo(ah.ctx, oauth2.StaticTokenSource(oauth2Token)) - if err != nil { - http.Error(w, "failed to get userinfo: "+err.Error(), http.StatusInternalServerError) - return - } + ah.SessionManager.Put(r.Context(), "access_token", oauth2Token.AccessToken) - resp := struct { - OAuth2Token *oauth2.Token - UserInfo *oidc.UserInfo - }{oauth2Token, userInfo} - data, err := json.MarshalIndent(resp, "", " ") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Write(data) + http.Redirect(w, r, "/", http.StatusFound) +} + +func (ah *AuthHandler) handleLogout(w http.ResponseWriter, r *http.Request) { + ah.SessionManager.Clear(r.Context()) + http.Redirect(w, r, "/", http.StatusFound) +} + +func (ah *AuthHandler) handleLogin(w http.ResponseWriter, r *http.Request) { + state := randstr.String(16) + setCallbackCookie(w, r, "state", state) + http.Redirect(w, r, ah.oauth2Config.AuthCodeURL(state, ah.authCodeOptions...), http.StatusFound) } func (ah *AuthHandler) AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - state, err := r.Cookie("state") - if err != nil { - http.Error(w, err.Error(), http.StatusUnauthorized) - return - } - oauth2Token, err := ah.oauth2Config.Exchange(ah.ctx, r.URL.Query().Get("code")) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + accessToken := ah.SessionManager.GetString(r.Context(), "access_token") + if accessToken == "" { + ah.handleLogin(w, r) return } - userInfo, err := ah.oidcProvider.UserInfo(ah.ctx, oauth2.StaticTokenSource(oauth2Token)) + userInfo, err := ah.oidcProvider.UserInfo(ah.ctx, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken})) if err != nil { - http.Error(w, err.Error(), http.StatusUnauthorized) + ah.handleLogin(w, r) return } - fmt.Println(userInfo) + var userClaims services.User + userInfo.Claims(&userClaims) + w.Header().Set("X-User-Name", userClaims.Name) + w.Header().Set("X-User-Email", userClaims.Email) + next.ServeHTTP(w, r) }) } diff --git a/handlers/routes.go b/handlers/routes.go index fe57f5f..1bce187 100644 --- a/handlers/routes.go +++ b/handlers/routes.go @@ -7,16 +7,16 @@ import ( ) func SetupRoutes(router *http.ServeMux, sse *sse.Server, appHandler *AppHandler, authHandler *AuthHandler) { - router.HandleFunc("GET /sse", sse.ServeHTTP) + router.Handle("GET /sse", authHandler.AuthMiddleware(http.HandlerFunc(sse.ServeHTTP))) fsAssets := http.FileServer(http.Dir("assets")) - router.Handle("GET /assets/", http.StripPrefix("/assets/", fsAssets)) + router.Handle("GET /assets/", authHandler.AuthMiddleware((http.StripPrefix("/assets/", fsAssets)))) icons := http.FileServer(http.Dir("storage/icons")) - router.Handle("GET /icons/", http.StripPrefix("/icons/", icons)) + router.Handle("GET /icons/", authHandler.AuthMiddleware(http.StripPrefix("/icons/", icons))) - router.HandleFunc("GET /login", authHandler.handleAuth) - router.HandleFunc("GET /auch/callback", authHandler.handleCallback) + router.HandleFunc("GET /auth/logout", http.HandlerFunc(authHandler.handleLogout)) + router.HandleFunc("GET /auth/callback", authHandler.handleCallback) router.Handle("GET /", authHandler.AuthMiddleware(http.HandlerFunc(appHandler.appHandler))) } diff --git a/main.go b/main.go index 150a715..f4f45c4 100644 --- a/main.go +++ b/main.go @@ -35,7 +35,7 @@ func main() { lis := fmt.Sprintf(":%d", env.Port) slog.Info("server listening, press ctrl+c to stop", "addr", "http://localhost"+lis) - err = http.ListenAndServe(lis, router) + err = http.ListenAndServe(lis, authHandler.SessionManager.LoadAndSave(router)) if !errors.Is(err, http.ErrServerClosed) { slog.Error("server terminated", "error", err) os.Exit(1) diff --git a/services/claims.services.go b/services/claims.services.go new file mode 100644 index 0000000..81dc7d7 --- /dev/null +++ b/services/claims.services.go @@ -0,0 +1,71 @@ +package services + +import ( + "crypto/sha256" + "encoding/hex" + "net/url" + "strconv" + "strings" +) + +type User struct { + Name string `json:"name"` + Email string `json:"email"` + Gravatar string `json:"gravatar"` +} + +const ( + defaultScheme = "https" + defaultHostname = "www.gravatar.com" +) + +func NewGravatarFromEmail(email string) Gravatar { + hasher := sha256.Sum256([]byte(strings.TrimSpace(email))) + hash := hex.EncodeToString(hasher[:]) + + g := NewGravatar() + g.Hash = hash + return g +} + +func NewGravatar() Gravatar { + return Gravatar{ + Scheme: defaultScheme, + Host: defaultHostname, + } +} + +type Gravatar struct { + Scheme string + Host string + Hash string + Default string + Rating string + Size int +} + +func (g Gravatar) GetURL() string { + path := "/avatar/" + g.Hash + + v := url.Values{} + if g.Size > 0 { + v.Add("s", strconv.Itoa(g.Size)) + } + + if g.Rating != "" { + v.Add("r", g.Rating) + } + + if g.Default != "" { + v.Add("d", g.Default) + } + + url := url.URL{ + Scheme: g.Scheme, + Host: g.Host, + Path: path, + RawQuery: v.Encode(), + } + + return url.String() +} diff --git a/views/home/home.templ b/views/home/home.templ index 8b4cb19..e5dc163 100644 --- a/views/home/home.templ +++ b/views/home/home.templ @@ -2,22 +2,22 @@ package home import ( "fmt" - + "gitlab.unjx.de/flohoss/godash/components" "gitlab.unjx.de/flohoss/godash/services" "gitlab.unjx.de/flohoss/godash/views/layout" - "gitlab.unjx.de/flohoss/godash/components" ) -templ Home(title string, bookmarks *services.Bookmarks, static *services.StaticInformation, live *services.LiveInformation, weather *services.OpenWeather) { +templ Home(title string, user services.User, bookmarks *services.Bookmarks, static *services.StaticInformation, live *services.LiveInformation, weather *services.OpenWeather) {
@components.Weather(weather) + @components.User(user)
- @components.System("icon-[bi--cpu]",static.CPU.Name,"",static.CPU.Threads,"systemCpuPercentage","",live.CPU) - @components.System("icon-[bi--nvme]",live.Disk.Value,fmt.Sprintf(" | %s", static.Disk.Total),static.Disk.Partitions,"systemDiskPercentage","systemDiskValue",live.Disk.Percentage) - @components.System("icon-[bi--memory]",live.Ram.Value,fmt.Sprintf(" | %s", static.Ram.Total),static.Ram.Swap,"systemRamPercentage","systemRamValue",live.Ram.Percentage) - @components.Uptime(static.Host.Architecture,"systemUptimePercentage",live.Uptime) + @components.System("icon-[bi--cpu]", static.CPU.Name, "", static.CPU.Threads, "systemCpuPercentage", "", live.CPU) + @components.System("icon-[bi--nvme]", live.Disk.Value, fmt.Sprintf(" | %s", static.Disk.Total), static.Disk.Partitions, "systemDiskPercentage", "systemDiskValue", live.Disk.Percentage) + @components.System("icon-[bi--memory]", live.Ram.Value, fmt.Sprintf(" | %s", static.Ram.Total), static.Ram.Swap, "systemRamPercentage", "systemRamValue", live.Ram.Percentage) + @components.Uptime(static.Host.Architecture, "systemUptimePercentage", live.Uptime)
for _, a := range bookmarks.Applications {