diff --git a/internal/webserver/controller.go b/internal/webserver/controller.go index 5a404ef..cab1f46 100644 --- a/internal/webserver/controller.go +++ b/internal/webserver/controller.go @@ -34,6 +34,7 @@ func SetupControllers(cfg Config, db *gorm.DB, metadataReaders map[string]metada usersCfg := user.Config{ MinPasswordLength: cfg.MinPasswordLength, WordsPerMinute: cfg.WordsPerMinute, + Secret: cfg.JwtSecret, } documentsCfg := document.Config{ diff --git a/internal/webserver/controller/auth/signin.go b/internal/webserver/controller/auth/signin.go index 26e432c..d2ee251 100644 --- a/internal/webserver/controller/auth/signin.go +++ b/internal/webserver/controller/auth/signin.go @@ -31,30 +31,17 @@ func (a *Controller) SignIn(c *fiber.Ctx) error { } // Send back JWT as a cookie. - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "userdata": model.User{ - ID: user.ID, - Name: user.Name, - Username: user.Username, - Email: user.Email, - Role: user.Role, - Uuid: user.Uuid, - SendToEmail: user.SendToEmail, - WordsPerMinute: user.WordsPerMinute, - }, - "exp": jwt.NewNumericDate(time.Now().Add(a.config.SessionTimeout)), - }, - ) - - signedToken, err := token.SignedString(a.config.Secret) + expiration := time.Now().Add(a.config.SessionTimeout) + signedToken, err := GenerateToken(c, user, expiration, a.config.Secret) if err != nil { return fiber.ErrInternalServerError } + c.Cookie(&fiber.Cookie{ Name: "coreander", Value: signedToken, Path: "/", - Expires: time.Now().Add(a.config.SessionTimeout), + Expires: expiration, Secure: false, HTTPOnly: true, }) @@ -66,3 +53,22 @@ func (a *Controller) SignIn(c *fiber.Ctx) error { return c.Redirect(fmt.Sprintf("/%s", c.Params("lang"))) } + +func GenerateToken(c *fiber.Ctx, user *model.User, expiration time.Time, secret []byte) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "userdata": model.User{ + ID: user.ID, + Name: user.Name, + Username: user.Username, + Email: user.Email, + Role: user.Role, + Uuid: user.Uuid, + SendToEmail: user.SendToEmail, + WordsPerMinute: user.WordsPerMinute, + }, + "exp": jwt.NewNumericDate(expiration), + }, + ) + + return token.SignedString(secret) +} diff --git a/internal/webserver/controller/document/detail.go b/internal/webserver/controller/document/detail.go index 8e54e3d..e468912 100644 --- a/internal/webserver/controller/document/detail.go +++ b/internal/webserver/controller/document/detail.go @@ -17,8 +17,8 @@ func (d *Controller) Detail(c *fiber.Ctx) error { emailSendingConfigured = false } - var session model.User - if val, ok := c.Locals("Session").(model.User); ok { + var session model.Session + if val, ok := c.Locals("Session").(model.Session); ok { session = val } diff --git a/internal/webserver/controller/document/search.go b/internal/webserver/controller/document/search.go index 0b2fa3b..64744f7 100644 --- a/internal/webserver/controller/document/search.go +++ b/internal/webserver/controller/document/search.go @@ -22,8 +22,8 @@ func (d *Controller) Search(c *fiber.Ctx) error { page = 1 } - var session model.User - if val, ok := c.Locals("Session").(model.User); ok { + var session model.Session + if val, ok := c.Locals("Session").(model.Session); ok { session = val } diff --git a/internal/webserver/controller/highlight/highlight.go b/internal/webserver/controller/highlight/highlight.go index e4220f7..3f16c85 100644 --- a/internal/webserver/controller/highlight/highlight.go +++ b/internal/webserver/controller/highlight/highlight.go @@ -6,7 +6,7 @@ import ( ) func (h *Controller) Highlight(c *fiber.Ctx) error { - user := c.Locals("Session").(model.User) + user := c.Locals("Session").(model.Session) document, err := h.idx.Document(c.FormValue("slug")) if err != nil { diff --git a/internal/webserver/controller/highlight/highlights.go b/internal/webserver/controller/highlight/highlights.go index d555bcb..98fcc53 100644 --- a/internal/webserver/controller/highlight/highlights.go +++ b/internal/webserver/controller/highlight/highlights.go @@ -23,8 +23,8 @@ func (h *Controller) Highlights(c *fiber.Ctx) error { page = 1 } - var session model.User - if val, ok := c.Locals("Session").(model.User); ok { + var session model.Session + if val, ok := c.Locals("Session").(model.Session); ok { session = val } diff --git a/internal/webserver/controller/highlight/remove.go b/internal/webserver/controller/highlight/remove.go index 672ad78..37341f5 100644 --- a/internal/webserver/controller/highlight/remove.go +++ b/internal/webserver/controller/highlight/remove.go @@ -6,7 +6,7 @@ import ( ) func (h *Controller) Remove(c *fiber.Ctx) error { - user := c.Locals("Session").(model.User) + user := c.Locals("Session").(model.Session) document, err := h.idx.Document(c.FormValue("slug")) if err != nil { diff --git a/internal/webserver/controller/user/controller.go b/internal/webserver/controller/user/controller.go index e71e815..c64a337 100644 --- a/internal/webserver/controller/user/controller.go +++ b/internal/webserver/controller/user/controller.go @@ -20,6 +20,7 @@ type usersRepository interface { type Config struct { MinPasswordLength int WordsPerMinute float64 + Secret []byte } type Controller struct { diff --git a/internal/webserver/controller/user/edit.go b/internal/webserver/controller/user/edit.go index 1740da2..1770a15 100644 --- a/internal/webserver/controller/user/edit.go +++ b/internal/webserver/controller/user/edit.go @@ -18,8 +18,8 @@ func (u *Controller) Edit(c *fiber.Ctx) error { return fiber.ErrNotFound } - var session model.User - if val, ok := c.Locals("Session").(model.User); ok { + var session model.Session + if val, ok := c.Locals("Session").(model.Session); ok { session = val } diff --git a/internal/webserver/controller/user/update.go b/internal/webserver/controller/user/update.go index 37f3dd5..d048ebd 100644 --- a/internal/webserver/controller/user/update.go +++ b/internal/webserver/controller/user/update.go @@ -4,8 +4,10 @@ import ( "log" "strconv" "strings" + "time" "github.com/gofiber/fiber/v2" + "github.com/svera/coreander/v3/internal/webserver/controller/auth" "github.com/svera/coreander/v3/internal/webserver/model" ) @@ -20,8 +22,8 @@ func (u *Controller) Update(c *fiber.Ctx) error { return fiber.ErrNotFound } - var session model.User - if val, ok := c.Locals("Session").(model.User); ok { + var session model.Session + if val, ok := c.Locals("Session").(model.Session); ok { session = val } @@ -30,48 +32,31 @@ func (u *Controller) Update(c *fiber.Ctx) error { } if c.FormValue("password-tab") == "true" { - return u.updateUserPassword(c, session, *user) + return u.updateUserPassword(c, *user, session) } - return u.updateUserData(user, c, session) + return u.updateUserData(c, user, session) } -func (u *Controller) updateUserData(user *model.User, c *fiber.Ctx, session model.User) error { +func (u *Controller) updateUserData(c *fiber.Ctx, user *model.User, session model.Session) error { user.Name = c.FormValue("name") user.Username = strings.ToLower(c.FormValue("username")) user.Email = c.FormValue("email") user.SendToEmail = c.FormValue("send-to-email") user.WordsPerMinute, _ = strconv.ParseFloat(c.FormValue("words-per-minute"), 64) - errs := user.Validate(u.config.MinPasswordLength) - - exists, err := u.usernameExists(c, session) - if err != nil { - log.Println(err.Error()) - return fiber.ErrInternalServerError - } - - if exists { - errs["username"] = "A user with this username already exists" - } - - exists, err = u.emailExists(c, session) + validationErrs, err := u.validate(c, user, session) if err != nil { - log.Println(err.Error()) - return fiber.ErrInternalServerError - } - - if exists { - errs["email"] = "A user with this email address already exists" + return err } - if len(errs) > 0 { + if len(validationErrs) > 0 { return c.Render("users/edit", fiber.Map{ "Title": "Edit user", "User": user, "MinPasswordLength": u.config.MinPasswordLength, "UsernamePattern": model.UsernamePattern, - "Errors": errs, + "Errors": validationErrs, }, "layout") } @@ -79,17 +64,60 @@ func (u *Controller) updateUserData(user *model.User, c *fiber.Ctx, session mode return fiber.ErrInternalServerError } + if session.Uuid == user.Uuid { + expiration := time.Unix(int64(session.Exp), 0) + signedToken, err := auth.GenerateToken(c, user, expiration, u.config.Secret) + if err != nil { + return fiber.ErrInternalServerError + } + + c.Cookie(&fiber.Cookie{ + Name: "coreander", + Value: signedToken, + Path: "/", + Expires: expiration, + Secure: false, + HTTPOnly: true, + }) + c.Locals("Session", user) + } + return c.Render("users/edit", fiber.Map{ "Title": "Edit user", "User": user, "MinPasswordLength": u.config.MinPasswordLength, "UsernamePattern": model.UsernamePattern, - "Errors": errs, + "Errors": validationErrs, "Message": "Profile updated", }, "layout") } -func (u *Controller) usernameExists(c *fiber.Ctx, session model.User) (bool, error) { +func (u *Controller) validate(c *fiber.Ctx, user *model.User, session model.Session) (map[string]string, error) { + errs := user.Validate(u.config.MinPasswordLength) + + exists, err := u.usernameExists(c, session) + if err != nil { + log.Println(err.Error()) + return nil, fiber.ErrInternalServerError + } + + if exists { + errs["username"] = "A user with this username already exists" + } + + exists, err = u.emailExists(c, session) + if err != nil { + log.Println(err.Error()) + return nil, fiber.ErrInternalServerError + } + + if exists { + errs["email"] = "A user with this email address already exists" + } + return errs, nil +} + +func (u *Controller) usernameExists(c *fiber.Ctx, session model.Session) (bool, error) { user, err := u.repository.FindByUsername(c.FormValue("username")) if err != nil { return true, fiber.ErrInternalServerError @@ -103,7 +131,7 @@ func (u *Controller) usernameExists(c *fiber.Ctx, session model.User) (bool, err return false, nil } -func (u *Controller) emailExists(c *fiber.Ctx, session model.User) (bool, error) { +func (u *Controller) emailExists(c *fiber.Ctx, session model.Session) (bool, error) { user, err := u.repository.FindByEmail(c.FormValue("email")) if err != nil { return true, fiber.ErrInternalServerError @@ -118,7 +146,7 @@ func (u *Controller) emailExists(c *fiber.Ctx, session model.User) (bool, error) } // updateUserPassword gathers information from the edit user form and updates user password -func (u *Controller) updateUserPassword(c *fiber.Ctx, session, user model.User) error { +func (u *Controller) updateUserPassword(c *fiber.Ctx, user model.User, session model.Session) error { user.Password = c.FormValue("password") errs := user.Validate(u.config.MinPasswordLength) diff --git a/internal/webserver/jwtclaimsreader.go b/internal/webserver/jwtclaimsreader.go index 72dab38..ad3a1f9 100644 --- a/internal/webserver/jwtclaimsreader.go +++ b/internal/webserver/jwtclaimsreader.go @@ -6,36 +6,39 @@ import ( "github.com/svera/coreander/v3/internal/webserver/model" ) -func sessionData(c *fiber.Ctx) model.User { - var user model.User +func sessionData(c *fiber.Ctx) model.Session { + var session model.Session + if t, ok := c.Locals("user").(*jwt.Token); ok { claims := t.Claims.(jwt.MapClaims) userDataMap := claims["userdata"].(map[string]interface{}) if value, ok := userDataMap["ID"].(float64); ok { - user.ID = uint(value) + session.ID = uint(value) } if value, ok := userDataMap["Name"].(string); ok { - user.Name = value + session.Name = value } if value, ok := userDataMap["Username"].(string); ok { - user.Username = value + session.Username = value } if value, ok := userDataMap["Email"].(string); ok { - user.Email = value + session.Email = value } if value, ok := userDataMap["Role"].(float64); ok { - user.Role = int(value) + session.Role = int(value) } if value, ok := userDataMap["Uuid"].(string); ok { - user.Uuid = value + session.Uuid = value } if value, ok := userDataMap["SendToEmail"].(string); ok { - user.SendToEmail = value + session.SendToEmail = value } if value, ok := userDataMap["WordsPerMinute"].(float64); ok { - user.WordsPerMinute = value + session.WordsPerMinute = value } + + session.Exp = claims["exp"].(float64) } - return user + return session } diff --git a/internal/webserver/middleware.go b/internal/webserver/middleware.go index 8eeef48..f64a98e 100644 --- a/internal/webserver/middleware.go +++ b/internal/webserver/middleware.go @@ -17,7 +17,7 @@ func RequireAdmin(c *fiber.Ctx) error { return fiber.ErrForbidden } - session := c.Locals("Session").(model.User) + session := c.Locals("Session").(model.Session) if session.Role != model.RoleAdmin { return fiber.ErrForbidden diff --git a/internal/webserver/model/session.go b/internal/webserver/model/session.go new file mode 100644 index 0000000..f1c9240 --- /dev/null +++ b/internal/webserver/model/session.go @@ -0,0 +1,6 @@ +package model + +type Session struct { + User + Exp float64 +} diff --git a/internal/webserver/webserver.go b/internal/webserver/webserver.go index 3c14095..020099e 100644 --- a/internal/webserver/webserver.go +++ b/internal/webserver/webserver.go @@ -16,6 +16,7 @@ import ( "github.com/svera/coreander/v3/internal/i18n" "github.com/svera/coreander/v3/internal/index" "github.com/svera/coreander/v3/internal/webserver/infrastructure" + "github.com/svera/coreander/v3/internal/webserver/model" "golang.org/x/exp/slices" "golang.org/x/text/message" ) @@ -175,6 +176,7 @@ func errorHandler(c *fiber.Ctx, err error) error { code = e.Code } + session, _ := c.Locals("Session").(model.Session) // Send custom error page err = c.Status(code).Render( fmt.Sprintf("errors/%d", code), @@ -182,6 +184,7 @@ func errorHandler(c *fiber.Ctx, err error) error { "Lang": chooseBestLanguage(c), "Title": "Coreander", "Version": c.App().Config().AppName, + "Session": session, }, "layout")