Compare commits

..

No commits in common. "d9c93b8797adb31e454652d5a2de87ff6bf06ec5" and "c7e34cc48833eb4331df534adf622fe96a0cb49f" have entirely different histories.

37 changed files with 225 additions and 670 deletions

View File

@ -1,24 +0,0 @@
-- Deploy numerus:build_cookie to pg
-- requires: schema_numerus
-- requires: current_user_email
-- requires: current_user_cookie
begin;
set search_path to numerus, public;
create or replace function build_cookie(user_email email default null, user_cookie text default null) returns text as
$$
select coalesce(user_cookie, current_user_cookie()) || '/' || coalesce(user_email, current_user_email());
$$
language sql
stable;
revoke execute on function build_cookie(email, text) from public;
grant execute on function build_cookie(email, text) to invoicer;
grant execute on function build_cookie(email, text) to admin;
comment on function build_cookie(email, text) is
'Build the cookie to send to the users browser, either for the given values or for the current user.';
commit;

View File

@ -1,28 +0,0 @@
-- Deploy numerus:change_password to pg
-- requires: schema_numerus
-- requires: user
begin;
set search_path to numerus, auth, public;
create or replace function change_password(new_password text) returns void as
$$
update "user"
set password = new_password
where email = current_user_email()
and cookie = current_user_cookie()
and cookie_expires_at > current_timestamp
and length(cookie) > 30
$$ language sql
security definer
set search_path to auth, numerus, pg_temp;
revoke execute on function change_password(text) from public;
grant execute on function change_password(text) to invoicer;
grant execute on function change_password(text) to admin;
comment on function change_password(text) is
'Changes the password for the current app user';
commit;

View File

@ -28,6 +28,7 @@ begin
user_cookie := ''; user_cookie := '';
user_role := 'guest'::name; user_role := 'guest'::name;
end if; end if;
perform set_config('request.user.id', uid, false);
perform set_config('request.user.email', user_email, false); perform set_config('request.user.email', user_email, false);
perform set_config('request.user.cookie', user_cookie, false); perform set_config('request.user.cookie', user_cookie, false);
return user_role; return user_role;
@ -39,7 +40,7 @@ stable
set search_path = auth, numerus, pg_temp; set search_path = auth, numerus, pg_temp;
comment on function check_cookie(text) is comment on function check_cookie(text) is
'Checks whether a given cookie is for a valid users, returning their role, and setting current_user_email and current_user_cookie'; 'Checks whether a given cookie is for a valid users, returning its email and role';
revoke execute on function check_cookie(text) from public; revoke execute on function check_cookie(text) from public;
grant execute on function check_cookie(text) to authenticator; grant execute on function check_cookie(text) to authenticator;

View File

@ -0,0 +1,23 @@
-- Deploy numerus:current_app_user to pg
-- requires: schema_numerus
begin;
set search_path to numerus;
create or replace function current_app_user() returns text as
$$
select current_setting('request.user.cookie', true);
$$
language sql
stable;
comment on function current_app_user() is
'Returns the ID of the current Numerus user';
revoke execute on function current_app_user() from public;
grant execute on function current_app_user() to guest;
grant execute on function current_app_user() to invoicer;
grant execute on function current_app_user() to admin;
commit;

View File

@ -1,23 +0,0 @@
-- Deploy numerus:current_user_cookie to pg
-- requires: schema_numerus
begin;
set search_path to numerus;
create or replace function current_user_cookie() returns text as
$$
select current_setting('request.user.cookie', true);
$$
language sql
stable;
comment on function current_user_cookie() is
'Returns the cookie of the current Numerus user';
revoke execute on function current_user_cookie() from public;
grant execute on function current_user_cookie() to guest;
grant execute on function current_user_cookie() to invoicer;
grant execute on function current_user_cookie() to admin;
commit;

View File

@ -1,23 +0,0 @@
-- Deploy numerus:current_user_email to pg
-- requires: schema_numerus
begin;
set search_path to numerus;
create or replace function current_user_email() returns text as
$$
select current_setting('request.user.email', true);
$$
language sql
stable;
comment on function current_user_email() is
'Returns the email of the current Numerus user';
revoke execute on function current_user_email() from public;
grant execute on function current_user_email() to guest;
grant execute on function current_user_email() to invoicer;
grant execute on function current_user_email() to admin;
commit;

View File

@ -6,7 +6,6 @@
-- requires: email -- requires: email
-- requires: user -- requires: user
-- requires: login_attempt -- requires: login_attempt
-- requires: build_cookie
begin; begin;
@ -49,7 +48,7 @@ begin
(user_name, ip_address, success) (user_name, ip_address, success)
values (login.email, login.ip_address, true); values (login.email, login.ip_address, true);
return build_cookie(email, user_cookie); return user_cookie || '/' || email;
end; end;
$$ $$
language plpgsql language plpgsql

