diff --git a/app/routes.go b/app/routes.go
index 2940397..21069f7 100644
--- a/app/routes.go
+++ b/app/routes.go
@@ -72,6 +72,14 @@ func (a *App) SetupRoutes() error {
return c.NoContent(http.StatusNotFound)
})
+ user := e.Group("/user", handlers.MiddlewareSession, xsrf)
+
+ user.GET("/initial-password-change", handlers.InitialPasswordChange())
+ user.POST("/initial-password-change", handlers.InitialPasswordChange())
+ user.GET("/hibp-search", handlers.GetSearchHIBP())
+ user.POST("/hibp-search", handlers.SearchHIBP())
+ user.GET("/hibp-breach-details/:name", handlers.ViewHIBP())
+
manage := e.Group("/manage", handlers.MiddlewareSession, xsrf)
manage.GET("/api-keys", handlers.ManageAPIKeys(), compress)
diff --git a/ent/hibp/hibp.go b/ent/hibp/hibp.go
index 4ac08d9..8f12128 100644
--- a/ent/hibp/hibp.go
+++ b/ent/hibp/hibp.go
@@ -5,6 +5,7 @@ package hibp
import (
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
+ "github.com/google/uuid"
)
const (
@@ -114,6 +115,8 @@ var (
DefaultIsSpamList bool
// DefaultIsMalware holds the default value on creation for the "is_malware" field.
DefaultIsMalware bool
+ // DefaultID holds the default value on creation for the "id" field.
+ DefaultID func() uuid.UUID
)
// OrderOption defines the ordering options for the HIBP queries.
diff --git a/ent/hibp_create.go b/ent/hibp_create.go
index ee32e22..9271f4a 100644
--- a/ent/hibp_create.go
+++ b/ent/hibp_create.go
@@ -188,6 +188,14 @@ func (hc *HIBPCreate) SetID(u uuid.UUID) *HIBPCreate {
return hc
}
+// SetNillableID sets the "id" field if the given value is not nil.
+func (hc *HIBPCreate) SetNillableID(u *uuid.UUID) *HIBPCreate {
+ if u != nil {
+ hc.SetID(*u)
+ }
+ return hc
+}
+
// SetTrackedBreachesID sets the "tracked_breaches" edge to the TrackedBreaches entity by ID.
func (hc *HIBPCreate) SetTrackedBreachesID(id uuid.UUID) *HIBPCreate {
hc.mutation.SetTrackedBreachesID(id)
@@ -266,6 +274,10 @@ func (hc *HIBPCreate) defaults() {
v := hibp.DefaultIsMalware
hc.mutation.SetIsMalware(v)
}
+ if _, ok := hc.mutation.ID(); !ok {
+ v := hibp.DefaultID()
+ hc.mutation.SetID(v)
+ }
}
// check runs all checks and user-defined validators on the builder.
diff --git a/ent/runtime.go b/ent/runtime.go
index 7506e01..3e019ed 100644
--- a/ent/runtime.go
+++ b/ent/runtime.go
@@ -69,6 +69,10 @@ func init() {
hibpDescIsMalware := hibpFields[15].Descriptor()
// hibp.DefaultIsMalware holds the default value on creation for the is_malware field.
hibp.DefaultIsMalware = hibpDescIsMalware.Default.(bool)
+ // hibpDescID is the schema descriptor for id field.
+ hibpDescID := hibpFields[0].Descriptor()
+ // hibp.DefaultID holds the default value on creation for the id field.
+ hibp.DefaultID = hibpDescID.Default.(func() uuid.UUID)
localbreachFields := schema.LocalBreach{}.Fields()
_ = localbreachFields
// localbreachDescName is the schema descriptor for name field.
@@ -196,8 +200,6 @@ func init() {
userDescLastLogin := userFields[8].Descriptor()
// user.DefaultLastLogin holds the default value on creation for the last_login field.
user.DefaultLastLogin = userDescLastLogin.Default.(time.Time)
- // user.UpdateDefaultLastLogin holds the default value on update for the last_login field.
- user.UpdateDefaultLastLogin = userDescLastLogin.UpdateDefault.(func() time.Time)
// userDescID is the schema descriptor for id field.
userDescID := userFields[0].Descriptor()
// user.DefaultID holds the default value on creation for the id field.
diff --git a/ent/schema/hibp.go b/ent/schema/hibp.go
index fa65b0e..2999e17 100644
--- a/ent/schema/hibp.go
+++ b/ent/schema/hibp.go
@@ -60,6 +60,7 @@ type HIBPSchema struct {
func (HIBP) Fields() []ent.Field {
return []ent.Field{
field.UUID("id", uuid.UUID{}).
+ Default(uuid.New).
Unique().
Immutable(),
// Unique and permanent.
diff --git a/ent/schema/user.go b/ent/schema/user.go
index f3ef348..30cc24f 100644
--- a/ent/schema/user.go
+++ b/ent/schema/user.go
@@ -55,8 +55,8 @@ func (User) Fields() []ent.Field {
Default(time.Now).
UpdateDefault(time.Now),
field.Time("last_login").
- Default(time.Unix(0, 0)).
- UpdateDefault(time.Now),
+ // UpdateDefault(time.Now).
+ Default(time.Unix(0, 0)),
// field.Bool("save_search_queries").
// Default(true),
}
diff --git a/ent/user/user.go b/ent/user/user.go
index 86c4b26..1c3d702 100644
--- a/ent/user/user.go
+++ b/ent/user/user.go
@@ -104,8 +104,6 @@ var (
UpdateDefaultUpdatedAt func() time.Time
// DefaultLastLogin holds the default value on creation for the "last_login" field.
DefaultLastLogin time.Time
- // UpdateDefaultLastLogin holds the default value on update for the "last_login" field.
- UpdateDefaultLastLogin func() time.Time
// DefaultID holds the default value on creation for the "id" field.
DefaultID func() uuid.UUID
)
diff --git a/ent/user_update.go b/ent/user_update.go
index efeacdd..f73972d 100644
--- a/ent/user_update.go
+++ b/ent/user_update.go
@@ -89,6 +89,14 @@ func (uu *UserUpdate) SetLastLogin(t time.Time) *UserUpdate {
return uu
}
+// SetNillableLastLogin sets the "last_login" field if the given value is not nil.
+func (uu *UserUpdate) SetNillableLastLogin(t *time.Time) *UserUpdate {
+ if t != nil {
+ uu.SetLastLogin(*t)
+ }
+ return uu
+}
+
// AddTrackedBreachIDs adds the "tracked_breaches" edge to the TrackedBreaches entity by IDs.
func (uu *UserUpdate) AddTrackedBreachIDs(ids ...uuid.UUID) *UserUpdate {
uu.mutation.AddTrackedBreachIDs(ids...)
@@ -200,10 +208,6 @@ func (uu *UserUpdate) defaults() {
v := user.UpdateDefaultUpdatedAt()
uu.mutation.SetUpdatedAt(v)
}
- if _, ok := uu.mutation.LastLogin(); !ok {
- v := user.UpdateDefaultLastLogin()
- uu.mutation.SetLastLogin(v)
- }
}
// check runs all checks and user-defined validators on the builder.
@@ -427,6 +431,14 @@ func (uuo *UserUpdateOne) SetLastLogin(t time.Time) *UserUpdateOne {
return uuo
}
+// SetNillableLastLogin sets the "last_login" field if the given value is not nil.
+func (uuo *UserUpdateOne) SetNillableLastLogin(t *time.Time) *UserUpdateOne {
+ if t != nil {
+ uuo.SetLastLogin(*t)
+ }
+ return uuo
+}
+
// AddTrackedBreachIDs adds the "tracked_breaches" edge to the TrackedBreaches entity by IDs.
func (uuo *UserUpdateOne) AddTrackedBreachIDs(ids ...uuid.UUID) *UserUpdateOne {
uuo.mutation.AddTrackedBreachIDs(ids...)
@@ -551,10 +563,6 @@ func (uuo *UserUpdateOne) defaults() {
v := user.UpdateDefaultUpdatedAt()
uuo.mutation.SetUpdatedAt(v)
}
- if _, ok := uuo.mutation.LastLogin(); !ok {
- v := user.UpdateDefaultLastLogin()
- uuo.mutation.SetLastLogin(v)
- }
}
// check runs all checks and user-defined validators on the builder.
diff --git a/handlers/error.go b/handlers/error.go
index 876994a..fd0dfa6 100644
--- a/handlers/error.go
+++ b/handlers/error.go
@@ -8,6 +8,7 @@ import (
"fmt"
"strconv"
+ moduser "git.dotya.ml/mirre-mt/pcmt/modules/user"
"github.com/labstack/echo/v4"
)
@@ -21,18 +22,22 @@ func renderErrorPage(c echo.Context, status int, statusText, error string) error
strStatus := strconv.Itoa(status)
+ p := newPage()
+
+ p.Title = fmt.Sprintf("Error %s - %s", strStatus, statusText)
+ p.Current = strStatus
+ p.Error = error
+ p.Status = strStatus
+ p.StatusText = statusText
+
+ u, ok := c.Get("sessUsr").(moduser.User)
+ if ok {
+ p.User = u
+ }
+
return c.Render(
status,
"errorPage.tmpl",
- page{
- AppName: setting.AppName(),
- AppVer: appver,
- Title: fmt.Sprintf("Error %s - %s", strStatus, statusText),
- DevelMode: setting.IsDevel(),
- Current: strStatus,
- Error: error,
- Status: strStatus,
- StatusText: statusText,
- },
+ p,
)
}
diff --git a/handlers/handlers.go b/handlers/handlers.go
index efa847c..3ccfdb6 100644
--- a/handlers/handlers.go
+++ b/handlers/handlers.go
@@ -54,6 +54,8 @@ func Index() echo.HandlerFunc {
}
func Healthz() echo.HandlerFunc {
+ // TODO: have this respond with some kind of internal state that gets
+ // read-locked when returning.
return func(c echo.Context) error {
return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
}
diff --git a/handlers/home.go b/handlers/home.go
index 9ce1ad6..6d3ca7a 100644
--- a/handlers/home.go
+++ b/handlers/home.go
@@ -55,6 +55,8 @@ func Home(client *ent.Client) echo.HandlerFunc {
u.ID = usr.ID
u.Username = usr.Username
u.IsActive = usr.IsActive
+ u.IsAdmin = usr.IsAdmin
+ // TODO: this is redundant, if there is a user object, the user is logged in...
u.IsLoggedIn = true
} else {
c.Logger().Error("failed to query usr", username)
@@ -67,6 +69,22 @@ func Home(client *ent.Client) echo.HandlerFunc {
)
}
+ if !u.IsAdmin {
+ f, err := moduser.UsrFinishedSetup(ctx, dbclient, u.ID)
+ if err != nil {
+ return renderErrorPage(
+ c,
+ http.StatusInternalServerError,
+ http.StatusText(http.StatusInternalServerError),
+ err.Error(),
+ )
+ }
+
+ if !f {
+ return c.Redirect(http.StatusSeeOther, "/user/initial-password-change")
+ }
+ }
+
csrf := c.Get("csrf").(string)
p := newPage()
@@ -76,9 +94,18 @@ func Home(client *ent.Client) echo.HandlerFunc {
p.Name = username
p.User = u
- err := c.Render(http.StatusOK, "home.tmpl",
- p,
- )
+ data := make(map[string]any)
+ flash := sess.Values["flash"]
+
+ if flash != nil {
+ data["flash"] = flash.(string)
+
+ delete(sess.Values, "flash")
+
+ _ = sess.Save(c.Request(), c.Response())
+ }
+
+ err := c.Render(http.StatusOK, "home.tmpl", p)
if err != nil {
c.Logger().Errorf("error: %q", err)
diff --git a/handlers/logout.go b/handlers/logout.go
index e18b18f..aa9b567 100644
--- a/handlers/logout.go
+++ b/handlers/logout.go
@@ -15,7 +15,7 @@ func Logout() echo.HandlerFunc {
addHeaders(c)
switch {
- case c.Request().Method == "POST":
+ case c.Request().Method == "POST": // nolint:goconst
sess, _ := session.Get(setting.SessionCookieName(), c)
if sess != nil {
log.Infof("max-age before logout: %d", sess.Options.MaxAge)
@@ -33,7 +33,7 @@ func Logout() echo.HandlerFunc {
return c.Redirect(http.StatusMovedPermanently, "/logout")
- case c.Request().Method == "GET":
+ case c.Request().Method == "GET": // nolint:goconst
sess, _ := session.Get(setting.SessionCookieName(), c)
if sess != nil {
if username := sess.Values["username"]; username != nil {
diff --git a/handlers/manage-user.go b/handlers/manage-user.go
index add9898..1732980 100644
--- a/handlers/manage-user.go
+++ b/handlers/manage-user.go
@@ -227,7 +227,7 @@ func CreateUser() echo.HandlerFunc { //nolint:gocognit
usr, err := moduser.CreateUser(ctx, dbclient, uc.Email, uc.Username, uc.Password, uc.IsAdmin)
if err == nil && usr != nil {
- msg = "created user '" + usr.Username + "'!"
+ msg = fmt.Sprintf("Successfully created user %q!", usr.Username)
if sess, ok := c.Get("sess").(*sessions.Session); ok {
sess.Values["flash"] = msg
@@ -282,6 +282,16 @@ func ViewUser() echo.HandlerFunc {
)
}
+ sess, ok := c.Get("sess").(*sessions.Session)
+ if !ok {
+ return renderErrorPage(
+ c,
+ http.StatusInternalServerError,
+ http.StatusText(http.StatusInternalServerError),
+ "missing the session",
+ )
+ }
+
ctx, ok := c.Get("sloggerCtx").(context.Context)
if !ok {
ctx = context.WithValue(context.Background(), moduser.CtxKey{}, slogger)
@@ -303,7 +313,7 @@ func ViewUser() echo.HandlerFunc {
if errors.Is(err, moduser.ErrUserNotFound) { //nolint:gocritic
c.Logger().Errorf("user not found by ID: '%s'", uid.ID)
- data["flash"] = fmt.Sprintf("No user found with the UUID: %q", uid.ID)
+ data["flash"] = fmt.Sprintf("No user found with UUID: %q", uid.ID)
p.Data = data
return c.Render(
@@ -314,7 +324,7 @@ func ViewUser() echo.HandlerFunc {
} else if errors.Is(err, moduser.ErrFailedToQueryUser) {
c.Logger().Errorf("failed to query user by ID: '%s'", uid.ID)
- data["flash"] = fmt.Sprintf("failed to query user by UUID %q", uid.ID)
+ data["flash"] = fmt.Sprintf("Error: failed to query user by UUID %q", uid.ID)
p.Data = data
return c.Render(
@@ -325,7 +335,7 @@ func ViewUser() echo.HandlerFunc {
} else if errors.Is(err, moduser.ErrBadUUID) {
c.Logger().Errorf("Invalid UUID '%s': %q", uid.ID, err)
- data["flash"] = fmt.Sprintf("invalid UUID %q", uid.ID)
+ data["flash"] = fmt.Sprintf("Error: invalid UUID %q", uid.ID)
p.Data = data
return c.Render(
@@ -346,6 +356,16 @@ func ViewUser() echo.HandlerFunc {
}
data["user"] = usr
+
+ flash := sess.Values["flash"]
+ if flash != nil {
+ data["flash"] = flash.(string)
+
+ delete(sess.Values, "flash")
+
+ _ = sess.Save(c.Request(), c.Response())
+ }
+
p.Data = data
return c.Render(
@@ -567,26 +587,54 @@ func UpdateUser() echo.HandlerFunc { //nolint:gocognit
)
}
- if uu.Username == "" || uu.Password == "" || uu.RepeatPassword == "" || uu.Password != uu.RepeatPassword {
- c.Logger().Error("username or password not set, returning to /manage/users/edit")
-
- msg := "Error: both username and password need to be set"
-
- if uu.Password != uu.RepeatPassword {
- msg += "; the same password needs to be passed in twice"
- }
-
- data["flash"] = msg
- data["form"] = uu
- p.Data = data
-
- return c.Render(
- http.StatusBadRequest,
- "manage/user-edit.tmpl",
- p,
+ f, err := moduser.UsrFinishedSetup(ctx, dbclient, usr.ID)
+ if err != nil {
+ return renderErrorPage(
+ c,
+ http.StatusInternalServerError,
+ http.StatusText(http.StatusInternalServerError),
+ err.Error(),
)
}
+ if f {
+ if uu.Username == "" {
+ c.Logger().Error("username not set, returning to /manage/users/edit")
+
+ msg := "Error: the username needs to be set"
+
+ data["flash"] = msg
+ data["form"] = uu
+ p.Data = data
+
+ return c.Render(
+ http.StatusBadRequest,
+ "manage/user-edit.tmpl",
+ p,
+ )
+ }
+ } else {
+ if uu.Username == "" || uu.Password == "" || uu.RepeatPassword == "" || uu.Password != uu.RepeatPassword {
+ c.Logger().Error("username or password not set, returning to /manage/users/edit")
+
+ msg := "Error: both username and password need to be set"
+
+ if uu.Password != uu.RepeatPassword {
+ msg += "; the same password needs to be passed in twice"
+ }
+
+ data["flash"] = msg
+ data["form"] = uu
+ p.Data = data
+
+ return c.Render(
+ http.StatusBadRequest,
+ "manage/user-edit.tmpl",
+ p,
+ )
+ }
+ }
+
if usr.Username != uu.Username {
exists, err := moduser.UsernameExists(ctx, dbclient, uu.Username)
if err != nil {
@@ -640,11 +688,28 @@ func UpdateUser() echo.HandlerFunc { //nolint:gocognit
}
// now update
+ err = moduser.UpdateUserByAdmin(
+ ctx, dbclient,
+ usr.ID, uu.Email, uu.Username, uu.Password, uu.IsAdmin, uu.IsActive,
+ )
+ if err != nil {
+ return renderErrorPage(
+ c,
+ http.StatusInternalServerError,
+ http.StatusText(http.StatusInternalServerError),
+ err.Error(),
+ )
+ }
data["user"] = usr
p.Data = data
- return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/manage/users/%s/edit", usr.ID))
+ if sess, ok := c.Get("sess").(*sessions.Session); ok {
+ sess.Values["flash"] = fmt.Sprintf("Successfully updated user %q", uu.Username)
+ _ = sess.Save(c.Request(), c.Response())
+ }
+
+ return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/manage/users/%s", usr.ID))
}
}
diff --git a/handlers/middleware.go b/handlers/middleware.go
index 3ccef46..e66c99e 100644
--- a/handlers/middleware.go
+++ b/handlers/middleware.go
@@ -22,12 +22,13 @@ func MiddlewareSession(next echo.HandlerFunc) echo.HandlerFunc {
var username string
- uname, ok := sess.Values["username"].(string)
+ // uname, ok := sess.Values["username"].(string)
+ uname := sess.Values["username"]
- if ok {
- username = uname
+ if uname != nil {
+ username = uname.(string)
- log.Info("gorilla session", "username", username)
+ log.Debug("Refreshing session cookie", "username", username, "module", "middleware")
refreshSession(
sess,
@@ -38,17 +39,6 @@ func MiddlewareSession(next echo.HandlerFunc) echo.HandlerFunc {
http.SameSiteStrictMode,
)
- if err := sess.Save(c.Request(), c.Response()); err != nil {
- c.Logger().Error("failed to save session")
-
- return renderErrorPage(
- c,
- http.StatusInternalServerError,
- http.StatusText(http.StatusInternalServerError)+" (make sure you've got cookies enabled)",
- err.Error(),
- )
- }
-
c.Set("sess", sess)
var u moduser.User
@@ -75,19 +65,35 @@ func MiddlewareSession(next echo.HandlerFunc) echo.HandlerFunc {
c.Set("sloggerCtx", ctx)
c.Set("sessUsr", u)
+ if err := sess.Save(c.Request(), c.Response()); err != nil {
+ c.Logger().Error("Failed to save session", "module", "middleware")
+
+ return renderErrorPage(
+ c,
+ http.StatusInternalServerError,
+ http.StatusText(http.StatusInternalServerError)+" (make sure you've got cookies enabled)",
+ err.Error(),
+ )
+ }
+
return next(c)
}
if !sess.IsNew {
- c.Logger().Debugf("%d - %s", http.StatusUnauthorized, "you need to log in")
+ c.Logger().Errorf("%d - %s", http.StatusUnauthorized, "you need to log in")
+ return c.Redirect(http.StatusTemporaryRedirect, "/signin")
}
- return renderErrorPage(
- c,
- http.StatusUnauthorized,
- http.StatusText(http.StatusUnauthorized),
- ErrNoSession.Error(),
- )
+ // return renderErrorPage(
+ // c,
+ // http.StatusUnauthorized,
+ // http.StatusText(http.StatusUnauthorized),
+ // ErrNoSession.Error(),
+ // )
+
+ c.Logger().Warn("Could not get username from the cookie")
+
+ return next(c)
}
}
diff --git a/handlers/search-hibp.go b/handlers/search-hibp.go
new file mode 100644
index 0000000..626bb57
--- /dev/null
+++ b/handlers/search-hibp.go
@@ -0,0 +1,174 @@
+// Copyright 2023 wanderer
+// SPDX-License-Identifier: AGPL-3.0-only
+
+package handlers
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+
+ "git.dotya.ml/mirre-mt/pcmt/modules/hibp"
+ moduser "git.dotya.ml/mirre-mt/pcmt/modules/user"
+ "github.com/gorilla/sessions"
+ "github.com/labstack/echo/v4"
+)
+
+func GetSearchHIBP() echo.HandlerFunc {
+ return func(c echo.Context) error {
+ addHeaders(c)
+
+ u, ok := c.Get("sessUsr").(moduser.User)
+ if !ok {
+ c.Logger().Warnf("Error getting user from session cookie")
+ } else if u.IsAdmin {
+ status := http.StatusUnauthorized
+ msg := http.StatusText(status)
+
+ return renderErrorPage(
+ c, status, msg+": You should not be here", "This endpoint is for users only",
+ )
+ }
+
+ csrf := c.Get("csrf").(string)
+ p := newPage()
+
+ if sess, ok := c.Get("sess").(*sessions.Session); ok {
+ if flash := sess.Values["flash"]; flash != nil {
+ p.Data["flash"] = flash.(string)
+
+ delete(sess.Values, "flash")
+
+ _ = sess.Save(c.Request(), c.Response())
+ }
+ }
+
+ p.Title = "Search HIBP"
+ p.Current = "hibp-search"
+ p.CSRF = csrf
+ p.User = u
+
+ err := c.Render(
+ http.StatusOK,
+ "user/hibp-search.tmpl",
+ p,
+ )
+ if err != nil {
+ c.Logger().Errorf("error: %q", err)
+
+ return renderErrorPage(
+ c,
+ http.StatusInternalServerError,
+ http.StatusText(http.StatusInternalServerError),
+ err.Error(),
+ )
+ }
+
+ return nil
+ }
+}
+
+func SearchHIBP() echo.HandlerFunc {
+ return func(c echo.Context) error {
+ addHeaders(c)
+
+ u, ok := c.Get("sessUsr").(moduser.User)
+ if !ok {
+ // return renderErrorPage(
+ // c,
+ // http.StatusUnauthorized,
+ // http.StatusText(http.StatusUnauthorized)+", perhaps you need to log in first?",
+ // "Username was nil",
+ // )
+ c.Logger().Warnf("Error getting user from session cookie")
+ } else if u.IsAdmin {
+ status := http.StatusUnauthorized
+ msg := http.StatusText(status)
+
+ return renderErrorPage(
+ c, status, msg+": You should not be here", "This endpoint is for users only",
+ )
+ }
+
+ csrf := c.Get("csrf").(string)
+
+ a := new(hibpSearch)
+ if err := c.Bind(a); err != nil {
+ return renderErrorPage(
+ c,
+ http.StatusBadRequest,
+ http.StatusText(http.StatusBadRequest),
+ err.Error(),
+ )
+ }
+
+ breachNames, err := hibp.GetAllBreachesForAccount(a.Account)
+ if err != nil {
+ msg := "Error getting breaches for this account"
+ status := http.StatusInternalServerError
+
+ switch {
+ case errors.Is(err, hibp.ErrRateLimited):
+ msg = http.StatusText(http.StatusTooManyRequests) + " - we have been rate limited from the API. Try again in a while."
+ status = http.StatusTooManyRequests
+
+ case errors.Is(err, hibp.ErrAuthKeyCheckValue):
+ msg = hibp.ErrAuthKeyCheckValue.Error()
+ status = http.StatusBadRequest
+ }
+
+ return renderErrorPage(
+ c,
+ status,
+ msg,
+ err.Error(),
+ )
+ }
+
+ if len(breachNames) == 0 {
+ if sess, ok := c.Get("sess").(*sessions.Session); ok {
+ sess.Values["flash"] = fmt.Sprintf("There is no mention of account %q in any of the HIBP breaches!", a.Account)
+ _ = sess.Save(c.Request(), c.Response())
+ }
+
+ return c.Redirect(http.StatusSeeOther, "/user/hibp-search")
+ }
+
+ sfx := "breach"
+ bCount := len(breachNames)
+
+ if bCount > 1 {
+ sfx = "breaches"
+ }
+
+ p := newPage()
+
+ p.Title = "Search HIBP"
+ p.Current = "hibp-search"
+ p.CSRF = csrf
+ p.User = u
+ p.Data["breachNames"] = breachNames
+ p.Data["flash"] = fmt.Sprintf(
+ "The account %q was found in %d %s!",
+ a.Account, bCount, sfx,
+ )
+
+ err = c.Render(
+ http.StatusOK,
+ "user/hibp-search.tmpl",
+ p,
+ )
+ if err != nil {
+ c.Logger().Errorf("error: %q", err)
+
+ return renderErrorPage(
+ c,
+ http.StatusInternalServerError,
+ http.StatusText(http.StatusInternalServerError),
+ err.Error(),
+ )
+ }
+
+ return nil
+ }
+}
diff --git a/handlers/signin.go b/handlers/signin.go
index 875abbb..c387063 100644
--- a/handlers/signin.go
+++ b/handlers/signin.go
@@ -67,7 +67,7 @@ func SigninPost(client *ent.Client) echo.HandlerFunc {
p.CSRF = csrf
if username == "" || password == "" {
- c.Logger().Error("username or password not set, returning to /signin")
+ c.Logger().Warn("username or password not set, returning to /signin")
data["flash"] = "you need to set both the username and the password"
data["form"] = cu
@@ -134,6 +134,9 @@ func SigninPost(client *ent.Client) echo.HandlerFunc {
SameSite: http.SameSiteStrictMode,
}
sess.Values["foo"] = "bar"
+
+ c.Logger().Debug("saving username to the session cookie")
+
sess.Values["username"] = username
err := sess.Save(c.Request(), c.Response())
diff --git a/handlers/type.go b/handlers/type.go
index 56965a1..7777248 100644
--- a/handlers/type.go
+++ b/handlers/type.go
@@ -14,11 +14,13 @@ type userSignup struct {
Password string `form:"password" json:"password" validate:"required,password"`
}
+// this struct is also used on update by admins, which is why the password fields are omitempty.
+// when users finish setting up, admins can no longer change their passwords.
type userCreate struct {
Username string `form:"username" json:"username" validate:"required,username"`
Email string `form:"email" json:"email" validate:"required,email"`
- Password string `form:"password" json:"password" validate:"required,password"`
- RepeatPassword string `form:"repeatPassword" json:"repeatPassword" validate:"required,repeatPassword"`
+ Password string `form:"password" json:"password" validate:"omitempty,password"`
+ RepeatPassword string `form:"repeatPassword" json:"repeatPassword" validate:"omitempty,repeatPassword"`
IsAdmin bool `form:"isAdmin" json:"isAdmin" validate:"required,isAdmin"`
IsActive *bool `form:"isActive" json:"isActive" validate:"omitempty,isActive"`
}
@@ -26,3 +28,15 @@ type userCreate struct {
type userID struct {
ID string `param:"id" validate:"required,id"`
}
+
+type initPasswordChange struct {
+ NewPassword string `form:"new-password" validate:"required,new-password"`
+}
+
+type hibpSearch struct {
+ Account string `form:"search" validate:"required,search"`
+}
+
+type hibpBreachDetail struct {
+ BreachName string `param:"name" validate:"required,name"`
+}
diff --git a/handlers/user-init-password-change.go b/handlers/user-init-password-change.go
new file mode 100644
index 0000000..e86a179
--- /dev/null
+++ b/handlers/user-init-password-change.go
@@ -0,0 +1,129 @@
+// Copyright 2023 wanderer
+// SPDX-License-Identifier: AGPL-3.0-only
+
+package handlers
+
+import (
+ "context"
+ "net/http"
+
+ moduser "git.dotya.ml/mirre-mt/pcmt/modules/user"
+ "github.com/gorilla/sessions"
+ "github.com/labstack/echo/v4"
+)
+
+func InitialPasswordChange() echo.HandlerFunc {
+ return func(c echo.Context) error {
+ addHeaders(c)
+
+ u, ok := c.Get("sessUsr").(moduser.User)
+ if !ok {
+ return renderErrorPage(
+ c,
+ http.StatusUnauthorized,
+ http.StatusText(http.StatusUnauthorized)+", perhaps you need to log in first?",
+ "Username was nil",
+ )
+ } else if u.IsAdmin {
+ status := http.StatusUnauthorized
+ msg := http.StatusText(status)
+
+ return renderErrorPage(
+ c, status, msg+": You should not be here", "This endpoint is for users only",
+ )
+ }
+
+ ctx, ok := c.Get("sloggerCtx").(context.Context)
+ if !ok {
+ ctx = context.WithValue(context.Background(), moduser.CtxKey{}, slogger)
+ }
+
+ f, err := moduser.UsrFinishedSetup(ctx, dbclient, u.ID)
+
+ switch {
+ case c.Request().Method == "POST":
+ switch {
+ case err != nil:
+ return renderErrorPage(
+ c,
+ http.StatusInternalServerError,
+ http.StatusText(http.StatusInternalServerError),
+ err.Error(),
+ )
+
+ case f:
+ return c.Redirect(http.StatusSeeOther, "/user/init-password-change")
+ }
+
+ pw := new(initPasswordChange)
+
+ if err := c.Bind(pw); err != nil {
+ return renderErrorPage(
+ c,
+ http.StatusBadRequest,
+ http.StatusText(http.StatusBadRequest),
+ err.Error(),
+ )
+ }
+
+ err = moduser.ChangePassFirstLogin(ctx, dbclient, u.ID, pw.NewPassword)
+ if err != nil {
+ c.Logger().Errorf("error changing initial user password: %q", err)
+
+ return renderErrorPage(
+ c,
+ http.StatusInternalServerError,
+ http.StatusText(http.StatusInternalServerError),
+ err.Error(),
+ )
+ }
+
+ if sess, ok := c.Get("sess").(*sessions.Session); ok {
+ sess.Values["flash"] = "Successfully updated your password"
+ _ = sess.Save(c.Request(), c.Response())
+ }
+
+ return c.Redirect(http.StatusSeeOther, "/home")
+
+ case c.Request().Method == "GET":
+ switch {
+ case err != nil:
+ return renderErrorPage(
+ c,
+ http.StatusInternalServerError,
+ http.StatusText(http.StatusInternalServerError),
+ err.Error(),
+ )
+
+ case f:
+ return c.Redirect(http.StatusSeeOther, "/home")
+ }
+
+ csrf := c.Get("csrf").(string)
+ p := newPage()
+
+ p.Title = "Initial password change"
+ p.Current = "init-password-change"
+ p.CSRF = csrf
+ p.User = u
+
+ err := c.Render(
+ http.StatusOK,
+ "user/init-password-change.tmpl",
+ p,
+ )
+ if err != nil {
+ c.Logger().Errorf("error: %q", err)
+
+ return renderErrorPage(
+ c,
+ http.StatusInternalServerError,
+ http.StatusText(http.StatusInternalServerError),
+ err.Error(),
+ )
+ }
+ }
+
+ return nil
+ }
+}
diff --git a/handlers/view-hibp.go b/handlers/view-hibp.go
new file mode 100644
index 0000000..68601d7
--- /dev/null
+++ b/handlers/view-hibp.go
@@ -0,0 +1,81 @@
+// Copyright 2023 wanderer
+// SPDX-License-Identifier: AGPL-3.0-only
+
+package handlers
+
+import (
+ "context"
+ "net/http"
+
+ "git.dotya.ml/mirre-mt/pcmt/modules/hibp"
+ moduser "git.dotya.ml/mirre-mt/pcmt/modules/user"
+ "github.com/labstack/echo/v4"
+)
+
+func ViewHIBP() echo.HandlerFunc {
+ return func(c echo.Context) error {
+ addHeaders(c)
+
+ u, ok := c.Get("sessUsr").(moduser.User)
+ if !ok {
+ c.Logger().Warnf("Error getting user from session cookie")
+ } else if u.IsAdmin {
+ status := http.StatusUnauthorized
+ msg := http.StatusText(status)
+
+ return renderErrorPage(
+ c, status, msg+": You should not be here", "This endpoint is for users only",
+ )
+ }
+
+ h := new(hibpBreachDetail)
+ if err := c.Bind(h); err != nil {
+ return renderErrorPage(
+ c,
+ http.StatusBadRequest,
+ http.StatusText(http.StatusBadRequest),
+ err.Error(),
+ )
+ }
+
+ ctx, ok := c.Get("sloggerCtx").(context.Context)
+ if !ok {
+ ctx = context.WithValue(context.Background(), hibp.CtxKey{}, slogger)
+ }
+
+ breachDetail, err := hibp.BreachForBreachName(ctx, dbclient, h.BreachName)
+ if err != nil {
+ return renderErrorPage(
+ c,
+ http.StatusInternalServerError,
+ http.StatusText(http.StatusInternalServerError),
+ err.Error(),
+ )
+ }
+
+ p := newPage()
+
+ p.Title = "HIBP breach details"
+ p.Current = "hibp-breach-details"
+ p.User = u
+ p.Data["hibp"] = breachDetail
+
+ err = c.Render(
+ http.StatusOK,
+ "user/hibp-details.tmpl",
+ p,
+ )
+ if err != nil {
+ c.Logger().Errorf("error: %q", err)
+
+ return renderErrorPage(
+ c,
+ http.StatusInternalServerError,
+ http.StatusText(http.StatusInternalServerError),
+ err.Error(),
+ )
+ }
+
+ return nil
+ }
+}
diff --git a/justfile b/justfile
index df95473..4b7c76d 100644
--- a/justfile
+++ b/justfile
@@ -1,6 +1,6 @@
cmd := "podman"
cfile := "Containerfile"
-tag := "docker.io/immawanderer/pcmt:testbuild"
+tag := "docker.io/immawanderer/mt-pcmt:testbuild"
args := "build -t "+ tag + " " + buildargs + " --no-cache --pull -f " + cfile
buildargs := "--build-arg VERSION=" + vcs_ref + " --build-arg BUILD_DATE=" + build_date + " --build-arg VCS_REF=" + vcs_ref
kanikoargs := "run -it -w=" + kanikowdir + " -v $(pwd):" + kanikowdir + ":z " + kanikoexecutorimg + " -f=" + cfile + " -c=" + kanikocontext + " --use-new-run --snapshotMode=redo --no-push " + buildargs
diff --git a/modules/db/db.go b/modules/db/db.go
index 102369a..f84f467 100644
--- a/modules/db/db.go
+++ b/modules/db/db.go
@@ -115,6 +115,7 @@ func PrintMigration(ctx context.Context, client *ent.Client) error {
// has not been set up prior, it creates and admin user with the initPasswd and
// the default username and email (see ../user/const.go).
func SetUp(ctx context.Context, client *ent.Client, createAdmin bool, initPasswd string) error {
+ // TODO: https://entgo.io/blog/2022/05/09/versioned-migrations-sum-file/
if err := client.Schema.Create(ctx); err != nil {
return fmt.Errorf("failed to create schema resources: %v", err)
}
diff --git a/modules/funcmap/funcmap.go b/modules/funcmap/funcmap.go
index ea5b423..57ed3e6 100644
--- a/modules/funcmap/funcmap.go
+++ b/modules/funcmap/funcmap.go
@@ -6,6 +6,8 @@ package funcmap
import (
"html/template"
"io/fs"
+ "strings"
+ "time"
modbluemonday "git.dotya.ml/mirre-mt/pcmt/modules/bluemonday"
)
@@ -24,6 +26,15 @@ func FuncMap() template.FuncMap {
modbluemonday.Policy.Sanitize(html),
)
},
+ // a voluntarily unsafe func.
+ "htmlRaw": func(value string) template.HTML {
+ // nolint:gosec
+ return template.HTML(value)
+ },
+ "htmlLinkStyle": func(value string) string {
+ value = strings.ReplaceAll(value, "", ``)
+ },
"pageIs": func(want, got string) bool {
return want == got
},
@@ -57,6 +68,9 @@ func FuncMap() template.FuncMap {
return *r
},
+ "usrFinishedSetup": func(lastLogin time.Time) bool {
+ return lastLogin.After(time.Unix(0, 0))
+ },
}
}
diff --git a/modules/hibp/hibp.go b/modules/hibp/hibp.go
index 0b41e7b..b74f860 100644
--- a/modules/hibp/hibp.go
+++ b/modules/hibp/hibp.go
@@ -7,7 +7,6 @@ import (
"context"
"encoding/json"
"io"
- "log"
"net/http"
"os"
"time"
@@ -57,6 +56,9 @@ const (
var (
apiKey = os.Getenv("PCMT_HIBP_API_KEY")
client = &http.Client{Timeout: reqTmOut}
+ log = slog.With(
+ slog.Group("pcmt extra", slog.String("module", "modules/hibp")),
+ )
)
// SubscriptionStatus models https://haveibeenpwned.com/API/v3#SubscriptionStatus.
@@ -65,7 +67,7 @@ func SubscriptionStatus() (*Subscription, error) {
req, err := http.NewRequest("GET", u, nil)
if err != nil {
- log.Fatalln(err)
+ log.Error("Could not create a new HTTP request", "error", err)
}
setUA(req)
@@ -100,7 +102,8 @@ func GetAllBreaches() (*[]schema.HIBPSchema, error) {
req, err := http.NewRequest("GET", u, nil)
if err != nil {
- log.Fatalln(err)
+ log.Error("Could not create a new HTTP request", "error", err)
+ return nil, err
}
respCh, errCh := rChans()
@@ -140,7 +143,8 @@ func GetAllBreachesForAccount(account string) ([]BreachName, error) {
req, err := http.NewRequest("GET", u, nil)
if err != nil {
- log.Fatalln(err)
+ log.Error("Could not create a new HTTP request", "error", err)
+ return nil, err
}
respCh, errCh := rChans()
@@ -187,13 +191,6 @@ func GetAllBreachesForAccount(account string) ([]BreachName, error) {
// GetBreachesForBreachNames retrieves HIBP breaches from the database for a
// list of names.
func GetBreachesForBreachNames(ctx context.Context, client *ent.Client, names []string) ([]*ent.HIBP, error) {
- slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
- log := *slogger
-
- log.Logger = log.Logger.With(
- slog.Group("pcmt extra", slog.String("module", "modules/hibp")),
- )
-
hs := make([]*ent.HIBP, 0)
for _, name := range names {
@@ -204,11 +201,11 @@ func GetBreachesForBreachNames(ctx context.Context, client *ent.Client, names []
if err != nil {
switch {
case ent.IsNotFound(err):
- log.Warnf("breach not found by name %q: %q", name, err)
+ log.Warn("Breach not found by name", "name", name, "error", err)
return nil, ErrBreachNotFound
case ent.IsNotSingular(err):
- log.Warnf("multiple breaches returned for name %q: %q", name, err)
+ log.Warn("Multiple breaches returned for name", "name", name, "error", err)
return nil, ErrBreachNotSingular
case err != nil:
@@ -226,6 +223,39 @@ func GetBreachesForBreachNames(ctx context.Context, client *ent.Client, names []
return hs, nil
}
+// BreachForBreachName retrieves a single HIBP breach from the database for a
+// given name.
+func BreachForBreachName(ctx context.Context, client *ent.Client, name string) (*ent.HIBP, error) {
+ log := slog.With(
+ slog.Group("pcmt extra", slog.String("module", "modules/hibp")),
+ )
+
+ b, err := client.HIBP.
+ Query().
+ Where(hibp.NameEQ(name)).
+ Only(ctx)
+ if err != nil {
+ switch {
+ case ent.IsNotFound(err):
+ log.Error("Breach not found by name", "name", name, "error", err)
+ return nil, ErrBreachNotFound
+
+ case ent.IsNotSingular(err):
+ log.Error("Multiple breaches returned for breach name", "name", name, "error", err)
+ return nil, ErrBreachNotSingular
+
+ case err != nil:
+ log.Error("Failed to query breach by name", "error", err, "name requested", name)
+ return nil, ErrFailedToQueryBreaches
+
+ default:
+ return nil, err
+ }
+ }
+
+ return b, nil
+}
+
// SaveAllBreaches saves all breaches to DB as a cache.
func SaveAllBreaches(ctx context.Context, client *ent.Client, breaches *[]schema.HIBPSchema) error {
slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
@@ -235,10 +265,13 @@ func SaveAllBreaches(ctx context.Context, client *ent.Client, breaches *[]schema
slog.Group("pcmt extra", slog.String("module", "modules/hibp")),
)
- if breaches == nil {
+ if breaches == nil || len(*breaches) == 0 {
+ log.Error("Received 0 HIBP breaches / nil breaches object")
return ErrNoBreachesToSave
}
+ log.Infof("HIBP API returned %d breaches, saving...", len(*breaches))
+
for _, b := range *breaches {
_, err := client.HIBP.
Create().
@@ -260,6 +293,7 @@ func SaveAllBreaches(ctx context.Context, client *ent.Client, breaches *[]schema
SetLogoPath(b.LogoPath).
Save(ctx)
if err != nil {
+ log.Errorf("Could not save HIBP breaches to DB, err: %q", err)
return err
}
}
@@ -267,6 +301,41 @@ func SaveAllBreaches(ctx context.Context, client *ent.Client, breaches *[]schema
return nil
}
+// CheckSaveAllBreaches checks if there are any in the DB and if not then
+// queries the API and saves what it gets. TODO: have this function consolidate
+// existing vs. new breaches.
+func CheckSaveAllBreaches(ctx context.Context, client *ent.Client) error {
+ slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
+ log := *slogger
+
+ log.Logger = log.Logger.With(
+ slog.Group("pcmt extra", slog.String("module", "modules/hibp")),
+ )
+
+ log.Info("Checking if we have any HIBP breaches saved")
+
+ alreadySaved, err := client.HIBP.Query().Count(ctx)
+
+ switch {
+ case err != nil:
+ return err
+
+ case alreadySaved > 0:
+ log.Infof("There are %d HIBP breaches already, not attempting to save new breaches into DB", alreadySaved)
+ return nil
+ }
+
+ log.Info("No HIBP breaches found in the DB, refreshing from API...")
+
+ breaches, err := GetAllBreaches()
+ if err != nil {
+ log.Error("Could not save HIBP breaches")
+ return err
+ }
+
+ return SaveAllBreaches(ctx, client, breaches)
+}
+
func setUA(r *http.Request) {
r.Header.Set(headerUA, appID)
}
diff --git a/modules/hibp/schedule.go b/modules/hibp/schedule.go
index b9f53d3..8aa7576 100644
--- a/modules/hibp/schedule.go
+++ b/modules/hibp/schedule.go
@@ -215,11 +215,12 @@ func doReqs(ok chan error, wg *sync.WaitGroup) {
nxckpLock.Lock()
timeToWait = time.Until(nextCheckpoint)
+ time.Sleep(timeToWait) // yes, sleep while locked.
ttwLock.Unlock()
nxckpLock.Unlock()
- break
+ return
}
}
@@ -311,7 +312,6 @@ func zeroQueue() bool {
defer rqqLock.RUnlock()
if len(queue.Requests) == 0 {
- slog.Debug("Queue empty")
return true
}
diff --git a/modules/user/user.go b/modules/user/user.go
index 1f44b64..47eaf60 100644
--- a/modules/user/user.go
+++ b/modules/user/user.go
@@ -26,6 +26,7 @@ type User struct {
CreatedAt time.Time
UpdatedAt time.Time
IsLoggedIn bool
+ LastLogin time.Time
}
// CreateUser adds a user entry to the database.
@@ -139,6 +140,146 @@ func QueryUserByID(ctx context.Context, client *ent.Client, strID string) (*ent.
return u, nil
}
+func UsrFinishedSetup(ctx context.Context, client *ent.Client, id uuid.UUID) (bool, error) {
+ u, err := client.User.Get(ctx, id)
+ if err != nil {
+ return false, err
+ }
+
+ if u.LastLogin.After(time.Unix(0, 0)) {
+ return true, nil
+ }
+
+ return false, nil
+}
+
+func ChangePassFirstLogin(ctx context.Context, client *ent.Client, id uuid.UUID, password string) error {
+ slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
+ log := *slogger
+
+ log.Logger = log.Logger.With(
+ slog.Group("pcmt extra", slog.String("module", "modules/user")),
+ )
+
+ finishedSetup, err := UsrFinishedSetup(ctx, client, id)
+
+ switch {
+ case err != nil:
+ return err
+
+ case finishedSetup:
+ return nil
+ }
+
+ var digest []byte
+
+ digest, err = passwd.GetHash(password)
+ if err != nil {
+ log.Errorf("error hashing password: %s", err)
+ return errors.New("could not hash password")
+ }
+
+ // TODO: turn this into a transaction.
+ u, err := client.User.
+ Update().Where(user.IDEQ(id)).
+ SetPassword(digest).
+ Save(ctx)
+
+ switch {
+ case err != nil:
+ return fmt.Errorf("failed to update user password: %w", err)
+
+ case u > 1:
+ return fmt.Errorf("somehow updated password of more than one user? count: %d", u)
+ }
+
+ u, err = client.User.
+ Update().Where(user.IDEQ(id)).
+ SetLastLogin(time.Now()).
+ Save(ctx)
+
+ switch {
+ case err != nil:
+ return fmt.Errorf("failed to set last login: %w", err)
+
+ case u > 1:
+ return fmt.Errorf("somehow updated last login information of more than one user? count: %d", u)
+ }
+
+ return nil
+}
+
+func UpdateUserByAdmin(ctx context.Context, client *ent.Client, id uuid.UUID, email, username, password string, isAdmin bool, isActive *bool) error {
+ slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
+ log := *slogger
+
+ log.Logger = log.Logger.With(
+ slog.Group("pcmt extra", slog.String("module", "modules/user")),
+ )
+
+ finishedSetup, err := UsrFinishedSetup(ctx, client, id)
+ if err != nil {
+ return err
+ }
+
+ var active bool
+
+ if isActive != nil {
+ active = *isActive
+ }
+
+ var u int
+
+ // ignore updates to password when user finished setting up (if not admin).
+ if !isAdmin && finishedSetup {
+ u, err = client.User.
+ Update().Where(user.IDEQ(id)).
+ SetEmail(email).
+ SetUsername(username).
+ SetIsAdmin(isAdmin).
+ SetIsActive(active).
+ Save(ctx)
+ } else {
+ var digest []byte
+
+ digest, err = passwd.GetHash(password)
+ if err != nil {
+ log.Errorf("error hashing password: %s", err)
+ return errors.New("could not hash password")
+ }
+
+ u, err = client.User.
+ Update().Where(user.IDEQ(id)).
+ SetEmail(email).
+ SetUsername(username).
+ SetPassword(digest).
+ SetIsAdmin(isAdmin).
+ SetIsActive(active).
+ Save(ctx)
+ }
+
+ switch {
+ case ent.IsConstraintError(err):
+ log.Errorf("the username '%s' already exists", username)
+ return errors.New("username is not unique")
+
+ case err != nil:
+ return fmt.Errorf("failed to update user: %w", err)
+
+ case u > 1:
+ return fmt.Errorf("somehow updated more than one user? count: %d", u)
+ }
+
+ log.Debug(
+ fmt.Sprintf(
+ "user successfully updated - id: %s, name: %s, active: %t, admin: %t",
+ id, username, *isActive, isAdmin,
+ ),
+ )
+
+ return nil
+}
+
// DeleteUserByID returns nil on successful deletion, err otherwise.
func DeleteUserByID(ctx context.Context, client *ent.Client, strID string) error {
slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
diff --git a/run.go b/run.go
index 1a9309a..855ef93 100644
--- a/run.go
+++ b/run.go
@@ -189,7 +189,7 @@ func run() error { //nolint:gocognit
ctx := context.WithValue(context.Background(), moddb.CtxKey{}, slogger)
if *printMigrationFlag {
- log.Debug("printing the following migration to stdout")
+ log.Debug("printing the upcoming migration to stdout")
return moddb.PrintMigration(ctx, db)
}
@@ -276,9 +276,23 @@ func run() error { //nolint:gocognit
handleSigs(schedQuit, quit)
- go func() {
+ wg.Add(1)
+ go func() { // nolint:wsl
+ defer wg.Done() // for this goroutine
+
+ // this is for the scheduler goroutine.
wg.Add(1) // nolint:staticcheck
go hibp.RunReqScheduler(schedQuit, errCh, wg) // nolint:wsl
+
+ // TODO: pass a chan to the scheduler for reporting readiness.
+ log.Info("Giving HIBP requests scheduler time to start")
+ time.Sleep(time.Second)
+
+ if err = hibp.CheckSaveAllBreaches(
+ context.WithValue(context.Background(), hibp.CtxKey{}, slogger),
+ db); err != nil {
+ log.Error("failed to refresh HIBP db cache")
+ }
}()
// non-blocking channel receive.
diff --git a/templates/head.tmpl b/templates/head.tmpl
index 3a0be69..8f41059 100644
--- a/templates/head.tmpl
+++ b/templates/head.tmpl
@@ -15,14 +15,16 @@
{{- end -}}
-
+
+
{{- if .DevelMode -}}
-
+
+
{{ template "browsersync.tmpl" }}
{{ else }}
diff --git a/templates/home.tmpl b/templates/home.tmpl
index f6e631c..b7e0f31 100644
--- a/templates/home.tmpl
+++ b/templates/home.tmpl
@@ -3,6 +3,11 @@
{{ template "navbar.tmpl" . }}
+ {{ if and .Data .Data.flash }}
+
+ {{ .Data.flash }}
+
+ {{- end }}
{{ if .Name -}}
Welcome, {{.Name}}
!
diff --git a/templates/manage/apikeys.tmpl b/templates/manage/apikeys.tmpl
index a9f0faf..c85b97f 100644
--- a/templates/manage/apikeys.tmpl
+++ b/templates/manage/apikeys.tmpl
@@ -19,17 +19,17 @@
{{ template "footer.tmpl" . }}
diff --git a/templates/manage/user-edit.tmpl b/templates/manage/user-edit.tmpl
index 16aae52..c71609b 100644
--- a/templates/manage/user-edit.tmpl
+++ b/templates/manage/user-edit.tmpl
@@ -43,18 +43,20 @@
Please provide a valid email address.
+ {{- if or (and .Data.user .Data.user.IsAdmin) (not (usrFinishedSetup .Data.user.LastLogin)) -}}
{{ template "svg-password.tmpl" }}
-
+
{{ template "svg-password.tmpl" }}
-
+
+ {{- end -}}
Username |
Email |
+
Last login |
Created |
Admin |
Active |
@@ -41,6 +42,15 @@
{{- $u.Email -}}
+
+
+ {{ if usrFinishedSetup $u.LastLogin }}
+ {{- $u.LastLogin -}}
+ {{- else -}}
+ never
+ {{- end -}}
+
+ |
{{- $u.CreatedAt -}}
diff --git a/templates/navbar.tmpl b/templates/navbar.tmpl
index 27f6b9b..937b248 100644
--- a/templates/navbar.tmpl
+++ b/templates/navbar.tmpl
@@ -24,6 +24,7 @@
{{ if and .User .User.IsLoggedIn }}
+ {{ if .User.IsAdmin }}
{{- if or (pageIs .Current "manage-users") (pageIs .Current "manage-users-new") (pageIs .Current "manage-users-user-details") (pageIs .Current "manage-users-edit-user") (pageIs .Current "manage-users-delete-user") -}}
@@ -42,7 +43,18 @@
API keys
- {{end}}
+ {{- else -}}
+
+ {{- if or (pageIs .Current "hibp-search") -}}
+
+ {{else}}
+
+ {{end}}
+ Search HIBP
+
+
+ {{- end -}}
+ {{- end -}}
diff --git a/templates/user/hibp-details.tmpl b/templates/user/hibp-details.tmpl
new file mode 100644
index 0000000..fd9682e
--- /dev/null
+++ b/templates/user/hibp-details.tmpl
@@ -0,0 +1,158 @@
+{{ template "head.tmpl" . }}
+
+ {{ template "navbar.tmpl" . }}
+
+
+
+
+ {{if and .Data .Data.hibp -}}
+
+
+ Name:
+
+ {{- .Data.hibp.Name -}}
+
+
+
+ Title:
+
+ {{- .Data.hibp.Title -}}
+
+
+
+ Domain:
+
+ {{- .Data.hibp.Domain -}}
+
+
+
+ Breach date:
+
+ {{- .Data.hibp.BreachDate -}}
+
+
+
+ Added date:
+
+ {{- .Data.hibp.AddedDate -}}
+
+
+
+ Modified date:
+
+ {{- .Data.hibp.ModifiedDate -}}
+
+
+
+ Pwn count:
+
+ {{- .Data.hibp.PwnCount -}}
+
+
+
+ Description:
+
+ {{- htmlRaw (htmlLinkStyle .Data.hibp.Description) -}}
+
+
+
+ Affects:
+
+ {{ $dcs := index .Data.hibp.Dataclasses }}
+ {{ range $_, $dc := $dcs }}
+
+ {{- $dc -}}
+
+ {{end}}
+
+
+
+
+ Verified:
+ {{- if .Data.hibp.IsVerified -}}
+ true
+ {{- else -}}
+ ✗
+ {{- end -}}
+
+ Fabricated:
+ {{- if .Data.hibp.IsFabricated -}}
+ true
+ {{- else -}}
+ ✗
+ {{- end -}}
+
+ Sensitive:
+ {{- if .Data.hibp.IsSensitive -}}
+ true
+ {{- else -}}
+ ✗
+ {{- end -}}
+
+ Retired:
+ {{- if .Data.hibp.IsRetired -}}
+ true
+ {{- else -}}
+ ✗
+ {{- end -}}
+
+ Spam list:
+ {{- if .Data.hibp.IsSpamList -}}
+ true
+ {{- else -}}
+ ✗
+ {{- end -}}
+
+ Malware:
+ {{- if .Data.hibp.IsMalware -}}
+ true
+ {{- else -}}
+ ✗
+ {{- end -}}
+
+
+
+
+ {{- else -}}
+
+
+ No data in the system for this breach
+
+
+ {{- end -}}
+ {{- if and .Data .Data.flash -}}
+
+
+ {{ .Data.flash -}}
+
+
+ {{- end}}
+
+
+{{ template "footer.tmpl" . }}
diff --git a/templates/user/hibp-search.tmpl b/templates/user/hibp-search.tmpl
new file mode 100644
index 0000000..b284b5e
--- /dev/null
+++ b/templates/user/hibp-search.tmpl
@@ -0,0 +1,78 @@
+{{ template "head.tmpl" . }}
+
+ {{ template "navbar.tmpl" . }}
+
+
+
+
+ Breach search
+
+
+
+ {{- if and .Data .Data.flash -}}
+
+
+ {{ .Data.flash -}}
+
+
+ {{- end}}
+
+ {{ if and .Data .Data.breachNames }}
+
+
+
+ The account was found in all of the following breaches:
+
+
+ {{ $bn := index .Data.breachNames }}
+ {{ range $_, $b := $bn }}
+ -
+
+ {{$b.Name}}
+
+
+ {{ end }}
+
+
+ {{end}}
+
+
+
+{{ template "footer.tmpl" . }}
diff --git a/templates/user/init-password-change.tmpl b/templates/user/init-password-change.tmpl
new file mode 100644
index 0000000..77edbe9
--- /dev/null
+++ b/templates/user/init-password-change.tmpl
@@ -0,0 +1,47 @@
+{{ template "head.tmpl" . }}
+
+ {{ template "navbar.tmpl" . }}
+
+
+ {{ if and .Data .Data.flash }}
+
+ {{ .Data.flash }}
+
+ {{- end }}
+ {{ if .User -}}
+
+ Welcome, {{.Name}} , since you have never changed your password here before, now is the time to do that!
+
+
+ This is necessary in order to make sure that only you know the password, since the account was pre-created for you.
+
+
+ {{- else }}
+
+ Please log in.
+
+ {{- end -}}
+
+
+{{ template "footer.tmpl" . }}
|