Add admin page to list the users

There is no way, for now, to add, edit or remove users, because
currently we only need to list users.

I can not give admins access to the user table, for security
permissions, so i had to create a new view.  I could name it also ‘user’
in ‘camper’ scheme, but then i was afraid i would have problems with
unit tests and their search_path, so instead i called it
‘company_user_profile’, which is like ‘user_profile’ but for all users
in ‘company_user’.

I created a new Go package for it, rather than add the admin handler in
‘auth’, because ‘template’ depends on ‘auth’, and rendering from ‘auth’
would cause a dependency loop.

I needed to have the roles in gettext to translate them, but there is
no obvious place where to put the call to PgettextNoop.  For now, there
are in ‘NewAdminHandler’ because it is called once in the application’s
lifetime and they actually do not matter much.
This commit is contained in:
jordi fita mas 2024-01-17 19:42:47 +01:00
parent c69c715ef3
commit f7fdc594d5
12 changed files with 536 additions and 96 deletions

View File

@ -0,0 +1,25 @@
-- Deploy camper:company_user_profile to pg
-- requires: roles
-- requires: schema_camper
-- requires: user
-- requires: company_user
-- requires: current_company_id
begin;
set search_path to camper, public;
create or replace view company_user_profile with (security_barrier) as
select user_id
, email
, name
, role
, lang_tag
from auth."user"
join company_user using (user_id)
where company_id = current_company_id()
;
grant select on table company_user_profile to admin;
commit;

View File

@ -22,6 +22,7 @@ import (
"dev.tandem.ws/tandem/camper/pkg/services"
"dev.tandem.ws/tandem/camper/pkg/surroundings"
"dev.tandem.ws/tandem/camper/pkg/template"
"dev.tandem.ws/tandem/camper/pkg/user"
)
type adminHandler struct {
@ -35,6 +36,7 @@ type adminHandler struct {
season *season.AdminHandler
services *services.AdminHandler
surroundings *surroundings.AdminHandler
user *user.AdminHandler
}
func newAdminHandler(mediaDir string) *adminHandler {
@ -49,6 +51,7 @@ func newAdminHandler(mediaDir string) *adminHandler {
season: season.NewAdminHandler(),
services: services.NewAdminHandler(),
surroundings: surroundings.NewAdminHandler(),
user: user.NewAdminHandler(),
}
}
@ -88,6 +91,8 @@ func (h *adminHandler) Handle(user *auth.User, company *auth.Company, conn *data
h.services.Handler(user, company, conn).ServeHTTP(w, r)
case "surroundings":
h.surroundings.Handler(user, company, conn).ServeHTTP(w, r)
case "users":
h.user.Handler(user, company, conn).ServeHTTP(w, r)
case "":
switch r.Method {
case http.MethodGet:

94
pkg/user/admin.go Normal file
View File

@ -0,0 +1,94 @@
package user
import (
"context"
"net/http"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/database"
httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/locale"
"dev.tandem.ws/tandem/camper/pkg/template"
)
type AdminHandler struct {
}
func NewAdminHandler() *AdminHandler {
locale.PgettextNoop("guest", "role")
locale.PgettextNoop("employee", "role")
locale.PgettextNoop("admin", "role")
return &AdminHandler{}
}
func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var head string
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch head {
case "":
switch r.Method {
case http.MethodGet:
serveUserIndex(w, r, user, company, conn)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
}
default:
http.NotFound(w, r)
}
})
}
func serveUserIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
users, err := collectUserEntries(r.Context(), conn)
if err != nil {
panic(err)
}
page := &userIndex{
Users: users,
}
page.MustRender(w, r, user, company)
}
func collectUserEntries(ctx context.Context, conn *database.Conn) ([]*userEntry, error) {
rows, err := conn.Query(ctx, `
select '/admin/users/' || user_id
, email
, name
, role
from company_user_profile
order by name
`)
if err != nil {
return nil, err
}
defer rows.Close()
var entries []*userEntry
for rows.Next() {
entry := &userEntry{}
if err = rows.Scan(&entry.URL, &entry.Email, &entry.Name, &entry.Role); err != nil {
return nil, err
}
entries = append(entries, entry)
}
return entries, nil
}
type userEntry struct {
URL string
Email string
Name string
Role string
}
type userIndex struct {
Users []*userEntry
}
func (page *userIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "user/index.gohtml", page)
}

