This commit is contained in:
Florian Hoss 2024-09-16 11:45:24 +02:00
parent 34486a6ca4
commit 00915c67d7
Signed by: flohoss
GPG key ID: 3F35C7F6E6F66F6B
12 changed files with 152 additions and 77 deletions

View file

@ -1,8 +1,6 @@
package components package components
import ( import "gitlab.unjx.de/flohoss/godash/services"
"gitlab.unjx.de/flohoss/godash/services"
)
templ Application(application services.Application) { templ Application(application services.Application) {
<a href={ templ.URL(application.URL) } class="flex items-center hover-effect"> <a href={ templ.URL(application.URL) } class="flex items-center hover-effect">

View file

@ -1,9 +1,9 @@
package components package components
import ( import (
"html/template"
"gitlab.unjx.de/flohoss/godash/services"
"fmt" "fmt"
"gitlab.unjx.de/flohoss/godash/services"
"html/template"
) )
var BarTemplate = template.Must(template.New("bar").Parse("<div id=\"{{ .Id }}\" class=\"progress-bar\" style=\"width: {{ .Percentage }}%\"></div>")) var BarTemplate = template.Must(template.New("bar").Parse("<div id=\"{{ .Id }}\" class=\"progress-bar\" style=\"width: {{ .Percentage }}%\"></div>"))
@ -20,7 +20,7 @@ templ System(icon string, infoPre string, infoPost string, extraInfo string, per
<div class="extra-info">{ extraInfo }</div> <div class="extra-info">{ extraInfo }</div>
<div class="truncate"><span id={ valueId }>{ infoPre }</span>{ infoPost }</div> <div class="truncate"><span id={ valueId }>{ infoPre }</span>{ infoPost }</div>
<div class="progress-bar-wrapper"> <div class="progress-bar-wrapper">
@templ.FromGoHTML(BarTemplate, Bar{Id:percentageId, Percentage:percentage}) @templ.FromGoHTML(BarTemplate, Bar{Id: percentageId, Percentage: percentage})
</div> </div>
</div> </div>
</div> </div>
@ -42,18 +42,18 @@ templ Uptime(extraInfo string, id string, uptime services.Uptime) {
<div class="truncate"> <div class="truncate">
<span><span id="uptimeDays">{ fmt.Sprintf("%d",uptime.Days) }</span> days</span> <span><span id="uptimeDays">{ fmt.Sprintf("%d",uptime.Days) }</span> days</span>
<span class="countdown"> <span class="countdown">
@templ.FromGoHTML(countDownTemplate, Countdown{Id:"uptimeHours", Value:uptime.Hours}) @templ.FromGoHTML(countDownTemplate, Countdown{Id: "uptimeHours", Value: uptime.Hours})
</span> hours </span> hours
<span class="countdown"> <span class="countdown">
@templ.FromGoHTML(countDownTemplate, Countdown{Id:"uptimeMinutes", Value:uptime.Minutes}) @templ.FromGoHTML(countDownTemplate, Countdown{Id: "uptimeMinutes", Value: uptime.Minutes})
</span> min </span> min
<span class="countdown"> <span class="countdown">
@templ.FromGoHTML(countDownTemplate, Countdown{Id:"uptimeSeconds", Value:uptime.Seconds}) @templ.FromGoHTML(countDownTemplate, Countdown{Id: "uptimeSeconds", Value: uptime.Seconds})
</span> sec </span> sec
</div> </div>
</div> </div>
<div class="progress-bar-wrapper"> <div class="progress-bar-wrapper">
@templ.FromGoHTML(BarTemplate, Bar{Id:id, Percentage:float64(uptime.Percentage)}) @templ.FromGoHTML(BarTemplate, Bar{Id: id, Percentage: float64(uptime.Percentage)})
</div> </div>
</div> </div>
</div> </div>

11
components/user.templ Normal file
View file

@ -0,0 +1,11 @@
package components
import "gitlab.unjx.de/flohoss/godash/services"
templ User(user services.User) {
<div class="avatar">
<div class="ring-primary ring-offset-base-100 w-16 rounded-full ring-2 ring-offset-2">
<img src={ string(templ.SafeURL(user.Gravatar)) }/>
</div>
</div>
}

View file

@ -1,8 +1,8 @@
package components package components
import ( import (
"gitlab.unjx.de/flohoss/godash/services"
"fmt" "fmt"
"gitlab.unjx.de/flohoss/godash/services"
) )
func getIcon(icon string) string { func getIcon(icon string) string {

2
go.mod
View file

@ -4,11 +4,13 @@ go 1.22
require ( require (
github.com/a-h/templ v0.2.778 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/caarlos0/env/v10 v10.0.0
github.com/coreos/go-oidc/v3 v3.11.0 github.com/coreos/go-oidc/v3 v3.11.0
github.com/go-playground/validator/v10 v10.22.1 github.com/go-playground/validator/v10 v10.22.1
github.com/r3labs/sse/v2 v2.10.0 github.com/r3labs/sse/v2 v2.10.0
github.com/shirou/gopsutil/v4 v4.24.8 github.com/shirou/gopsutil/v4 v4.24.8
github.com/thanhpk/randstr v1.0.6
golang.org/x/oauth2 v0.23.0 golang.org/x/oauth2 v0.23.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )

4
go.sum
View file

@ -1,5 +1,7 @@
github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM= github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM=
github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w= 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 h1:yIHUBZGsyqCnpTkbjk8asUlx6RFhhEs+h7TOBdgdzXA=
github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9aGxzeN7G18= github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9aGxzeN7G18=
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= 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.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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= 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= github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=

View file

@ -38,10 +38,6 @@ type AppHandler struct {
} }
func (bh *AppHandler) appHandler(w http.ResponseWriter, r *http.Request) { 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() bookmarks := bh.bookmarkService.GetAllBookmarks()
staticSystem := bh.systemService.GetStaticInformation() staticSystem := bh.systemService.GetStaticInformation()
liveSystem := bh.systemService.GetLiveInformation() liveSystem := bh.systemService.GetLiveInformation()
@ -49,5 +45,12 @@ func (bh *AppHandler) appHandler(w http.ResponseWriter, r *http.Request) {
titlePage := bh.env.Title 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)
} }

View file

@ -5,28 +5,21 @@ import (
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt"
"io" "io"
"log/slog" "log/slog"
"net/http" "net/http"
"os" "os"
"time" "time"
"github.com/alexedwards/scs/v2"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/thanhpk/randstr"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gitlab.unjx.de/flohoss/godash/internal/env" "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) { func setCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
c := &http.Cookie{ c := &http.Cookie{
Name: name, Name: name,
@ -80,15 +73,21 @@ func NewAuthHandler(env *env.Config) *AuthHandler {
} }
codeChallenge := generateCodeChallenge(codeVerifier) codeChallenge := generateCodeChallenge(codeVerifier)
authCodeOptions := []oauth2.AuthCodeOption{ authCodeOptions := []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("redirect_uri", env.OIDCRedirectURI),
oauth2.SetAuthURLParam("code_challenge", codeChallenge), oauth2.SetAuthURLParam("code_challenge", codeChallenge),
oauth2.SetAuthURLParam("code_challenge_method", "S256"), oauth2.SetAuthURLParam("code_challenge_method", "S256"),
oauth2.SetAuthURLParam("code_verifier", codeVerifier),
} }
sessionManager := scs.New()
sessionManager.Lifetime = 24 * time.Hour
return &AuthHandler{ return &AuthHandler{
ctx: ctx, ctx: ctx,
oidcProvider: oidcProvider, oidcProvider: oidcProvider,
oauth2Config: oauth2Config, oauth2Config: oauth2Config,
authCodeOptions: authCodeOptions, authCodeOptions: authCodeOptions,
SessionManager: sessionManager,
} }
} }
@ -97,17 +96,7 @@ type AuthHandler struct {
oidcProvider *oidc.Provider oidcProvider *oidc.Provider
oauth2Config *oauth2.Config oauth2Config *oauth2.Config
authCodeOptions []oauth2.AuthCodeOption authCodeOptions []oauth2.AuthCodeOption
} SessionManager *scs.SessionManager
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)
} }
func (ah *AuthHandler) handleCallback(w http.ResponseWriter, r *http.Request) { 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 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 { if err != nil {
http.Error(w, "failed to exchange token: "+err.Error(), http.StatusInternalServerError) http.Error(w, "failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return return
} }
userInfo, err := ah.oidcProvider.UserInfo(ah.ctx, oauth2.StaticTokenSource(oauth2Token)) ah.SessionManager.Put(r.Context(), "access_token", oauth2Token.AccessToken)
if err != nil {
http.Error(w, "failed to get userinfo: "+err.Error(), http.StatusInternalServerError)
return
}
resp := struct { http.Redirect(w, r, "/", http.StatusFound)
OAuth2Token *oauth2.Token }
UserInfo *oidc.UserInfo
}{oauth2Token, userInfo} func (ah *AuthHandler) handleLogout(w http.ResponseWriter, r *http.Request) {
data, err := json.MarshalIndent(resp, "", " ") ah.SessionManager.Clear(r.Context())
if err != nil { http.Redirect(w, r, "/", http.StatusFound)
http.Error(w, err.Error(), http.StatusInternalServerError) }
return
} func (ah *AuthHandler) handleLogin(w http.ResponseWriter, r *http.Request) {
w.Write(data) 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 { func (ah *AuthHandler) AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
state, err := r.Cookie("state") accessToken := ah.SessionManager.GetString(r.Context(), "access_token")
if err != nil { if accessToken == "" {
http.Error(w, err.Error(), http.StatusUnauthorized) ah.handleLogin(w, r)
return
}
oauth2Token, err := ah.oauth2Config.Exchange(ah.ctx, r.URL.Query().Get("code"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return 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 { if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized) ah.handleLogin(w, r)
return 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) next.ServeHTTP(w, r)
}) })
} }

