diff options
author | Julien Dessaux | 2025-01-06 00:41:32 +0100 |
---|---|---|
committer | Julien Dessaux | 2025-01-06 00:41:32 +0100 |
commit | 6e069484cb0a911ba541e07bf04331fadbb76612 (patch) | |
tree | 3457c759d54ae91e5ac1e64fe0bbf9c4b8ac18f0 /pkg/webui/login.go | |
parent | feat(tfstated): add syscall.SIGTERM handling (diff) | |
download | tfstated-6e069484cb0a911ba541e07bf04331fadbb76612.tar.gz tfstated-6e069484cb0a911ba541e07bf04331fadbb76612.tar.bz2 tfstated-6e069484cb0a911ba541e07bf04331fadbb76612.zip |
feat(webui): bootstrap session handling and login process
Diffstat (limited to '')
-rw-r--r-- | pkg/webui/login.go | 115 |
1 files changed, 115 insertions, 0 deletions
diff --git a/pkg/webui/login.go b/pkg/webui/login.go new file mode 100644 index 0000000..d004d82 --- /dev/null +++ b/pkg/webui/login.go @@ -0,0 +1,115 @@ +package webui + +import ( + "context" + "fmt" + "html/template" + "net/http" + "regexp" + + "git.adyxax.org/adyxax/tfstated/pkg/database" + "git.adyxax.org/adyxax/tfstated/pkg/model" +) + +var loginTemplate = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/login.html")) + +type loginPage struct { + Forbidden bool + Username string +} + +func handleLoginGET() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-store, no-cache") + + session := r.Context().Value(model.SessionContextKey{}) + if session != nil { + http.Redirect(w, r, "/", http.StatusFound) + return + } + + render(w, loginTemplate, http.StatusOK, loginPage{}) + }) +} + +func handleLoginPOST(db *database.DB) http.Handler { + var validUsername = regexp.MustCompile(`^[a-zA-Z]\w*$`) + renderForbidden := func(w http.ResponseWriter, username string) { + render(w, loginTemplate, http.StatusForbidden, loginPage{ + Forbidden: true, + Username: username, + }) + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + errorResponse(w, http.StatusBadRequest, err) + return + } + username := r.FormValue("username") + password := r.FormValue("password") + + if username == "" || password == "" { // the webui cannot issue this + errorResponse(w, http.StatusBadRequest, fmt.Errorf("Forbidden")) + return + } + if ok := validUsername.MatchString(username); !ok { + renderForbidden(w, username) + return + } + account, err := db.LoadAccountByUsername(username) + if err != nil { + errorResponse(w, http.StatusInternalServerError, err) + return + } + if account == nil || !account.CheckPassword(password) { + renderForbidden(w, username) + return + } + if err := db.TouchAccount(account); err != nil { + errorResponse(w, http.StatusInternalServerError, err) + return + } + sessionId, err := db.CreateSession(account) + if err != nil { + errorResponse(w, http.StatusInternalServerError, err) + return + } + http.SetCookie(w, &http.Cookie{ + Name: cookieName, + Value: sessionId, + Quoted: false, + Path: "/", + MaxAge: 8 * 3600, // 1 hour sessions + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + Secure: true, + }) + http.Redirect(w, r, "/", http.StatusFound) + }) +} + +func loginMiddleware(db *database.DB) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-store, no-cache") + session := r.Context().Value(model.SessionContextKey{}) + if session == nil { + http.Redirect(w, r, "/login", http.StatusFound) + return + } + account, err := db.LoadAccountById(session.(*model.Session).AccountId) + if err != nil { + errorResponse(w, http.StatusInternalServerError, err) + return + } + if account == nil { + // this could happen if the account was deleted in the short + // time between retrieving the session and here + http.Redirect(w, r, "/login", http.StatusFound) + return + } + ctx := context.WithValue(r.Context(), model.AccountContextKey{}, account) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} |