Allow guest access to user_profile with an empty profile

I want this so that the Go application does not need to know the exact
details of the settings that the database sets when applying the cookie;
it just needs to select from the user_profile that already knows this.

Also, that way i can get the user’s language from its profile with a
single select, without having to check whether we are guest or
authenticated.

With that, i can skip the content negotiation if the user already told
us what language they want.
This commit is contained in:
jordi fita mas 2023-01-23 01:18:47 +01:00
parent b5968b1179
commit c84f3f9e80
8 changed files with 64 additions and 20 deletions

View File

@ -17,8 +17,33 @@ select user_id
, lang_tag
from auth."user"
where cookie = current_app_user()
and cookie_expires_at > current_timestamp
and length(cookie) > 30
union all
select 0
, null::email
, ''
, 'guest'::name
, 'und'
where not exists (
select 1
from auth."user"
where cookie = current_app_user()
and cookie_expires_at > current_timestamp
and length(cookie) > 30
);
create rule update_user_profile as on update to user_profile
do instead update auth."user"
set email = new.email
, name = new.name
, lang_tag = new.lang_tag
where cookie = current_app_user()
and cookie_expires_at > current_timestamp
and length(cookie) > 30
;
grant select on table user_profile to guest;
grant select, update(email, name, lang_tag) on table user_profile to invoicer;
grant select, update(email, name, lang_tag) on table user_profile to admin;

View File

