From 92c0cb4de0c096d10e4be6b57433e282d05a63d5 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Fri, 3 May 2024 20:45:14 +0200 Subject: [PATCH] Add filters and pagination to login attempts --- pkg/user/filter.go | 98 +++++++++++++++++++ pkg/user/login_attempt.go | 50 +++++++--- .../admin/user/login-attempts.gohtml | 64 ++++++++---- web/templates/admin/user/results.gohtml | 9 ++ 4 files changed, 189 insertions(+), 32 deletions(-) create mode 100644 pkg/user/filter.go create mode 100644 web/templates/admin/user/results.gohtml diff --git a/pkg/user/filter.go b/pkg/user/filter.go new file mode 100644 index 0000000..f52cbf5 --- /dev/null +++ b/pkg/user/filter.go @@ -0,0 +1,98 @@ +package user + +import ( + "fmt" + "net/http" + "strconv" + "strings" + + "dev.tandem.ws/tandem/camper/pkg/auth" + "dev.tandem.ws/tandem/camper/pkg/database" + "dev.tandem.ws/tandem/camper/pkg/form" +) + +type filterForm struct { + company *auth.Company + FromDate *form.Input + ToDate *form.Input + Cursor *form.Cursor +} + +func newFilterForm() *filterForm { + return &filterForm{ + FromDate: &form.Input{ + Name: "from_date", + }, + ToDate: &form.Input{ + Name: "to_date", + }, + Cursor: &form.Cursor{ + Name: "cursor", + PerPage: 5, + }, + } +} + +func (f *filterForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + f.FromDate.FillValue(r) + f.ToDate.FillValue(r) + f.Cursor.FillValue(r) + return nil +} + +func (f *filterForm) BuildQuery(args []interface{}) (string, []interface{}) { + var where []string + appendWhere := func(expression string, value interface{}) { + args = append(args, value) + where = append(where, fmt.Sprintf(expression, len(args))) + } + maybeAppendWhere := func(expression string, value string, conv func(string) interface{}) { + if value != "" { + if conv == nil { + appendWhere(expression, value) + } else { + appendWhere(expression, conv(value)) + } + } + } + + maybeAppendWhere("attempted_at >= $%d", f.FromDate.Val, nil) + maybeAppendWhere("attempted_at <= $%d", f.ToDate.Val, nil) + + if f.Paginated() { + params := f.Cursor.Params() + if len(params) == 2 { + where = append(where, fmt.Sprintf("(attempted_at, attempt_id) < ($%d, $%d)", len(args)+1, len(args)+2)) + args = append(args, params[0]) + args = append(args, params[1]) + } + } + + if len(where) == 0 { + return "1=1", args + } + + return strings.Join(where, ") AND ("), args +} + +func (f *filterForm) buildCursor(customers []*loginAttemptEntry) []*loginAttemptEntry { + return form.BuildCursor(f.Cursor, customers, func(entry *loginAttemptEntry) []string { + return []string{entry.Date.Format(database.ISODateTimeFormat), strconv.Itoa(entry.ID)} + }) +} + +func (f *filterForm) HasValue() bool { + return f.FromDate.Val != "" || + f.ToDate.Val != "" +} + +func (f *filterForm) PerPage() int { + return f.Cursor.PerPage +} + +func (f *filterForm) Paginated() bool { + return f.Cursor.Pagination +} diff --git a/pkg/user/login_attempt.go b/pkg/user/login_attempt.go index c70fb8f..9c34928 100644 --- a/pkg/user/login_attempt.go +++ b/pkg/user/login_attempt.go @@ -2,6 +2,8 @@ package user import ( "context" + httplib "dev.tandem.ws/tandem/camper/pkg/http" + "fmt" "net/http" "time" @@ -11,32 +13,44 @@ import ( ) func serveLoginAttemptIndex(w http.ResponseWriter, r *http.Request, loginAttempt *auth.User, company *auth.Company, conn *database.Conn) { - loginAttempts, err := collectLoginAttemptEntries(r.Context(), conn) + filters := newFilterForm() + if err := filters.Parse(r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + loginAttempts, err := collectLoginAttemptEntries(r.Context(), conn, filters) if err != nil { panic(err) } - loginAttempts.MustRender(w, r, loginAttempt, company) + index := &loginAttemptIndex{ + Attempts: filters.buildCursor(loginAttempts), + Filters: filters, + } + index.MustRender(w, r, loginAttempt, company) } -func collectLoginAttemptEntries(ctx context.Context, conn *database.Conn) (loginAttemptIndex, error) { - rows, err := conn.Query(ctx, ` - select user_name +func collectLoginAttemptEntries(ctx context.Context, conn *database.Conn, filters *filterForm) ([]*loginAttemptEntry, error) { + where, args := filters.BuildQuery(nil) + rows, err := conn.Query(ctx, fmt.Sprintf(` + select attempt_id + , user_name , host(ip_address) , attempted_at , success from company_login_attempt - order by attempted_at desc - limit 500 - `) + where (%s) + order by attempted_at desc, attempt_id desc + limit %d + `, where, filters.PerPage()+1), args...) if err != nil { return nil, err } defer rows.Close() - var entries loginAttemptIndex + var entries []*loginAttemptEntry for rows.Next() { entry := &loginAttemptEntry{} - if err = rows.Scan(&entry.UserName, &entry.IPAddress, &entry.Date, &entry.Success); err != nil { + if err = rows.Scan(&entry.ID, &entry.UserName, &entry.IPAddress, &entry.Date, &entry.Success); err != nil { return nil, err } entries = append(entries, entry) @@ -46,14 +60,22 @@ func collectLoginAttemptEntries(ctx context.Context, conn *database.Conn) (login } type loginAttemptEntry struct { + ID int UserName string IPAddress string Date time.Time Success bool } -type loginAttemptIndex []*loginAttemptEntry - -func (page *loginAttemptIndex) MustRender(w http.ResponseWriter, r *http.Request, loginAttempt *auth.User, company *auth.Company) { - template.MustRenderAdmin(w, r, loginAttempt, company, "user/login-attempts.gohtml", page) +type loginAttemptIndex struct { + Attempts []*loginAttemptEntry + Filters *filterForm +} + +func (page *loginAttemptIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + if httplib.IsHTMxRequest(r) && page.Filters.Paginated() { + template.MustRenderAdminNoLayout(w, r, user, company, "user/results.gohtml", page) + } else { + template.MustRenderAdminFiles(w, r, user, company, page, "user/login-attempts.gohtml", "user/results.gohtml") + } } diff --git a/web/templates/admin/user/login-attempts.gohtml b/web/templates/admin/user/login-attempts.gohtml index 9daadb7..08d45e4 100644 --- a/web/templates/admin/user/login-attempts.gohtml +++ b/web/templates/admin/user/login-attempts.gohtml @@ -12,25 +12,53 @@ {{ define "content" -}} {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/user.loginAttemptIndex*/ -}} + {{ template "filters-toggle" }} +
+ {{ with .Filters }} +
+ {{ with .FromDate -}} + + {{- end }} + {{ with .ToDate -}} + + {{- end }} +
+ {{ end }} + {{ if .Filters.HasValue }} + {{( pgettext "Reset" "action" )}} + {{ end }} +

{{( pgettext "Login Attempts" "title" )}}

- - - - - - - - - - - {{ range . -}} + {{ if .Attempts -}} +
{{( pgettext "Date" "header" )}}{{( pgettext "Email" "header" )}}{{( pgettext "IP Address" "header" )}}{{( pgettext "Success" "header" )}}
+ - - - - + + + + - {{- end }} - -
{{ .Date }}{{ .UserName }}{{ .IPAddress }}{{ if .Success }}{{( gettext "Yes" )}}{{ else }}{{( gettext "No" )}}{{ end }}{{( pgettext "Date" "header" )}}{{( pgettext "Email" "header" )}}{{( pgettext "IP Address" "header" )}}{{( pgettext "Success" "header" )}}
+ + + {{ template "results.gohtml" . }} + + + {{- else -}} +

{{( gettext "No logging attempts found." )}}

+ {{- end }} {{- end }} diff --git a/web/templates/admin/user/results.gohtml b/web/templates/admin/user/results.gohtml new file mode 100644 index 0000000..0a3ed64 --- /dev/null +++ b/web/templates/admin/user/results.gohtml @@ -0,0 +1,9 @@ +{{ range .Attempts -}} + + {{ .Date }} + {{ .UserName }} + {{ .IPAddress }} + {{ if .Success }}{{( gettext "Yes" )}}{{ else }}{{( gettext "No" )}}{{ end }} + +{{- end }} +{{ template "pagination" .Filters.Cursor | colspan 4 }}