Add page to see login attempts for a company

This commit is contained in:
jordi fita mas 2024-01-17 20:28:42 +01:00
parent f7fdc594d5
commit a11ca5b470
12 changed files with 387 additions and 51 deletions

View File

@ -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;

View File

@ -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)
}

59
pkg/user/login_attempt.go Normal file
View File

@ -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)
}

View File

@ -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 <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\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 sha 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 dentrada"
#: 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"

View File

@ -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 <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\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"

View File

@ -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 <info@oriolcarbonell.cat>\n"
"Language-Team: French <traduc@traduc.org>\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 na 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"

View File

@ -0,0 +1,7 @@
-- Revert camper:company_login_attempt from pg
begin;
drop view if exists camper.company_login_attempt;
commit;

View File

@ -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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add view to see login attempts for current company

View File

@ -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;

View File

@ -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;

View File

@ -9,36 +9,33 @@
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/user.userIndex*/ -}}
<a href="/admin/users/new">{{( pgettext "Add User" "action" )}}</a>
<a href="/admin/users/login-attempts">{{( pgettext "Logs" "action" )}}</a>
<h2>{{( pgettext "Users" "title" )}}</h2>
{{ if .Users -}}
<table>
<thead>
<table>
<thead>
<tr>
<th scope="col">{{( pgettext "Name" "header" )}}</th>
<th scope="col">{{( pgettext "Email" "header" )}}</th>
<th scope="col">{{( pgettext "Role" "header" )}}</th>
<th scope="col">{{( pgettext "Actions" "header" )}}</th>
</tr>
</thead>
<tbody>
{{ $confirm := ( gettext "Are you sure you wish to delete this user?" )}}
{{ range .Users -}}
<tr>
<th scope="col">{{( pgettext "Name" "header" )}}</th>
<th scope="col">{{( pgettext "Email" "header" )}}</th>
<th scope="col">{{( pgettext "Role" "header" )}}</th>
<th scope="col">{{( pgettext "Actions" "header" )}}</th>
<td><a href="{{ .URL }}">{{ .Name }}</a></td>
<td><a href="{{ .URL }}">{{ .Email }}</a></td>
<td>{{( pgettext .Role "role" )}}</td>
<td>
<button data-hx-delete="{{ .URL }}"
data-hx-confirm="{{ $confirm }}"
data-hx-headers='{ {{ CSRFHeader }} }'>
{{( pgettext "Delete" "action" )}}
</button>
</td>
</tr>
</thead>
<tbody>
{{ $confirm := ( gettext "Are you sure you wish to delete this user?" )}}
{{ range .Users -}}
<tr>
<td><a href="{{ .URL }}">{{ .Name }}</a></td>
<td><a href="{{ .URL }}">{{ .Email }}</a></td>
<td>{{( pgettext .Role "role" )}}</td>
<td>
<button data-hx-delete="{{ .URL }}"
data-hx-confirm="{{ $confirm }}"
data-hx-headers='{ {{ CSRFHeader }} }'>
{{( pgettext "Delete" "action" )}}
</button>
</td>
</tr>
{{- end }}
</tbody>
</table>
{{ else -}}
<p>{{( gettext "No legal texts added yet." )}}</p>
{{- end }}
{{- end }}
</tbody>
</table>
{{- end }}

View File

@ -0,0 +1,32 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{( pgettext "Login Attempts" "title" )}}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/user.loginAttemptIndex*/ -}}
<h2>{{( pgettext "Login Attempts" "title" )}}</h2>
<table>
<thead>
<tr>
<th scope="col">{{( pgettext "Date" "header" )}}</th>
<th scope="col">{{( pgettext "Email" "header" )}}</th>
<th scope="col">{{( pgettext "IP Address" "header" )}}</th>
<th scope="col">{{( pgettext "Success" "header" )}}</th>
</tr>
</thead>
<tbody>
{{ range . -}}
<tr>
<td>{{ .Date }}</td>
<td>{{ .UserName }}</td>
<td>{{ .IPAddress }}</td>
<td>{{ if .Success }}{{( gettext "Yes" )}}{{ else }}{{( gettext "No" )}}{{ end }}</td>
</tr>
{{- end }}
</tbody>
</table>
{{- end }}