109
po/ca.po
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-16 20:32+0100\n"
"POT-Creation-Date: 2024-01-17 19:40+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"
@ -67,24 +67,24 @@ msgctxt "title"
msgid "Home"
msgstr "Inici"
#: web/templates/public/home.gohtml:24
#: web/templates/public/home.gohtml:25
msgctxt "link"
msgid "Booking"
msgstr "Reserva"
#: web/templates/public/home.gohtml:31
#: web/templates/public/home.gohtml:28
msgid "The pleasure of camping in the middle of nature…"
msgstr "El plaer dacampar en plena natura…"
#: web/templates/public/home.gohtml:43
#: web/templates/public/home.gohtml:40
msgid "Our services"
msgstr "Els nostres serveis"
#: web/templates/public/home.gohtml:45
#: web/templates/public/home.gohtml:42
msgid "Come and enjoy!"
msgstr "Vine a gaudir!"
#: web/templates/public/home.gohtml:47
#: web/templates/public/home.gohtml:44
#: web/templates/public/surroundings.gohtml:7
#: web/templates/public/surroundings.gohtml:12
#: web/templates/public/layout.gohtml:68 web/templates/public/layout.gohtml:96
@ -93,19 +93,19 @@ msgctxt "title"
msgid "Surroundings"
msgstr "Lentorn"
#: web/templates/public/home.gohtml:50
#: web/templates/public/home.gohtml:47
msgid "Located in <strong>Alta Garrotxa</strong>, between the <strong>Pyrenees</strong> and the <strong>Costa Brava</strong>."
msgstr "Situats a l<strong>Alta Garrotxa</strong>, entre els <strong>Pirineus</strong> i la <strong>Costa Brava</strong>."
#: web/templates/public/home.gohtml:51
#: web/templates/public/home.gohtml:48
msgid "Nearby there are the <strong>gorges of Sadernes</strong>, <strong>volcanoes</strong>, <strong>La Fageda den Jordà</strong>, the Jewish quarter of <strong>Besalú</strong>, the basaltic cliff of <strong>Castellfollit de la Roca</strong>… much to see and much to do."
msgstr "A prop teniu els <strong>gorgs de Sadernes</strong>, <strong>volcans</strong>, <strong>La Fageda den Jordà</strong>, el call jueu de <strong>Besalú</strong>, la cinglera basàltica de <strong>Castellfollit de la Roca</strong>… molt per veure i molt per fer."
#: web/templates/public/home.gohtml:52
#: web/templates/public/home.gohtml:49
msgid "Less than an hour from <strong>Girona</strong>, one from <strong>La Bisbal dEmpordà</strong>, and two from <strong>Barcelona</strong>."
msgstr "A menys duna hora de <strong>Girona</strong>, a una de <strong>La Bisbal dEmpordà</strong> i a dues de <strong>Barcelona</strong>."
#: web/templates/public/home.gohtml:53
#: web/templates/public/home.gohtml:50
msgid "Discover the surroundings"
msgstr "Descobreix lentorn"
@ -120,7 +120,7 @@ msgid "Check-out Date"
msgstr "Data de sortida"
#: web/templates/public/campsite/type.gohtml:54
#: web/templates/public/booking.gohtml:156
#: web/templates/public/booking.gohtml:164
msgctxt "action"
msgid "Book"
msgstr "Reserva"
@ -301,91 +301,91 @@ msgctxt "title"
msgid "Campground"
msgstr "El càmping"
#: web/templates/public/booking.gohtml:6 web/templates/public/booking.gohtml:11
#: web/templates/public/booking.gohtml:7 web/templates/public/booking.gohtml:12
#: web/templates/public/layout.gohtml:70
msgctxt "title"
msgid "Booking"
msgstr "Reserva"
#: web/templates/public/booking.gohtml:15
#: web/templates/public/booking.gohtml:16
msgctxt "title"
msgid "Customer Details"
msgstr "Detalls del client"
#: web/templates/public/booking.gohtml:18
#: web/templates/public/booking.gohtml:19
msgctxt "input"
msgid "Full name"
msgstr "Nom i cognoms"
#: web/templates/public/booking.gohtml:27
#: web/templates/public/booking.gohtml:28
msgctxt "input"
msgid "Address (optional)"
msgstr "Adreça (opcional)"
#: web/templates/public/booking.gohtml:36
#: web/templates/public/booking.gohtml:37
msgctxt "input"
msgid "Postcode (optional)"
msgstr "Codi postal (opcional)"
#: web/templates/public/booking.gohtml:45
#: web/templates/public/booking.gohtml:46
msgctxt "input"
msgid "Town or village (optional)"
msgstr "Població (opcional)"
#: web/templates/public/booking.gohtml:54
#: web/templates/public/booking.gohtml:55
#: web/templates/admin/taxDetails.gohtml:98
msgctxt "input"
msgid "Country"
msgstr "País"
#: web/templates/public/booking.gohtml:57
#: web/templates/public/booking.gohtml:58
msgid "Choose a country"
msgstr "Esculli un país"
#: web/templates/public/booking.gohtml:65 web/templates/admin/login.gohtml:22
#: web/templates/public/booking.gohtml:66 web/templates/admin/login.gohtml:22
#: web/templates/admin/profile.gohtml:35
#: web/templates/admin/taxDetails.gohtml:50
msgctxt "input"
msgid "Email"
msgstr "Correu-e"
#: web/templates/public/booking.gohtml:74
#: web/templates/public/booking.gohtml:75
#: web/templates/admin/taxDetails.gohtml:42
msgctxt "input"
msgid "Phone"
msgstr "Telèfon"
#: web/templates/public/booking.gohtml:83
#: web/templates/public/booking.gohtml:84
msgctxt "title"
msgid "Accommodation"
msgstr "Allotjaments"
#: web/templates/public/booking.gohtml:98
#: web/templates/public/booking.gohtml:99
msgctxt "input"
msgid "Area preferences (optional)"
msgstr "Preferències dàrea (opcional)"
#: web/templates/public/booking.gohtml:120
#: web/templates/public/booking.gohtml:121
msgctxt "title"
msgid "Booking Period"
msgstr "Període de reserva"
#: web/templates/public/booking.gohtml:123
#: web/templates/public/booking.gohtml:124
msgctxt "input"
msgid "Arrival date"
msgstr "Data darribada"
#: web/templates/public/booking.gohtml:132
#: web/templates/public/booking.gohtml:133
msgctxt "input"
msgid "Departure date"
msgstr "Data de sortida"
#: web/templates/public/booking.gohtml:143
#: web/templates/public/booking.gohtml:144
msgctxt "input"
msgid "ACSI card? (optional)"
msgstr "Targeta ACSI? (opcional)"
#: web/templates/public/booking.gohtml:150
#: web/templates/public/booking.gohtml:151
msgctxt "input"
msgid "I have read and I accept <a href=\"%s\" target=\"_blank\">the reservation conditions</a>"
msgstr "He llegit i accepto <a href=\"%s\" target=\"_blank\">les condicions de reserves</a>"
@ -406,7 +406,7 @@ msgstr "Menú"
#: web/templates/public/layout.gohtml:58 web/templates/public/layout.gohtml:104
#: web/templates/admin/campsite/index.gohtml:6
#: web/templates/admin/campsite/index.gohtml:12
#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:86
#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:89
msgctxt "title"
msgid "Campsites"
msgstr "Allotjaments"
@ -505,12 +505,14 @@ msgstr "Afegeix text legal"
#: web/templates/admin/campsite/option/index.gohtml:25
#: web/templates/admin/campsite/type/index.gohtml:25
#: web/templates/admin/season/index.gohtml:26
#: web/templates/admin/user/index.gohtml:17
#: web/templates/admin/surroundings/index.gohtml:26
msgctxt "header"
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."
@ -637,6 +639,7 @@ msgstr "Llegenda"
#: web/templates/admin/campsite/carousel/index.gohtml:27
#: web/templates/admin/services/index.gohtml:27
#: web/templates/admin/services/index.gohtml:72
#: web/templates/admin/user/index.gohtml:20
#: web/templates/admin/surroundings/index.gohtml:27
#: web/templates/admin/home/index.gohtml:27
#: web/templates/admin/home/index.gohtml:72
@ -653,6 +656,7 @@ msgstr "Esteu segur de voler esborrar aquesta diapositiva?"
#: web/templates/admin/campsite/carousel/index.gohtml:45
#: web/templates/admin/services/index.gohtml:44
#: web/templates/admin/services/index.gohtml:88
#: web/templates/admin/user/index.gohtml:34
#: web/templates/admin/surroundings/index.gohtml:44
#: web/templates/admin/home/index.gohtml:44
#: web/templates/admin/home/index.gohtml:89
@ -965,7 +969,7 @@ msgid "Integration"
msgstr "Integració"
#: web/templates/admin/dashboard.gohtml:6
#: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:83
#: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:86
msgctxt "title"
msgid "Dashboard"
msgstr "Tauler"
@ -1058,6 +1062,32 @@ msgctxt "input"
msgid "Language"
msgstr "Idioma"
#: web/templates/admin/user/index.gohtml:6
#: web/templates/admin/user/index.gohtml:12
#: web/templates/admin/layout.gohtml:70
msgctxt "title"
msgid "Users"
msgstr "Usuaris"
#: web/templates/admin/user/index.gohtml:11
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:19
msgctxt "header"
msgid "Role"
msgstr "Rol"
#: web/templates/admin/user/index.gohtml:24
msgid "Are you sure you wish to delete this user?"
msgstr "Esteu segur de voler esborrar aquest usuari?"
#: web/templates/admin/taxDetails.gohtml:6
#: web/templates/admin/taxDetails.gohtml:12
msgctxt "title"
@ -1197,7 +1227,7 @@ msgctxt "title"
msgid "Home Page"
msgstr "Pàgina dinici"
#: web/templates/admin/layout.gohtml:72
#: web/templates/admin/layout.gohtml:75
msgctxt "action"
msgid "Logout"
msgstr "Surt"
@ -1352,7 +1382,7 @@ msgstr "Lidioma escollit no és vàlid."
msgid "File must be a valid PNG or JPEG image."
msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida."
#: pkg/app/admin.go:64
#: pkg/app/admin.go:67
msgid "Access forbidden"
msgstr "Accés prohibit"
@ -1535,6 +1565,21 @@ msgstr "No podeu deixar la data de fi en blanc."
msgid "End date must be a valid date."
msgstr "La data de fi ha de ser una data vàlida."
#: pkg/user/admin.go:18
msgctxt "role"
msgid "guest"
msgstr "convidat"
#: pkg/user/admin.go:19
msgctxt "role"
msgid "employee"
msgstr "treballador"
#: pkg/user/admin.go:20
msgctxt "role"
msgid "admin"
msgstr "administrador"
#: pkg/surroundings/admin.go:267
msgctxt "input"
msgid "Highlight image"

