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 , 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 := usr_session_key_gen(); 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 (key, expires_at, usr, location, device, ip) values (key, expires_at, usr.id, location, device, ip); return key; end; $$;