From a11ca5b470b6fc5aec97e010df74571002ef87bd Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Wed, 17 Jan 2024 20:28:42 +0100 Subject: [PATCH] Add page to see login attempts for a company --- deploy/company_login_attempt.sql | 28 +++++ pkg/user/admin.go | 7 ++ pkg/user/login_attempt.go | 59 ++++++++++ po/ca.po | 42 +++++-- po/es.po | 44 +++++-- po/fr.po | 42 +++++-- revert/company_login_attempt.sql | 7 ++ sqitch.plan | 1 + test/company_login_attempt.sql | 107 ++++++++++++++++++ verify/company_login_attempt.sql | 14 +++ web/templates/admin/user/index.gohtml | 55 +++++---- .../admin/user/login-attempts.gohtml | 32 ++++++ 12 files changed, 387 insertions(+), 51 deletions(-) create mode 100644 deploy/company_login_attempt.sql create mode 100644 pkg/user/login_attempt.go create mode 100644 revert/company_login_attempt.sql create mode 100644 test/company_login_attempt.sql create mode 100644 verify/company_login_attempt.sql create mode 100644 web/templates/admin/user/login-attempts.gohtml diff --git a/deploy/company_login_attempt.sql b/deploy/company_login_attempt.sql new file mode 100644 index 0000000..69b5878 --- /dev/null +++ b/deploy/company_login_attempt.sql @@ -0,0 +1,28 @@ +-- Deploy camper:company_login_attempt to pg +-- requires: roles +-- requires: schema_camper +-- requires: login_attempt +-- requires: user +-- requires: company_user +-- requires: current_company_id + +begin; + +set search_path to camper, public; + +create or replace view company_login_attempt with (security_barrier) as + select attempt_id + , user_name + , ip_address + , success + , attempted_at + from auth.login_attempt + join auth."user" on "user".email = user_name + join company_user using (user_id) + where company_id = current_company_id() + ; +; + +grant select on table company_login_attempt to admin; + +commit; diff --git a/pkg/user/admin.go b/pkg/user/admin.go index 9d5f060..1cea81b 100644 --- a/pkg/user/admin.go +++ b/pkg/user/admin.go @@ -35,6 +35,13 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) } + case "login-attempts": + switch r.Method { + case http.MethodGet: + serveLoginAttemptIndex(w, r, user, company, conn) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) + } default: http.NotFound(w, r) } diff --git a/pkg/user/login_attempt.go b/pkg/user/login_attempt.go new file mode 100644 index 0000000..c70fb8f --- /dev/null +++ b/pkg/user/login_attempt.go @@ -0,0 +1,59 @@ +package user + +import ( + "context" + "net/http" + "time" + + "dev.tandem.ws/tandem/camper/pkg/auth" + "dev.tandem.ws/tandem/camper/pkg/database" + "dev.tandem.ws/tandem/camper/pkg/template" +) + +func serveLoginAttemptIndex(w http.ResponseWriter, r *http.Request, loginAttempt *auth.User, company *auth.Company, conn *database.Conn) { + loginAttempts, err := collectLoginAttemptEntries(r.Context(), conn) + if err != nil { + panic(err) + } + loginAttempts.MustRender(w, r, loginAttempt, company) +} + +func collectLoginAttemptEntries(ctx context.Context, conn *database.Conn) (loginAttemptIndex, error) { + rows, err := conn.Query(ctx, ` + select user_name + , host(ip_address) + , attempted_at + , success + from company_login_attempt + order by attempted_at desc + limit 500 + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var entries loginAttemptIndex + for rows.Next() { + entry := &loginAttemptEntry{} + if err = rows.Scan(&entry.UserName, &entry.IPAddress, &entry.Date, &entry.Success); err != nil { + return nil, err + } + entries = append(entries, entry) + } + + return entries, nil +} + +type loginAttemptEntry struct { + 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) +} diff --git a/po/ca.po b/po/ca.po index 7fca88f..2baded7 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2024-01-17 19:40+0100\n" +"POT-Creation-Date: 2024-01-17 20:25+0100\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -512,7 +512,6 @@ msgid "Name" msgstr "Nom" #: web/templates/admin/legal/index.gohtml:29 -#: web/templates/admin/user/index.gohtml:42 msgid "No legal texts added yet." msgstr "No s’ha afegit cap text legal encara." @@ -763,12 +762,14 @@ msgstr "Tipus" #: web/templates/admin/campsite/index.gohtml:28 #: web/templates/admin/campsite/type/index.gohtml:49 #: web/templates/admin/season/index.gohtml:41 +#: web/templates/admin/user/login-attempts.gohtml:27 msgid "Yes" msgstr "Sí" #: web/templates/admin/campsite/index.gohtml:28 #: web/templates/admin/campsite/type/index.gohtml:49 #: web/templates/admin/season/index.gohtml:41 +#: web/templates/admin/user/login-attempts.gohtml:27 msgid "No" msgstr "No" @@ -1062,8 +1063,35 @@ msgctxt "input" msgid "Language" msgstr "Idioma" +#: web/templates/admin/user/login-attempts.gohtml:6 +#: web/templates/admin/user/login-attempts.gohtml:11 +msgctxt "title" +msgid "Login Attempts" +msgstr "Intents d’entrada" + +#: web/templates/admin/user/login-attempts.gohtml:15 +msgctxt "header" +msgid "Date" +msgstr "Data" + +#: web/templates/admin/user/login-attempts.gohtml:16 +#: web/templates/admin/user/index.gohtml:18 +msgctxt "header" +msgid "Email" +msgstr "Correu-e" + +#: web/templates/admin/user/login-attempts.gohtml:17 +msgctxt "header" +msgid "IP Address" +msgstr "Adreça IP" + +#: web/templates/admin/user/login-attempts.gohtml:18 +msgctxt "header" +msgid "Success" +msgstr "Èxit" + #: web/templates/admin/user/index.gohtml:6 -#: web/templates/admin/user/index.gohtml:12 +#: web/templates/admin/user/index.gohtml:13 #: web/templates/admin/layout.gohtml:70 msgctxt "title" msgid "Users" @@ -1074,10 +1102,10 @@ msgctxt "action" msgid "Add User" msgstr "Afegeix usuari" -#: web/templates/admin/user/index.gohtml:18 -msgctxt "header" -msgid "Email" -msgstr "Correu-e" +#: web/templates/admin/user/index.gohtml:12 +msgctxt "action" +msgid "Logs" +msgstr "Registres" #: web/templates/admin/user/index.gohtml:19 msgctxt "header" diff --git a/po/es.po b/po/es.po index dea7f88..fb4be79 100644 --- a/po/es.po +++ b/po/es.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2024-01-17 19:40+0100\n" +"POT-Creation-Date: 2024-01-17 20:25+0100\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -512,7 +512,6 @@ msgid "Name" msgstr "Nombre" #: web/templates/admin/legal/index.gohtml:29 -#: web/templates/admin/user/index.gohtml:42 msgid "No legal texts added yet." msgstr "No se ha añadido ningún texto legal todavía." @@ -763,12 +762,14 @@ msgstr "Tipo" #: web/templates/admin/campsite/index.gohtml:28 #: web/templates/admin/campsite/type/index.gohtml:49 #: web/templates/admin/season/index.gohtml:41 +#: web/templates/admin/user/login-attempts.gohtml:27 msgid "Yes" msgstr "Sí" #: web/templates/admin/campsite/index.gohtml:28 #: web/templates/admin/campsite/type/index.gohtml:49 #: web/templates/admin/season/index.gohtml:41 +#: web/templates/admin/user/login-attempts.gohtml:27 msgid "No" msgstr "No" @@ -1062,22 +1063,49 @@ msgctxt "input" msgid "Language" msgstr "Idioma" +#: web/templates/admin/user/login-attempts.gohtml:6 +#: web/templates/admin/user/login-attempts.gohtml:11 +msgctxt "title" +msgid "Login Attempts" +msgstr "Intentos de entrada" + +#: web/templates/admin/user/login-attempts.gohtml:15 +msgctxt "header" +msgid "Date" +msgstr "Fecha" + +#: web/templates/admin/user/login-attempts.gohtml:16 +#: web/templates/admin/user/index.gohtml:18 +msgctxt "header" +msgid "Email" +msgstr "Correo-e" + +#: web/templates/admin/user/login-attempts.gohtml:17 +msgctxt "header" +msgid "IP Address" +msgstr "Dirección IP" + +#: web/templates/admin/user/login-attempts.gohtml:18 +msgctxt "header" +msgid "Success" +msgstr "Éxito" + #: web/templates/admin/user/index.gohtml:6 -#: web/templates/admin/user/index.gohtml:12 +#: web/templates/admin/user/index.gohtml:13 #: web/templates/admin/layout.gohtml:70 msgctxt "title" msgid "Users" -msgstr "" +msgstr "Usuarios" #: web/templates/admin/user/index.gohtml:11 msgctxt "action" msgid "Add User" msgstr "Añadir usuario" -#: web/templates/admin/user/index.gohtml:18 -msgctxt "header" -msgid "Email" -msgstr "Correo-e" +#: web/templates/admin/user/index.gohtml:12 +msgctxt "action" +msgid "Logs" +msgstr "Registros" #: web/templates/admin/user/index.gohtml:19 msgctxt "header" diff --git a/po/fr.po b/po/fr.po index fe8b63c..3a91f7a 100644 --- a/po/fr.po +++ b/po/fr.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2024-01-17 19:40+0100\n" +"POT-Creation-Date: 2024-01-17 20:25+0100\n" "PO-Revision-Date: 2023-12-20 10:13+0100\n" "Last-Translator: Oriol Carbonell \n" "Language-Team: French \n" @@ -513,7 +513,6 @@ msgid "Name" msgstr "Nom" #: web/templates/admin/legal/index.gohtml:29 -#: web/templates/admin/user/index.gohtml:42 msgid "No legal texts added yet." msgstr "Aucune texte juridique n’a encore été ajoutée." @@ -764,12 +763,14 @@ msgstr "Type" #: web/templates/admin/campsite/index.gohtml:28 #: web/templates/admin/campsite/type/index.gohtml:49 #: web/templates/admin/season/index.gohtml:41 +#: web/templates/admin/user/login-attempts.gohtml:27 msgid "Yes" msgstr "Oui" #: web/templates/admin/campsite/index.gohtml:28 #: web/templates/admin/campsite/type/index.gohtml:49 #: web/templates/admin/season/index.gohtml:41 +#: web/templates/admin/user/login-attempts.gohtml:27 msgid "No" msgstr "Non" @@ -1063,8 +1064,35 @@ msgctxt "input" msgid "Language" msgstr "Langue" +#: web/templates/admin/user/login-attempts.gohtml:6 +#: web/templates/admin/user/login-attempts.gohtml:11 +msgctxt "title" +msgid "Login Attempts" +msgstr "Tentatives de connexion" + +#: web/templates/admin/user/login-attempts.gohtml:15 +msgctxt "header" +msgid "Date" +msgstr "Date" + +#: web/templates/admin/user/login-attempts.gohtml:16 +#: web/templates/admin/user/index.gohtml:18 +msgctxt "header" +msgid "Email" +msgstr "E-mail" + +#: web/templates/admin/user/login-attempts.gohtml:17 +msgctxt "header" +msgid "IP Address" +msgstr "Adresse IP" + +#: web/templates/admin/user/login-attempts.gohtml:18 +msgctxt "header" +msgid "Success" +msgstr "Succès" + #: web/templates/admin/user/index.gohtml:6 -#: web/templates/admin/user/index.gohtml:12 +#: web/templates/admin/user/index.gohtml:13 #: web/templates/admin/layout.gohtml:70 msgctxt "title" msgid "Users" @@ -1075,10 +1103,10 @@ msgctxt "action" msgid "Add User" msgstr "Ajouter un utilisateur" -#: web/templates/admin/user/index.gohtml:18 -msgctxt "header" -msgid "Email" -msgstr "E-mail" +#: web/templates/admin/user/index.gohtml:12 +msgctxt "action" +msgid "Logs" +msgstr "Journaux" #: web/templates/admin/user/index.gohtml:19 msgctxt "header" diff --git a/revert/company_login_attempt.sql b/revert/company_login_attempt.sql new file mode 100644 index 0000000..35ea1b4 --- /dev/null +++ b/revert/company_login_attempt.sql @@ -0,0 +1,7 @@ +-- Revert camper:company_login_attempt from pg + +begin; + +drop view if exists camper.company_login_attempt; + +commit; diff --git a/sqitch.plan b/sqitch.plan index 0eae170..7827ce3 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -155,3 +155,4 @@ translate_cover_carousel_slide [roles schema_camper cover_carousel_i18n] 2024-01 remove_cover_carousel_slide [roles schema_camper cover_carousel cover_carousel_i18n] 2024-01-16T18:27:48Z jordi fita mas # Add function to remove sliders from the cover carousel order_cover_carousel [schema_camper roles cover_carousel] 2024-01-16T18:40:12Z jordi fita mas # Add function to order cover carousel company_user_profile [roles schema_camper user company_user current_company_id] 2024-01-17T17:37:19Z jordi fita mas # Add view to list users for admins +company_login_attempt [roles schema_camper login_attempt user company_user current_company_id] 2024-01-17T19:02:26Z jordi fita mas # Add view to see login attempts for current company diff --git a/test/company_login_attempt.sql b/test/company_login_attempt.sql new file mode 100644 index 0000000..d0042ad --- /dev/null +++ b/test/company_login_attempt.sql @@ -0,0 +1,107 @@ +-- Test company_login_attempt +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(17); + +set search_path to camper, auth, public; + +select has_view('company_login_attempt'); +select table_privs_are('company_login_attempt', 'guest', array []::text[]); +select table_privs_are('company_login_attempt', 'employee', array []::text[]); +select table_privs_are('company_login_attempt', 'admin', array ['SELECT']); +select table_privs_are('company_login_attempt', 'authenticator', array []::text[]); + +select has_column('company_login_attempt', 'attempt_id'); +select col_type_is('company_login_attempt', 'attempt_id', 'bigint'); + +select has_column('company_login_attempt', 'user_name'); +select col_type_is('company_login_attempt', 'user_name', 'text'); + +select has_column('company_login_attempt', 'ip_address'); +select col_type_is('company_login_attempt', 'ip_address', 'inet'); + +select has_column('company_login_attempt', 'success'); +select col_type_is('company_login_attempt', 'success', 'boolean'); + +select has_column('company_login_attempt', 'attempted_at'); +select col_type_is('company_login_attempt', 'attempted_at', 'timestamp with time zone'); + + +set client_min_messages to warning; +truncate company_host cascade; +truncate company_user cascade; +truncate company cascade; +truncate auth."user" cascade; +truncate auth.login_attempt cascade; +reset client_min_messages; + +insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at, lang_tag) +values (1, 'demo@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month', 'ca') + , (5, 'admin@tandem.blog', 'Admin', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month', 'es') + , (7, 'another@tandem.blog', 'Another Employee', 'test', default, default, default) +; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 'ES', 'EUR', 'ca') + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 60, 'FR', 'USD', 'es') +; + +insert into company_user (company_id, user_id, role) +values (2, 1, 'admin') + , (4, 5, 'admin') + , (4, 7, 'employee') +; + +insert into company_host (company_id, host) +values (2, 'co2') + , (4, 'co4') +; + +prepare login_attempt as +select user_name, ip_address, success, attempted_at +from company_login_attempt +; + +select login('demo@tandem.blog', 'test', '::1'::inet); +select login('demo@tandem.blog', '123', '127.0.0.1'::inet); +select login('admin@tandem.blog', '123', '192.168.1.1'::inet); +select login('admin@tandem.blog', 'test', '::1'::inet); +select login('admin@tandem.blog', 'test', '192.168.2.1'::inet); +select login('another@tandem.blog', 'test', '192.168.3.1'::inet); +select login('unknown@tandem.blog', 'test', '192.168.4.1'::inet); + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2'); + +select bag_eq( + 'login_attempt', + $$ values ('demo@tandem.blog', '::1'::inet, true, current_timestamp) + , ('demo@tandem.blog', '127.0.0.1'::inet, false, current_timestamp) + $$, + 'Should only see login attempts from the first company' +); + +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog', 'co4'); + +select bag_eq( + 'login_attempt', + $$ values ('admin@tandem.blog', '192.168.1.1'::inet, false, current_timestamp) + , ('admin@tandem.blog', '::1'::inet, true, current_timestamp) + , ('admin@tandem.blog', '192.168.2.1'::inet, true, current_timestamp) + , ('another@tandem.blog', '192.168.3.1'::inet, true, current_timestamp) + $$, + 'Should only see login_attempts from the second company' +); + +reset role; + + +select * +from finish(); + +rollback; diff --git a/verify/company_login_attempt.sql b/verify/company_login_attempt.sql new file mode 100644 index 0000000..047be16 --- /dev/null +++ b/verify/company_login_attempt.sql @@ -0,0 +1,14 @@ +-- Verify camper:company_login_attempt on pg + +begin; + +select attempt_id + , user_name + , ip_address + , success + , attempted_at +from camper.company_login_attempt +where false +; + +rollback; diff --git a/web/templates/admin/user/index.gohtml b/web/templates/admin/user/index.gohtml index 56bca12..facdf1a 100644 --- a/web/templates/admin/user/index.gohtml +++ b/web/templates/admin/user/index.gohtml @@ -9,36 +9,33 @@ {{ define "content" -}} {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/user.userIndex*/ -}} {{( pgettext "Add User" "action" )}} + {{( pgettext "Logs" "action" )}}

{{( pgettext "Users" "title" )}}

- {{ if .Users -}} - - +
+ + + + + + + + + + {{ $confirm := ( gettext "Are you sure you wish to delete this user?" )}} + {{ range .Users -}} - - - - + + + + - - - {{ $confirm := ( gettext "Are you sure you wish to delete this user?" )}} - {{ range .Users -}} - - - - - - - {{- end }} - -
{{( pgettext "Name" "header" )}}{{( pgettext "Email" "header" )}}{{( pgettext "Role" "header" )}}{{( pgettext "Actions" "header" )}}
{{( pgettext "Name" "header" )}}{{( pgettext "Email" "header" )}}{{( pgettext "Role" "header" )}}{{( pgettext "Actions" "header" )}}{{ .Name }}{{ .Email }}{{( pgettext .Role "role" )}} + +
{{ .Name }}{{ .Email }}{{( pgettext .Role "role" )}} - -
- {{ else -}} -

{{( gettext "No legal texts added yet." )}}

- {{- end }} + {{- end }} + + {{- end }} diff --git a/web/templates/admin/user/login-attempts.gohtml b/web/templates/admin/user/login-attempts.gohtml new file mode 100644 index 0000000..708396a --- /dev/null +++ b/web/templates/admin/user/login-attempts.gohtml @@ -0,0 +1,32 @@ + +{{ define "title" -}} + {{( pgettext "Login Attempts" "title" )}} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/user.loginAttemptIndex*/ -}} +

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

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