View File

@ -1,8 +1,7 @@
-- Deploy numerus:logout to pg -- Deploy numerus:logout to pg
-- requires: schema_auth -- requires: schema_auth
-- requires: user -- requires: user
-- requires: current_user_cookie -- requires: current_app_user
-- requires: current_user_email
begin; begin;
@ -13,8 +12,7 @@ $$
update "user" update "user"
set cookie = default set cookie = default
, cookie_expires_at = default , cookie_expires_at = default
where email = current_user_email() where cookie = current_app_user()
and cookie = current_user_cookie()
and cookie_expires_at > current_timestamp and cookie_expires_at > current_timestamp
and length(cookie) > 30 and length(cookie) > 30
$$ $$
@ -23,7 +21,7 @@ security definer
set search_path to auth, numerus, pg_temp; set search_path to auth, numerus, pg_temp;
comment on function logout() is comment on function logout() is
'Removes the cookie and its expiry data from the current user, as returned by current_user_email and current_user_cookie'; 'Removes the cookie and its expiry data from the current user, set as request.user setting';
revoke execute on function logout() from public; revoke execute on function logout() from public;
grant execute on function logout() to invoicer; grant execute on function logout() to invoicer;

View File

@ -1,8 +1,7 @@
-- Deploy numerus:user_profile to pg -- Deploy numerus:user_profile to pg
-- requires: schema_numerus -- requires: schema_numerus
-- requires: user -- requires: user
-- requires: current_user_cookie -- requires: current_app_user
-- requires: current_user_email
begin; begin;
@ -17,8 +16,7 @@ select user_id
, role , role
, lang_tag , lang_tag
from auth."user" from auth."user"
where email = current_user_email() where cookie = current_app_user()
and cookie = current_user_cookie()
and cookie_expires_at > current_timestamp and cookie_expires_at > current_timestamp
and length(cookie) > 30 and length(cookie) > 30
union all union all
@ -30,40 +28,23 @@ select 0
where not exists ( where not exists (
select 1 select 1
from auth."user" from auth."user"
where email = current_user_email() where cookie = current_app_user()
and cookie = current_user_cookie()
and cookie_expires_at > current_timestamp and cookie_expires_at > current_timestamp
and length(cookie) > 30 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 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 invoicer;
grant select, update(email, name, lang_tag) on table user_profile to admin; grant select, update(email, name, lang_tag) on table user_profile to admin;
create or replace function update_user_profile() returns trigger as
$$
begin
update auth."user"
set email = new.email
, name = new.name
, lang_tag = new.lang_tag
where email = current_user_email()
and cookie = current_user_cookie()
and cookie_expires_at > current_timestamp
and length(cookie) > 30
;
perform set_config('request.user.email', new.email, false);
return new;
end;
$$
language plpgsql
security definer
set search_path to auth, numerus, pg_temp;
create trigger update_user_profile
instead of update on user_profile
for each row execute procedure update_user_profile();
commit; commit;

View File

@ -46,7 +46,7 @@ func LoginHandler() http.Handler {
conn := getConn(r) conn := getConn(r)
cookie := conn.MustGetText(r.Context(), "", "select login($1, $2, $3)", page.Email, page.Password, remoteAddr(r)) cookie := conn.MustGetText(r.Context(), "", "select login($1, $2, $3)", page.Email, page.Password, remoteAddr(r))
if cookie != "" { if cookie != "" {
setSessionCookie(w, cookie) http.SetCookie(w, createSessionCookie(cookie, 8766*24*time.Hour))
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
@ -79,10 +79,6 @@ func remoteAddr(r *http.Request) string {
return address return address
} }
func setSessionCookie(w http.ResponseWriter, cookie string) {
http.SetCookie(w, createSessionCookie(cookie, 8766*24*time.Hour))
}
func createSessionCookie(value string, duration time.Duration) *http.Cookie { func createSessionCookie(value string, duration time.Duration) *http.Cookie {
return &http.Cookie{ return &http.Cookie{
Name: sessionCookie, Name: sessionCookie,

View File

@ -32,7 +32,7 @@ func ProfileHandler() http.Handler {
page := ProfilePage{ page := ProfilePage{
Title: pgettext("title", "User Settings", locale), Title: pgettext("title", "User Settings", locale),
Email: user.Email, Email: user.Email,
Language: user.Language.String(), Languages: mustGetLanguageOptions(r.Context(), conn),
} }
if r.Method == "POST" { if r.Method == "POST" {
r.ParseForm() r.ParseForm()
@ -41,16 +41,11 @@ func ProfileHandler() http.Handler {
page.Password = r.FormValue("password") page.Password = r.FormValue("password")
page.PasswordConfirm = r.FormValue("password_confirm") page.PasswordConfirm = r.FormValue("password_confirm")
page.Language = r.FormValue("language") page.Language = r.FormValue("language")
cookie := conn.MustGetText(r.Context(), "", "update user_profile set name = $1, email = $2, lang_tag = $3 returning build_cookie()", page.Name, page.Email, page.Language) conn.MustExec(r.Context(), "update user_profile set name = $1, email = $2, lang_tag = $3", page.Name, page.Email, page.Language)
setSessionCookie(w, cookie) http.Redirect(w, r, "/profile", http.StatusSeeOther);
if page.Password != "" && page.Password == page.PasswordConfirm { return;
conn.MustExec(r.Context(), "select change_password($1)", page.Password)
}
http.Redirect(w, r, "/profile", http.StatusSeeOther)
return
} else { } else {
page.Languages = mustGetLanguageOptions(r.Context(), conn) if err := conn.QueryRow(r.Context(), "select name, lang_tag from user_profile").Scan(&page.Name, &page.Language); err != nil {
if err := conn.QueryRow(r.Context(), "select name from user_profile").Scan(&page.Name); err != nil {
panic(nil) panic(nil)
} }
} }

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: numerus\n" "Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-01-23 18:50+0100\n" "POT-Creation-Date: 2023-01-23 00:41+0100\n"
"PO-Revision-Date: 2023-01-18 17:08+0100\n" "PO-Revision-Date: 2023-01-18 17:08+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n" "Language-Team: Catalan <ca@dodds.net>\n"
@ -81,11 +81,11 @@ msgctxt "action"
msgid "Save changes" msgid "Save changes"
msgstr "Desa canvis" msgstr "Desa canvis"
#: web/template/app.html:20 #: web/template/app.html:16
msgid "Account" msgid "Account"
msgstr "Compte" msgstr "Compte"
#: web/template/app.html:27 #: web/template/app.html:19
msgctxt "action" msgctxt "action"
msgid "Logout" msgid "Logout"
msgstr "Surt" msgstr "Surt"

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: numerus\n" "Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-01-23 18:50+0100\n" "POT-Creation-Date: 2023-01-23 00:41+0100\n"
"PO-Revision-Date: 2023-01-18 17:45+0100\n" "PO-Revision-Date: 2023-01-18 17:45+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n" "Language-Team: Spanish <es@tp.org.es>\n"
@ -81,11 +81,11 @@ msgctxt "action"
msgid "Save changes" msgid "Save changes"
msgstr "Guardar cambios" msgstr "Guardar cambios"
#: web/template/app.html:20 #: web/template/app.html:16
msgid "Account" msgid "Account"
msgstr "Cuenta" msgstr "Cuenta"
#: web/template/app.html:27 #: web/template/app.html:19
msgctxt "action" msgctxt "action"
msgid "Logout" msgid "Logout"
msgstr "Salir" msgstr "Salir"

View File

@ -1,7 +0,0 @@
-- Revert numerus:build_cookie from pg
begin;
drop function if exists numerus.build_cookie(numerus.email, text);
commit;

View File

@ -1,7 +0,0 @@
-- Revert numerus:change_password from pg
begin;
drop function if exists numerus.change_password(text);
commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:current_app_user from pg
begin;
drop function if exists numerus.current_app_user();
commit;

View File

@ -1,7 +0,0 @@
-- Revert numerus:current_user_cookie from pg
begin;
drop function if exists numerus.current_user_cookie();
commit;

View File

@ -1,7 +0,0 @@
-- Revert numerus:current_user_email from pg
begin;
drop function if exists numerus.current_user_email();
commit;

View File

@ -2,8 +2,6 @@
begin; begin;
drop trigger if exists update_user_profile on numerus.user_profile;
drop function if exists numerus.update_user_profile();
drop view if exists numerus.user_profile; drop view if exists numerus.user_profile;
commit; commit;

View File

@ -15,12 +15,9 @@ extension_pgcrypto [schema_auth] 2023-01-13T00:11:50Z jordi fita mas <jordi@tand
encrypt_password [schema_auth user extension_pgcrypto] 2023-01-13T00:14:30Z jordi fita mas <jordi@tandem.blog> # Add trigger to encrypt users password encrypt_password [schema_auth user extension_pgcrypto] 2023-01-13T00:14:30Z jordi fita mas <jordi@tandem.blog> # Add trigger to encrypt users password
login_attempt [schema_auth] 2023-01-17T14:05:49Z jordi fita mas <jordi@tandem.blog> # Add table to log login attempts 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 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
current_user_cookie [schema_numerus] 2023-01-21T20:16:28Z jordi fita mas <jordi@tandem.blog> # Add function to get the cookie of the current Numerus user 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
current_user_email [schema_numerus] 2023-01-23T19:11:53Z jordi fita mas <jordi@tandem.blog> # Add function to get the email of the current Numerus user 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
build_cookie [schema_numerus current_user_email current_user_cookie] 2023-01-23T19:46:13Z jordi fita mas <jordi@tandem.blog> # Add function to build the cookie for the current user logout [schema_auth current_app_user user] 2023-01-17T19:10:21Z jordi fita mas <jordi@tandem.blog> # Add function to logout
check_cookie [schema_public user build_cookie] 2023-01-17T17:48:49Z jordi fita mas <jordi@tandem.blog> # Add function to check if a user cookie is valid
logout [schema_auth current_user_email current_user_cookie 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 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 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_user_email current_user_cookie] 2023-01-21T23:18:20Z jordi fita mas <jordi@tandem.blog> # Add view for user profile user_profile [schema_numerus user current_app_user] 2023-01-21T23:18:20Z jordi fita mas <jordi@tandem.blog> # Add view for user profile
change_password [schema_numerus user] 2023-01-23T20:22:45Z jordi fita mas <jordi@tandem.blog> # Add function to change the current users password

View File

@ -1,70 +0,0 @@
-- Test build_cookie
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(13);
set search_path to numerus, auth, public;
select has_function('numerus', 'build_cookie', array ['email', 'text']);
select function_lang_is('numerus', 'build_cookie', array ['email', 'text'], 'sql');
select function_returns('numerus', 'build_cookie', array ['email', 'text'], 'text');
select isnt_definer('numerus', 'build_cookie', array ['email', 'text']);
select volatility_is('numerus', 'build_cookie', array ['email', 'text'], 'stable');
select function_privs_are('numerus', 'build_cookie', array ['email', 'text'], 'guest', array []::text[]);
select function_privs_are('numerus', 'build_cookie', array ['email', 'text'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'build_cookie', array ['email', 'text'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'build_cookie', array ['email', 'text'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (9, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
select is(
build_cookie('test@example.com'::email, '123abc'),
'123abc/test@example.com',
'Should build the cookie with the given user and cookie value'
);
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
reset role;
select is(
build_cookie(),
'44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog',
'Should build the cookie for the logged in user'
);
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
reset role;
select is(
build_cookie(),
'12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog',
'Should build the cookie for the other logged in user'
);
select set_cookie('ashtasth');
reset role;
select is(
build_cookie(),
'/',
'Should build the cookie for the guest user'
);
select *
from finish();
rollback;

View File

@ -1,57 +0,0 @@
-- Test change_password
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(14);
set search_path to numerus, auth, public;
select has_function('numerus', 'change_password', array ['text']);
select function_lang_is('numerus', 'change_password', array ['text'], 'sql');
select function_returns('numerus', 'change_password', array ['text'], 'void');
select is_definer('numerus', 'change_password', array ['text']);
select volatility_is('numerus', 'change_password', array ['text'], 'volatile');
select function_privs_are('numerus', 'change_password', array ['text'], 'guest', array []::text[]);
select function_privs_are('numerus', 'change_password', array ['text'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'change_password', array ['text'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'change_password', array ['text'], 'authenticator', array []::text[]);
set client_min_messages to warning;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (9, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
select lives_ok( $$ select change_password('another') $$, 'Should run even without current user' );
select isnt_empty (
$$ select * from auth."user" where password = crypt('test', password) $$,
'Should not have changed any password'
);
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
select lives_ok( $$ select change_password('another') $$, 'Should run with the correct user' );
reset role;
select isnt_empty (
$$ select * from auth."user" where email = 'demo@tandem.blog' and password = crypt('another', password) $$,
'Should have changed the password of the current user'
);
select isnt_empty (
$$ select * from auth."user" where email = 'admin@tandem.blog' and password = crypt('test', password) $$,
'Should not have changed any other password'
);
select *
from finish();
rollback;

View File

@ -29,7 +29,10 @@ values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4b
; ;
prepare user_info as prepare user_info as
select current_user_email(), current_user_cookie(); select current_setting('request.user.id', true)::integer
, current_setting('request.user.email', true)
, current_setting('request.user.cookie', true)
;
select is ( select is (
check_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'), check_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'),
@ -39,7 +42,7 @@ select is (
select results_eq ( select results_eq (
'user_info', 'user_info',
$$ values ('demo@tandem.blog', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e') $$, $$ values (1, 'demo@tandem.blog', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e') $$,
'Should have updated the settings with the user info' 'Should have updated the settings with the user info'
); );
@ -51,7 +54,7 @@ select is (
select results_eq ( select results_eq (
'user_info', 'user_info',
$$ values ('admin@tandem.blog', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524') $$, $$ values (9, 'admin@tandem.blog', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524') $$,
'Should have updated the settings with the other user info' 'Should have updated the settings with the other user info'
); );
@ -63,7 +66,7 @@ select is (
select results_eq ( select results_eq (
'user_info', 'user_info',
$$ values ('', '') $$, $$ values (0, '', '') $$,
'Should have updated the settings with a guest user' 'Should have updated the settings with a guest user'
); );
@ -75,7 +78,7 @@ select is (
select results_eq ( select results_eq (
'user_info', 'user_info',
$$ values ('', '') $$, $$ values (0, '', '') $$,
'Should have left the settings with a guest user' 'Should have left the settings with a guest user'
); );
@ -89,7 +92,7 @@ select is (
select results_eq ( select results_eq (
'user_info', 'user_info',
$$ values ('', '') $$, $$ values (0, '', '') $$,
'Should have left the settings with a guest user' 'Should have left the settings with a guest user'
); );
@ -101,7 +104,7 @@ select is (
select results_eq ( select results_eq (
'user_info', 'user_info',
$$ values ('', '') $$, $$ values (0, '', '') $$,
'Should have left the settings with a guest user' 'Should have left the settings with a guest user'
); );

62
test/current_app_user.sql Normal file
View File

@ -0,0 +1,62 @@
-- Test current_app_user
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
set search_path to numerus, auth, public;
select plan(15);
select has_function('numerus', 'current_app_user', array []::name[]);
select function_lang_is('numerus', 'current_app_user', array []::name[], 'sql');
select function_returns('numerus', 'current_app_user', array []::name[], 'text');
select isnt_definer('numerus', 'current_app_user', array []::name[]);
select volatility_is('numerus', 'current_app_user', array []::name[], 'stable');
select function_privs_are('numerus', 'current_app_user', array []::name[], 'guest', array ['EXECUTE']);
select function_privs_are('numerus', 'current_app_user', array []::name[], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'current_app_user', array []::name[], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'current_app_user', array []::name[], 'authenticator', array []::text []);
set client_min_messages to warning;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
select lives_ok(
$$ select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog') $$,
'Should change ok for the first user'
);
select is(current_app_user(), '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', 'Should be running as the first user');
reset role;
select lives_ok(
$$ select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog') $$,
'Should change ok for the second user'
);
select is(current_app_user(), '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', 'Should be running as the second user');
reset role;
select lives_ok(
$$ select set_cookie('') $$,
'Should change ok for a guest user'
);
select is(current_app_user(), '', 'Should be running as the first user');
reset role;
select *
from finish();
rollback;

View File

@ -1,62 +0,0 @@
-- Test current_user_cookie
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
set search_path to numerus, auth, public;
select plan(15);
select has_function('numerus', 'current_user_cookie', array []::name[]);
select function_lang_is('numerus', 'current_user_cookie', array []::name[], 'sql');
select function_returns('numerus', 'current_user_cookie', array []::name[], 'text');
select isnt_definer('numerus', 'current_user_cookie', array []::name[]);
select volatility_is('numerus', 'current_user_cookie', array []::name[], 'stable');
select function_privs_are('numerus', 'current_user_cookie', array []::name[], 'guest', array ['EXECUTE']);
select function_privs_are('numerus', 'current_user_cookie', array []::name[], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'current_user_cookie', array []::name[], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'current_user_cookie', array []::name[], 'authenticator', array []::text []);
set client_min_messages to warning;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
select lives_ok(
$$ select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog') $$,
'Should change ok for the first user'
);
select is(current_user_cookie(), '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', 'Should return the cookie of the first user');
reset role;
select lives_ok(
$$ select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog') $$,
'Should change ok for the second user'
);
select is(current_user_cookie(), '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', 'Should return the cookie of the second user');
reset role;
select lives_ok(
$$ select set_cookie('') $$,
'Should change ok for a guest user'
);
select is(current_user_cookie(), '', 'Should return an empty string');
reset role;
select *
from finish();
rollback;

View File

@ -1,63 +0,0 @@
-- Test current_user_email
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
set search_path to numerus, auth, public;
select plan(15);
select has_function('numerus', 'current_user_email', array []::name[]);
select function_lang_is('numerus', 'current_user_email', array []::name[], 'sql');
select function_returns('numerus', 'current_user_email', array []::name[], 'text');
select isnt_definer('numerus', 'current_user_email', array []::name[]);
select volatility_is('numerus', 'current_user_email', array []::name[], 'stable');
select function_privs_are('numerus', 'current_user_email', array []::name[], 'guest', array ['EXECUTE']);
select function_privs_are('numerus', 'current_user_email', array []::name[], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'current_user_email', array []::name[], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'current_user_email', array []::name[], 'authenticator', array []::text []);
set client_min_messages to warning;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
select lives_ok(
$$ select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog') $$,
'Should change ok for the first user'
);
select is(current_user_email(), 'demo@tandem.blog', 'Should return the email of the first user');
reset role;
select lives_ok(
$$ select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog') $$,
'Should change ok for the second user'
);
select is(current_user_email(), 'admin@tandem.blog', 'Should return the email of the second user');
reset role;
select lives_ok(
$$ select set_cookie('') $$,
'Should change ok for a guest user'
);
select is(current_user_email(), '', 'Should return an empty string');
reset role;
select *
from finish();
rollback;

View File

@ -5,7 +5,7 @@ reset client_min_messages;
begin; begin;
select plan(17); select plan(15);
set search_path to auth, numerus, public; set search_path to auth, numerus, public;
@ -33,7 +33,6 @@ select cookie, cookie_expires_at from "user" order by user_id
; ;
select set_config('request.user.cookie', '', false); select set_config('request.user.cookie', '', false);
select set_config('request.user.email', '', false);
select lives_ok( $$ select * from logout() $$, 'Can logout “nobody”' ); select lives_ok( $$ select * from logout() $$, 'Can logout “nobody”' );
select results_eq( select results_eq(
@ -41,23 +40,10 @@ select results_eq(
$$ values ('8c23d4a8d777775f8fc507676a0d99d3dfa54b03b1b257c838', current_timestamp + interval '1 day') $$ values ('8c23d4a8d777775f8fc507676a0d99d3dfa54b03b1b257c838', current_timestamp + interval '1 day')
, ('0169e5f668eec1e6749fd25388b057997358efa8dfd697961a', current_timestamp + interval '2 day') , ('0169e5f668eec1e6749fd25388b057997358efa8dfd697961a', current_timestamp + interval '2 day')
$$, $$,
'Should have changed nothing' 'Nothing changed'
);
select set_config('request.user.cookie', '0169e5f668eec1e6749fd25388b057997358efa8dfd697961a', false);
select set_config('request.user.email', 'info@tandem.blog', false);
select lives_ok( $$ select * from logout() $$, 'Can logout even if the email and cookie does not match' );
select results_eq(
'user_cookies',
$$ values ('8c23d4a8d777775f8fc507676a0d99d3dfa54b03b1b257c838', current_timestamp + interval '1 day')
, ('0169e5f668eec1e6749fd25388b057997358efa8dfd697961a', current_timestamp + interval '2 day')
$$,
'Should have changed nothing'
); );
select set_config('request.user.cookie', '8c23d4a8d777775f8fc507676a0d99d3dfa54b03b1b257c838', false); select set_config('request.user.cookie', '8c23d4a8d777775f8fc507676a0d99d3dfa54b03b1b257c838', false);
select set_config('request.user.email', 'info@tandem.blog', false);
select lives_ok( $$ select * from logout() $$, 'Can logout the first user' ); select lives_ok( $$ select * from logout() $$, 'Can logout the first user' );
select results_eq( select results_eq(
@ -69,7 +55,6 @@ select results_eq(
); );
select set_config('request.user.cookie', '0169e5f668eec1e6749fd25388b057997358efa8dfd697961a', false); select set_config('request.user.cookie', '0169e5f668eec1e6749fd25388b057997358efa8dfd697961a', false);
select set_config('request.user.email', 'admin@tandem.blog', false);
select lives_ok( $$ select * from logout() $$, 'Can logout the second user' ); select lives_ok( $$ select * from logout() $$, 'Can logout the second user' );
select results_eq( select results_eq(

View File

@ -29,7 +29,7 @@ values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4b
; ;
prepare user_info as prepare user_info as
select current_user_email(), current_user_cookie(), current_user; select current_setting('request.user.id', true)::integer, current_setting('request.user.email', true), current_user;
select lives_ok( select lives_ok(
$$ select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog') $$, $$ select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog') $$,
@ -38,7 +38,7 @@ select lives_ok(
select results_eq( select results_eq(
'user_info', 'user_info',
$$ values ('demo@tandem.blog', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', 'invoicer'::name) $$, $$ values (1, 'demo@tandem.blog', 'invoicer'::name) $$,
'Should have updated the info with the correct user' 'Should have updated the info with the correct user'
); );
@ -51,7 +51,7 @@ select lives_ok(
select results_eq( select results_eq(
'user_info', 'user_info',
$$ values ('admin@tandem.blog', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', 'admin'::name) $$, $$ values (5, 'admin@tandem.blog', 'admin'::name) $$,
'Should have updated the info with the other user' 'Should have updated the info with the other user'
); );
@ -64,7 +64,7 @@ select lives_ok(
select results_eq( select results_eq(
'user_info', 'user_info',
$$ values ('', '', 'guest'::name) $$, $$ values (0, '', 'guest'::name) $$,
'Should have updated the info as a guest user' 'Should have updated the info as a guest user'
); );

View File

@ -1,7 +0,0 @@
-- Verify numerus:build_cookie on pg
begin;
select has_function_privilege('numerus.build_cookie(numerus.email, text)', 'execute');
rollback;

View File

@ -1,7 +0,0 @@
-- Verify numerus:change_password on pg
begin;
select has_function_privilege('numerus.change_password(text)', 'execute');
rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:current_app_user on pg
begin;
select has_function_privilege('numerus.current_app_user()', 'execute');
rollback;

View File

@ -1,7 +0,0 @@
-- Verify numerus:current_user_cookie on pg
begin;
select has_function_privilege('numerus.current_user_cookie()', 'execute');
rollback;

View File

@ -1,7 +0,0 @@
-- Verify numerus:current_user_email on pg
begin;
select has_function_privilege('numerus.current_user_email()', 'execute');
rollback;

View File

@ -11,35 +11,4 @@ select
from numerus.user_profile from numerus.user_profile
where false; where false;
select has_function_privilege('numerus.update_user_profile()', 'execute');
select 1/count(*)
from pg_trigger
where not tgisinternal
and tgname = 'update_user_profile'
and tgrelid = 'numerus.user_profile'::regclass
and tgtype = b'01010001'::int;
-- │││││││
-- ││││││└─> row
-- │││││└──> before
-- ││││└───> insert
-- │││└────> delete
-- ││└─────> update
-- │└──────> truncate
-- └───────> instead
select 1/count(*)
from pg_trigger
where not tgisinternal
and tgname = 'encrypt_password'
and tgrelid = 'auth.user'::regclass
and tgtype = b'00010111'::int;
-- │││││││
-- ││││││└─> row
-- │││││└──> before
-- ││││└───> insert
-- │││└────> delete
-- ││└─────> update
-- │└──────> truncate
-- └───────> instead
rollback; rollback;

View File

@ -189,7 +189,6 @@ p, h1, h2, h3, h4, h5, h6 {
} }
input[type="submit"], button { input[type="submit"], button {
min-width: 34rem;
background-color: var(--numerus--color--white); background-color: var(--numerus--color--white);
border: 2px solid var(--numerus--color--black); border: 2px solid var(--numerus--color--black);
text-transform: uppercase; text-transform: uppercase;
@ -280,59 +279,11 @@ input[type="text"], input[type="password"], input[type="email"], select {
transition: 0.2s; transition: 0.2s;
} }
fieldset { .relative {
border: none;
padding: 2rem 0 0;
margin-top: 3rem;
border-top: 1px solid var(--numerus--color--light-gray);
}
legend {
float: left;
font-style: italic;
margin-bottom: 3rem;
width: 100%;
}
legend + * {
clear: both;
}
fieldset {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.full-width {
gap: 2rem;
}
.full-width legend {
margin-bottom: initial;
}
.full-width .input {
flex: 1;
}
.full-width input {
width: 100%;
}
.dialog-content {
max-width: 120rem;
margin: 0 auto;
}
/* Profile Menu */
#profilemenu {
position: relative; position: relative;
} }
#profilemenu summary { #profilebutton {
width: 7rem; width: 7rem;
height: 7rem; height: 7rem;
margin: 1rem 0; margin: 1rem 0;
@ -341,41 +292,45 @@ fieldset {
align-items: center; align-items: center;
border-radius: 50%; border-radius: 50%;
border: none; border: none;
list-style: none;
} }
#profilemenu summary::-webkit-details-marker { #profilebutton, #profilemenu button {
display: none;
}
#profilemenu summary, #profilemenu button {
cursor: pointer; cursor: pointer;
} }
#profilemenu summary, #profilemenu ul { #profilemenu {
background-color: var(--numerus--background-color);
}
#profilemenu[open] summary::before {
background-color: var(--numerus--header--background-color);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
content: "";
cursor: default;
z-index: 10;
mix-blend-mode: multiply;
}
#profilemenu ul {
list-style: none; list-style: none;
position: absolute; position: absolute;
right: -1.875em; right: -1.875em;
top: 100%; top: 100%;
padding: 1rem 2rem; padding: 1rem 2rem;
z-index: 20; background-color: var(--numerus--color--white);
display: none;
opacity: 0;
z-index: 10;
}
header div:hover #profilemenu {
opacity: 1;
display: initial;
}
header .overlay {
background-color: var(--numerus--header--background-color);
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
display: none;
pointer-events: none;
mix-blend-mode: multiply;
}
header div:hover + .overlay {
display: block;
opacity: 1;
} }
#profilemenu li + li { #profilemenu li + li {
@ -396,12 +351,12 @@ fieldset {
text-transform: initial; text-transform: initial;
} }
#profilemenu li i[class^='ri-'] { #profilemenu i[class^='ri-'] {
margin-right: 2rem; margin-right: 2rem;
color: var(--numerus--color--dark-gray); color: var(--numerus--color--dark-gray);
} }
#profilemenu summary:hover, #profilemenu summary:focus, #profilemenu button:hover, #profilemenu a:hover { #profilemenu button:hover, #profilemenu a:hover {
background-color: var(--numerus--color--light-gray); background-color: var(--numerus--color--light-gray);
} }

View File

@ -9,27 +9,18 @@
<body> <body>
<header> <header>
<h1><img src="/static/numerus.svg" alt="Numerus" width="261" height="33"></h1> <h1><img src="/static/numerus.svg" alt="Numerus" width="261" height="33"></h1>
<details id="profilemenu"> <div class="relative">
<summary> <button id="profilebutton" aria-controls="profilemenu" aria-haspopup="true"><i class="ri-eye-close-line ri-3x"></i></button>
<i class="ri-eye-close-line ri-3x"></i> <ul id="profilemenu" role="menu" aria-labelledby="profilebutton">
</summary>
<ul role="menu">
<li role="presentation"> <li role="presentation">
<a role="menuitem" href="/profile"> <a role="menuitem" href="/profile"><i class="ri-account-circle-line"></i> {{( gettext "Account" )}}</a>
<i class="ri-account-circle-line"></i>
{{( gettext "Account" )}}
</a>
</li> </li>
<li role="presentation"> <li role="presentation">
<form method="POST" action="/logout"> <form method="POST" action="/logout"><button type="submit" role="menuitem"><i class="ri-logout-circle-line"></i> {{( pgettext "Logout" "action" )}}</button></form>
<button type="submit" role="menuitem">
<i class="ri-logout-circle-line"></i>
{{( pgettext "Logout" "action" )}}
</button>
</form>
</li> </li>
</ul> </ul>
</details> </div>
<div class="overlay"></div>
</header> </header>
<main> <main>
{{- template "content" . }} {{- template "content" . }}

View File

@ -1,8 +1,7 @@
{{ define "content" }} {{ define "content" }}
<section class="dialog-content">
<h2>{{(pgettext "User Settings" "title")}}</h2> <h2>{{(pgettext "User Settings" "title")}}</h2>
<form method="POST" action="/profile"> <form method="POST" action="/profile">
<fieldset class="full-width"> <fieldset>
<legend>{{( pgettext "User Access Data" "title" )}}</legend> <legend>{{( pgettext "User Access Data" "title" )}}</legend>
<div class="input"> <div class="input">
@ -15,7 +14,7 @@
<label for="email">{{( pgettext "Email" "input" )}}</label> <label for="email">{{( pgettext "Email" "input" )}}</label>
</div> </div>
</fieldset> </fieldset>
<fieldset class="full-width"> <fieldset>
<legend>{{( pgettext "Password Change" "title" )}}</legend> <legend>{{( pgettext "Password Change" "title" )}}</legend>
<div class="input"> <div class="input">
@ -29,18 +28,13 @@
</div> </div>
</fieldset> </fieldset>
<fieldset> <label for="language">{{( pgettext "Language" "input" )}}</label>
<legend id="language-legend">{{( pgettext "Language" "input" )}}</legend> <select id="language" name="language">
<select id="language" name="language" aria-labelledby="language-legend">
<option value="und">{{( pgettext "Automatic" "language option" )}}</option> <option value="und">{{( pgettext "Automatic" "language option" )}}</option>
{{- range $language := .Languages }} {{- range $language := .Languages }}
<option value="{{ .Tag }}" {{ if eq .Tag $.Language }}selected="selected"{{ end }}>{{ .Name }}</option> <option value="{{ .Tag }}" {{ if eq .Tag $.Language }}selected="selected"{{ end }}>{{ .Name }}</option>
{{- end }} {{- end }}
</select> </select>
<button type="submit">{{( pgettext "Save changes" "action" )}}</button> <button type="submit">{{( pgettext "Save changes" "action" )}}</button>
</fieldset>
</form> </form>
</section>
{{- end }} {{- end }}