summaryrefslogtreecommitdiff
path: root/internal/handlers
diff options
context:
space:
mode:
authorGrail Finder <wohilas@gmail.com>2025-03-29 11:12:53 +0300
committerGrail Finder <wohilas@gmail.com>2025-03-29 11:12:53 +0300
commit3921db6166e2da895257496bb76dd115556699d3 (patch)
tree1be4f739121761085f69cb7706c60dbbe98a93e9 /internal/handlers
init
Diffstat (limited to 'internal/handlers')
-rw-r--r--internal/handlers/auth.go162
-rw-r--r--internal/handlers/main.go64
-rw-r--r--internal/handlers/middleware.go75
3 files changed, 301 insertions, 0 deletions
diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go
new file mode 100644
index 0000000..d0ccdff
--- /dev/null
+++ b/internal/handlers/auth.go
@@ -0,0 +1,162 @@
+package handlers
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+ "demoon/internal/models"
+ "demoon/pkg/utils"
+ "encoding/base64"
+ "encoding/json"
+ "html/template"
+ "net/http"
+ "strings"
+ "time"
+
+ "golang.org/x/crypto/bcrypt"
+)
+
+func abortWithError(w http.ResponseWriter, msg string) {
+ tmpl := template.Must(template.ParseGlob("components/*.html"))
+ tmpl.ExecuteTemplate(w, "error", msg)
+}
+
+func (h *Handlers) HandleSignup(w http.ResponseWriter, r *http.Request) {
+ r.ParseForm()
+ username := r.PostFormValue("username")
+ if username == "" {
+ msg := "username not provided"
+ h.log.Error(msg)
+ abortWithError(w, msg)
+ return
+ }
+ password := r.PostFormValue("password")
+ if password == "" {
+ msg := "password not provided"
+ h.log.Error(msg)
+ abortWithError(w, msg)
+ return
+ }
+ // make sure username does not exists
+ cleanName := utils.RemoveSpacesFromStr(username)
+ hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 8)
+ // create user in db
+ now := time.Now()
+ nextMidnight := time.Date(now.Year(), now.Month(), now.Day(),
+ 0, 0, 0, 0, time.UTC).Add(time.Hour * 24)
+ newUser := &models.UserScore{
+ Username: cleanName, Password: string(hashedPassword),
+ BurnTime: nextMidnight, CreatedAt: now,
+ }
+ // login user
+ cookie, err := h.makeCookie(cleanName, r.RemoteAddr)
+ if err != nil {
+ h.log.Error("failed to login", "error", err)
+ abortWithError(w, err.Error())
+ return
+ }
+ http.SetCookie(w, cookie)
+ // http.Redirect(w, r, "/", 302)
+ tmpl, err := template.ParseGlob("components/*.html")
+ if err != nil {
+ abortWithError(w, err.Error())
+ return
+ }
+ tmpl.ExecuteTemplate(w, "main", newUser)
+}
+
+func (h *Handlers) HandleLogin(w http.ResponseWriter, r *http.Request) {
+ r.ParseForm()
+ username := r.PostFormValue("username")
+ if username == "" {
+ msg := "username not provided"
+ h.log.Error(msg)
+ abortWithError(w, msg)
+ return
+ }
+ password := r.PostFormValue("password")
+ if password == "" {
+ msg := "password not provided"
+ h.log.Error(msg)
+ abortWithError(w, msg)
+ return
+ }
+ cleanName := utils.RemoveSpacesFromStr(username)
+ tmpl, err := template.ParseGlob("components/*.html")
+ if err != nil {
+ abortWithError(w, err.Error())
+ return
+ }
+ cookie, err := h.makeCookie(cleanName, r.RemoteAddr)
+ if err != nil {
+ h.log.Error("failed to login", "error", err)
+ abortWithError(w, err.Error())
+ return
+ }
+ http.SetCookie(w, cookie)
+ tmpl.ExecuteTemplate(w, "main", nil)
+}
+
+func (h *Handlers) makeCookie(username string, remote string) (*http.Cookie, error) {
+ // secret
+ // Create a new random session token
+ // sessionToken := xid.New().String()
+ sessionToken := "token"
+ expiresAt := time.Now().Add(time.Duration(h.cfg.SessionLifetime) * time.Second)
+ // Set the token in the session map, along with the session information
+ session := &models.Session{
+ Username: username,
+ Expiry: expiresAt,
+ }
+ cookieName := "session_token"
+ // hmac to protect cookies
+ hm := hmac.New(sha256.New, []byte(h.cfg.CookieSecret))
+ hm.Write([]byte(cookieName))
+ hm.Write([]byte(sessionToken))
+ signature := hm.Sum(nil)
+ // b64 enc to avoid non-ascii
+ cookieValue := base64.URLEncoding.EncodeToString([]byte(
+ string(signature) + sessionToken))
+ cookie := &http.Cookie{
+ Name: cookieName,
+ Value: cookieValue,
+ Secure: true,
+ HttpOnly: true,
+ SameSite: http.SameSiteNoneMode,
+ Domain: h.cfg.ServerConfig.Host,
+ }
+ h.log.Info("check remote addr for cookie set",
+ "remote", remote, "session", session)
+ if strings.Contains(remote, "192.168.0") {
+ // no idea what is going on
+ cookie.Domain = "192.168.0.101"
+ }
+ // set ctx?
+ // set user in session
+ if err := h.cacheSetSession(sessionToken, session); err != nil {
+ return nil, err
+ }
+ return cookie, nil
+}
+
+func (h *Handlers) cacheGetSession(key string) (*models.Session, error) {
+ userSessionB, err := h.mc.Get(key)
+ if err != nil {
+ return nil, err
+ }
+ var us *models.Session
+ if err := json.Unmarshal(userSessionB, &us); err != nil {
+ return nil, err
+ }
+ return us, nil
+}
+
+func (h *Handlers) cacheSetSession(key string, session *models.Session) error {
+ sesb, err := json.Marshal(session)
+ if err != nil {
+ return err
+ }
+ h.mc.Set(key, sesb)
+ // expire in 10 min
+ h.mc.Expire(key, 10*60)
+ return nil
+}
diff --git a/internal/handlers/main.go b/internal/handlers/main.go
new file mode 100644
index 0000000..960b26d
--- /dev/null
+++ b/internal/handlers/main.go
@@ -0,0 +1,64 @@
+package handlers
+
+import (
+ "demoon/config"
+ "demoon/internal/database/repos"
+ "demoon/internal/models"
+ "demoon/pkg/cache"
+ "html/template"
+ "log/slog"
+ "net/http"
+ "os"
+)
+
+var defUS = models.UserScore{}
+
+// Handlers structure
+type Handlers struct {
+ cfg config.Config
+ log *slog.Logger
+ repo repos.FullRepo
+ mc cache.Cache
+}
+
+// NewHandlers constructor
+func NewHandlers(
+ cfg config.Config, l *slog.Logger, repo repos.FullRepo,
+) *Handlers {
+ if l == nil {
+ l = slog.New(slog.NewJSONHandler(os.Stdout, nil))
+ }
+ h := &Handlers{
+ cfg: cfg,
+ log: l,
+ repo: repo,
+ mc: cache.MemCache,
+ }
+ return h
+}
+
+func (h *Handlers) Ping(w http.ResponseWriter, r *http.Request) {
+ h.log.Info("got ping request")
+ w.Write([]byte("pong"))
+}
+
+func (h *Handlers) MainPage(w http.ResponseWriter, r *http.Request) {
+ tmpl, err := template.ParseGlob("components/*.html")
+ if err != nil {
+ abortWithError(w, err.Error())
+ return
+ }
+ // get recommendations
+ usernameRaw := r.Context().Value("username")
+ h.log.Info("got mainpage request", "username", usernameRaw)
+ if usernameRaw == nil {
+ tmpl.ExecuteTemplate(w, "main", defUS)
+ return
+ }
+ username := usernameRaw.(string)
+ if username == "" {
+ tmpl.ExecuteTemplate(w, "main", defUS)
+ return
+ }
+ tmpl.ExecuteTemplate(w, "main", nil)
+}
diff --git a/internal/handlers/middleware.go b/internal/handlers/middleware.go
new file mode 100644
index 0000000..8b871a2
--- /dev/null
+++ b/internal/handlers/middleware.go
@@ -0,0 +1,75 @@
+package handlers
+
+import (
+ "context"
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
+ "errors"
+ "net/http"
+)
+
+func (h *Handlers) GetSession(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ cookieName := "session_token"
+ sessionCookie, err := r.Cookie(cookieName)
+ if err != nil {
+ msg := "auth failed; failed to get session token from cookies"
+ h.log.Debug(msg, "error", err)
+ next.ServeHTTP(w, r)
+ return
+ }
+ cookieValueB, err := base64.URLEncoding.
+ DecodeString(sessionCookie.Value)
+ if err != nil {
+ msg := "auth failed; failed to decode b64 cookie"
+ h.log.Debug(msg, "error", err)
+ next.ServeHTTP(w, r)
+ return
+ }
+ cookieValue := string(cookieValueB)
+ if len(cookieValue) < sha256.Size {
+ h.log.Warn("small cookie", "size", len(cookieValue))
+ next.ServeHTTP(w, r)
+ return
+ }
+ // Split apart the signature and original cookie value.
+ signature := cookieValue[:sha256.Size]
+ sessionToken := cookieValue[sha256.Size:]
+ //verify signature
+ mac := hmac.New(sha256.New, []byte(h.cfg.CookieSecret))
+ mac.Write([]byte(cookieName))
+ mac.Write([]byte(sessionToken))
+ expectedSignature := mac.Sum(nil)
+ if !hmac.Equal([]byte(signature), expectedSignature) {
+ h.log.Debug("cookie with an invalid sign")
+ next.ServeHTTP(w, r)
+ return
+ }
+ userSession, err := h.cacheGetSession(sessionToken)
+ if err != nil {
+ msg := "auth failed; session does not exists"
+ err = errors.New(msg)
+ h.log.Debug(msg, "error", err)
+ next.ServeHTTP(w, r)
+ return
+ }
+ if userSession.IsExpired() {
+ h.mc.RemoveKey(sessionToken)
+ msg := "session is expired"
+ h.log.Debug(msg, "error", err, "token", sessionToken)
+ next.ServeHTTP(w, r)
+ return
+ }
+ ctx := context.WithValue(r.Context(),
+ "username", userSession.Username)
+ if err := h.cacheSetSession(sessionToken,
+ userSession); err != nil {
+ msg := "failed to marshal user session"
+ h.log.Warn(msg, "error", err)
+ next.ServeHTTP(w, r)
+ return
+ }
+ next.ServeHTTP(w, r.WithContext(ctx))
+ })
+}