View file

@ -7,16 +7,16 @@ import (
) )
func SetupRoutes(router *http.ServeMux, sse *sse.Server, appHandler *AppHandler, authHandler *AuthHandler) { 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")) 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")) 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 /auth/logout", http.HandlerFunc(authHandler.handleLogout))
router.HandleFunc("GET /auch/callback", authHandler.handleCallback) router.HandleFunc("GET /auth/callback", authHandler.handleCallback)
router.Handle("GET /", authHandler.AuthMiddleware(http.HandlerFunc(appHandler.appHandler))) router.Handle("GET /", authHandler.AuthMiddleware(http.HandlerFunc(appHandler.appHandler)))
} }

View file

@ -35,7 +35,7 @@ func main() {
lis := fmt.Sprintf(":%d", env.Port) lis := fmt.Sprintf(":%d", env.Port)
slog.Info("server listening, press ctrl+c to stop", "addr", "http://localhost"+lis) 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) { if !errors.Is(err, http.ErrServerClosed) {
slog.Error("server terminated", "error", err) slog.Error("server terminated", "error", err)
os.Exit(1) os.Exit(1)

View file

@ -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()
}

View file

@ -2,22 +2,22 @@ package home
import ( import (
"fmt" "fmt"
"gitlab.unjx.de/flohoss/godash/components"
"gitlab.unjx.de/flohoss/godash/services" "gitlab.unjx.de/flohoss/godash/services"
"gitlab.unjx.de/flohoss/godash/views/layout" "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) {
<section class="grid gap-10"> <section class="grid gap-10">
<div class="flex w-full justify-between items-center"> <div class="flex w-full justify-between items-center">
@components.Weather(weather) @components.Weather(weather)
@components.User(user)
</div> </div>
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-3 select-none"> <div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-3 select-none">
@components.System("icon-[bi--cpu]",static.CPU.Name,"",static.CPU.Threads,"systemCpuPercentage","",live.CPU) @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--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.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.Uptime(static.Host.Architecture, "systemUptimePercentage", live.Uptime)
</div> </div>
<div class="grid gap-4"> <div class="grid gap-4">
for _, a := range bookmarks.Applications { for _, a := range bookmarks.Applications {