@ -24,14 +24,18 @@ func Locale(db *Db, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var locale *gotext.Locale
t, _, err := language.ParseAcceptLanguage(r.Header.Get("Accept-Language"))
if err == nil {
tag, _, _ := matcher.Match(t...)
var ok bool
locale, ok = locales[tag]
for !ok && !tag.IsRoot() {
tag = tag.Parent()
user := getUser(r)
locale = locales[user.Language]
if locale == nil {
t, _, err := language.ParseAcceptLanguage(r.Header.Get("Accept-Language"))
if err == nil {
tag, _, _ := matcher.Match(t...)
var ok bool
locale, ok = locales[tag]
for !ok && !tag.IsRoot() {
tag = tag.Parent()
locale, ok = locales[tag]
}
}
}
if locale == nil {

View File

@ -5,6 +5,8 @@ import (
"net"
"net/http"
"time"
"golang.org/x/text/language"
)
const (
@ -25,6 +27,7 @@ type AppUser struct {
Email string
LoggedIn bool
Role string
Language language.Tag
}
func LoginHandler() http.Handler {
@ -103,11 +106,13 @@ func CheckLogin(db *Db, next http.Handler) http.Handler {
LoggedIn: false,
Role: defaultRole,
}
row := conn.QueryRow(ctx, "select current_setting('request.user.email', true), current_user")
if err := row.Scan(&user.Email, &user.Role); err != nil {
row := conn.QueryRow(ctx, "select coalesce(email, ''), role, lang_tag from user_profile")
var langTag string
if err := row.Scan(&user.Email, &user.Role, &langTag); err != nil {
panic(err)
}
user.LoggedIn = user.Email != ""
user.Language, _ = language.Parse(langTag)
ctx = context.WithValue(ctx, ContextUserKey, user)
next.ServeHTTP(w, r.WithContext(ctx))

View File

@ -42,6 +42,8 @@ func ProfileHandler() http.Handler {
page.PasswordConfirm = r.FormValue("password_confirm")
page.Language = r.FormValue("language")
conn.MustExec(r.Context(), "update user_profile set name = $1, email = $2, lang_tag = $3", page.Name, page.Email, page.Language)
http.Redirect(w, r, "/profile", http.StatusSeeOther);
return;
} else {
if err := conn.QueryRow(r.Context(), "select name, lang_tag from user_profile").Scan(&page.Name, &page.Language); err != nil {
panic(nil)

View File

@ -2,6 +2,6 @@
begin;
delete from numerus.language;
delete from public.language;
commit;

View File

@ -16,8 +16,8 @@ encrypt_password [schema_auth user extension_pgcrypto] 2023-01-13T00:14:30Z jord
login_attempt [schema_auth] 2023-01-17T14:05:49Z jordi fita mas <jordi@tandem.blog> # Add table to log login attempts
login [roles schema_numerus schema_auth extension_pgcrypto email user login_attempt] 2023-01-13T00:32:32Z jordi fita mas <jordi@tandem.blog> # Add function to login
check_cookie [schema_public user] 2023-01-17T17:48:49Z jordi fita mas <jordi@tandem.blog> # Add function to check if a user cookie is valid
logout [schema_auth user] 2023-01-17T19:10:21Z jordi fita mas <jordi@tandem.blog> # Add function to logout
set_cookie [schema_public check_cookie] 2023-01-19T11:00:22Z jordi fita mas <jordi@tandem.blog> # Add function to set the role based on the cookie
current_app_user [schema_numerus] 2023-01-21T20:16:28Z jordi fita mas <jordi@tandem.blog> # Add function to get the ID of the current Numerus user
logout [schema_auth current_app_user user] 2023-01-17T19:10:21Z jordi fita mas <jordi@tandem.blog> # Add function to logout
set_cookie [schema_public check_cookie] 2023-01-19T11:00:22Z jordi fita mas <jordi@tandem.blog> # Add function to set the role based on the cookie
available_languages [schema_numerus language] 2023-01-21T21:11:08Z jordi fita mas <jordi@tandem.blog> # Add the initial available languages
user_profile [schema_numerus user current_app_user] 2023-01-21T23:18:20Z jordi fita mas <jordi@tandem.blog> # Add view for user profile

View File

@ -12,7 +12,7 @@ set search_path to numerus, public;
select has_domain('email');
select domain_type_is('email', 'citext');
select lives_ok($$ SELECT 'test@tandem.com'::email $$, 'Should be able to cast strings to email');
select lives_ok($$ select 'test@tandem.com'::email $$, 'Should be able to cast strings to email');
select throws_ok(
$$ SELECT 'test@tandem,,co.uk'::email $$,

View File

@ -10,42 +10,42 @@ select plan(47);
set search_path to numerus, auth, public;
select has_view('user_profile');
select table_privs_are('user_profile', 'guest', array []::text[]);
select table_privs_are('user_profile', 'guest', array ['SELECT']);
select table_privs_are('user_profile', 'invoicer', array['SELECT']);
select table_privs_are('user_profile', 'admin', array['SELECT']);
select table_privs_are('user_profile', 'authenticator', array[]::text[]);
select has_column('user_profile', 'user_id');
select col_type_is('user_profile', 'user_id', 'integer');
select column_privs_are('user_profile', 'user_id', 'guest', array []::text[]);
select column_privs_are('user_profile', 'user_id', 'guest', array ['SELECT']);
select column_privs_are('user_profile', 'user_id', 'invoicer', array['SELECT']);
select column_privs_are('user_profile', 'user_id', 'admin', array['SELECT']);
select column_privs_are('user_profile', 'user_id', 'authenticator', array[]::text[]);
select has_column('user_profile', 'email');
select col_type_is('user_profile', 'email', 'email');
select column_privs_are('user_profile', 'email', 'guest', array []::text[]);
select column_privs_are('user_profile', 'email', 'guest', array ['SELECT']);
select column_privs_are('user_profile', 'email', 'invoicer', array['SELECT', 'UPDATE']);
select column_privs_are('user_profile', 'email', 'admin', array['SELECT', 'UPDATE']);
select column_privs_are('user_profile', 'email', 'authenticator', array[]::text[]);
select has_column('user_profile', 'name');
select col_type_is('user_profile', 'name', 'text');
select column_privs_are('user_profile', 'name', 'guest', array []::text[]);
select column_privs_are('user_profile', 'name', 'guest', array ['SELECT']);
select column_privs_are('user_profile', 'name', 'invoicer', array['SELECT', 'UPDATE']);
select column_privs_are('user_profile', 'name', 'admin', array['SELECT', 'UPDATE']);
select column_privs_are('user_profile', 'name', 'authenticator', array[]::text[]);
select has_column('user_profile', 'role');
select col_type_is('user_profile', 'role', 'name');
select column_privs_are('user_profile', 'role', 'guest', array []::text[]);
select column_privs_are('user_profile', 'role', 'guest', array ['SELECT']);
select column_privs_are('user_profile', 'role', 'invoicer', array['SELECT']);
select column_privs_are('user_profile', 'role', 'admin', array['SELECT']);
select column_privs_are('user_profile', 'role', 'authenticator', array[]::text[]);
select has_column('user_profile', 'lang_tag');
select col_type_is('user_profile', 'lang_tag', 'text');
select column_privs_are('user_profile', 'lang_tag', 'guest', array []::text[]);
select column_privs_are('user_profile', 'lang_tag', 'guest', array ['SELECT']);
select column_privs_are('user_profile', 'lang_tag', 'invoicer', array['SELECT', 'UPDATE']);
select column_privs_are('user_profile', 'lang_tag', 'admin', array['SELECT', 'UPDATE']);
select column_privs_are('user_profile', 'lang_tag', 'authenticator', array[]::text[]);
@ -58,13 +58,20 @@ reset client_min_messages;
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at, lang_tag)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month', 'ca')
, (5, 'admin@tandem.blog', 'Admin', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month', 'es')
, (7, 'another@tandem.blog', 'Another Admin', 'test', 'admin', default, default, default)
;
prepare profile as
select user_id, email, name, role, lang_tag
from user_profile;
select is_empty( 'profile', 'Should be empty when no user is logger in' );
select set_config('request.user.cookie', '', false);
select results_eq(
'profile',
$$ values (0, null::email, '', 'guest'::name, 'und') $$,
'Should be set up with the guest user when no user logged in yet.'
);
select set_cookie( '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog' );
@ -144,6 +151,7 @@ select results_eq(
$$ select user_id, email, name, lang_tag from auth."user" order by user_id $$,
$$ values (1, 'demo+update@tandem.blog'::email, 'Demo Update', 'es')
, (5, 'admin+update@tandem.blog'::email, 'Admin Update', 'ca')
, (7, 'another@tandem.blog'::email, 'Another Admin', 'und')
$$,
'Should have updated the base tables data'
);