109
po/es.po
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-16 20:32+0100\n"
"POT-Creation-Date: 2024-01-17 19:40+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"
@ -67,24 +67,24 @@ msgctxt "title"
msgid "Home"
msgstr "Inicio"
#: web/templates/public/home.gohtml:24
#: web/templates/public/home.gohtml:25
msgctxt "link"
msgid "Booking"
msgstr "Reservar"
#: web/templates/public/home.gohtml:31
#: web/templates/public/home.gohtml:28
msgid "The pleasure of camping in the middle of nature…"
msgstr "El placer de acampar en plena naturaleza…"
#: web/templates/public/home.gohtml:43
#: web/templates/public/home.gohtml:40
msgid "Our services"
msgstr "Nuestros servicios"
#: web/templates/public/home.gohtml:45
#: web/templates/public/home.gohtml:42
msgid "Come and enjoy!"
msgstr "¡Ven a disfrutar!"
#: web/templates/public/home.gohtml:47
#: web/templates/public/home.gohtml:44
#: web/templates/public/surroundings.gohtml:7
#: web/templates/public/surroundings.gohtml:12
#: web/templates/public/layout.gohtml:68 web/templates/public/layout.gohtml:96
@ -93,19 +93,19 @@ msgctxt "title"
msgid "Surroundings"
msgstr "El entorno"
#: web/templates/public/home.gohtml:50
#: web/templates/public/home.gohtml:47
msgid "Located in <strong>Alta Garrotxa</strong>, between the <strong>Pyrenees</strong> and the <strong>Costa Brava</strong>."
msgstr "Situados en la <strong>Alta Garrotxa</strong>, entre los <strong>Pirineos</strong> y la <strong>Costa Brava</strong>."
#: web/templates/public/home.gohtml:51
#: web/templates/public/home.gohtml:48
msgid "Nearby there are the <strong>gorges of Sadernes</strong>, <strong>volcanoes</strong>, <strong>La Fageda den Jordà</strong>, the Jewish quarter of <strong>Besalú</strong>, the basaltic cliff of <strong>Castellfollit de la Roca</strong>… much to see and much to do."
msgstr "Cerca tenéis los <strong>piletones de Sadernes</strong>, <strong>volcanes</strong>, <strong>La Fageda den Jordà</strong>, la judería de <strong>Besalú</strong>, el riscal basáltico de <strong>Castellfollit de la Roca</strong>… mucho por ver y mucho por hacer."
#: web/templates/public/home.gohtml:52
#: web/templates/public/home.gohtml:49
msgid "Less than an hour from <strong>Girona</strong>, one from <strong>La Bisbal dEmpordà</strong>, and two from <strong>Barcelona</strong>."
msgstr "A menos de una hora de <strong>Girona</strong>, a una de <strong>La Bisbal dEmpordà</strong> y a dos de <strong>Barcelona</strong>."
#: web/templates/public/home.gohtml:53
#: web/templates/public/home.gohtml:50
msgid "Discover the surroundings"
msgstr "Descubre el entorno"
@ -120,7 +120,7 @@ msgid "Check-out Date"
msgstr "Fecha de salida"
#: web/templates/public/campsite/type.gohtml:54
#: web/templates/public/booking.gohtml:156
#: web/templates/public/booking.gohtml:164
msgctxt "action"
msgid "Book"
msgstr "Reservar"
@ -301,91 +301,91 @@ msgctxt "title"
msgid "Campground"
msgstr "El camping"
#: web/templates/public/booking.gohtml:6 web/templates/public/booking.gohtml:11
#: web/templates/public/booking.gohtml:7 web/templates/public/booking.gohtml:12
#: web/templates/public/layout.gohtml:70
msgctxt "title"
msgid "Booking"
msgstr "Reserva"
#: web/templates/public/booking.gohtml:15
#: web/templates/public/booking.gohtml:16
msgctxt "title"
msgid "Customer Details"
msgstr "Detalles del cliente"
#: web/templates/public/booking.gohtml:18
#: web/templates/public/booking.gohtml:19
msgctxt "input"
msgid "Full name"
msgstr "Nombre y apellidos"
#: web/templates/public/booking.gohtml:27
#: web/templates/public/booking.gohtml:28
msgctxt "input"
msgid "Address (optional)"
msgstr "Dirección (opcional)"
#: web/templates/public/booking.gohtml:36
#: web/templates/public/booking.gohtml:37
msgctxt "input"
msgid "Postcode (optional)"
msgstr "Código postal (opcional)"
#: web/templates/public/booking.gohtml:45
#: web/templates/public/booking.gohtml:46
msgctxt "input"
msgid "Town or village (optional)"
msgstr "Población (opcional)"
#: web/templates/public/booking.gohtml:54
#: web/templates/public/booking.gohtml:55
#: web/templates/admin/taxDetails.gohtml:98
msgctxt "input"
msgid "Country"
msgstr "País"
#: web/templates/public/booking.gohtml:57
#: web/templates/public/booking.gohtml:58
msgid "Choose a country"
msgstr "Escoja un país"
#: web/templates/public/booking.gohtml:65 web/templates/admin/login.gohtml:22
#: web/templates/public/booking.gohtml:66 web/templates/admin/login.gohtml:22
#: web/templates/admin/profile.gohtml:35
#: web/templates/admin/taxDetails.gohtml:50
msgctxt "input"
msgid "Email"
msgstr "Correo-e"
#: web/templates/public/booking.gohtml:74
#: web/templates/public/booking.gohtml:75
#: web/templates/admin/taxDetails.gohtml:42
msgctxt "input"
msgid "Phone"
msgstr "Teléfono"
#: web/templates/public/booking.gohtml:83
#: web/templates/public/booking.gohtml:84
msgctxt "title"
msgid "Accommodation"
msgstr "Alojamientos"
#: web/templates/public/booking.gohtml:98
#: web/templates/public/booking.gohtml:99
msgctxt "input"
msgid "Area preferences (optional)"
msgstr "Preferencias de área (opcional)"
#: web/templates/public/booking.gohtml:120
#: web/templates/public/booking.gohtml:121
msgctxt "title"
msgid "Booking Period"
msgstr "Periodo de reserva"
#: web/templates/public/booking.gohtml:123
#: web/templates/public/booking.gohtml:124
msgctxt "input"
msgid "Arrival date"
msgstr "Fecha de llegada"
#: web/templates/public/booking.gohtml:132
#: web/templates/public/booking.gohtml:133
msgctxt "input"
msgid "Departure date"
msgstr "Fecha de salida"
#: web/templates/public/booking.gohtml:143
#: web/templates/public/booking.gohtml:144
msgctxt "input"
msgid "ACSI card? (optional)"
msgstr "¿Tarjeta ACSI? (opcional)"
#: web/templates/public/booking.gohtml:150
#: web/templates/public/booking.gohtml:151
msgctxt "input"
msgid "I have read and I accept <a href=\"%s\" target=\"_blank\">the reservation conditions</a>"
msgstr "He leído y acepto <a href=\"%s\" target=\"_blank\">las condiciones de reserva</a>"
@ -406,7 +406,7 @@ msgstr "Menú"
#: web/templates/public/layout.gohtml:58 web/templates/public/layout.gohtml:104
#: web/templates/admin/campsite/index.gohtml:6
#: web/templates/admin/campsite/index.gohtml:12
#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:86
#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:89
msgctxt "title"
msgid "Campsites"
msgstr "Alojamientos"
@ -505,12 +505,14 @@ msgstr "Añadir texto legal"
#: web/templates/admin/campsite/option/index.gohtml:25
#: web/templates/admin/campsite/type/index.gohtml:25
#: web/templates/admin/season/index.gohtml:26
#: web/templates/admin/user/index.gohtml:17
#: web/templates/admin/surroundings/index.gohtml:26
msgctxt "header"
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."
@ -637,6 +639,7 @@ msgstr "Leyenda"
#: web/templates/admin/campsite/carousel/index.gohtml:27
#: web/templates/admin/services/index.gohtml:27
#: web/templates/admin/services/index.gohtml:72
#: web/templates/admin/user/index.gohtml:20
#: web/templates/admin/surroundings/index.gohtml:27
#: web/templates/admin/home/index.gohtml:27
#: web/templates/admin/home/index.gohtml:72
@ -653,6 +656,7 @@ msgstr "¿Estáis seguro de querer borrar esta diapositiva?"
#: web/templates/admin/campsite/carousel/index.gohtml:45
#: web/templates/admin/services/index.gohtml:44
#: web/templates/admin/services/index.gohtml:88
#: web/templates/admin/user/index.gohtml:34
#: web/templates/admin/surroundings/index.gohtml:44
#: web/templates/admin/home/index.gohtml:44
#: web/templates/admin/home/index.gohtml:89
@ -965,7 +969,7 @@ msgid "Integration"
msgstr "Integración"
#: web/templates/admin/dashboard.gohtml:6
#: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:83
#: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:86
msgctxt "title"
msgid "Dashboard"
msgstr "Panel"
@ -1058,6 +1062,32 @@ msgctxt "input"
msgid "Language"
msgstr "Idioma"
#: web/templates/admin/user/index.gohtml:6
#: web/templates/admin/user/index.gohtml:12
#: web/templates/admin/layout.gohtml:70
msgctxt "title"
msgid "Users"
msgstr ""
#: 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:19
msgctxt "header"
msgid "Role"
msgstr "Rol"
#: web/templates/admin/user/index.gohtml:24
msgid "Are you sure you wish to delete this user?"
msgstr "¿Estáis seguro de querer borrar este usuario?"
#: web/templates/admin/taxDetails.gohtml:6
#: web/templates/admin/taxDetails.gohtml:12
msgctxt "title"
@ -1197,7 +1227,7 @@ msgctxt "title"
msgid "Home Page"
msgstr "Página de inicio"
#: web/templates/admin/layout.gohtml:72
#: web/templates/admin/layout.gohtml:75
msgctxt "action"
msgid "Logout"
msgstr "Salir"
@ -1352,7 +1382,7 @@ msgstr "El idioma escogido no es válido."
msgid "File must be a valid PNG or JPEG image."
msgstr "El archivo tiene que ser una imagen PNG o JPEG válida."
#: pkg/app/admin.go:64
#: pkg/app/admin.go:67
msgid "Access forbidden"
msgstr "Acceso prohibido"
@ -1535,6 +1565,21 @@ msgstr "No podéis dejar la fecha final en blanco."
msgid "End date must be a valid date."
msgstr "La fecha final tiene que ser una fecha válida."
#: pkg/user/admin.go:18
msgctxt "role"
msgid "guest"
msgstr "invitado"
#: pkg/user/admin.go:19
msgctxt "role"
msgid "employee"
msgstr "trabajador"
#: pkg/user/admin.go:20
msgctxt "role"
msgid "admin"
msgstr "administrador"
#: pkg/surroundings/admin.go:267
msgctxt "input"
msgid "Highlight image"

