summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--form/errors.go12
-rw-r--r--main.go1
-rw-r--r--model/users.sql.go39
-rw-r--r--pages/base.templ4
-rw-r--r--pages/base_templ.go4
-rw-r--r--pages/chpw.go84
-rw-r--r--pages/chpw.templ25
-rw-r--r--pages/chpw_templ.go89
-rw-r--r--pages/login.go33
-rw-r--r--sql/users.sql12
10 files changed, 290 insertions, 13 deletions
diff --git a/form/errors.go b/form/errors.go
index 52206c4..ab9abd4 100644
--- a/form/errors.go
+++ b/form/errors.go
@@ -5,6 +5,18 @@ import (
"fmt"
)
+type FormErrors struct {
+ Errors []error `form:"-"`
+}
+
+func (f *FormErrors) AddError(field, issue string) {
+ f.Errors = append(f.Errors, FieldError{field, issue})
+}
+
+func (f *FormErrors) HasError() bool {
+ return len(f.Errors) > 0
+}
+
type FieldError struct {
Field string
Issue string
diff --git a/main.go b/main.go
index 2eadfd9..352ed56 100644
--- a/main.go
+++ b/main.go
@@ -66,6 +66,7 @@ func main() {
authRouter.Mount("/", pages.Init())
authRouter.Mount("/recur", pages.Recur())
authRouter.Mount("/categories", pages.Categories())
+ authRouter.Mount("/cpw", pages.ChangePassword())
authRouter.NotFound(pages.NotFound())
log.Fatal(http.ListenAndServe(os.Getenv("GOSTEN_ADDRESS"), router))
diff --git a/model/users.sql.go b/model/users.sql.go
index e63dacc..bd881b7 100644
--- a/model/users.sql.go
+++ b/model/users.sql.go
@@ -9,6 +9,24 @@ import (
"context"
)
+const getPwdById = `-- name: GetPwdById :one
+ SELECT pwd
+ FROM users
+ WHERE id = $1
+`
+
+// GetPwdById
+//
+// SELECT pwd
+// FROM users
+// WHERE id = $1
+func (q *Queries) GetPwdById(ctx context.Context, id int32) (string, error) {
+ row := q.db.QueryRow(ctx, getPwdById, id)
+ var pwd string
+ err := row.Scan(&pwd)
+ return pwd, err
+}
+
const getUserById = `-- name: GetUserById :one
SELECT id, name, pwd, description
FROM users
@@ -88,3 +106,24 @@ func (q *Queries) GetUsers(ctx context.Context) ([]User, error) {
}
return items, nil
}
+
+const updatePwd = `-- name: UpdatePwd :exec
+ UPDATE users
+ SET pwd = $1
+ WHERE id = $2
+`
+
+type UpdatePwdParams struct {
+ Pwd string
+ ID int32
+}
+
+// UpdatePwd
+//
+// UPDATE users
+// SET pwd = $1
+// WHERE id = $2
+func (q *Queries) UpdatePwd(ctx context.Context, arg UpdatePwdParams) error {
+ _, err := q.db.Exec(ctx, updatePwd, arg.Pwd, arg.ID)
+ return err
+}
diff --git a/pages/base.templ b/pages/base.templ
index 33edd65..47de938 100644
--- a/pages/base.templ
+++ b/pages/base.templ
@@ -43,6 +43,7 @@ templ navlinks() {
templ userlinks() {
<li><h4 class="dropdown-header">{getUser(ctx).Name}</h4></li>
+ <li><a class="dropdown-item" href="/cpw">Passwort Ändern</a></li>
<li><a class="dropdown-item" href="/logout">Logout</a></li>
}
@@ -55,6 +56,9 @@ templ navbar() {
<symbol id="person" viewBox="0 0 16 16">
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10s-3.516.68-4.168 1.332c-.678.678-.83 1.418-.832 1.664z"/>
</symbol>
+ <symbol id="check-circle-fill" viewBox="0 0 16 16">
+ <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
+ </symbol>
</svg>
/* The Navbar */
diff --git a/pages/base_templ.go b/pages/base_templ.go
index d871263..f355b61 100644
--- a/pages/base_templ.go
+++ b/pages/base_templ.go
@@ -236,7 +236,7 @@ func userlinks() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h4></li><li><a class=\"dropdown-item\" href=\"/logout\">Logout</a></li>")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h4></li><li><a class=\"dropdown-item\" href=\"/cpw\">Passwort Ändern</a></li><li><a class=\"dropdown-item\" href=\"/logout\">Logout</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -265,7 +265,7 @@ func navbar() templ.Component {
templ_7745c5c3_Var11 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"d-none\"><symbol id=\"search\" viewBox=\"0 0 16 16\"><path d=\"M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z\"></path></symbol> <symbol id=\"person\" viewBox=\"0 0 16 16\"><path d=\"M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10s-3.516.68-4.168 1.332c-.678.678-.83 1.418-.832 1.664z\"></path></symbol></svg><nav class=\"navbar sticky-top bg-dark-subtle navbar-expand-lg shadow mb-2\"><div class=\"container-fluid\"><a class=\"navbar-brand\" href=\"/\"><img src=\"/static/euro-money.svg\" alt=\"Logo\" width=\"32\" height=\"32\" class=\"d-inline-block\"> Kosten</a> <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarNav\" aria-controls=\"navbarNav\" aria-expanded=\"false\" aria-label=\"Toggle navigation\"><span class=\"navbar-toggler-icon\"></span></button><div class=\"collapse navbar-collapse\" id=\"navbarNav\"><ul class=\"navbar-nav nav-underline me-auto\">")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"d-none\"><symbol id=\"search\" viewBox=\"0 0 16 16\"><path d=\"M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z\"></path></symbol> <symbol id=\"person\" viewBox=\"0 0 16 16\"><path d=\"M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10s-3.516.68-4.168 1.332c-.678.678-.83 1.418-.832 1.664z\"></path></symbol> <symbol id=\"check-circle-fill\" viewBox=\"0 0 16 16\"><path d=\"M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z\"></path></symbol></svg><nav class=\"navbar sticky-top bg-dark-subtle navbar-expand-lg shadow mb-2\"><div class=\"container-fluid\"><a class=\"navbar-brand\" href=\"/\"><img src=\"/static/euro-money.svg\" alt=\"Logo\" width=\"32\" height=\"32\" class=\"d-inline-block\"> Kosten</a> <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarNav\" aria-controls=\"navbarNav\" aria-expanded=\"false\" aria-label=\"Toggle navigation\"><span class=\"navbar-toggler-icon\"></span></button><div class=\"collapse navbar-collapse\" id=\"navbarNav\"><ul class=\"navbar-nav nav-underline me-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/pages/chpw.go b/pages/chpw.go
new file mode 100644
index 0000000..671c180
--- /dev/null
+++ b/pages/chpw.go
@@ -0,0 +1,84 @@
+package pages
+
+import (
+ "context"
+ "fmt"
+ "gosten/csrf"
+ "gosten/form"
+ "gosten/model"
+ "net/http"
+
+ "github.com/go-chi/chi/v5"
+ "golang.org/x/crypto/bcrypt"
+)
+
+type chpw struct {
+ Password string `form:"type=password;options=required"`
+ NewPw1 string `form:"label=Neues Password;type=password;options=required"`
+ NewPw2 string `form:"label=Wiederholung;type=password;options=required"`
+ Success bool `form:"-"`
+ form.FormErrors
+ csrf.CsrfField
+}
+
+func ChangePassword() Page {
+ r := chi.NewRouter()
+
+ r.Get("/", func(w http.ResponseWriter, r *http.Request) {
+ c := chpw{}
+ c.SetCsrfField(r)
+ render(changePassword(c))(w, r)
+ })
+
+ r.Post("/", handleChPw)
+
+ return r
+}
+
+func handleChPw(w http.ResponseWriter, r *http.Request) {
+ c := chpw{}
+ form.Parse(r, &c)
+
+ ctx := r.Context()
+ userId := getUser(ctx).ID
+ dbPwd, err := Q.GetPwdById(ctx, userId)
+ if err != nil {
+ panic(fmt.Sprintf("Q.GetPwdById: %v", err))
+ }
+
+ if c.NewPw1 != c.NewPw2 {
+ c.AddError("NewPw2", "Neues Passwort stimmt nicht überein!")
+ }
+
+ if !validatePwd(dbPwd, c.Password) {
+ c.AddError("Password", "Passwort falsch!")
+ }
+
+ if !c.HasError() {
+ updatePwd(ctx, userId, c.NewPw1)
+
+ // update context
+ ctx, _ = setUserInContext(ctx, userId)
+ r = r.WithContext(ctx)
+
+ // reset form
+ c = chpw{Success: true}
+ }
+
+ c.SetCsrfField(r)
+ render(changePassword(c))(w, r)
+}
+
+func updatePwd(ctx context.Context, userId int32, pwd string) {
+ hash, err := bcrypt.GenerateFromPassword([]byte(pwd), -1)
+ if err != nil {
+ panic(fmt.Sprintf("Generating password hash: %v", err))
+ }
+
+ err = Q.UpdatePwd(ctx, model.UpdatePwdParams{
+ Pwd: string(hash),
+ ID: userId})
+ if err != nil {
+ panic(fmt.Sprintf("Updating password: %v", err))
+ }
+}
diff --git a/pages/chpw.templ b/pages/chpw.templ
new file mode 100644
index 0000000..3cf5ded
--- /dev/null
+++ b/pages/chpw.templ
@@ -0,0 +1,25 @@
+package pages
+
+import (
+ "gosten/form"
+)
+
+templ changePassword(chpw chpw) {
+ @content(){
+ if chpw.Success {
+ <div class="alert alert-success d-flex align-items-center" role="alert">
+ <svg class="svg me-2" role="img" aria-label="Success:"><use xlink:href="#check-circle-fill"/></svg>
+ <div>
+ Passwort ändern erfolgreich!
+ </div>
+ </div>
+ }
+
+ <form action={templ.SafeURL(getCurrPath(ctx))} method="post" class="container mx-auto" style="max-width: 440px;">
+ <h3>Passwort Ändern</h3>
+ @form.Form(chpw, chpw.Errors)
+ @chpw.Csrf()
+ <input class="btn btn-primary w-100" type="submit" />
+ </form>
+ }
+} \ No newline at end of file
diff --git a/pages/chpw_templ.go b/pages/chpw_templ.go
new file mode 100644
index 0000000..0f402c2
--- /dev/null
+++ b/pages/chpw_templ.go
@@ -0,0 +1,89 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.2.778
+package pages
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import (
+ "gosten/form"
+)
+
+func changePassword(chpw chpw) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ if chpw.Success {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"alert alert-success d-flex align-items-center\" role=\"alert\"><svg class=\"svg me-2\" role=\"img\" aria-label=\"Success:\"><use xlink:href=\"#check-circle-fill\"></use></svg><div>Passwort ändern erfolgreich!</div></div>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <form action=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var3 templ.SafeURL = templ.SafeURL(getCurrPath(ctx))
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var3)))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" method=\"post\" class=\"container mx-auto\" style=\"max-width: 440px;\"><h3>Passwort Ändern</h3>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = form.Form(chpw, chpw.Errors).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = chpw.Csrf().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<input class=\"btn btn-primary w-100\" type=\"submit\"></form>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return templ_7745c5c3_Err
+ })
+ templ_7745c5c3_Err = content().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/pages/login.go b/pages/login.go
index 84119e9..d433937 100644
--- a/pages/login.go
+++ b/pages/login.go
@@ -27,15 +27,24 @@ const (
loginQueryMarker = "next"
)
+func setUserInContext(ctx context.Context, uid int32) (context.Context, error) {
+ u, err := Q.GetUserById(ctx, uid)
+ if err != nil {
+ return ctx, err
+ }
+
+ u.Pwd = "" // don't carry pwd around
+ return context.WithValue(ctx, userContextKey{}, u), nil
+}
+
func RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s := session.From(r)
if !s.IsNew() && s.Authenticated {
- u, err := Q.GetUserById(r.Context(), s.UserID)
+ ctx, err := setUserInContext(r.Context(), s.UserID)
if err == nil {
// authenticated --> done
- ctx := context.WithValue(r.Context(), userContextKey{}, u)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
@@ -53,10 +62,10 @@ func RequireAuth(next http.Handler) http.Handler {
}
type user struct {
- Name string `form:"options=required,autofocus"`
- Password string `form:"type=password;options=required"`
- RememberMe bool `form:"type=checkbox;value=y;options=checked"`
- Errors []error `form:"-"`
+ Name string `form:"options=required,autofocus"`
+ Password string `form:"type=password;options=required"`
+ RememberMe bool `form:"type=checkbox;value=y;options=checked"`
+ form.FormErrors
csrf.CsrfField
}
@@ -77,13 +86,17 @@ func Login() Page {
return r
}
+func validatePwd(hash, pwd string) bool {
+ hashB := []byte(hash)
+ pwdB := []byte(pwd)
+
+ return bcrypt.CompareHashAndPassword(hashB, pwdB) == nil
+}
+
func checkLogin(ctx context.Context, user user) (bool, int32) {
dbUser, err := Q.GetUserByName(ctx, user.Name)
if err == nil {
- hash := []byte(dbUser.Pwd)
- pwd := []byte(user.Password)
-
- if bcrypt.CompareHashAndPassword(hash, pwd) != nil {
+ if !validatePwd(dbUser.Pwd, user.Password) {
return false, 0
}
} else if errors.Is(err, sql.ErrNoRows) {
diff --git a/sql/users.sql b/sql/users.sql
index 2e1d997..e37e782 100644
--- a/sql/users.sql
+++ b/sql/users.sql
@@ -10,4 +10,14 @@ SELECT *
-- name: GetUserById :one
SELECT *
FROM users
- WHERE id = $1; \ No newline at end of file
+ WHERE id = $1;
+
+-- name: GetPwdById :one
+ SELECT pwd
+ FROM users
+ WHERE id = $1;
+
+-- name: UpdatePwd :exec
+ UPDATE users
+ SET pwd = $1
+ WHERE id = $2; \ No newline at end of file