feat(user): save user to session

ref: #6
master
Niclas Thobaben 2024-02-28 21:36:20 +01:00
parent 49f1c7e483
commit 2509485a1b
12 changed files with 197 additions and 15 deletions

View File

@ -0,0 +1,17 @@
package artist
import (
"git.l--n.de/nclazz/soundwerft/api/handler"
"git.l--n.de/nclazz/soundwerft/api/web/templates/artist"
"github.com/labstack/echo/v4"
)
func Routes(h *handler.Handler) {
h.Echo.GET("/artist", getArtistDashboard(h))
}
func getArtistDashboard(h *handler.Handler) echo.HandlerFunc {
return func(c echo.Context) error {
return h.Render(c, artist.ArtistPage(h.View()))
}
}

View File

@ -7,6 +7,7 @@ import (
"github.com/a-h/templ"
"github.com/labstack/echo/v4"
"github.com/nicksnyder/go-i18n/v2/i18n"
"log/slog"
)
@ -42,7 +43,12 @@ func (h *Handler) View() web.View {
}
func (h *Handler) Translate(lc *i18n.LocalizeConfig) string {
return h.Localizer.MustLocalize(lc)
msg, err := h.Localizer.Localize(lc)
if err != nil {
slog.Error("failed to translate message", slog.Group("message", "id", lc.MessageID))
return lc.DefaultMessage.Other
}
return msg
}
func (h *Handler) TranslateMessage(m *i18n.Message) string {

View File

@ -0,0 +1,63 @@
package handler
import (
"encoding/gob"
"git.l--n.de/nclazz/soundwerft/internal/app/types"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
)
const (
SessionName = "user_session"
SessionKeyUser = "user"
)
type SessionUser struct {
ID types.ID
Username string
IsAdmin bool
}
func (h *Handler) User(c echo.Context) *SessionUser {
sess, _ := session.Get(SessionName, c)
if sess == nil {
return nil
}
usr, _ := sess.Values[SessionKeyUser].(*SessionUser)
return usr
}
func (h *Handler) Session(c echo.Context) *sessions.Session {
log := h.Logger(c)
sess, err := session.Get(SessionName, c)
if err != nil {
log.Warn("failed to get user session", "error", err)
return nil
}
return sess
}
func (h *Handler) SaveSession(c echo.Context) error {
log := h.Logger(c)
sess := h.Session(c)
log.Debug("save session")
if sess == nil {
log.Warn("no session to save")
return nil
}
return sess.Save(c.Request(), c.Response())
}
func (h *Handler) CloseSession(c echo.Context) error {
sess := h.Session(c)
if sess == nil {
return nil
}
sess.Options.MaxAge = -1
return sess.Save(c.Request(), c.Response())
}
func init() {
gob.Register(&SessionUser{})
}

View File

@ -4,22 +4,29 @@ import (
"git.l--n.de/nclazz/soundwerft/api/handler"
"git.l--n.de/nclazz/soundwerft/api/web/templates"
"git.l--n.de/nclazz/soundwerft/internal/app/user"
"github.com/gorilla/sessions"
"github.com/labstack/echo/v4"
"log/slog"
"net/http"
)
func defaultLoginFormView(h *handler.Handler) templates.LoginFormView {
return templates.LoginFormView{
MsgUsernameLabel: h.TranslateMessage(MsgLoginUsernameLabel),
MsgPasswordLabel: h.TranslateMessage(MsgLoginPasswordLabel),
MsgSignUpNow: h.TranslateMessage(MsgRegisterUpNow),
MsgNoAccountYet: h.TranslateMessage(MsgNoAccountYet),
MsgForgotPassword: h.TranslateMessage(MsgForgotPassword),
MsgUsernameLabel: h.TranslateMessage(MsgLoginUsernameLabel),
MsgPasswordLabel: h.TranslateMessage(MsgLoginPasswordLabel),
MsgSignUpNow: h.TranslateMessage(MsgRegisterUpNow),
MsgNoAccountYet: h.TranslateMessage(MsgNoAccountYet),
MsgForgotPassword: h.TranslateMessage(MsgForgotPassword),
MsgKeepLoggedInLabel: h.TranslateMessage(MsgLoginKeepLoggedIn),
}
}
func getLoginPage(h *handler.Handler) echo.HandlerFunc {
return func(c echo.Context) error {
usr := h.User(c)
if usr != nil {
return c.Redirect(http.StatusFound, h.View().URLString("/artist"))
}
return h.Render(c, templates.LoginPage(h.View(), defaultLoginFormView(h)))
}
}
@ -32,14 +39,34 @@ func postLogin(h *handler.Handler) echo.HandlerFunc {
Login: c.FormValue("login"),
Password: c.FormValue("password"),
}
keepLoggedIn := c.FormValue("keep_logged_in")
view := defaultLoginFormView(h)
view.Login = cmd.Login
user, err := h.Services.Users.LoginUser(ctx, cmd)
usr, err := h.Services.Users.LoginUser(ctx, cmd)
if err != nil {
view.Err = h.TranslateMessage(MsgIncorrectUsernameOrPassword)
return h.Render(c, templates.LoginForm(h.View(), view))
}
log.Info("user logged in", slog.Group("user", "id", user.ID, "name", user.Name, "isAdmin", user.IsAdmin))
return h.Render(c, templates.LoginPage(h.View(), view))
log.Info("usr logged in", slog.Group("usr", "id", usr.ID, "name", usr.Name, "isAdmin", usr.IsAdmin), "keepLoggedIn", keepLoggedIn)
sess := h.Session(c)
maxAge := 0
if keepLoggedIn != "" {
maxAge = 60 * 60 * 24 * 3 //3 days
}
sess.Options = &sessions.Options{
Path: "/",
MaxAge: maxAge,
Secure: true,
HttpOnly: true,
}
sess.Values[handler.SessionKeyUser] = &handler.SessionUser{
ID: usr.ID,
Username: usr.Name.String(),
IsAdmin: true,
}
if err := h.SaveSession(c); err != nil {
return err
}
return c.Redirect(http.StatusFound, h.View().URLString("/artist"))
}
}