109
po/fr.po
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-16 20:32+0100\n"
"POT-Creation-Date: 2024-01-17 19:40+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"
@ -68,24 +68,24 @@ msgctxt "title"
msgid "Home"
msgstr "Accueil"
#: web/templates/public/home.gohtml:24
#: web/templates/public/home.gohtml:25
msgctxt "link"
msgid "Booking"
msgstr "Réservation"
#: web/templates/public/home.gohtml:31
#: web/templates/public/home.gohtml:28
msgid "The pleasure of camping in the middle of nature…"
msgstr "Le plaisir de camper en pleine nature…"
#: web/templates/public/home.gohtml:43
#: web/templates/public/home.gohtml:40
msgid "Our services"
msgstr "Nos services"
#: web/templates/public/home.gohtml:45
#: web/templates/public/home.gohtml:42
msgid "Come and enjoy!"
msgstr "Venez et profitez-en !"
#: web/templates/public/home.gohtml:47
#: web/templates/public/home.gohtml:44
#: web/templates/public/surroundings.gohtml:7
#: web/templates/public/surroundings.gohtml:12
#: web/templates/public/layout.gohtml:68 web/templates/public/layout.gohtml:96
@ -94,19 +94,19 @@ msgctxt "title"
msgid "Surroundings"
msgstr "Entourage"
#: web/templates/public/home.gohtml:50
#: web/templates/public/home.gohtml:47
msgid "Located in <strong>Alta Garrotxa</strong>, between the <strong>Pyrenees</strong> and the <strong>Costa Brava</strong>."
msgstr "Situé dans <strong>lAlta Garrotxa</strong>, entre les <strong>Pyrénées</strong> et la <strong>Costa Brava</strong>."
#: web/templates/public/home.gohtml:51
#: web/templates/public/home.gohtml:48
msgid "Nearby there are the <strong>gorges of Sadernes</strong>, <strong>volcanoes</strong>, <strong>La Fageda den Jordà</strong>, the Jewish quarter of <strong>Besalú</strong>, the basaltic cliff of <strong>Castellfollit de la Roca</strong>… much to see and much to do."
msgstr "A proximité il y a les <strong>gorges de Sadernes</strong>, les <strong>volcans</strong>, <strong>La Fageda dâen Jordã </strong>, le quartier juif de <strong>Besalú</strong>, la falaise basaltique de <strong>Castellfollit de la Roca</strong>… beaucoup à voir et beaucoup à faire."
#: web/templates/public/home.gohtml:52
#: web/templates/public/home.gohtml:49
msgid "Less than an hour from <strong>Girona</strong>, one from <strong>La Bisbal dEmpordà</strong>, and two from <strong>Barcelona</strong>."
msgstr "À moins dune heure de <strong>Gérone</strong>, un de <strong>La Bisbal dEmpordà</strong>et deux de <strong>Barcelone</strong>."
#: web/templates/public/home.gohtml:53
#: web/templates/public/home.gohtml:50
msgid "Discover the surroundings"
msgstr "Découvrir les environs"
@ -121,7 +121,7 @@ msgid "Check-out Date"
msgstr "Date de départ"
#: web/templates/public/campsite/type.gohtml:54
#: web/templates/public/booking.gohtml:156
#: web/templates/public/booking.gohtml:164
msgctxt "action"
msgid "Book"
msgstr "Réserver"
@ -302,91 +302,91 @@ msgctxt "title"
msgid "Campground"
msgstr "Camping"
#: web/templates/public/booking.gohtml:6 web/templates/public/booking.gohtml:11
#: web/templates/public/booking.gohtml:7 web/templates/public/booking.gohtml:12
#: web/templates/public/layout.gohtml:70
msgctxt "title"
msgid "Booking"
msgstr "Reservation"
#: web/templates/public/booking.gohtml:15
#: web/templates/public/booking.gohtml:16
msgctxt "title"
msgid "Customer Details"
msgstr "Détails du client"
#: web/templates/public/booking.gohtml:18
#: web/templates/public/booking.gohtml:19
msgctxt "input"
msgid "Full name"
msgstr "Nom et prénom"
#: web/templates/public/booking.gohtml:27
#: web/templates/public/booking.gohtml:28
msgctxt "input"
msgid "Address (optional)"
msgstr "Adresse (Facultatif)"
#: web/templates/public/booking.gohtml:36
#: web/templates/public/booking.gohtml:37
msgctxt "input"
msgid "Postcode (optional)"
msgstr "Code postal (Facultatif)"
#: web/templates/public/booking.gohtml:45
#: web/templates/public/booking.gohtml:46
msgctxt "input"
msgid "Town or village (optional)"
msgstr "Ville (Facultatif)"
#: web/templates/public/booking.gohtml:54
#: web/templates/public/booking.gohtml:55
#: web/templates/admin/taxDetails.gohtml:98
msgctxt "input"
msgid "Country"
msgstr "Pays"
#: web/templates/public/booking.gohtml:57
#: web/templates/public/booking.gohtml:58
msgid "Choose a country"
msgstr "Choisissez un pays"
#: web/templates/public/booking.gohtml:65 web/templates/admin/login.gohtml:22
#: web/templates/public/booking.gohtml:66 web/templates/admin/login.gohtml:22
#: web/templates/admin/profile.gohtml:35
#: web/templates/admin/taxDetails.gohtml:50
msgctxt "input"
msgid "Email"
msgstr "E-mail"
#: web/templates/public/booking.gohtml:74
#: web/templates/public/booking.gohtml:75
#: web/templates/admin/taxDetails.gohtml:42
msgctxt "input"
msgid "Phone"
msgstr "Téléphone"
#: web/templates/public/booking.gohtml:83
#: web/templates/public/booking.gohtml:84
msgctxt "title"
msgid "Accommodation"
msgstr "Hébergement"
#: web/templates/public/booking.gohtml:98
#: web/templates/public/booking.gohtml:99
msgctxt "input"
msgid "Area preferences (optional)"
msgstr "Préférences de zone (facultatif)"
#: web/templates/public/booking.gohtml:120
#: web/templates/public/booking.gohtml:121
msgctxt "title"
msgid "Booking Period"
msgstr "Période de réservation"
#: web/templates/public/booking.gohtml:123
#: web/templates/public/booking.gohtml:124
msgctxt "input"
msgid "Arrival date"
msgstr "Date darrivée"
#: web/templates/public/booking.gohtml:132
#: web/templates/public/booking.gohtml:133
msgctxt "input"
msgid "Departure date"
msgstr "Date de depart"
#: web/templates/public/booking.gohtml:143
#: web/templates/public/booking.gohtml:144
msgctxt "input"
msgid "ACSI card? (optional)"
msgstr "Carte ACSI ? (Facultatif)"
#: web/templates/public/booking.gohtml:150
#: web/templates/public/booking.gohtml:151
msgctxt "input"
msgid "I have read and I accept <a href=\"%s\" target=\"_blank\">the reservation conditions</a>"
msgstr "Jai lu et jaccepte <a href=\"%s\" target=\"_blank\">les conditions de réservation</a>"
@ -407,7 +407,7 @@ msgstr "Menu"
#: web/templates/public/layout.gohtml:58 web/templates/public/layout.gohtml:104
#: web/templates/admin/campsite/index.gohtml:6
#: web/templates/admin/campsite/index.gohtml:12
#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:86
#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:89
msgctxt "title"
msgid "Campsites"
msgstr "Locatifs"
@ -506,12 +506,14 @@ msgstr "Ajouter un texte juridique"
#: web/templates/admin/campsite/option/index.gohtml:25
#: web/templates/admin/campsite/type/index.gohtml:25
#: web/templates/admin/season/index.gohtml:26
#: web/templates/admin/user/index.gohtml:17
#: web/templates/admin/surroundings/index.gohtml:26
msgctxt "header"
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."
@ -638,6 +640,7 @@ msgstr "Légende"
#: web/templates/admin/campsite/carousel/index.gohtml:27
#: web/templates/admin/services/index.gohtml:27
#: web/templates/admin/services/index.gohtml:72
#: web/templates/admin/user/index.gohtml:20
#: web/templates/admin/surroundings/index.gohtml:27
#: web/templates/admin/home/index.gohtml:27
#: web/templates/admin/home/index.gohtml:72
@ -654,6 +657,7 @@ msgstr "Êtes-vous sûr de vouloir supprimer cette diapositive ?"
#: web/templates/admin/campsite/carousel/index.gohtml:45
#: web/templates/admin/services/index.gohtml:44
#: web/templates/admin/services/index.gohtml:88
#: web/templates/admin/user/index.gohtml:34
#: web/templates/admin/surroundings/index.gohtml:44
#: web/templates/admin/home/index.gohtml:44
#: web/templates/admin/home/index.gohtml:89
@ -966,7 +970,7 @@ msgid "Integration"
msgstr "Intégration"
#: web/templates/admin/dashboard.gohtml:6
#: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:83
#: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:86
msgctxt "title"
msgid "Dashboard"
msgstr "Tableau de bord"
@ -1059,6 +1063,32 @@ msgctxt "input"
msgid "Language"
msgstr "Langue"
#: web/templates/admin/user/index.gohtml:6
#: web/templates/admin/user/index.gohtml:12
#: web/templates/admin/layout.gohtml:70
msgctxt "title"
msgid "Users"
msgstr "Utilisateurs"
#: web/templates/admin/user/index.gohtml:11
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:19
msgctxt "header"
msgid "Role"
msgstr "Rôle"
#: web/templates/admin/user/index.gohtml:24
msgid "Are you sure you wish to delete this user?"
msgstr "Êtes-vous sûr de vouloir supprimer ce utilisateur ?"
#: web/templates/admin/taxDetails.gohtml:6
#: web/templates/admin/taxDetails.gohtml:12
msgctxt "title"
@ -1198,7 +1228,7 @@ msgctxt "title"
msgid "Home Page"
msgstr "Page d'accueil"
#: web/templates/admin/layout.gohtml:72
#: web/templates/admin/layout.gohtml:75
msgctxt "action"
msgid "Logout"
msgstr "Déconnexion"
@ -1353,7 +1383,7 @@ msgstr "La langue sélectionnée nest pas valide."
msgid "File must be a valid PNG or JPEG image."
msgstr "Le fichier doit être une image PNG ou JPEG valide."
#: pkg/app/admin.go:64
#: pkg/app/admin.go:67
msgid "Access forbidden"
msgstr "Accès interdit"
@ -1536,6 +1566,21 @@ msgstr "La date de fin ne peut pas être vide."
msgid "End date must be a valid date."
msgstr "La date de fin doit être une date valide."
#: pkg/user/admin.go:18
msgctxt "role"
msgid "guest"
msgstr "invité"
#: pkg/user/admin.go:19
msgctxt "role"
msgid "employee"
msgstr "employé"
#: pkg/user/admin.go:20
msgctxt "role"
msgid "admin"
msgstr "administrateur"
#: pkg/surroundings/admin.go:267
msgctxt "input"
msgid "Highlight image"

