1087 lines
28 KiB
MySQL
1087 lines
28 KiB
MySQL
|
-- LegacyWorlds Beta 6
|
||
|
-- PostgreSQL database scripts
|
||
|
--
|
||
|
-- User management functions
|
||
|
--
|
||
|
-- Copyright(C) 2004-2010, DeepClone Development
|
||
|
-- --------------------------------------------------------
|
||
|
|
||
|
|
||
|
--
|
||
|
-- E-mail address creation
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.create_address ( addr TEXT )
|
||
|
RETURNS INT
|
||
|
STRICT VOLATILE SECURITY INVOKER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
pk INT;
|
||
|
BEGIN
|
||
|
INSERT INTO users.addresses (address) VALUES (addr)
|
||
|
RETURNING id INTO pk;
|
||
|
RETURN pk;
|
||
|
EXCEPTION WHEN unique_violation THEN
|
||
|
RETURN NULL;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Generates a random token.
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- len Length of the token
|
||
|
--
|
||
|
-- Returns:
|
||
|
-- the token
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.make_token( len INT )
|
||
|
RETURNS TEXT
|
||
|
STRICT VOLATILE
|
||
|
SECURITY INVOKER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
i INT;
|
||
|
result TEXT;
|
||
|
ok_chars CHAR ARRAY[16] := ARRAY['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'];
|
||
|
BEGIN
|
||
|
result := '';
|
||
|
FOR i IN 1 .. len
|
||
|
LOOP
|
||
|
result := result || ok_chars[ 1 + floor( random() * 36 )::INT ];
|
||
|
END LOOP;
|
||
|
RETURN result;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Credentials creation
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- addr mail address
|
||
|
-- lang selected language
|
||
|
-- pmd5 MD5 hash of password
|
||
|
-- psha1 SHA-1 hash of password
|
||
|
--
|
||
|
-- Returns:
|
||
|
-- err_code error code:
|
||
|
-- 0 on success
|
||
|
-- -1 if the address exists
|
||
|
-- -2 if the language does not exist
|
||
|
-- a_id the account's identifier
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.create_credentials ( addr TEXT , lang TEXT , pmd5 TEXT , psha1 TEXT ,
|
||
|
OUT err_code INT , OUT a_id INT)
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
vkey TEXT;
|
||
|
l_id INT;
|
||
|
BEGIN
|
||
|
-- Get language
|
||
|
SELECT INTO l_id l.id FROM defs.languages l
|
||
|
CROSS JOIN defs.strings s
|
||
|
LEFT OUTER JOIN defs.translations t
|
||
|
ON t.lang_id = l.id AND t.string_id = s.id
|
||
|
WHERE l.language = lang
|
||
|
GROUP BY l.id
|
||
|
HAVING count( s.* ) = count( t.* );
|
||
|
IF NOT FOUND THEN
|
||
|
err_code := -2;
|
||
|
a_id := NULL;
|
||
|
RETURN;
|
||
|
END IF;
|
||
|
|
||
|
-- Create address
|
||
|
a_id := users.create_address( addr );
|
||
|
IF a_id IS NULL
|
||
|
THEN
|
||
|
err_code := -1;
|
||
|
RETURN;
|
||
|
END IF;
|
||
|
|
||
|
-- Insert credentials
|
||
|
INSERT INTO users.credentials (address_id, pass_md5, pass_sha1, credits, language_id)
|
||
|
VALUES ( a_id , pmd5 , psha1 , floor( sys.get_constant( 'game.initialCredits' ) ) , l_id );
|
||
|
vkey := users.make_token(64);
|
||
|
INSERT INTO users.validation_keys (credentials_id, token)
|
||
|
VALUES ( a_id , vkey );
|
||
|
PERFORM users.write_log( a_id , 'INFO'::log_level , 'Account created; validation key: ' || vkey );
|
||
|
err_code := 0;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.create_credentials(TEXT,TEXT,TEXT,TEXT) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Attempts to re-activate a disabled account
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- addr Account e-mail address
|
||
|
--
|
||
|
-- Returns:
|
||
|
-- success Whether the operation was successful
|
||
|
-- a_id Account identifier
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.reactivate( IN addr TEXT , OUT success BOOLEAN , OUT a_id INT )
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
vkey TEXT;
|
||
|
BEGIN
|
||
|
SELECT INTO a_id id FROM users.accounts_view
|
||
|
WHERE address = addr AND status = 'DISABLED';
|
||
|
success := FOUND;
|
||
|
|
||
|
IF success THEN
|
||
|
vkey := users.make_token( 64 );
|
||
|
INSERT INTO users.validation_keys (credentials_id, token)
|
||
|
VALUES ( a_id , vkey );
|
||
|
PERFORM users.write_log( a_id , 'INFO'::log_level , 'Account re-activation requested; validation key: ' || vkey );
|
||
|
END IF;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.reactivate(TEXT) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- This function is called when an account's validation token cannot be sent.
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- a_id Account identifier
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.validation_mail_failure( IN a_id INT )
|
||
|
RETURNS VOID
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
st TEXT;
|
||
|
BEGIN
|
||
|
SELECT INTO st status FROM users.accounts_view WHERE id = a_id;
|
||
|
IF st = 'UNCONFIRMED' THEN
|
||
|
DELETE FROM users.addresses WHERE id = a_id;
|
||
|
ELSEIF st = 'REACTIVATING' THEN
|
||
|
DELETE FROM users.validation_keys WHERE credentials_id = a_id;
|
||
|
ELSE
|
||
|
RAISE EXCEPTION 'Invalid account status % (account #%)' , st , a_id;
|
||
|
END IF;
|
||
|
PERFORM sys.write_log( 'AccountManagement' , 'ERROR'::log_level , 'Account re-activation mail could not be sent for account #' || a_id || ', deleting validation key' );
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.validation_mail_failure( INT ) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Request password recovery
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- addr account mail address
|
||
|
--
|
||
|
-- Returns:
|
||
|
-- err_code error code:
|
||
|
-- 0 on success,
|
||
|
-- 1 if there's already a password recovery request for the account
|
||
|
-- 2 if the account doesn't exist.
|
||
|
-- a_id account identifier
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.request_password_recovery( IN addr TEXT , OUT err_code INT , OUT a_id INT)
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
vkey TEXT;
|
||
|
BEGIN
|
||
|
SELECT INTO a_id ad.id FROM users.addresses ad
|
||
|
INNER JOIN users.credentials c ON c.address_id = ad.id
|
||
|
INNER JOIN users.accounts_view av ON av.id = ad.id
|
||
|
WHERE ad.address = addr AND av.status IN ( 'ACTIVE' , 'VACATION' , 'START_VACATION' , 'QUITTING' )
|
||
|
FOR UPDATE OF ad , c;
|
||
|
IF NOT FOUND THEN
|
||
|
err_code := 2;
|
||
|
ELSE
|
||
|
vkey := users.make_token( 64 );
|
||
|
BEGIN
|
||
|
INSERT INTO users.pwd_recovery_requests (credentials_id , token)
|
||
|
VALUES ( a_id , vkey );
|
||
|
PERFORM users.write_log( a_id , 'INFO'::log_level , 'Password recovery requested; validation key: ' || vkey );
|
||
|
err_code := 0;
|
||
|
EXCEPTION
|
||
|
WHEN unique_violation THEN
|
||
|
err_code := 1;
|
||
|
a_id := NULL;
|
||
|
END;
|
||
|
END IF;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.request_password_recovery( TEXT ) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Confirm password recovery
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- umail account mail address
|
||
|
-- tok recovery token
|
||
|
-- pmd5 new password's MD5 hash
|
||
|
-- psha1 new password's SHA-1 hash
|
||
|
--
|
||
|
-- Returns:
|
||
|
-- err_code error code:
|
||
|
-- 0 on success
|
||
|
-- 1 if the account doesn't exist, if the token is wrong or if the request is used
|
||
|
-- 2 if the new password is the same as the user's administrative access
|
||
|
-- a_id account identifier
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.confirm_password_recovery( IN umail TEXT, IN tok TEXT, IN pmd5 TEXT, IN psha1 TEXT ,
|
||
|
OUT err_code INT , OUT a_id INT )
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
amd5 TEXT;
|
||
|
asha1 TEXT;
|
||
|
BEGIN
|
||
|
SELECT INTO a_id, amd5, asha1 addr.id, admin.pass_md5 , admin.pass_sha1
|
||
|
FROM users.addresses addr
|
||
|
INNER JOIN users.credentials creds
|
||
|
ON creds.address_id = addr.id
|
||
|
INNER JOIN users.pwd_recovery_requests req
|
||
|
ON req.credentials_id = creds.address_id AND req.token = tok AND NOT req.used
|
||
|
LEFT OUTER JOIN admin.admin_credentials acreds
|
||
|
ON acreds.credentials_id = creds.address_id
|
||
|
LEFT OUTER JOIN admin.administrators admin
|
||
|
ON admin.id = acreds.administrator_id
|
||
|
WHERE addr.address = umail
|
||
|
FOR UPDATE OF creds , addr , req;
|
||
|
|
||
|
IF NOT FOUND
|
||
|
THEN
|
||
|
err_code := 1;
|
||
|
RETURN;
|
||
|
ELSEIF amd5 IS NOT NULL AND amd5 = pmd5 AND asha1 = psha1
|
||
|
THEN
|
||
|
err_code := 2;
|
||
|
RETURN;
|
||
|
END IF;
|
||
|
|
||
|
UPDATE users.pwd_recovery_requests SET used = TRUE WHERE credentials_id = a_id;
|
||
|
UPDATE users.credentials SET pass_md5 = pmd5 , pass_sha1 = psha1 WHERE address_id = a_id;
|
||
|
PERFORM users.write_log( a_id , 'INFO'::log_level , 'Password recovery successful' );
|
||
|
err_code := 0;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.confirm_password_recovery( TEXT , TEXT , TEXT , TEXT ) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Cancel password recovery request after a mail failure
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- a_id account identifier
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.cancel_password_recovery( a_id INT )
|
||
|
RETURNS VOID
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
BEGIN
|
||
|
PERFORM users.write_log( a_id , 'ERROR'::log_level , 'Password recovery: could not send e-mail, aborting' );
|
||
|
DELETE FROM users.pwd_recovery_requests WHERE credentials_id = a_id;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Request mail address change
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- cid account identifier
|
||
|
-- nmail new address
|
||
|
--
|
||
|
-- Returns:
|
||
|
-- err_code 0 on success
|
||
|
-- -1 if there's already such a request in the DB
|
||
|
-- -2 if the mail address exists
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.request_address_change( cid INT , nmail TEXT , OUT err_code INT )
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
vkey TEXT;
|
||
|
BEGIN
|
||
|
vkey := users.make_token( 64 );
|
||
|
INSERT INTO users.address_change_requests( credentials_id , address_id , token )
|
||
|
VALUES ( cid , users.create_address( nmail ) , vkey );
|
||
|
err_code := 0;
|
||
|
PERFORM users.write_log( cid , 'INFO'::log_level , 'E-mail address change requested; validation key: ' || vkey );
|
||
|
EXCEPTION
|
||
|
WHEN unique_violation THEN
|
||
|
err_code := -1;
|
||
|
WHEN not_null_violation THEN
|
||
|
err_code := -2;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.request_address_change( INT , TEXT ) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Confirm mail address change
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- cid account identifier
|
||
|
-- tok validation token
|
||
|
--
|
||
|
-- Returns:
|
||
|
-- n_id The account's new identifier
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.confirm_address_change( cid INT , tok TEXT , OUT n_id INT )
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
pk INT;
|
||
|
npk INT;
|
||
|
oaddr TEXT;
|
||
|
BEGIN
|
||
|
SELECT INTO pk, npk, oaddr addr.id , acr.address_id , addr.address
|
||
|
FROM users.addresses addr, users.credentials creds, users.address_change_requests acr
|
||
|
WHERE addr.id = cid AND creds.address_id = cid AND acr.credentials_id = cid
|
||
|
AND acr.token = tok
|
||
|
AND NOT acr.used
|
||
|
FOR UPDATE;
|
||
|
|
||
|
IF FOUND
|
||
|
THEN
|
||
|
UPDATE users.credentials SET address_id = npk WHERE address_id = pk;
|
||
|
UPDATE users.address_change_requests SET used = TRUE WHERE credentials_id = npk;
|
||
|
DELETE FROM users.addresses WHERE id = pk;
|
||
|
n_id := npk;
|
||
|
PERFORM users.write_log( npk , 'INFO'::log_level , 'E-mail address change confirmed (old address: '
|
||
|
|| oaddr || ')' );
|
||
|
ELSE
|
||
|
n_id := 0;
|
||
|
END IF;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.confirm_address_change( INT , TEXT ) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Cancels an address change request
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- cid account identifier
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.cancel_address_change( cid INT )
|
||
|
RETURNS VOID
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
BEGIN
|
||
|
UPDATE users.address_change_requests
|
||
|
SET used = TRUE
|
||
|
WHERE credentials_id = cid;
|
||
|
PERFORM users.write_log( cid , 'INFO'::log_level , 'E-mail address change aborted' );
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.cancel_address_change( INT ) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Account validation
|
||
|
--
|
||
|
-- /!\ Transaction rollback required if there is an error /!\
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- addr Account email address
|
||
|
-- vtoken Validation token
|
||
|
-- empname Empire name
|
||
|
-- plname Initial planet name
|
||
|
--
|
||
|
-- Returns:
|
||
|
-- account_error 0 on success
|
||
|
-- 1 if the account's status is incorrect
|
||
|
-- 2 if the validation token doesn't match
|
||
|
-- empire_error 0 on success
|
||
|
-- 1 if the name is banned
|
||
|
-- 2 if the empire name is unavailable
|
||
|
-- planet_error 0 on success
|
||
|
-- 1 if the name is banned
|
||
|
-- 2 if the planet name is unavailable
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.validate( addr TEXT , vtoken TEXT , empname TEXT , plname TEXT ,
|
||
|
OUT account_error INT , OUT empire_error INT , OUT planet_error INT )
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
accid INT;
|
||
|
astat TEXT;
|
||
|
empid INT;
|
||
|
plid INT;
|
||
|
BEGIN
|
||
|
-- Initialise return values
|
||
|
account_error := 0;
|
||
|
empire_error := 0;
|
||
|
planet_error := 0;
|
||
|
|
||
|
-- Find account
|
||
|
SELECT INTO accid c.address_id
|
||
|
FROM users.credentials c
|
||
|
INNER JOIN users.addresses a
|
||
|
ON a.id = c.address_id
|
||
|
WHERE a.address = addr
|
||
|
FOR UPDATE;
|
||
|
IF NOT FOUND THEN
|
||
|
PERFORM sys.write_log( 'AccountManagement' , 'WARNING'::log_level , 'account for "'
|
||
|
|| addr || '" not found' );
|
||
|
account_error := 1;
|
||
|
RETURN;
|
||
|
END IF;
|
||
|
|
||
|
-- Get account status
|
||
|
SELECT INTO astat status FROM users.accounts_view
|
||
|
WHERE id = accid;
|
||
|
IF astat NOT IN ('UNCONFIRMED' , 'REACTIVATING') THEN
|
||
|
PERFORM users.write_log( accid , 'WARNING'::log_level , 'incorrect account status: ' || astat );
|
||
|
account_error := 1;
|
||
|
ELSE
|
||
|
-- Delete validation key
|
||
|
DELETE FROM users.validation_keys
|
||
|
WHERE credentials_id = accid AND token = vtoken;
|
||
|
IF NOT FOUND THEN
|
||
|
PERFORM users.write_log( accid , 'WARNING'::log_level , 'confirmation code "'
|
||
|
|| vtoken || '" not found' );
|
||
|
account_error := 2;
|
||
|
END IF;
|
||
|
END IF;
|
||
|
|
||
|
-- Create / get empire
|
||
|
empid := naming.goc_empire_name( accid , empname );
|
||
|
IF empid < 0 THEN
|
||
|
empire_error := -empid;
|
||
|
END IF;
|
||
|
|
||
|
-- Get free planet, rename it
|
||
|
plid := verse.get_random_planet( );
|
||
|
IF plid IS NULL THEN
|
||
|
PERFORM users.write_log( accid , 'WARNING'::log_level , 'no free planets found!' );
|
||
|
planet_error := 2;
|
||
|
ELSE
|
||
|
planet_error := - naming.change_map_name( plid , accid , plname );
|
||
|
END IF;
|
||
|
|
||
|
-- Activate account and create empire
|
||
|
IF planet_error = 0 AND empire_error = 0 AND account_error = 0 THEN
|
||
|
IF astat = 'REACTIVATING' THEN
|
||
|
DELETE FROM users.inactive_accounts
|
||
|
WHERE credentials_id = accid;
|
||
|
END IF;
|
||
|
INSERT INTO users.active_accounts (credentials_id, vacation_credits)
|
||
|
VALUES ( accid , sys.get_constant( 'vacation.initial' ) );
|
||
|
|
||
|
PERFORM emp.create_empire( empid , plid , sys.get_constant( 'game.initialCash' ) );
|
||
|
PERFORM users.write_log( accid , 'INFO'::log_level , 'created empire #' || empid
|
||
|
|| ' ("' || empname || '") with initial planet #' || plid || ' ("' || plname || '")' );
|
||
|
END IF;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Creates an inactive account (used by command line tools)
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- addr mail address
|
||
|
-- lang selected language
|
||
|
-- pmd5 MD5 hash of password
|
||
|
-- psha1 SHA-1 hash of password
|
||
|
--
|
||
|
-- Returns:
|
||
|
-- err_code error code:
|
||
|
-- 0 on success
|
||
|
-- -1 if the address exists
|
||
|
-- -2 if the language does not exist
|
||
|
-- a_id the account's identifier
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.create_inactive_account ( addr TEXT , lang TEXT , pmd5 TEXT , psha1 TEXT ,
|
||
|
OUT err_code INT , OUT a_id INT)
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
vkey TEXT;
|
||
|
l_id INT;
|
||
|
BEGIN
|
||
|
-- Get language
|
||
|
SELECT INTO l_id l.id FROM defs.languages l
|
||
|
CROSS JOIN defs.strings s
|
||
|
LEFT OUTER JOIN defs.translations t
|
||
|
ON t.lang_id = l.id AND t.string_id = s.id
|
||
|
WHERE l.language = lang
|
||
|
GROUP BY l.id
|
||
|
HAVING count( s.* ) = count( t.* );
|
||
|
IF NOT FOUND THEN
|
||
|
err_code := -2;
|
||
|
a_id := NULL;
|
||
|
RETURN;
|
||
|
END IF;
|
||
|
|
||
|
-- Create address
|
||
|
a_id := users.create_address( addr );
|
||
|
IF a_id IS NULL
|
||
|
THEN
|
||
|
err_code := -1;
|
||
|
RETURN;
|
||
|
END IF;
|
||
|
|
||
|
-- Insert credentials
|
||
|
INSERT INTO users.credentials (address_id, pass_md5, pass_sha1, credits, language_id)
|
||
|
VALUES ( a_id , pmd5 , psha1 , floor( sys.get_constant( 'game.initialCredits' ) ) , l_id );
|
||
|
INSERT INTO users.inactive_accounts ( credentials_id , since , status )
|
||
|
VALUES ( a_id , now() - '1s'::INTERVAL , 'PROCESSED' );
|
||
|
err_code := 0;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.create_inactive_account(TEXT,TEXT,TEXT,TEXT) TO :dbuser;
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Sets an account's language
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- u_mail User address
|
||
|
-- l_tid Language identifier
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.set_language( u_mail TEXT , l_tid TEXT )
|
||
|
RETURNS VOID
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
l_id INT;
|
||
|
BEGIN
|
||
|
SELECT INTO l_id l.id FROM defs.languages l
|
||
|
CROSS JOIN defs.strings s
|
||
|
LEFT OUTER JOIN defs.translations t
|
||
|
ON t.lang_id = l.id AND t.string_id = s.id
|
||
|
WHERE l.language = l_tid
|
||
|
GROUP BY l.id
|
||
|
HAVING count( s.* ) = count( t.* );
|
||
|
IF NOT FOUND THEN
|
||
|
RETURN;
|
||
|
END IF;
|
||
|
|
||
|
UPDATE users.credentials c SET language_id = l_id
|
||
|
FROM users.addresses a
|
||
|
WHERE c.address_id = a.id AND a.address = u_mail;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.set_language( TEXT , TEXT ) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Sets an account's password
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- u_id Account identifier
|
||
|
-- p_sha1 SHA1 hash of the new password
|
||
|
-- p_md5 MD-5 hash of the new password
|
||
|
--
|
||
|
-- Returns:
|
||
|
-- success Whether the operation was successful
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.set_password( u_id INT , p_sha1 TEXT , p_md5 TEXT , OUT success BOOLEAN )
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
amd5 TEXT;
|
||
|
asha1 TEXT;
|
||
|
BEGIN
|
||
|
SELECT INTO amd5, asha1 admin.pass_md5 , admin.pass_sha1
|
||
|
FROM users.credentials creds
|
||
|
LEFT OUTER JOIN admin.admin_credentials acreds
|
||
|
ON acreds.credentials_id = creds.address_id
|
||
|
LEFT OUTER JOIN admin.administrators admin
|
||
|
ON admin.id = acreds.administrator_id
|
||
|
WHERE creds.address_id = u_id
|
||
|
FOR UPDATE OF creds;
|
||
|
|
||
|
IF amd5 IS NOT NULL AND amd5 = p_md5 AND asha1 = p_sha1
|
||
|
THEN
|
||
|
success := FALSE;
|
||
|
ELSE
|
||
|
UPDATE users.credentials
|
||
|
SET pass_md5 = p_md5 , pass_sha1 = p_sha1
|
||
|
WHERE address_id = u_id;
|
||
|
PERFORM users.write_log( u_id , 'INFO'::log_level , 'Password changed' );
|
||
|
success := TRUE;
|
||
|
END IF;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.set_password( INT , TEXT , TEXT ) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Causes old password recovery requests to expire
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.expire_pwd_recovery_requests()
|
||
|
RETURNS VOID
|
||
|
STRICT VOLATILE
|
||
|
SECURITY INVOKER
|
||
|
AS $$
|
||
|
DELETE FROM users.pwd_recovery_requests
|
||
|
WHERE created <= now() - ( floor( sys.get_constant('accounts.prrDelay') ) || 's' )::INTERVAL;
|
||
|
$$ LANGUAGE SQL;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Causes old address change requests to expire
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.expire_addr_change_requests()
|
||
|
RETURNS VOID
|
||
|
STRICT VOLATILE
|
||
|
SECURITY INVOKER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
rec RECORD;
|
||
|
BEGIN
|
||
|
FOR rec IN SELECT * FROM users.address_change_requests
|
||
|
WHERE created <= now() - ( floor( sys.get_constant('accounts.acrDelay') ) || 's' )::INTERVAL
|
||
|
FOR UPDATE
|
||
|
LOOP
|
||
|
IF rec.address_id = rec.credentials_id THEN
|
||
|
DELETE FROM users.address_change_requests
|
||
|
WHERE credentials_id = rec.credentials_id;
|
||
|
ELSE
|
||
|
DELETE FROM users.addresses
|
||
|
WHERE id = rec.address_id;
|
||
|
END IF;
|
||
|
END LOOP;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Causes unconfirmed new accounts to be deleted and unconfirmed reactived accounts to be disabled
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.expire_validation_keys()
|
||
|
RETURNS VOID
|
||
|
STRICT VOLATILE
|
||
|
SECURITY INVOKER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
rec RECORD;
|
||
|
BEGIN
|
||
|
FOR rec IN SELECT v.credentials_id AS id , ( ia.credentials_id IS NULL ) AS was_new
|
||
|
FROM users.validation_keys v
|
||
|
INNER JOIN users.credentials c ON c.address_id = v.credentials_id
|
||
|
INNER JOIN users.addresses a ON c.address_id = a.id
|
||
|
LEFT OUTER JOIN users.inactive_accounts ia ON ia.credentials_id = c.address_id
|
||
|
WHERE v.created <= now() - ( floor( sys.get_constant('accounts.cacDelay') ) || 's' )::INTERVAL
|
||
|
FOR UPDATE OF v , c , a
|
||
|
LOOP
|
||
|
IF rec.was_new
|
||
|
THEN
|
||
|
DELETE FROM users.credentials WHERE address_id = rec.id;
|
||
|
DELETE FROM users.addresses WHERE id = rec.id;
|
||
|
ELSE
|
||
|
DELETE FROM users.validation_keys WHERE credentials_id = rec.id;
|
||
|
END IF;
|
||
|
END LOOP;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Handle account requests expiry
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.expire_requests( )
|
||
|
RETURNS VOID
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
BEGIN
|
||
|
PERFORM users.expire_pwd_recovery_requests();
|
||
|
PERFORM users.expire_addr_change_requests();
|
||
|
PERFORM users.expire_validation_keys();
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.expire_requests() TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Disable quitting accounts
|
||
|
--
|
||
|
|
||
|
CREATE TYPE quitting_account AS (
|
||
|
id INT ,
|
||
|
address TEXT ,
|
||
|
language TEXT
|
||
|
);
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.process_quit_requests( )
|
||
|
RETURNS SETOF quitting_account
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
addr quitting_account;
|
||
|
rec RECORD;
|
||
|
BEGIN
|
||
|
UPDATE users.inactive_accounts
|
||
|
SET status = 'PROCESSING'
|
||
|
WHERE status = 'FUTURE' AND since <= now();
|
||
|
|
||
|
FOR rec IN SELECT ia.credentials_id AS account , e.name_id AS empire ,
|
||
|
ma.address AS address , l.language AS language
|
||
|
FROM users.inactive_accounts ia
|
||
|
INNER JOIN users.credentials c ON c.address_id = ia.credentials_id
|
||
|
INNER JOIN defs.languages l ON l.id = c.language_id
|
||
|
INNER JOIN users.active_accounts aa ON aa.credentials_id = ia.credentials_id
|
||
|
INNER JOIN users.addresses ma ON ma.id = c.address_id
|
||
|
INNER JOIN naming.empire_names en ON en.owner_id = ia.credentials_id
|
||
|
INNER JOIN emp.empires e ON e.name_id = en.id
|
||
|
WHERE ia.status = 'PROCESSING'
|
||
|
FOR UPDATE OF ia , c , aa , ma , en , e
|
||
|
LOOP
|
||
|
PERFORM emp.delete_empire( rec.empire );
|
||
|
DELETE FROM users.active_accounts WHERE credentials_id = rec.account;
|
||
|
UPDATE users.inactive_accounts
|
||
|
SET status = 'PROCESSED'
|
||
|
WHERE credentials_id = rec.account;
|
||
|
|
||
|
addr.id = rec.account;
|
||
|
addr.address = rec.address;
|
||
|
addr.language := rec.language;
|
||
|
RETURN NEXT addr;
|
||
|
END LOOP;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.process_quit_requests( ) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Sets an account in the QUITTING status
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- a_id Account identifier
|
||
|
-- r_txt Reason
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.set_account_quit( a_id INT , r_txt TEXT )
|
||
|
RETURNS VOID
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
BEGIN
|
||
|
-- Verify and lock record
|
||
|
PERFORM aa.credentials_id FROM users.active_accounts aa
|
||
|
INNER JOIN users.credentials c ON aa.credentials_id = c.address_id
|
||
|
LEFT OUTER JOIN users.inactive_accounts ia ON ia.credentials_id = c.address_id
|
||
|
LEFT OUTER JOIN users.vacations v ON v.account_id = aa.credentials_id
|
||
|
WHERE aa.credentials_id = a_id AND ia.credentials_id IS NULL AND v.account_id IS NULL
|
||
|
FOR UPDATE OF aa , c;
|
||
|
IF NOT FOUND
|
||
|
THEN
|
||
|
RETURN;
|
||
|
END IF;
|
||
|
|
||
|
-- Insert de-activation record
|
||
|
INSERT INTO users.inactive_accounts ( credentials_id , status , since )
|
||
|
VALUES ( a_id , 'FUTURE' , now() + ( floor( sys.get_constant( 'accounts.quitDelay' ) ) || 's' )::INTERVAL );
|
||
|
IF r_txt <> '' THEN
|
||
|
INSERT INTO users.reasons ( account_id , reason )
|
||
|
VALUES ( a_id , r_txt );
|
||
|
END IF;
|
||
|
PERFORM users.write_log( a_id , 'INFO'::log_level , 'Quitting' );
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.set_account_quit( INT , TEXT ) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Prevents an account from being disabled
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- a_id Account identifier
|
||
|
-- r_txt Reason
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.cancel_account_quit( a_id INT )
|
||
|
RETURNS VOID
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
BEGIN
|
||
|
-- Verify and lock record
|
||
|
PERFORM aa.credentials_id FROM users.active_accounts aa
|
||
|
INNER JOIN users.credentials c ON aa.credentials_id = c.address_id
|
||
|
INNER JOIN users.inactive_accounts ia ON ia.credentials_id = c.address_id
|
||
|
LEFT OUTER JOIN users.bans b ON b.account_id = aa.credentials_id
|
||
|
WHERE aa.credentials_id = a_id AND b.account_id IS NULL AND ia.status = 'FUTURE'
|
||
|
FOR UPDATE OF aa , c , ia;
|
||
|
IF NOT FOUND
|
||
|
THEN
|
||
|
RETURN;
|
||
|
END IF;
|
||
|
DELETE FROM users.inactive_accounts WHERE credentials_id = a_id;
|
||
|
PERFORM users.write_log( a_id , 'INFO'::log_level , 'No longer quitting' );
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.cancel_account_quit( INT ) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Prepares an account for vacation mode
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- a_id Account identifier
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.set_vacation( a_id INT )
|
||
|
RETURNS VOID
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
BEGIN
|
||
|
PERFORM aa.credentials_id FROM users.active_accounts aa
|
||
|
INNER JOIN users.credentials c ON aa.credentials_id = c.address_id
|
||
|
LEFT OUTER JOIN users.vacations v ON v.account_id = aa.credentials_id
|
||
|
LEFT OUTER JOIN users.inactive_accounts ia ON ia.credentials_id = aa.credentials_id
|
||
|
WHERE aa.credentials_id = a_id AND v.account_id IS NULL AND ia.credentials_id IS NULL
|
||
|
AND aa.vacation_credits > 0
|
||
|
FOR UPDATE OF aa , c;
|
||
|
IF NOT FOUND
|
||
|
THEN
|
||
|
RETURN;
|
||
|
END IF;
|
||
|
|
||
|
INSERT INTO users.vacations ( account_id , since , status )
|
||
|
VALUES ( a_id , now() + ( floor( sys.get_constant( 'vacation.delay' ) ) || 's' )::INTERVAL , 'FUTURE' );
|
||
|
PERFORM users.write_log( a_id , 'INFO'::log_level , 'Preparing to enter vacation mode' );
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.set_vacation( INT ) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Causes an account to exit vacation mode, or to avoid entering it
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- a_id Account identifier
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.leave_vacation( a_id INT )
|
||
|
RETURNS VOID
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
active BOOLEAN;
|
||
|
e_id INT;
|
||
|
BEGIN
|
||
|
SELECT INTO active , e_id ( v.status = 'PROCESSED' ) , e.name_id
|
||
|
FROM users.active_accounts aa
|
||
|
INNER JOIN users.credentials c ON aa.credentials_id = c.address_id
|
||
|
INNER JOIN users.vacations v ON v.account_id = aa.credentials_id
|
||
|
INNER JOIN naming.empire_names en ON en.owner_id = v.account_id
|
||
|
INNER JOIN emp.empires e ON e.name_id = en.id
|
||
|
WHERE aa.credentials_id = a_id
|
||
|
FOR UPDATE OF v , aa , c;
|
||
|
IF NOT FOUND
|
||
|
THEN
|
||
|
RETURN;
|
||
|
END IF;
|
||
|
|
||
|
IF active
|
||
|
THEN
|
||
|
PERFORM emp.switch_enemies( e_id );
|
||
|
PERFORM users.write_log( a_id , 'INFO'::log_level , 'Left vacation mode' );
|
||
|
ELSE
|
||
|
PERFORM users.write_log( a_id , 'INFO'::log_level , 'Cancelled vacation mode' );
|
||
|
END IF;
|
||
|
|
||
|
DELETE FROM users.vacations WHERE account_id = a_id;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.leave_vacation( INT ) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Manage vacation credits and activate / disable vacation mode
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION users.process_vacations( )
|
||
|
RETURNS VOID
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
DECLARE
|
||
|
a_id INT;
|
||
|
e_id INT;
|
||
|
BEGIN
|
||
|
-- Increase vacation credits for accounts that are not quitting or on/entering vacation
|
||
|
UPDATE users.active_accounts aa
|
||
|
SET vacation_credits = aa.vacation_credits + 1
|
||
|
FROM users.credentials c
|
||
|
LEFT OUTER JOIN users.vacations v ON v.account_id = c.address_id
|
||
|
LEFT OUTER JOIN users.inactive_accounts ia ON ia.credentials_id = c.address_id
|
||
|
WHERE aa.credentials_id = c.address_id AND v.account_id IS NULL AND ia.credentials_id IS NULL
|
||
|
AND aa.vacation_credits < floor( sys.get_constant( 'vacation.max' ) );
|
||
|
|
||
|
-- Process accounts that should enter vacation mode
|
||
|
FOR a_id , e_id IN SELECT v.account_id , e.name_id
|
||
|
FROM users.vacations v
|
||
|
INNER JOIN users.active_accounts aa ON aa.credentials_id = v.account_id
|
||
|
INNER JOIN users.credentials c ON c.address_id = v.account_id
|
||
|
INNER JOIN naming.empire_names n ON n.owner_id = v.account_id
|
||
|
INNER JOIN emp.empires e ON e.name_id = n.id
|
||
|
WHERE v.status = 'FUTURE' AND v.since <= now( )
|
||
|
FOR UPDATE OF v , aa , c , n , e
|
||
|
LOOP
|
||
|
UPDATE users.vacations SET status = 'PROCESSED' WHERE account_id = a_id;
|
||
|
END LOOP;
|
||
|
|
||
|
-- Decrease vacation credits
|
||
|
UPDATE users.active_accounts aa
|
||
|
SET vacation_credits = ( CASE
|
||
|
WHEN aa.vacation_credits > floor( sys.get_constant( 'vacation.cost' ) ) THEN
|
||
|
aa.vacation_credits - floor( sys.get_constant( 'vacation.cost' ) )
|
||
|
ELSE
|
||
|
0
|
||
|
END )
|
||
|
FROM users.vacations v
|
||
|
WHERE aa.credentials_id = v.account_id AND v.status = 'PROCESSED';
|
||
|
|
||
|
-- Process accounts that should exit vacation mode
|
||
|
FOR a_id , e_id IN SELECT v.account_id , e.name_id
|
||
|
FROM users.vacations v
|
||
|
INNER JOIN users.active_accounts aa ON aa.credentials_id = v.account_id
|
||
|
INNER JOIN users.credentials c ON c.address_id = v.account_id
|
||
|
INNER JOIN naming.empire_names n ON n.owner_id = v.account_id
|
||
|
INNER JOIN emp.empires e ON e.name_id = n.id
|
||
|
WHERE v.status = 'PROCESSED' AND aa.vacation_credits = 0
|
||
|
FOR UPDATE OF v , aa , c , n , e
|
||
|
LOOP
|
||
|
PERFORM users.write_log( a_id , 'INFO'::log_level , 'Out of vacation credits - leaving vacation mode' );
|
||
|
PERFORM users.sessions_terminate( users.sessions_login( a_id , 'N/A' , 'vac_end' , 'N/A' ) , 'MANUAL' );
|
||
|
DELETE FROM users.vacations WHERE account_id = a_id;
|
||
|
PERFORM emp.switch_enemies( e_id );
|
||
|
END LOOP;
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION users.process_vacations( ) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- Grants credits to an account
|
||
|
--
|
||
|
-- Parameters:
|
||
|
-- a_id Administrator identifier
|
||
|
-- c_id Account identifier
|
||
|
-- c_inc Credits to grant
|
||
|
--
|
||
|
|
||
|
CREATE OR REPLACE FUNCTION admin.grant_user_credits( a_id INT , c_id INT , c_inc INT )
|
||
|
RETURNS VOID
|
||
|
STRICT VOLATILE
|
||
|
SECURITY DEFINER
|
||
|
AS $$
|
||
|
BEGIN
|
||
|
UPDATE users.credentials SET credits = credits + c_inc
|
||
|
WHERE address_id = c_id;
|
||
|
PERFORM admin.write_log( a_id , 'INFO'::log_level , 'Granted ' || c_inc || ' credit(s) to user #' || c_id );
|
||
|
END;
|
||
|
$$ LANGUAGE plpgsql;
|
||
|
|
||
|
GRANT EXECUTE ON FUNCTION admin.grant_user_credits( INT , INT , INT ) TO :dbuser;
|
||
|
|
||
|
|
||
|
|
||
|
--
|
||
|
-- View for mail address change requests
|
||
|
--
|
||
|
|
||
|
CREATE VIEW users.mail_change_view
|
||
|
AS SELECT r.credentials_id AS id , r.used ,
|
||
|
( r.created + ( floor( sys.get_constant( 'accounts.acrDelay' ) ) || 's' )::INTERVAL ) AS expires ,
|
||
|
( CASE
|
||
|
WHEN r.used THEN
|
||
|
NULL
|
||
|
ELSE
|
||
|
a.address
|
||
|
END ) AS new_address
|
||
|
FROM users.address_change_requests r
|
||
|
INNER JOIN users.addresses a ON a.id = r.address_id;
|
||
|
|
||
|
GRANT SELECT ON users.mail_change_view TO :dbuser;
|