diff --git a/client_min_messages b/client_min_messages new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml index a85ff3a..0e882ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: ports: - "5433:5432" volumes: - - "./data/base:/var/lib/postgres/data" + - "./data/base:/var/lib/postgresql/data" env_file: - "./.env.schema" head: @@ -17,6 +17,6 @@ services: ports: - "5432:5432" volumes: - - "./data/head:/var/lib/postgres/data" + - "./data/head:/var/lib/postgresql/data" env_file: - "./.env.schema" diff --git a/schema/020_user.sql b/schema/020_user.sql index 7b1f3ab..64493d4 100644 --- a/schema/020_user.sql +++ b/schema/020_user.sql @@ -1,5 +1,33 @@ select create_newtype_text('public.usr_tag'); +select create_newtype_text('public.usr_tag_or_email'); + +create function usr_tag_or_email_to_email(toe public.usr_tag_or_email) + returns public.email + language plpgsql + immutable as $$ +begin + if position('@' in usr_tag_or_email_to_string(toe)) > 0 then + return email_of_string(usr_tag_or_email_to_string(toe)); + else + return null; + end if; +end; +$$; + +create function usr_tag_or_email_to_tag(toe public.usr_tag_or_email) + returns public.usr_tag + language plpgsql + immutable as $$ +begin + if usr_tag_or_email_to_email(toe) is null then + return usr_tag_of_string(usr_tag_or_email_to_string(toe)); + else + return null; + end if; +end; +$$; + create table public.usr ( id int not null primary key generated always as identity , uid uuid not null default gen_random_uuid() @@ -10,7 +38,6 @@ create table public.usr ); insert into public.usr (tag, password, email) -overriding system value values (usr_tag_of_string('system'), hashed_text_of_string(''), email_of_string('')); select audit( 'public' diff --git a/schema/021_community.sql b/schema/021_community.sql index 3235a9b..0a5e068 100644 --- a/schema/021_community.sql +++ b/schema/021_community.sql @@ -118,4 +118,3 @@ select immutable( 'public' , 'usr' ] ); - diff --git a/schema/021_user.sql b/schema/021_user.sql new file mode 100644 index 0000000..aaecc23 --- /dev/null +++ b/schema/021_user.sql @@ -0,0 +1,118 @@ +select create_newtype_text('public.usr_session_key'); + +create function usr_session_key_gen() + returns public.usr_session_key + volatile + language sql as $$ +select usr_session_key_of_string( + md5(extract(epoch from now()) || gen_random_bytes(32) :: text) +); +$$; + +create type usr_session_device as enum + ( 'linux' + , 'macos' + , 'win' + , 'android' + , 'ios' + , 'other' + ); + +create table public.usr_session + ( id int not null primary key generated always as identity + , key public.usr_session_key not null unique default usr_session_key_gen() + , expired boolean not null default false + , expires_at timestamp not null + , usr int not null references public.usr (id) + , location text null + , device usr_session_device null + , ip inet null + ); + +select immutable( 'public' + , 'usr_session' + , array[ 'id' + , 'key' + , 'expires_at' + , 'usr' + , 'location' + , 'device' + , 'ip' + ] + ); + +create function public.usr_session_login_validate + ( tag_or_email public.usr_tag_or_email + , password text + ) + returns public.usr + language plpgsql + stable + as $$ +declare + usr_email public.email := public.usr_tag_or_email_to_email(tag_or_email); + usr_tag public.usr_tag := public.usr_tag_or_email_to_tag(tag_or_email); + usr public.usr; +begin + select * + from public.usr as u + where u.email = usr_email + or u.tag = usr_tag + into usr; + + if usr.id = 1 or usr.tag = usr_tag_of_string('system') then + raise notice 'system user may not be logged into'; + raise exception 'incorrect password for user %', usr_tag_or_email_to_string(tag_or_email); + end if; + + if usr is null then + -- prevent email guess bruteforces by raising the same exception + -- for invalid password and user not found + raise notice 'user % not found', usr_tag_or_email_to_string(tag_or_email); + raise exception 'incorrect password for user %', usr_tag_or_email_to_string(tag_or_email); + end if; + + if not hashed_text_matches(password, usr.password) then + raise notice 'password does not match for user %', usr_tag_or_email_to_string(tag_or_email); + raise exception 'incorrect password for user %', usr_tag_or_email_to_string(tag_or_email); + end if; + + return usr; +end; +$$; + +create function public.usr_session_login + ( tag_or_email public.usr_tag_or_email + , password text + , remember boolean default false + , location text default null + , device public.usr_session_device default null + , ip inet default null + ) + returns public.usr_session_key + language plpgsql + volatile + as $$ +declare + usr public.usr; + key public.usr_session_key; + expires_at timestamp; +begin + usr := public.usr_session_login_validate(tag_or_email, password); + + if remember then + expires_at := now() + interval '1 week'; + else + expires_at := now() + interval '1 hour'; + end if; + + insert into public.usr_session + (expires_at, usr, location, device, ip) + values + (expires_at, usr.id, location, device, ip) + returning usr_session.key + into key; + + return key; +end; +$$;