summaryrefslogtreecommitdiff
path: root/pkg/webui/login.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/webui/login.go')
-rw-r--r--pkg/webui/login.go115
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))
+ })
+ }
+}