View File

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

View File

@ -154,3 +154,4 @@ add_cover_carousel_slide [roles schema_camper cover_carousel] 2024-01-16T17:49:2
translate_cover_carousel_slide [roles schema_camper cover_carousel_i18n] 2024-01-16T18:17:36Z jordi fita mas <jordi@tandem.blog> # Add function to translate a cover carousel slider
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

View File

@ -0,0 +1,113 @@
-- Test company_user_profile
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(37);
set search_path to camper, auth, public;
select has_view('company_user_profile');
select table_privs_are('company_user_profile', 'guest', array []::text[]);
select table_privs_are('company_user_profile', 'employee', array []::text[]);
select table_privs_are('company_user_profile', 'admin', array ['SELECT']);
select table_privs_are('company_user_profile', 'authenticator', array []::text[]);
select has_column('company_user_profile', 'user_id');
select col_type_is('company_user_profile', 'user_id', 'integer');
select column_privs_are('company_user_profile', 'user_id', 'guest', array []::text[]);
select column_privs_are('company_user_profile', 'user_id', 'employee', array []::text[]);
select column_privs_are('company_user_profile', 'user_id', 'admin', array ['SELECT']);
select column_privs_are('company_user_profile', 'user_id', 'authenticator', array []::text[]);
select has_column('company_user_profile', 'email');
select col_type_is('company_user_profile', 'email', 'email');
select column_privs_are('company_user_profile', 'email', 'guest', array []::text[]);
select column_privs_are('company_user_profile', 'email', 'employee', array []::text[]);
select column_privs_are('company_user_profile', 'email', 'admin', array ['SELECT']);
select column_privs_are('company_user_profile', 'email', 'authenticator', array []::text[]);
select has_column('company_user_profile', 'name');
select col_type_is('company_user_profile', 'name', 'text');
select column_privs_are('company_user_profile', 'name', 'guest', array []::text[]);
select column_privs_are('company_user_profile', 'name', 'employee', array []::text[]);
select column_privs_are('company_user_profile', 'name', 'admin', array ['SELECT']);
select column_privs_are('company_user_profile', 'name', 'authenticator', array []::text[]);
select has_column('company_user_profile', 'role');
select col_type_is('company_user_profile', 'role', 'name');
select column_privs_are('company_user_profile', 'role', 'guest', array []::text[]);
select column_privs_are('company_user_profile', 'role', 'employee', array []::text[]);
select column_privs_are('company_user_profile', 'role', 'admin', array ['SELECT']);
select column_privs_are('company_user_profile', 'role', 'authenticator', array []::text[]);
select has_column('company_user_profile', 'lang_tag');
select col_type_is('company_user_profile', 'lang_tag', 'text');
select column_privs_are('company_user_profile', 'lang_tag', 'guest', array []::text[]);
select column_privs_are('company_user_profile', 'lang_tag', 'employee', array []::text[]);
select column_privs_are('company_user_profile', 'lang_tag', 'admin', array ['SELECT']);
select column_privs_are('company_user_profile', 'lang_tag', 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate company_host cascade;
truncate company_user cascade;
truncate company cascade;
truncate auth."user" 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 profile as
select user_id, email::text, name, role::text, lang_tag
from company_user_profile
;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2');
select bag_eq(
'profile',
$$ values (1, 'demo@tandem.blog', 'Demo', 'admin', 'ca') $$,
'Should only see profiles from the first company'
);
reset role;
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog', 'co4');
select bag_eq(
'profile',
$$ values (5, 'admin@tandem.blog', 'Admin', 'admin', 'es')
, (7, 'another@tandem.blog', 'Another Employee', 'employee', 'und')
$$,
'Should only see profiles from the second company'
);
reset role;
select *
from finish();
rollback;

View File

@ -0,0 +1,13 @@
-- Verify camper:company_user_profile on pg
begin;
select user_id
, email
, name
, role
, lang_tag
from camper.company_user_profile
where false;
rollback;

View File

@ -66,6 +66,9 @@
<li>
<a href="/admin/legal">{{( pgettext "Legal Texts" "title" )}}</a>
</li>
<li>
<a href="/admin/users">{{( pgettext "Users" "title" )}}</a>
</li>
{{- end }}
<li class="icon_logout">
<button data-hx-delete="/me/session" data-hx-headers='{ {{ CSRFHeader }} }'

View File

@ -0,0 +1,44 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{( pgettext "Users" "title" )}}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/user.userIndex*/ -}}
<a href="/admin/users/new">{{( pgettext "Add User" "action" )}}</a>
<h2>{{( pgettext "Users" "title" )}}</h2>
{{ if .Users -}}
<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>
<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 }}