go: add user onboarding, HIBP search functionality
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
* add user onboarding workflow * fix user editing (no edits of passwords of regular users after onboarding) * refresh HIBP breach cache in DB on app start-up * display HIBP breach details * fix request scheduling to prevent panics (this still needs some love..) * fix middleware auth * add TODOs * update head.tmpl * reword some error messages
This commit is contained in:
parent
dcb3dfdecc
commit
6b45213649
@ -72,6 +72,14 @@ func (a *App) SetupRoutes() error {
|
|||||||
return c.NoContent(http.StatusNotFound)
|
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 := e.Group("/manage", handlers.MiddlewareSession, xsrf)
|
||||||
|
|
||||||
manage.GET("/api-keys", handlers.ManageAPIKeys(), compress)
|
manage.GET("/api-keys", handlers.ManageAPIKeys(), compress)
|
||||||
|
@ -5,6 +5,7 @@ package hibp
|
|||||||
import (
|
import (
|
||||||
"entgo.io/ent/dialect/sql"
|
"entgo.io/ent/dialect/sql"
|
||||||
"entgo.io/ent/dialect/sql/sqlgraph"
|
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -114,6 +115,8 @@ var (
|
|||||||
DefaultIsSpamList bool
|
DefaultIsSpamList bool
|
||||||
// DefaultIsMalware holds the default value on creation for the "is_malware" field.
|
// DefaultIsMalware holds the default value on creation for the "is_malware" field.
|
||||||
DefaultIsMalware bool
|
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.
|
// OrderOption defines the ordering options for the HIBP queries.
|
||||||
|
@ -188,6 +188,14 @@ func (hc *HIBPCreate) SetID(u uuid.UUID) *HIBPCreate {
|
|||||||
return hc
|
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.
|
// SetTrackedBreachesID sets the "tracked_breaches" edge to the TrackedBreaches entity by ID.
|
||||||
func (hc *HIBPCreate) SetTrackedBreachesID(id uuid.UUID) *HIBPCreate {
|
func (hc *HIBPCreate) SetTrackedBreachesID(id uuid.UUID) *HIBPCreate {
|
||||||
hc.mutation.SetTrackedBreachesID(id)
|
hc.mutation.SetTrackedBreachesID(id)
|
||||||
@ -266,6 +274,10 @@ func (hc *HIBPCreate) defaults() {
|
|||||||
v := hibp.DefaultIsMalware
|
v := hibp.DefaultIsMalware
|
||||||
hc.mutation.SetIsMalware(v)
|
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.
|
// check runs all checks and user-defined validators on the builder.
|
||||||
|
@ -69,6 +69,10 @@ func init() {
|
|||||||
hibpDescIsMalware := hibpFields[15].Descriptor()
|
hibpDescIsMalware := hibpFields[15].Descriptor()
|
||||||
// hibp.DefaultIsMalware holds the default value on creation for the is_malware field.
|
// hibp.DefaultIsMalware holds the default value on creation for the is_malware field.
|
||||||
hibp.DefaultIsMalware = hibpDescIsMalware.Default.(bool)
|
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 := schema.LocalBreach{}.Fields()
|
||||||
_ = localbreachFields
|
_ = localbreachFields
|
||||||
// localbreachDescName is the schema descriptor for name field.
|
// localbreachDescName is the schema descriptor for name field.
|
||||||
@ -196,8 +200,6 @@ func init() {
|
|||||||
userDescLastLogin := userFields[8].Descriptor()
|
userDescLastLogin := userFields[8].Descriptor()
|
||||||
// user.DefaultLastLogin holds the default value on creation for the last_login field.
|
// user.DefaultLastLogin holds the default value on creation for the last_login field.
|
||||||
user.DefaultLastLogin = userDescLastLogin.Default.(time.Time)
|
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 is the schema descriptor for id field.
|
||||||
userDescID := userFields[0].Descriptor()
|
userDescID := userFields[0].Descriptor()
|
||||||
// user.DefaultID holds the default value on creation for the id field.
|
// user.DefaultID holds the default value on creation for the id field.
|
||||||
|
@ -60,6 +60,7 @@ type HIBPSchema struct {
|
|||||||
func (HIBP) Fields() []ent.Field {
|
func (HIBP) Fields() []ent.Field {
|
||||||
return []ent.Field{
|
return []ent.Field{
|
||||||
field.UUID("id", uuid.UUID{}).
|
field.UUID("id", uuid.UUID{}).
|
||||||
|
Default(uuid.New).
|
||||||
Unique().
|
Unique().
|
||||||
Immutable(),
|
Immutable(),
|
||||||
// Unique and permanent.
|
// Unique and permanent.
|
||||||
|
@ -55,8 +55,8 @@ func (User) Fields() []ent.Field {
|
|||||||
Default(time.Now).
|
Default(time.Now).
|
||||||
UpdateDefault(time.Now),
|
UpdateDefault(time.Now),
|
||||||
field.Time("last_login").
|
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").
|
// field.Bool("save_search_queries").
|
||||||
// Default(true),
|
// Default(true),
|
||||||
}
|
}
|
||||||
|
@ -104,8 +104,6 @@ var (
|
|||||||
UpdateDefaultUpdatedAt func() time.Time
|
UpdateDefaultUpdatedAt func() time.Time
|
||||||
// DefaultLastLogin holds the default value on creation for the "last_login" field.
|
// DefaultLastLogin holds the default value on creation for the "last_login" field.
|
||||||
DefaultLastLogin time.Time
|
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 holds the default value on creation for the "id" field.
|
||||||
DefaultID func() uuid.UUID
|
DefaultID func() uuid.UUID
|
||||||
)
|
)
|
||||||
|
@ -89,6 +89,14 @@ func (uu *UserUpdate) SetLastLogin(t time.Time) *UserUpdate {
|
|||||||
return uu
|
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.
|
// AddTrackedBreachIDs adds the "tracked_breaches" edge to the TrackedBreaches entity by IDs.
|
||||||
func (uu *UserUpdate) AddTrackedBreachIDs(ids ...uuid.UUID) *UserUpdate {
|
func (uu *UserUpdate) AddTrackedBreachIDs(ids ...uuid.UUID) *UserUpdate {
|
||||||
uu.mutation.AddTrackedBreachIDs(ids...)
|
uu.mutation.AddTrackedBreachIDs(ids...)
|
||||||
@ -200,10 +208,6 @@ func (uu *UserUpdate) defaults() {
|
|||||||
v := user.UpdateDefaultUpdatedAt()
|
v := user.UpdateDefaultUpdatedAt()
|
||||||
uu.mutation.SetUpdatedAt(v)
|
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.
|
// 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
|
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.
|
// AddTrackedBreachIDs adds the "tracked_breaches" edge to the TrackedBreaches entity by IDs.
|
||||||
func (uuo *UserUpdateOne) AddTrackedBreachIDs(ids ...uuid.UUID) *UserUpdateOne {
|
func (uuo *UserUpdateOne) AddTrackedBreachIDs(ids ...uuid.UUID) *UserUpdateOne {
|
||||||
uuo.mutation.AddTrackedBreachIDs(ids...)
|
uuo.mutation.AddTrackedBreachIDs(ids...)
|
||||||
@ -551,10 +563,6 @@ func (uuo *UserUpdateOne) defaults() {
|
|||||||
v := user.UpdateDefaultUpdatedAt()
|
v := user.UpdateDefaultUpdatedAt()
|
||||||
uuo.mutation.SetUpdatedAt(v)
|
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.
|
// check runs all checks and user-defined validators on the builder.
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
moduser "git.dotya.ml/mirre-mt/pcmt/modules/user"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,18 +22,22 @@ func renderErrorPage(c echo.Context, status int, statusText, error string) error
|
|||||||
|
|
||||||
strStatus := strconv.Itoa(status)
|
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(
|
return c.Render(
|
||||||
status,
|
status,
|
||||||
"errorPage.tmpl",
|
"errorPage.tmpl",
|
||||||
page{
|
p,
|
||||||
AppName: setting.AppName(),
|
|
||||||
AppVer: appver,
|
|
||||||
Title: fmt.Sprintf("Error %s - %s", strStatus, statusText),
|
|
||||||
DevelMode: setting.IsDevel(),
|
|
||||||
Current: strStatus,
|
|
||||||
Error: error,
|
|
||||||
Status: strStatus,
|
|
||||||
StatusText: statusText,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,8 @@ func Index() echo.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Healthz() 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 func(c echo.Context) error {
|
||||||
return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
|
return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,8 @@ func Home(client *ent.Client) echo.HandlerFunc {
|
|||||||
u.ID = usr.ID
|
u.ID = usr.ID
|
||||||
u.Username = usr.Username
|
u.Username = usr.Username
|
||||||
u.IsActive = usr.IsActive
|
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
|
u.IsLoggedIn = true
|
||||||
} else {
|
} else {
|
||||||
c.Logger().Error("failed to query usr", username)
|
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)
|
csrf := c.Get("csrf").(string)
|
||||||
p := newPage()
|
p := newPage()
|
||||||
|
|
||||||
@ -76,9 +94,18 @@ func Home(client *ent.Client) echo.HandlerFunc {
|
|||||||
p.Name = username
|
p.Name = username
|
||||||
p.User = u
|
p.User = u
|
||||||
|
|
||||||
err := c.Render(http.StatusOK, "home.tmpl",
|
data := make(map[string]any)
|
||||||
p,
|
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 {
|
if err != nil {
|
||||||
c.Logger().Errorf("error: %q", err)
|
c.Logger().Errorf("error: %q", err)
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ func Logout() echo.HandlerFunc {
|
|||||||
addHeaders(c)
|
addHeaders(c)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case c.Request().Method == "POST":
|
case c.Request().Method == "POST": // nolint:goconst
|
||||||
sess, _ := session.Get(setting.SessionCookieName(), c)
|
sess, _ := session.Get(setting.SessionCookieName(), c)
|
||||||
if sess != nil {
|
if sess != nil {
|
||||||
log.Infof("max-age before logout: %d", sess.Options.MaxAge)
|
log.Infof("max-age before logout: %d", sess.Options.MaxAge)
|
||||||
@ -33,7 +33,7 @@ func Logout() echo.HandlerFunc {
|
|||||||
|
|
||||||
return c.Redirect(http.StatusMovedPermanently, "/logout")
|
return c.Redirect(http.StatusMovedPermanently, "/logout")
|
||||||
|
|
||||||
case c.Request().Method == "GET":
|
case c.Request().Method == "GET": // nolint:goconst
|
||||||
sess, _ := session.Get(setting.SessionCookieName(), c)
|
sess, _ := session.Get(setting.SessionCookieName(), c)
|
||||||
if sess != nil {
|
if sess != nil {
|
||||||
if username := sess.Values["username"]; username != nil {
|
if username := sess.Values["username"]; username != nil {
|
||||||
|
@ -227,7 +227,7 @@ func CreateUser() echo.HandlerFunc { //nolint:gocognit
|
|||||||
|
|
||||||
usr, err := moduser.CreateUser(ctx, dbclient, uc.Email, uc.Username, uc.Password, uc.IsAdmin)
|
usr, err := moduser.CreateUser(ctx, dbclient, uc.Email, uc.Username, uc.Password, uc.IsAdmin)
|
||||||
if err == nil && usr != nil {
|
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 {
|
if sess, ok := c.Get("sess").(*sessions.Session); ok {
|
||||||
sess.Values["flash"] = msg
|
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)
|
ctx, ok := c.Get("sloggerCtx").(context.Context)
|
||||||
if !ok {
|
if !ok {
|
||||||
ctx = context.WithValue(context.Background(), moduser.CtxKey{}, slogger)
|
ctx = context.WithValue(context.Background(), moduser.CtxKey{}, slogger)
|
||||||
@ -303,7 +313,7 @@ func ViewUser() echo.HandlerFunc {
|
|||||||
if errors.Is(err, moduser.ErrUserNotFound) { //nolint:gocritic
|
if errors.Is(err, moduser.ErrUserNotFound) { //nolint:gocritic
|
||||||
c.Logger().Errorf("user not found by ID: '%s'", uid.ID)
|
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
|
p.Data = data
|
||||||
|
|
||||||
return c.Render(
|
return c.Render(
|
||||||
@ -314,7 +324,7 @@ func ViewUser() echo.HandlerFunc {
|
|||||||
} else if errors.Is(err, moduser.ErrFailedToQueryUser) {
|
} else if errors.Is(err, moduser.ErrFailedToQueryUser) {
|
||||||
c.Logger().Errorf("failed to query user by ID: '%s'", uid.ID)
|
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
|
p.Data = data
|
||||||
|
|
||||||
return c.Render(
|
return c.Render(
|
||||||
@ -325,7 +335,7 @@ func ViewUser() echo.HandlerFunc {
|
|||||||
} else if errors.Is(err, moduser.ErrBadUUID) {
|
} else if errors.Is(err, moduser.ErrBadUUID) {
|
||||||
c.Logger().Errorf("Invalid UUID '%s': %q", uid.ID, err)
|
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
|
p.Data = data
|
||||||
|
|
||||||
return c.Render(
|
return c.Render(
|
||||||
@ -346,6 +356,16 @@ func ViewUser() echo.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data["user"] = usr
|
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
|
p.Data = data
|
||||||
|
|
||||||
return c.Render(
|
return c.Render(
|
||||||
@ -567,6 +587,33 @@ func UpdateUser() echo.HandlerFunc { //nolint:gocognit
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
if uu.Username == "" || uu.Password == "" || uu.RepeatPassword == "" || uu.Password != uu.RepeatPassword {
|
||||||
c.Logger().Error("username or password not set, returning to /manage/users/edit")
|
c.Logger().Error("username or password not set, returning to /manage/users/edit")
|
||||||
|
|
||||||
@ -586,6 +633,7 @@ func UpdateUser() echo.HandlerFunc { //nolint:gocognit
|
|||||||
p,
|
p,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if usr.Username != uu.Username {
|
if usr.Username != uu.Username {
|
||||||
exists, err := moduser.UsernameExists(ctx, dbclient, uu.Username)
|
exists, err := moduser.UsernameExists(ctx, dbclient, uu.Username)
|
||||||
@ -640,11 +688,28 @@ func UpdateUser() echo.HandlerFunc { //nolint:gocognit
|
|||||||
}
|
}
|
||||||
|
|
||||||
// now update
|
// 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
|
data["user"] = usr
|
||||||
p.Data = data
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,12 +22,13 @@ func MiddlewareSession(next echo.HandlerFunc) echo.HandlerFunc {
|
|||||||
|
|
||||||
var username string
|
var username string
|
||||||
|
|
||||||
uname, ok := sess.Values["username"].(string)
|
// uname, ok := sess.Values["username"].(string)
|
||||||
|
uname := sess.Values["username"]
|
||||||
|
|
||||||
if ok {
|
if uname != nil {
|
||||||
username = uname
|
username = uname.(string)
|
||||||
|
|
||||||
log.Info("gorilla session", "username", username)
|
log.Debug("Refreshing session cookie", "username", username, "module", "middleware")
|
||||||
|
|
||||||
refreshSession(
|
refreshSession(
|
||||||
sess,
|
sess,
|
||||||
@ -38,17 +39,6 @@ func MiddlewareSession(next echo.HandlerFunc) echo.HandlerFunc {
|
|||||||
http.SameSiteStrictMode,
|
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)
|
c.Set("sess", sess)
|
||||||
|
|
||||||
var u moduser.User
|
var u moduser.User
|
||||||
@ -75,19 +65,35 @@ func MiddlewareSession(next echo.HandlerFunc) echo.HandlerFunc {
|
|||||||
c.Set("sloggerCtx", ctx)
|
c.Set("sloggerCtx", ctx)
|
||||||
c.Set("sessUsr", u)
|
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)
|
return next(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !sess.IsNew {
|
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(
|
// return renderErrorPage(
|
||||||
c,
|
// c,
|
||||||
http.StatusUnauthorized,
|
// http.StatusUnauthorized,
|
||||||
http.StatusText(http.StatusUnauthorized),
|
// http.StatusText(http.StatusUnauthorized),
|
||||||
ErrNoSession.Error(),
|
// ErrNoSession.Error(),
|
||||||
)
|
// )
|
||||||
|
|
||||||
|
c.Logger().Warn("Could not get username from the cookie")
|
||||||
|
|
||||||
|
return next(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
174
handlers/search-hibp.go
Normal file
174
handlers/search-hibp.go
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
// Copyright 2023 wanderer <a_mirre at utb dot cz>
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
@ -67,7 +67,7 @@ func SigninPost(client *ent.Client) echo.HandlerFunc {
|
|||||||
p.CSRF = csrf
|
p.CSRF = csrf
|
||||||
|
|
||||||
if username == "" || password == "" {
|
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["flash"] = "you need to set both the username and the password"
|
||||||
data["form"] = cu
|
data["form"] = cu
|
||||||
@ -134,6 +134,9 @@ func SigninPost(client *ent.Client) echo.HandlerFunc {
|
|||||||
SameSite: http.SameSiteStrictMode,
|
SameSite: http.SameSiteStrictMode,
|
||||||
}
|
}
|
||||||
sess.Values["foo"] = "bar"
|
sess.Values["foo"] = "bar"
|
||||||
|
|
||||||
|
c.Logger().Debug("saving username to the session cookie")
|
||||||
|
|
||||||
sess.Values["username"] = username
|
sess.Values["username"] = username
|
||||||
|
|
||||||
err := sess.Save(c.Request(), c.Response())
|
err := sess.Save(c.Request(), c.Response())
|
||||||
|
@ -14,11 +14,13 @@ type userSignup struct {
|
|||||||
Password string `form:"password" json:"password" validate:"required,password"`
|
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 {
|
type userCreate struct {
|
||||||
Username string `form:"username" json:"username" validate:"required,username"`
|
Username string `form:"username" json:"username" validate:"required,username"`
|
||||||
Email string `form:"email" json:"email" validate:"required,email"`
|
Email string `form:"email" json:"email" validate:"required,email"`
|
||||||
Password string `form:"password" json:"password" validate:"required,password"`
|
Password string `form:"password" json:"password" validate:"omitempty,password"`
|
||||||
RepeatPassword string `form:"repeatPassword" json:"repeatPassword" validate:"required,repeatPassword"`
|
RepeatPassword string `form:"repeatPassword" json:"repeatPassword" validate:"omitempty,repeatPassword"`
|
||||||
IsAdmin bool `form:"isAdmin" json:"isAdmin" validate:"required,isAdmin"`
|
IsAdmin bool `form:"isAdmin" json:"isAdmin" validate:"required,isAdmin"`
|
||||||
IsActive *bool `form:"isActive" json:"isActive" validate:"omitempty,isActive"`
|
IsActive *bool `form:"isActive" json:"isActive" validate:"omitempty,isActive"`
|
||||||
}
|
}
|
||||||
@ -26,3 +28,15 @@ type userCreate struct {
|
|||||||
type userID struct {
|
type userID struct {
|
||||||
ID string `param:"id" validate:"required,id"`
|
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"`
|
||||||
|
}
|
||||||
|
129
handlers/user-init-password-change.go
Normal file
129
handlers/user-init-password-change.go
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
// Copyright 2023 wanderer <a_mirre at utb dot cz>
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
81
handlers/view-hibp.go
Normal file
81
handlers/view-hibp.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// Copyright 2023 wanderer <a_mirre at utb dot cz>
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
2
justfile
2
justfile
@ -1,6 +1,6 @@
|
|||||||
cmd := "podman"
|
cmd := "podman"
|
||||||
cfile := "Containerfile"
|
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
|
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
|
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
|
kanikoargs := "run -it -w=" + kanikowdir + " -v $(pwd):" + kanikowdir + ":z " + kanikoexecutorimg + " -f=" + cfile + " -c=" + kanikocontext + " --use-new-run --snapshotMode=redo --no-push " + buildargs
|
||||||
|
@ -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
|
// has not been set up prior, it creates and admin user with the initPasswd and
|
||||||
// the default username and email (see ../user/const.go).
|
// the default username and email (see ../user/const.go).
|
||||||
func SetUp(ctx context.Context, client *ent.Client, createAdmin bool, initPasswd string) error {
|
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 {
|
if err := client.Schema.Create(ctx); err != nil {
|
||||||
return fmt.Errorf("failed to create schema resources: %v", err)
|
return fmt.Errorf("failed to create schema resources: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ package funcmap
|
|||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
modbluemonday "git.dotya.ml/mirre-mt/pcmt/modules/bluemonday"
|
modbluemonday "git.dotya.ml/mirre-mt/pcmt/modules/bluemonday"
|
||||||
)
|
)
|
||||||
@ -24,6 +26,15 @@ func FuncMap() template.FuncMap {
|
|||||||
modbluemonday.Policy.Sanitize(html),
|
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, "<a ", `<span><a class="w-auto py-1 mt-2 text-center text-blue-500 md:mt-0 mx-0 hover:underline dark:text-blue-400"`)
|
||||||
|
return strings.ReplaceAll(value, "</a>", `</a></span>`)
|
||||||
|
},
|
||||||
"pageIs": func(want, got string) bool {
|
"pageIs": func(want, got string) bool {
|
||||||
return want == got
|
return want == got
|
||||||
},
|
},
|
||||||
@ -57,6 +68,9 @@ func FuncMap() template.FuncMap {
|
|||||||
|
|
||||||
return *r
|
return *r
|
||||||
},
|
},
|
||||||
|
"usrFinishedSetup": func(lastLogin time.Time) bool {
|
||||||
|
return lastLogin.After(time.Unix(0, 0))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
@ -57,6 +56,9 @@ const (
|
|||||||
var (
|
var (
|
||||||
apiKey = os.Getenv("PCMT_HIBP_API_KEY")
|
apiKey = os.Getenv("PCMT_HIBP_API_KEY")
|
||||||
client = &http.Client{Timeout: reqTmOut}
|
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.
|
// SubscriptionStatus models https://haveibeenpwned.com/API/v3#SubscriptionStatus.
|
||||||
@ -65,7 +67,7 @@ func SubscriptionStatus() (*Subscription, error) {
|
|||||||
|
|
||||||
req, err := http.NewRequest("GET", u, nil)
|
req, err := http.NewRequest("GET", u, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Error("Could not create a new HTTP request", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
setUA(req)
|
setUA(req)
|
||||||
@ -100,7 +102,8 @@ func GetAllBreaches() (*[]schema.HIBPSchema, error) {
|
|||||||
|
|
||||||
req, err := http.NewRequest("GET", u, nil)
|
req, err := http.NewRequest("GET", u, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Error("Could not create a new HTTP request", "error", err)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
respCh, errCh := rChans()
|
respCh, errCh := rChans()
|
||||||
@ -140,7 +143,8 @@ func GetAllBreachesForAccount(account string) ([]BreachName, error) {
|
|||||||
|
|
||||||
req, err := http.NewRequest("GET", u, nil)
|
req, err := http.NewRequest("GET", u, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Error("Could not create a new HTTP request", "error", err)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
respCh, errCh := rChans()
|
respCh, errCh := rChans()
|
||||||
@ -187,13 +191,6 @@ func GetAllBreachesForAccount(account string) ([]BreachName, error) {
|
|||||||
// GetBreachesForBreachNames retrieves HIBP breaches from the database for a
|
// GetBreachesForBreachNames retrieves HIBP breaches from the database for a
|
||||||
// list of names.
|
// list of names.
|
||||||
func GetBreachesForBreachNames(ctx context.Context, client *ent.Client, names []string) ([]*ent.HIBP, error) {
|
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)
|
hs := make([]*ent.HIBP, 0)
|
||||||
|
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
@ -204,11 +201,11 @@ func GetBreachesForBreachNames(ctx context.Context, client *ent.Client, names []
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case ent.IsNotFound(err):
|
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
|
return nil, ErrBreachNotFound
|
||||||
|
|
||||||
case ent.IsNotSingular(err):
|
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
|
return nil, ErrBreachNotSingular
|
||||||
|
|
||||||
case err != nil:
|
case err != nil:
|
||||||
@ -226,6 +223,39 @@ func GetBreachesForBreachNames(ctx context.Context, client *ent.Client, names []
|
|||||||
return hs, nil
|
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.
|
// SaveAllBreaches saves all breaches to DB as a cache.
|
||||||
func SaveAllBreaches(ctx context.Context, client *ent.Client, breaches *[]schema.HIBPSchema) error {
|
func SaveAllBreaches(ctx context.Context, client *ent.Client, breaches *[]schema.HIBPSchema) error {
|
||||||
slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
|
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")),
|
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
|
return ErrNoBreachesToSave
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Infof("HIBP API returned %d breaches, saving...", len(*breaches))
|
||||||
|
|
||||||
for _, b := range *breaches {
|
for _, b := range *breaches {
|
||||||
_, err := client.HIBP.
|
_, err := client.HIBP.
|
||||||
Create().
|
Create().
|
||||||
@ -260,6 +293,7 @@ func SaveAllBreaches(ctx context.Context, client *ent.Client, breaches *[]schema
|
|||||||
SetLogoPath(b.LogoPath).
|
SetLogoPath(b.LogoPath).
|
||||||
Save(ctx)
|
Save(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf("Could not save HIBP breaches to DB, err: %q", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -267,6 +301,41 @@ func SaveAllBreaches(ctx context.Context, client *ent.Client, breaches *[]schema
|
|||||||
return nil
|
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) {
|
func setUA(r *http.Request) {
|
||||||
r.Header.Set(headerUA, appID)
|
r.Header.Set(headerUA, appID)
|
||||||
}
|
}
|
||||||
|
@ -215,11 +215,12 @@ func doReqs(ok chan error, wg *sync.WaitGroup) {
|
|||||||
nxckpLock.Lock()
|
nxckpLock.Lock()
|
||||||
|
|
||||||
timeToWait = time.Until(nextCheckpoint)
|
timeToWait = time.Until(nextCheckpoint)
|
||||||
|
time.Sleep(timeToWait) // yes, sleep while locked.
|
||||||
|
|
||||||
ttwLock.Unlock()
|
ttwLock.Unlock()
|
||||||
nxckpLock.Unlock()
|
nxckpLock.Unlock()
|
||||||
|
|
||||||
break
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -311,7 +312,6 @@ func zeroQueue() bool {
|
|||||||
defer rqqLock.RUnlock()
|
defer rqqLock.RUnlock()
|
||||||
|
|
||||||
if len(queue.Requests) == 0 {
|
if len(queue.Requests) == 0 {
|
||||||
slog.Debug("Queue empty")
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ type User struct {
|
|||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
IsLoggedIn bool
|
IsLoggedIn bool
|
||||||
|
LastLogin time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser adds a user entry to the database.
|
// 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
|
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.
|
// DeleteUserByID returns nil on successful deletion, err otherwise.
|
||||||
func DeleteUserByID(ctx context.Context, client *ent.Client, strID string) error {
|
func DeleteUserByID(ctx context.Context, client *ent.Client, strID string) error {
|
||||||
slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
|
slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
|
||||||
|
18
run.go
18
run.go
@ -189,7 +189,7 @@ func run() error { //nolint:gocognit
|
|||||||
ctx := context.WithValue(context.Background(), moddb.CtxKey{}, slogger)
|
ctx := context.WithValue(context.Background(), moddb.CtxKey{}, slogger)
|
||||||
|
|
||||||
if *printMigrationFlag {
|
if *printMigrationFlag {
|
||||||
log.Debug("printing the following migration to stdout")
|
log.Debug("printing the upcoming migration to stdout")
|
||||||
return moddb.PrintMigration(ctx, db)
|
return moddb.PrintMigration(ctx, db)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,9 +276,23 @@ func run() error { //nolint:gocognit
|
|||||||
|
|
||||||
handleSigs(schedQuit, quit)
|
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
|
wg.Add(1) // nolint:staticcheck
|
||||||
go hibp.RunReqScheduler(schedQuit, errCh, wg) // nolint:wsl
|
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.
|
// non-blocking channel receive.
|
||||||
|
@ -15,14 +15,16 @@
|
|||||||
<meta property="og:site_name" content="{{ .AppName }}">
|
<meta property="og:site_name" content="{{ .AppName }}">
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta name="referrer" content="no-referrer, strict-origin-when-cross-origin">
|
<!-- <meta name="referrer" content="no-referrer, strict-origin-when-cross-origin"> -->
|
||||||
|
<meta name="referrer" content="strict-origin-when-cross-origin">
|
||||||
|
|
||||||
<link rel="icon" href="/assets/img/logo-pcmt.svg" type="image/svg+xml">
|
<link rel="icon" href="/assets/img/logo-pcmt.svg" type="image/svg+xml">
|
||||||
<link href="/assets/css/pcmt.css" rel="preload" as="style" integrity="{{- sha384 "css/pcmt.css" -}}">
|
<link href="/assets/css/pcmt.css" rel="preload" as="style" integrity="{{- sha384 "css/pcmt.css" -}}">
|
||||||
<link href="/assets/css/pcmt.css" rel="stylesheet" integrity="{{- sha384 "css/pcmt.css" -}}">
|
<link href="/assets/css/pcmt.css" rel="stylesheet" integrity="{{- sha384 "css/pcmt.css" -}}">
|
||||||
|
|
||||||
{{- if .DevelMode -}}
|
{{- if .DevelMode -}}
|
||||||
<meta http-equiv="content-security-policy" content="upgrade-insecure-requests; default-src 'self'; connect-src 'self' ws://localhost:3002 http://localhost:3002; script-src 'self' http://localhost:3002;"/>
|
<!-- <meta http-equiv="content-security-policy" content="upgrade-insecure-requests; default-src 'self'; connect-src 'self' ws://localhost:3002 http://localhost:3002; script-src 'self' http://localhost:3002; style-src 'self' 'unsafe-inline';"/> -->
|
||||||
|
<meta http-equiv="content-security-policy" content="default-src 'self'; connect-src 'self' ws://localhost:3002 http://localhost:3002; script-src 'self' http://localhost:3002;"/>
|
||||||
<!-- inject browsersync script if running in devel mode -->
|
<!-- inject browsersync script if running in devel mode -->
|
||||||
{{ template "browsersync.tmpl" }}
|
{{ template "browsersync.tmpl" }}
|
||||||
{{ else }}
|
{{ else }}
|
||||||
|
@ -3,6 +3,11 @@
|
|||||||
{{ template "navbar.tmpl" . }}
|
{{ template "navbar.tmpl" . }}
|
||||||
<main class="grow">
|
<main class="grow">
|
||||||
<div class="px-2 md:px-0 place-items-center text-center">
|
<div class="px-2 md:px-0 place-items-center text-center">
|
||||||
|
{{ if and .Data .Data.flash }}
|
||||||
|
<h1 class="text-xl text-pink-600 dark:text-pink-500 py-2">
|
||||||
|
{{ .Data.flash }}
|
||||||
|
</h1>
|
||||||
|
{{- end }}
|
||||||
{{ if .Name -}}
|
{{ if .Name -}}
|
||||||
<h1 class="mt-20 text-2xl text-pink-400 font-bold">
|
<h1 class="mt-20 text-2xl text-pink-400 font-bold">
|
||||||
Welcome, <code>{{.Name}}</code>!<br>
|
Welcome, <code>{{.Name}}</code>!<br>
|
||||||
|
@ -19,17 +19,17 @@
|
|||||||
<form method="post" class="w-full lg:max-w-7xl px-2">
|
<form method="post" class="w-full lg:max-w-7xl px-2">
|
||||||
<input type="hidden" name="csrf" value="{{- .CSRF -}}">
|
<input type="hidden" name="csrf" value="{{- .CSRF -}}">
|
||||||
<div class="items-center">
|
<div class="items-center">
|
||||||
<label for="hibp-api-key" class="text-purple-500 font-bold">
|
<label for="hibpApiKey" class="text-purple-500 font-bold">
|
||||||
Have I Been Pwned?
|
Have I Been Pwned?
|
||||||
</label>
|
</label>
|
||||||
<input name="hibp-api-key" type="text" placeholder="HIBP API key" {{if and .Data.form .Data.form.HibpAPIKey}}value="{{.Data.form.HibpAPIKey}}"{{end}} autofocus class="py-3 px-8 mt-2 w-full valid:border text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:valid:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
<input name="hibpApiKey" type="text" placeholder="HIBP API key" {{if and .Data .Data.hibpApiKey}}value="{{.Data.hibpApiKey}}"{{end}} autofocus class="py-3 px-8 mt-2 w-full valid:border text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:valid:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||||
<p id="hibp-expl" class="hover:cursor-help mt-2 text-sm text-gray-500 dark:text-gray-400">Adds support for querying Troy Hunt's <a href="https://haveibeenpwned.com/API/v3" class="font-medium text-blue-600 hover:underline dark:text-blue-500">Have I Been Pwned? API</a>.</p>
|
<p id="hibp-expl" class="hover:cursor-help mt-2 text-sm text-gray-500 dark:text-gray-400">Adds support for querying Troy Hunt's <a href="https://haveibeenpwned.com/API/v3" class="font-medium text-blue-600 hover:underline dark:text-blue-500">Have I Been Pwned? API</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 block md:flex md:items-center place-items-end space-x-0 md:space-x-2">
|
<div class="mt-4 block md:flex md:items-center place-items-end space-x-0 md:space-x-2">
|
||||||
<button class="px-2 py-3 w-full md:w-auto text-sm font-medium tracking-wide text-white transition-colors duration-300 transform bg-blue-500 rounded-lg hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-50">
|
<button class="px-2 py-3 w-full md:w-auto text-sm font-medium tracking-wide text-white transition-colors duration-300 transform bg-blue-500 rounded-lg hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-50">
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
<button {{if and (not .Data.form) (not .Data.form.HubpAPIKey)}}disabled class="cursor-not-allowed{{else}}class="transition-colors duration-300 transform hover:bg-gray-400 focus:outline-none focus:ring focus:ring-gray-300 focus:ring-opacity-50{{end}} px-2 py-3 mt-2 md:mt-0 w-full md:w-auto text-sm font-medium tracking-wide text-white bg-gray-500 rounded-lg">
|
<button {{if and (not .Data) (not .Data.hibpApiKey)}}disabled class="cursor-not-allowed{{else}}class="transition-colors duration-300 transform hover:bg-gray-400 focus:outline-none focus:ring focus:ring-gray-300 focus:ring-opacity-50{{end}} px-2 py-3 mt-2 md:mt-0 w-full md:w-auto text-sm font-medium tracking-wide text-white bg-gray-500 rounded-lg">
|
||||||
Test key
|
Test key
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,11 @@
|
|||||||
{{ template "navbar.tmpl" . }}
|
{{ template "navbar.tmpl" . }}
|
||||||
<main class="grow">
|
<main class="grow">
|
||||||
<div class="container mx-auto place-items-center px-8 md:px-12 lg:px-14">
|
<div class="container mx-auto place-items-center px-8 md:px-12 lg:px-14">
|
||||||
|
{{ if and .Data .Data.flash }}
|
||||||
|
<h1 class="text-xl text-pink-600 dark:text-pink-500 py-2">
|
||||||
|
{{ .Data.flash }}
|
||||||
|
</h1>
|
||||||
|
{{- end }}
|
||||||
<div class="flex justify-between place-items-center">
|
<div class="flex justify-between place-items-center">
|
||||||
<h1 class="text-xl font-bold text-fuchsia-600 dark:text-fuchsia-400 capitalize py-2">
|
<h1 class="text-xl font-bold text-fuchsia-600 dark:text-fuchsia-400 capitalize py-2">
|
||||||
User details
|
User details
|
||||||
@ -51,15 +56,30 @@
|
|||||||
{{- .Data.user.IsActive -}}
|
{{- .Data.user.IsActive -}}
|
||||||
</h3>
|
</h3>
|
||||||
</div><!-- isActive -->
|
</div><!-- isActive -->
|
||||||
|
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Last login:</span>
|
||||||
|
<h3 class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||||
|
{{ if usrFinishedSetup .Data.user.LastLogin }}
|
||||||
|
{{- .Data.user.LastLogin -}}
|
||||||
|
{{- else -}}
|
||||||
|
never
|
||||||
|
{{- end -}}
|
||||||
|
</h3>
|
||||||
|
</div><!-- updated -->
|
||||||
|
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Created:</span>
|
||||||
|
<h3 class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||||
|
{{- .Data.user.CreatedAt -}}
|
||||||
|
</h3>
|
||||||
|
</div><!-- created -->
|
||||||
|
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Updated:</span>
|
||||||
|
<h3 class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||||
|
{{- .Data.user.UpdatedAt -}}
|
||||||
|
</h3>
|
||||||
|
</div><!-- updated -->
|
||||||
</div>
|
</div>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- if and .Data .Data.flash -}}
|
|
||||||
<div class="place-items-center">
|
|
||||||
<h1 class="text-2xl text-pink-600 dark:text-pink-500 py-2">
|
|
||||||
{{ .Data.flash -}}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
{{- end}}
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{{ template "footer.tmpl" . }}
|
{{ template "footer.tmpl" . }}
|
||||||
|
@ -43,18 +43,20 @@
|
|||||||
Please provide a valid email address.
|
Please provide a valid email address.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{{- if or (and .Data.user .Data.user.IsAdmin) (not (usrFinishedSetup .Data.user.LastLogin)) -}}
|
||||||
<div class="relative flex items-center mt-4">
|
<div class="relative flex items-center mt-4">
|
||||||
<span class="absolute" role="img" aria-label="password lock icon">
|
<span class="absolute" role="img" aria-label="password lock icon">
|
||||||
{{ template "svg-password.tmpl" }}
|
{{ template "svg-password.tmpl" }}
|
||||||
</span>
|
</span>
|
||||||
<input name="password" type="password" {{if and .Data.user .Data.user.Password}}value="{{.Data.user.Password}}"{{end}} placeholder="Password" required class="block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
<input name="password" type="password" value="" placeholder="Password" required class="block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||||
</div>
|
</div>
|
||||||
<div class="relative flex items-center mt-4">
|
<div class="relative flex items-center mt-4">
|
||||||
<span class="absolute" role="img" aria-label="password lock icon">
|
<span class="absolute" role="img" aria-label="password lock icon">
|
||||||
{{ template "svg-password.tmpl" }}
|
{{ template "svg-password.tmpl" }}
|
||||||
</span>
|
</span>
|
||||||
<input name="repeatPassword" type="password" {{if and .Data.user .Data.user.Password}}value="{{.Data.user.Password}}"{{end}} placeholder="Repeat Password" required class="block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
<input name="repeatPassword" type="password" value="" placeholder="Repeat Password" required class="block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||||
</div>
|
</div>
|
||||||
|
{{- end -}}
|
||||||
<div class="flex pt-2 px-2 items-center justify-center gap-6">
|
<div class="flex pt-2 px-2 items-center justify-center gap-6">
|
||||||
<div class="mb-1 block min-h-3">
|
<div class="mb-1 block min-h-3">
|
||||||
<input
|
<input
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="md:p-2 sm:p-0 text-slate-600 dark:text-slate-400">Username</th>
|
<th scope="col" class="md:p-2 sm:p-0 text-slate-600 dark:text-slate-400">Username</th>
|
||||||
<th scope="col" class="md:p-2 sm:p-0 text-left text-slate-600 dark:text-slate-400">Email</th>
|
<th scope="col" class="md:p-2 sm:p-0 text-left text-slate-600 dark:text-slate-400">Email</th>
|
||||||
|
<th scope="col" class="md:p-2 sm:p-0 text-left px-2 text-slate-600 dark:text-slate-400">Last login</th>
|
||||||
<th scope="col" class="md:p-2 sm:p-0 text-left px-2 text-slate-600 dark:text-slate-400">Created</th>
|
<th scope="col" class="md:p-2 sm:p-0 text-left px-2 text-slate-600 dark:text-slate-400">Created</th>
|
||||||
<th scope="col" class="md:p-2 sm:p-0 sm:px-1 text-slate-600 dark:text-slate-400">Admin</th>
|
<th scope="col" class="md:p-2 sm:p-0 sm:px-1 text-slate-600 dark:text-slate-400">Admin</th>
|
||||||
<th scope="col" class="md:p-2 sm:p-0 text-slate-600 dark:text-slate-400">Active</th>
|
<th scope="col" class="md:p-2 sm:p-0 text-slate-600 dark:text-slate-400">Active</th>
|
||||||
@ -41,6 +42,15 @@
|
|||||||
{{- $u.Email -}}
|
{{- $u.Email -}}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="text-left">
|
||||||
|
<span class="p-2 font-mono select-all">
|
||||||
|
{{ if usrFinishedSetup $u.LastLogin }}
|
||||||
|
{{- $u.LastLogin -}}
|
||||||
|
{{- else -}}
|
||||||
|
never
|
||||||
|
{{- end -}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td class="text-left">
|
<td class="text-left">
|
||||||
<span class="p-2 text-sm mx-auto font-mono">
|
<span class="p-2 text-sm mx-auto font-mono">
|
||||||
{{- $u.CreatedAt -}}
|
{{- $u.CreatedAt -}}
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{{ if and .User .User.IsLoggedIn }}
|
{{ if and .User .User.IsLoggedIn }}
|
||||||
|
{{ if .User.IsAdmin }}
|
||||||
<li>
|
<li>
|
||||||
{{- 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") -}}
|
{{- 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") -}}
|
||||||
<a href="/manage/users" class="block py-2 pl-3 pr-4 text-white bg-blue-500 rounded md:bg-transparent md:text-blue-700 md:p-0 md:dark:text-blue-500 dark:bg-blue-500 md:dark:bg-transparent" aria-current="page">
|
<a href="/manage/users" class="block py-2 pl-3 pr-4 text-white bg-blue-500 rounded md:bg-transparent md:text-blue-700 md:p-0 md:dark:text-blue-500 dark:bg-blue-500 md:dark:bg-transparent" aria-current="page">
|
||||||
@ -42,7 +43,18 @@
|
|||||||
API keys
|
API keys
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{{- else -}}
|
||||||
|
<li>
|
||||||
|
{{- if or (pageIs .Current "hibp-search") -}}
|
||||||
|
<a href="/user/hibp-search" class="block py-2 pl-3 pr-4 text-white bg-blue-500 rounded md:bg-transparent md:text-blue-700 md:p-0 md:dark:text-blue-500 dark:bg-blue-500 md:dark:bg-transparent" aria-current="page">
|
||||||
|
{{else}}
|
||||||
|
<a href="/user/hibp-search" class="block py-2 pl-3 pr-4 text-gray-900 rounded hover:bg-gray-300 md:hover:bg-transparent md:border-0 md:hover:text-blue-500 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">
|
||||||
{{end}}
|
{{end}}
|
||||||
|
Search HIBP
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
<li>
|
<li>
|
||||||
<a href="https://git.dotya.ml/mirre-mt/masters-thesis/" target="_blank" rel="noopener"
|
<a href="https://git.dotya.ml/mirre-mt/masters-thesis/" target="_blank" rel="noopener"
|
||||||
class="block py-2 pl-3 pr-4 text-gray-900 rounded hover:bg-gray-300 md:hover:bg-transparent md:border-0 md:hover:text-blue-500 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">
|
class="block py-2 pl-3 pr-4 text-gray-900 rounded hover:bg-gray-300 md:hover:bg-transparent md:border-0 md:hover:text-blue-500 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">
|
||||||
|
158
templates/user/hibp-details.tmpl
Normal file
158
templates/user/hibp-details.tmpl
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
{{ template "head.tmpl" . }}
|
||||||
|
<body class="h-screen bg-white dark:bg-gray-900">
|
||||||
|
{{ template "navbar.tmpl" . }}
|
||||||
|
<main class="grow">
|
||||||
|
<div class="container mx-auto place-items-center px-8 md:px-12 lg:px-14">
|
||||||
|
<div class="flex justify-between place-items-center">
|
||||||
|
<h1 class="text-xl font-bold text-fuchsia-600 dark:text-fuchsia-400 capitalize py-2">
|
||||||
|
Breach details
|
||||||
|
</h1>
|
||||||
|
<a href="/user/hibp-search" class="w-auto py-1 mt-2 text-center text-blue-500 md:mt-0 md:mx-6 lg:mx-4 hover:underline dark:text-blue-400">
|
||||||
|
⏎ Breach search
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between place-items-center">
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-300 hover:cursor-help">
|
||||||
|
<span class="text-lg text-pink-500 dark:text-pink-300">
|
||||||
|
🛈
|
||||||
|
</span>
|
||||||
|
Breach data on this page is sourced from
|
||||||
|
<a href="https://haveibeenpwned.com" target="_blank" rel="noopener"
|
||||||
|
class="w-auto py-1 mt-2 text-center text-blue-500 md:mt-0 mx-0 hover:underline dark:text-blue-400">
|
||||||
|
Have I Been Pwned?</a>
|
||||||
|
and is
|
||||||
|
<a href="https://haveibeenpwned.com/API/v3#License" target="_blank" rel="noopener"
|
||||||
|
class="w-auto py-1 mt-2 text-center text-blue-500 md:mt-0 mx-0 hover:underline dark:text-blue-400">
|
||||||
|
available</a>
|
||||||
|
under
|
||||||
|
<a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener"
|
||||||
|
class="w-auto py-1 mt-2 text-center text-blue-500 md:mt-0 mx-0 hover:underline dark:text-blue-400">
|
||||||
|
CC-BY-4.0
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{if and .Data .Data.hibp -}}
|
||||||
|
<div class="p-2 mt-3 lg:mx-auto border-2 dark:border-slate-500 rounded-sm space-y-0">
|
||||||
|
<div class="flex max-h-14 place-items-baseline justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Name:</span>
|
||||||
|
<span class="text-md text-fuchsia-500 dark:text-fuchsia-400 px-2 overflow-x-auto text-ellipsis select-all">
|
||||||
|
{{- .Data.hibp.Name -}}
|
||||||
|
</span>
|
||||||
|
</div><!-- name -->
|
||||||
|
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Title:</span>
|
||||||
|
<span class="text-md text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||||
|
{{- .Data.hibp.Title -}}
|
||||||
|
</span>
|
||||||
|
</div><!-- title -->
|
||||||
|
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Domain:</span>
|
||||||
|
<span class="text-md text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||||
|
{{- .Data.hibp.Domain -}}
|
||||||
|
</span>
|
||||||
|
</div><!-- domain -->
|
||||||
|
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Breach date:</span>
|
||||||
|
<span class="text-md text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||||
|
{{- .Data.hibp.BreachDate -}}
|
||||||
|
</span>
|
||||||
|
</div><!-- breachDate -->
|
||||||
|
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Added date:</span>
|
||||||
|
<span class="text-md text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||||
|
{{- .Data.hibp.AddedDate -}}
|
||||||
|
</span>
|
||||||
|
</div><!-- addedDate -->
|
||||||
|
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Modified date:</span>
|
||||||
|
<span class="text-md text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||||
|
{{- .Data.hibp.ModifiedDate -}}
|
||||||
|
</span>
|
||||||
|
</div><!-- modifiedDate -->
|
||||||
|
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Pwn count:</span>
|
||||||
|
<span class="text-md text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||||
|
{{- .Data.hibp.PwnCount -}}
|
||||||
|
</span>
|
||||||
|
</div><!-- pwnCount -->
|
||||||
|
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Description:</span>
|
||||||
|
<span class="text-md italic text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||||
|
{{- htmlRaw (htmlLinkStyle .Data.hibp.Description) -}}
|
||||||
|
</span>
|
||||||
|
</div><!-- description -->
|
||||||
|
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Affects:</span>
|
||||||
|
<div class="justify-center">
|
||||||
|
{{ $dcs := index .Data.hibp.Dataclasses }}
|
||||||
|
{{ range $_, $dc := $dcs }}
|
||||||
|
<span class="text-sm italic text-pink-500 dark:text-pink-400 px-2">
|
||||||
|
{{- $dc -}}
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div><!-- dataClasses -->
|
||||||
|
<div class="flex place-items-center justify-left mt-2 lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<div class="justify-center">
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Verified:
|
||||||
|
{{- if .Data.hibp.IsVerified -}}
|
||||||
|
<span class="w-24 md:w-32 px-2 text-blue-500 dark:text-blue-300">true</span>
|
||||||
|
{{- else -}}
|
||||||
|
<span class="w-16 md:w-16 px-2 text-pink-500 dark:text-pink-300">✗</span>
|
||||||
|
{{- end -}}
|
||||||
|
</span>
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Fabricated:
|
||||||
|
{{- if .Data.hibp.IsFabricated -}}
|
||||||
|
<span class="w-24 md:w-32 px-2 text-pink-500 dark:text-pink-300">true</span>
|
||||||
|
{{- else -}}
|
||||||
|
<span class="w-16 md:w-16 px-2 text-blue-500 dark:text-blue-300">✗</span>
|
||||||
|
{{- end -}}
|
||||||
|
</span>
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Sensitive:
|
||||||
|
{{- if .Data.hibp.IsSensitive -}}
|
||||||
|
<span class="w-24 md:w-32 px-2 text-pink-500 dark:text-pink-300">true</span>
|
||||||
|
{{- else -}}
|
||||||
|
<span class="w-16 md:w-16 px-2 text-blue-500 dark:text-blue-300">✗</span>
|
||||||
|
{{- end -}}
|
||||||
|
</span>
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Retired:
|
||||||
|
{{- if .Data.hibp.IsRetired -}}
|
||||||
|
<span class="w-24 md:w-32 px-2 text-pink-500 dark:text-pink-300">true</span>
|
||||||
|
{{- else -}}
|
||||||
|
<span class="w-16 md:w-16 px-2 text-blue-500 dark:text-blue-300">✗</span>
|
||||||
|
{{- end -}}
|
||||||
|
</span>
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Spam list:
|
||||||
|
{{- if .Data.hibp.IsSpamList -}}
|
||||||
|
<span class="w-24 md:w-32 px-2 text-pink-500 dark:text-pink-300">true</span>
|
||||||
|
{{- else -}}
|
||||||
|
<span class="w-16 md:w-16 px-2 text-blue-500 dark:text-blue-300">✗</span>
|
||||||
|
{{- end -}}
|
||||||
|
</span>
|
||||||
|
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Malware:
|
||||||
|
{{- if .Data.hibp.IsMalware -}}
|
||||||
|
<span class="w-24 md:w-32 px-2 text-pink-500 dark:text-pink-300">true</span>
|
||||||
|
{{- else -}}
|
||||||
|
<span class="w-16 md:w-16 px-2 text-blue-500 dark:text-blue-300">✗</span>
|
||||||
|
{{- end -}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{- else -}}
|
||||||
|
<div class="flex justify-between place-items-center">
|
||||||
|
<h1 class="text-2xl text-pink-600 dark:text-pink-500 py-2">
|
||||||
|
No data in the system for this breach
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
|
{{- if and .Data .Data.flash -}}
|
||||||
|
<div class="place-items-center">
|
||||||
|
<h1 class="text-2xl text-pink-600 dark:text-pink-500 py-2">
|
||||||
|
{{ .Data.flash -}}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
{{- end}}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{{ template "footer.tmpl" . }}
|
78
templates/user/hibp-search.tmpl
Normal file
78
templates/user/hibp-search.tmpl
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
{{ template "head.tmpl" . }}
|
||||||
|
<body class="h-screen bg-white dark:bg-gray-900">
|
||||||
|
{{ template "navbar.tmpl" . }}
|
||||||
|
<main class="grow">
|
||||||
|
<div class="container mx-auto place-items-center px-8 md:px-12 lg:px-14">
|
||||||
|
<div class="flex justify-between place-items-center">
|
||||||
|
<h1 class="text-xl font-bold text-fuchsia-600 dark:text-fuchsia-400 capitalize py-2">
|
||||||
|
Breach search
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between place-items-center">
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-300 hover:cursor-help">
|
||||||
|
<span class="text-lg text-pink-500 dark:text-pink-300">
|
||||||
|
🛈
|
||||||
|
</span>
|
||||||
|
Breach data on this page is sourced from
|
||||||
|
<a href="https://haveibeenpwned.com" target="_blank" rel="noopener"
|
||||||
|
class="w-auto py-1 mt-2 text-center text-blue-500 md:mt-0 mx-0 hover:underline dark:text-blue-400">
|
||||||
|
Have I Been Pwned?</a>
|
||||||
|
and is
|
||||||
|
<a href="https://haveibeenpwned.com/API/v3#License" target="_blank" rel="noopener"
|
||||||
|
class="w-auto py-1 mt-2 text-center text-blue-500 md:mt-0 mx-0 hover:underline dark:text-blue-400">
|
||||||
|
available</a>
|
||||||
|
under
|
||||||
|
<a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener"
|
||||||
|
class="w-auto py-1 mt-2 text-center text-blue-500 md:mt-0 mx-0 hover:underline dark:text-blue-400">
|
||||||
|
CC-BY-4.0
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{- if and .Data .Data.flash -}}
|
||||||
|
<div class="place-items-center">
|
||||||
|
<h1 class="text-2xl text-pink-600 dark:text-pink-500 py-2">
|
||||||
|
{{ .Data.flash -}}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
{{- end}}
|
||||||
|
<div class="mt-6 md:flex md:items-center md:place-items-center md:justify-between">
|
||||||
|
<form method="POST" class="w-full md:w-5xl" action="/user/hibp-search">
|
||||||
|
<input type="hidden" name="csrf" value="{{- .CSRF -}}">
|
||||||
|
<div class="relative mb-2 flex w-full flex-wrap items-stretch">
|
||||||
|
<input
|
||||||
|
name="search"
|
||||||
|
type="search"
|
||||||
|
placeholder="Search by entering an email address"
|
||||||
|
aria-label="Search the Have I Been Pwned? API by entering an email address"
|
||||||
|
required
|
||||||
|
class="relative m-0 px-3 py-1 block w-[2px] min-w-0 flex-auto rounded border border-solid border-gray-300 bg-transparent bg-clip-padding text-base text-gray-700 outline-none transition duration-100 ease-in-out focus:z-[3] focus:border-gray-300 focus:text-gray-700 dark:focus:text-gray-200 focus:shadow-sm focus:outline-none dark:border-gray-600 dark:text-gray-200 dark:placeholder:text-gray-300 dark:focus:border-gray-300"/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="w-full my-0 text-sm font-medium tracking-wide text-white capitalize transition-colors duration-300 transform bg-blue-500 rounded-md md:w-1/4 hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-50"
|
||||||
|
type="submit">Submit
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{ if and .Data .Data.breachNames }}
|
||||||
|
<div class="mt-6 md:flex md:items-center md:place-items-center md:justify-between">
|
||||||
|
<div class="block">
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-300">
|
||||||
|
The account was found in all of the following breaches:
|
||||||
|
</span>
|
||||||
|
<ul class="list-none list-outside">
|
||||||
|
{{ $bn := index .Data.breachNames }}
|
||||||
|
{{ range $_, $b := $bn }}
|
||||||
|
<li>
|
||||||
|
<a href="/user/hibp-breach-details/{{$b.Name}}" target="_blank" rel="noopener"
|
||||||
|
class="text-xl w-auto py-1 mt-2 text-center text-blue-500 md:mt-0 md:mx-6 lg:mx-4 hover:underline dark:text-blue-400">
|
||||||
|
{{$b.Name}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{{ template "footer.tmpl" . }}
|
47
templates/user/init-password-change.tmpl
Normal file
47
templates/user/init-password-change.tmpl
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{{ template "head.tmpl" . }}
|
||||||
|
<body class="min-h-screen flex flex-col justify-between bg-white dark:bg-gray-900">
|
||||||
|
{{ template "navbar.tmpl" . }}
|
||||||
|
<main class="grow mb-auto">
|
||||||
|
<div class="container mx-auto place-items-center px-8">
|
||||||
|
{{ if and .Data .Data.flash }}
|
||||||
|
<h1 class="text-xl text-pink-600 dark:text-pink-500 py-2">
|
||||||
|
{{ .Data.flash }}
|
||||||
|
</h1>
|
||||||
|
{{- end }}
|
||||||
|
{{ if .User -}}
|
||||||
|
<h1 class="mt-20 text-2xl text-pink-400 font-bold">
|
||||||
|
Welcome, <code>{{.Name}}</code>, since you have never changed your password here before, now is the time to do that!<br>
|
||||||
|
</h1>
|
||||||
|
<span class="text-lg text-gray-500 dark:text-gray-300 font-italic">
|
||||||
|
This is necessary in order to make sure that only you know the password, since the account was pre-created <em>for</em> you.
|
||||||
|
</span>
|
||||||
|
<div class="block">
|
||||||
|
<div class="mt-8 md:flex md:items-center md:place-items-center md:justify-between">
|
||||||
|
<form method="POST" class="w-full md:w-5xl" action="/user/initial-password-change">
|
||||||
|
<input type="hidden" name="csrf" value="{{- .CSRF -}}">
|
||||||
|
<div class="relative flex items-center mt-4">
|
||||||
|
<span class="absolute" role="img" aria-label="password lock icon">
|
||||||
|
{{ template "svg-password.tmpl" }}
|
||||||
|
</span>
|
||||||
|
<input name="password" type="new-password" placeholder="New password" {{if and .Data.form .Data.form.Password}}value="{{.Data.form.Password}}"{{else}}{{end}} minlength=20 required class="block w-full px-10 py-3 required:border-slate-500 dark:required:border-slate-300 required:border-3 valid:border text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:valid:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="w-full my-2 text-sm font-medium tracking-wide text-white capitalize transition-colors duration-300 transform bg-blue-500 rounded-lg md:w-1/4 hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-50"
|
||||||
|
type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<form method="POST" class="w-full md:w-5xl" action="/logout">
|
||||||
|
<input type="hidden" name="csrf" value="{{- .CSRF -}}">
|
||||||
|
<button
|
||||||
|
class="w-full my-2 text-sm font-medium tracking-wide text-white capitalize transition-colors duration-300 transform bg-blue-500 rounded-lg md:w-1/4 hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-50"
|
||||||
|
type="submit">Logout</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{- else }}
|
||||||
|
<h1 class="mt-20 text-2xl text-pink-400 font-bold">
|
||||||
|
Please log in.
|
||||||
|
</h1>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{{ template "footer.tmpl" . }}
|
Loading…
Reference in New Issue
Block a user