View File

@ -15,6 +15,10 @@ var (
ID: "User.LoginPasswordLabel",
Other: "Password",
}
MsgLoginKeepLoggedIn = &i18n.Message{
ID: "User.LoginKeepLoggedIn",
Other: "Keep logged in",
}
MsgRegisterUpNow = &i18n.Message{
ID: "User.RegisterNow",
Other: "Register now",

View File

@ -3,9 +3,12 @@ package api
import (
"fmt"
"git.l--n.de/nclazz/soundwerft/api/handler"
"git.l--n.de/nclazz/soundwerft/api/handler/artist"
"git.l--n.de/nclazz/soundwerft/api/handler/json"
"git.l--n.de/nclazz/soundwerft/api/handler/pages"
"git.l--n.de/nclazz/soundwerft/api/handler/user"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"log/slog"
@ -22,14 +25,19 @@ func (a *API) routes() {
a.echo.Pre(middleware.Rewrite(map[string]string{
matchUrl: "/$1",
}))
a.echo.Use(middleware.RequestID())
a.echo.Use(
middleware.RequestID(),
// TODO make secret configurable or auto generated
session.Middleware(sessions.NewCookieStore([]byte("secret"))),
)
a.echo.Static("/assets", path.Join(a.config.WorkingDir, "api/web/assets"))
h := &handler.Handler{
Echo: a.echo,
BaseUrl: a.config.BaseUrl,
Services: *a.services,
Repos: *a.repos,
Echo: a.echo,
BaseUrl: a.config.BaseUrl,
Services: *a.services,
Repos: *a.repos,
Localizer: a.localizer,
}
eh := a.echo.HTTPErrorHandler
@ -42,6 +50,7 @@ func (a *API) routes() {
json.Routes(h)
pages.Routes(h)
user.Routes(h)
artist.Routes(h)
for _, r := range a.echo.Routes() {
slog.Debug("router", "route", fmt.Sprintf("%s %s", r.Method, r.Path))

View File

@ -0,0 +1,9 @@
package artist
import (
"git.l--n.de/nclazz/soundwerft/api/web"
)
templ ArtistPage(view web.View) {
<div>Artist Page</div>
}

View File

@ -8,9 +8,11 @@ import (
type LoginFormView struct {
Login string
KeepLoggedIn bool
Err string
MsgUsernameLabel string
MsgPasswordLabel string
MsgKeepLoggedInLabel string
MsgSignUpNow string
MsgNoAccountYet string
MsgForgotPassword string
@ -61,6 +63,11 @@ templ LoginForm(view web.View, form LoginFormView) {
Required: true,
HasErr: form.Err != "",
})
@partials.FormSwitch(partials.SwitchAttr{
Name: "keep_logged_in",
Label: form.MsgKeepLoggedInLabel,
Value: form.KeepLoggedIn,
})
<div class="d-grid gap-2">
<input type="submit" class="btn btn-primary btn-lg" value="Login"/>
<p>{ form.MsgNoAccountYet } <a href={ view.URL("/signup") } hx-boost="true"

View File

@ -0,0 +1,22 @@
package partials
type SwitchAttr struct {
ID string
Name string
Label string
Value bool
}
func (s SwitchAttr) InputID() string {
if s.ID == "" {
return "form__" + s.Name
}
return s.ID
}
templ FormSwitch(attr SwitchAttr) {
<div class="form-check form-switch">
<input class="form-check-input" name={ attr.Name } type="checkbox" role="switch" id={ attr.InputID() }/>
<label class="form-check-label" for={ attr.InputID() }>{ attr.Label }</label>
</div>
}

5
go.mod
View File

@ -8,8 +8,10 @@ require (
github.com/fergusstrange/embedded-postgres v1.25.0
github.com/golang-migrate/migrate/v4 v4.17.0
github.com/google/uuid v1.4.0
github.com/gorilla/sessions v1.2.2
github.com/jmoiron/sqlx v1.2.0
github.com/johejo/golang-migrate-extra v0.0.0-20211005021153-c17dd75f8b4a
github.com/labstack/echo-contrib v0.15.0
github.com/labstack/echo/v4 v4.11.4
github.com/nicksnyder/go-i18n/v2 v2.4.0
github.com/pelletier/go-toml/v2 v2.1.1
@ -22,6 +24,8 @@ require (
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/labstack/gommon v0.4.2 // indirect
@ -29,6 +33,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect

14
go.sum
View File

@ -159,6 +159,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@ -175,11 +177,16 @@ github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
@ -230,13 +237,16 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4=
github.com/labstack/echo-contrib v0.15.0 h1:9K+oRU265y4Mu9zpRDv3X+DGTqUALY6oRHCSZZKCRVU=
github.com/labstack/echo-contrib v0.15.0/go.mod h1:lei+qt5CLB4oa7VHTE0yEfQSEB9XTJI1LUqko9UWvo4=
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
@ -293,6 +303,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=

View File

@ -4,6 +4,7 @@
"User.ErrInvalidPassword" = "Password is invalid"
"User.ErrUsernameAlreadyExists" = "Username already exists"
"User.ForgotPassword" = "Forgot Password"
"User.LoginKeepLoggedIn" = "Keep logged in"
"User.LoginPasswordLabel" = "Password"
"User.LoginUsernameLabel" = "Username or email address"
"User.NoAccountYet" = "No account yet?"