-- https://github.com/jeremyevans/rodauth GRANT CREATE ON SCHEMA :schema TO :password_role; CREATE COLLATION ignore_accents (provider = icu, locale = 'und-u-ks-level1-kc-true', deterministic = false); CREATE TABLE account_statuses( account_status_id smallint, name text NOT NULL, UNIQUE (name), CONSTRAINT account_statuses_pk PRIMARY KEY (account_status_id) ); INSERT INTO account_statuses (account_status_id, name) VALUES (1, 'Unverified'); INSERT INTO account_statuses (account_status_id, name) VALUES (2, 'Verified'); INSERT INTO account_statuses (account_status_id, name) VALUES (3, 'Closed'); CREATE TABLE accounts( account_id bigint NOT NULL GENERATED ALWAYS AS IDENTITY, status_id smallint NOT NULL DEFAULT 1, -- email citext UNIQUE, email text UNIQUE, name text, created_at timestamptz NOT NULL DEFAULT now(), modified_at timestamptz NOT NULL DEFAULT now(), UNIQUE (email), CONSTRAINT accounts_pk PRIMARY KEY (account_id), CONSTRAINT accounts_account_statuses_fk FOREIGN KEY (status_id) REFERENCES account_statuses(account_status_id) MATCH SIMPLE ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT accounts_email_ck CHECK (char_length(email) <= 255) ); CREATE INDEX accounts_email_uniq ON accounts (email) WHERE status_id IN (1, 2); CREATE TABLE account_active_session_keys( session_id text NOT NULL, account_id bigint NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), last_use timestamptz NOT NULL DEFAULT now(), CONSTRAINT account_active_session_keys_pk PRIMARY KEY (session_id, account_id), CONSTRAINT account_active_session_keys_accounts_fk FOREIGN KEY (account_id) REFERENCES accounts(account_id) MATCH SIMPLE ON DELETE NO ACTION ON UPDATE NO ACTION ); CREATE OR REPLACE FUNCTION create_account(_email text, _password text) RETURNS int AS $$ INSERT INTO accounts (email) VALUES (lower(_email)) RETURNING account_id; $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION create_session(account_id bigint) RETURNS text AS $$ INSERT INTO account_active_session_keys (session_id, account_id) VALUES (pgsodium.crypto_auth_hmacsha256_keygen(), account_id) RETURNING session_id; $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION get_account_by_session_id(_session_id text) RETURNS SETOF accounts AS $$ SELECT accounts.* FROM account_active_session_keys JOIN accounts USING(account_id) WHERE session_id = _session_id; $$ LANGUAGE SQL; GRANT SELECT, INSERT, UPDATE, DELETE ON account_statuses TO :password_role; GRANT SELECT, INSERT, UPDATE, DELETE ON accounts TO :password_role; SET ROLE teamdraft_password; \c :database_name DROP TABLE IF EXISTS account_password_hashes; CREATE TABLE account_password_hashes( account_id bigint NOT NULL, password_hash int NOT NULL, CONSTRAINT account_password_hashes_accounts_fk FOREIGN KEY (account_id) REFERENCES accounts(account_id) MATCH SIMPLE ON DELETE NO ACTION ON UPDATE NO ACTION ); CREATE OR REPLACE FUNCTION get_salt(_account_id int8) RETURNS text AS $$ DECLARE salt text; BEGIN SELECT CASE WHEN password_hash ~ '^\\$argon2id' THEN substring(password_hash from '\\$argon2id\\$v=\\d+\\$m=\\d+,t=\\d+,p=\\d+\\$.+\\$') ELSE substr(password_hash, 0, 30) END INTO salt END FROM account_password_hashes WHERE _account_id = account_id; RETURN salt; END; $$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = :schema, public; CREATE OR REPLACE FUNCTION valid_password_hash(_account_id int8, hash text) RETURNS boolean AS $$ DECLARE valid boolean; BEGIN SELECT password_hash = hash INTO valid FROM account_password_hashes WHERE _account_id = account_id; RETURN valid; END; $$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = :schema, public; REVOKE ALL ON account_password_hashes FROM :schema; REVOKE ALL ON FUNCTION get_salt(int8) FROM :schema; REVOKE ALL ON FUNCTION valid_password_hash(int8, text) FROM :schema; GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO :password_role; GRANT SELECT(account_id) ON account_password_hashes TO :password_role; GRANT EXECUTE ON FUNCTION get_salt(int8) TO :password_role; GRANT EXECUTE ON FUNCTION valid_password_hash(int8, text) TO :password_role; SET ROLE teamdraft;