From ae740ba3afedf2f1b99d34247c5a1cc3cc6d3f4e Mon Sep 17 00:00:00 2001 From: Linty Date: Mon, 9 Jun 2025 20:35:57 +0200 Subject: [PATCH] fixes #2355 implement API key management system - Added API key get, creation, editing, and revocation methods. - Updated the profile template to include API key management features. - Updated the database schema to support the new API key system, including additional fields for key management. - Added client-side JavaScript functionality to handle API key operations and display responses. - Update tools/htm.ws with the new way to authenticate. - Restriction of certain api methods when used with an api key - Backward compatibility with older apps --- identification.php | 2 + include/config_default.inc.php | 28 ++ include/functions_session.inc.php | 7 + include/functions_user.inc.php | 258 +++++++++++- include/user.inc.php | 39 ++ include/ws_core.inc.php | 27 ++ include/ws_functions/pwg.php | 28 +- include/ws_functions/pwg.users.php | 156 ++++++++ install/db/176-database.php | 36 ++ language/en_UK/common.lang.php | 39 ++ language/fr_FR/common.lang.php | 39 ++ profile.php | 43 +- themes/standard_pages/js/profile.js | 436 ++++++++++++++++++++- themes/standard_pages/template/profile.tpl | 251 +++++++++++- themes/standard_pages/template/toaster.tpl | 25 +- themes/standard_pages/theme.css | 428 ++++++++++++++++++-- tools/ws.htm | 19 +- tools/ws/ws.css | 23 ++ tools/ws/ws.js | 103 +++-- ws.php | 52 +++ 20 files changed, 1937 insertions(+), 102 deletions(-) create mode 100644 install/db/176-database.php diff --git a/identification.php b/identification.php index c1699caef..dc84521a8 100644 --- a/identification.php +++ b/identification.php @@ -74,6 +74,8 @@ if (isset($_POST['login'])) // {redirect (final) = http://localhost/piwigo/git/admin.php} $root_url = get_absolute_root_url(); + $_SESSION['connected_with'] = 'pwg_ui'; + redirect( empty($redirect_to) ? get_gallery_home_url() diff --git a/include/config_default.inc.php b/include/config_default.inc.php index b1e27bc6a..21336b389 100644 --- a/include/config_default.inc.php +++ b/include/config_default.inc.php @@ -460,6 +460,34 @@ $conf['session_use_ip_address'] = true; // session"). $conf['session_gc_probability'] = 1; +// +-----------------------------------------------------------------------+ +// | api key | +// +-----------------------------------------------------------------------+ + +// api_key_duration: available duration options (in days) for API key creation. +// Array of predefined durations that will be displayed in the select dropdown +// when creating a new API key. Use 'custom' to allow users to set a specific +// expiration date with a date picker input. +$conf['api_key_duration'] = ['30', '90', '180', '365', 'custom']; + +// The following API methods are prohibited when making requests with an API key. +// These restrictions are in place for security reasons and to prevent unauthorized +// access to sensitive operations that require higher-level authentication. +$conf['api_key_forbidden_methods'] = array( + // users + 'pwg.users.generatePasswordLink', + 'pwg.users.getAuthKey', + 'pwg.users.setMainUser', + 'pwg.users.setInfo', + // plugins + 'pwg.plugins.performAction', + // themes + 'pwg.themes.performAction', + // extensions + 'pwg.extensions.ignoreUpdate', + 'pwg.extensions.update', +); + // +-----------------------------------------------------------------------+ // | debug/performance | // +-----------------------------------------------------------------------+ diff --git a/include/functions_session.inc.php b/include/functions_session.inc.php index b36b39743..f7718a100 100644 --- a/include/functions_session.inc.php +++ b/include/functions_session.inc.php @@ -153,6 +153,13 @@ SELECT data */ function pwg_session_write($session_id, $data) { + // when the request is authenticated via api_key (PWG_API_KEY_REQUEST), + // you do not want the session to be written to the database (no user session persistence) + // this avoids polluting the session table with stateless API accesses + if (defined('PWG_API_KEY_REQUEST')) + { + return true; + } $query = ' REPLACE INTO '.SESSIONS_TABLE.' (id,data,expiration) diff --git a/include/functions_user.inc.php b/include/functions_user.inc.php index 02e0a5fda..350c44638 100644 --- a/include/functions_user.inc.php +++ b/include/functions_user.inc.php @@ -1661,14 +1661,28 @@ function get_recent_photos_sql($db_field) * * @return bool */ -function auth_key_login($auth_key) +function auth_key_login($auth_key, $connection_by_header=false) { global $conf, $user, $page; - if (!preg_match('/^[a-z0-9]{30}$/i', $auth_key)) + $valid_key = false; + $secret_key = null; + if (preg_match('/^[a-z0-9]{30}$/i', $auth_key)) { - return false; + $valid_key = 'auth_key'; } + else if ( + preg_match('/^pkid-\d{8}-[a-z0-9]{20}:[a-z0-9]{40}$/i', $auth_key) + and $connection_by_header + ) + { + $valid_key = 'api_key'; + $tmp_key = explode(':', $auth_key); + $auth_key = $tmp_key[0]; + $secret_key = $tmp_key[1]; + } + + if (!$valid_key) return false; $query = ' SELECT @@ -1689,6 +1703,22 @@ SELECT $key = $keys[0]; + // the key is an api_key + if ('api_key' === $valid_key) + { + // check secret + if (!pwg_password_verify($secret_key, $key['apikey_secret'])) + { + return false; + } + + // is the key is revoked? + if (null != $key['revoked_on']) + { + return false; + } + } + // is the key still valid? if (strtotime($key['expired_on']) < strtotime($key['dbnow'])) { @@ -1697,12 +1727,34 @@ SELECT } // admin/webmaster/guest can't get connected with authentication keys - if (!in_array($key['status'], array('normal','generic'))) + if ('auth_key' === $valid_key and !in_array($key['status'], array('normal','generic'))) { return false; } $user['id'] = $key['user_id']; + + // update last used key + single_update( + USER_AUTH_KEYS_TABLE, + array('last_used_on' => $key['dbnow']), + array( + 'user_id' => $user['id'], + 'auth_key' => $key['auth_key'] + ), + ); + + // set the type of connection + $_SESSION['connected_with'] = $valid_key; + + // if the connection is made via an API key in the header, + // access is authenticated without creating a persistent user session + // this enables stateless authentication for API calls + if ($connection_by_header) + { + return true; + } + log_user($user['id'], false); trigger_notify('login_success', $key['username']); @@ -1771,6 +1823,7 @@ SELECT 'created_on' => $now, 'duration' => $conf['auth_key_duration'], 'expired_on' => $expiration, + 'key_type' => 'auth_key', ); single_insert(USER_AUTH_KEYS_TABLE, $key); @@ -1799,6 +1852,7 @@ UPDATE '.USER_AUTH_KEYS_TABLE.' SET expired_on = NOW() WHERE user_id = '.$user_id.' AND expired_on > NOW() + AND key_type = \'auth_key\' ;'; pwg_query($query); } @@ -2383,4 +2437,200 @@ SELECT 'account' => $updates ); } + +/** + * Create a new api_key + * + * @since 16 + * @param int $user_id + * @param int|null $duration + * @param string $key_name + * @return array auth_key / apikey_secret / apikey_name / + * user_id / created_on / duration / expired_on / key_type + */ +function create_api_key($user_id, $duration, $key_name) +{ + $key_id = 'pkid-'.date('Ymd').'-'.generate_key(20); + $key_secret = generate_key(40); + + list($dbnow) = pwg_db_fetch_row(pwg_query('SELECT NOW();')); + + $key = array( + 'auth_key' => $key_id, + 'apikey_secret' => pwg_password_hash($key_secret), + 'apikey_name' => $key_name, + 'user_id' => $user_id, + 'created_on' => $dbnow, + 'key_type' => 'api_key' + ); + + if (!empty($duration)) + { + $query = ' +SELECT + ADDDATE(NOW(), INTERVAL '.($duration * 60 * 60 * 24).' SECOND) +;'; + list($expiration) = pwg_db_fetch_row(pwg_query($query)); + $key['duration'] = $duration; + } + $key['expired_on'] = $expiration; + + single_insert(USER_AUTH_KEYS_TABLE, $key); + + $key['apikey_secret'] = $key_secret; + return $key; +} + +/** + * Revoke a api_key + * + * @since 16 + * @param int $user_id + * @param string $pkid + * @return string|bool + */ +function revoke_api_key($user_id, $pkid) +{ + $query = ' +SELECT + COUNT(*), + NOW() + FROM `'.USER_AUTH_KEYS_TABLE.'` + WHERE auth_key = "'.$pkid.'" + AND user_id = '.$user_id.' +;'; + + list($key, $now) = pwg_db_fetch_row(pwg_query($query)); + if ($key == 0) + { + return l10n('API Key not found'); + } + + single_update( + USER_AUTH_KEYS_TABLE, + array('revoked_on' => $now), + array( + 'auth_key' => $pkid, + 'user_id' => $user_id + ) + ); + + return true; +} + +/** + * Edit a api_key + * + * @since 16 + * @param int $user_id + * @param string $pkid + * @return string|bool + */ +function edit_api_key($user_id, $pkid, $api_name) +{ + $query = ' +SELECT + COUNT(*) + FROM `'.USER_AUTH_KEYS_TABLE.'` + WHERE auth_key = "'.$pkid.'" + AND user_id = '.$user_id.' +;'; + + list($key) = pwg_db_fetch_row(pwg_query($query)); + if ($key == 0) + { + return l10n('API Key not found'); + } + + single_update( + USER_AUTH_KEYS_TABLE, + array('apikey_name' => $api_name), + array( + 'auth_key' => $pkid, + 'user_id' => $user_id + ) + ); + + return true; +} + +/** + * Get all api_key + * + * @since 16 + * @param string $user_id + * @return array|false + */ +function get_api_key($user_id) +{ + $query = ' +SELECT * + FROM `'.USER_AUTH_KEYS_TABLE.'` + WHERE user_id = '.$user_id.' + AND key_type = "api_key" +;'; + + $api_keys = query2array($query); + if (!$api_keys) return false; + + $query = ' +SELECT + NOW() +;'; + list($now) = pwg_db_fetch_row(pwg_query($query)); + + foreach ($api_keys as $i => $api_key) + { + $api_key['apikey_secret'] = str_repeat("*", 40); + unset($api_key['auth_key_id'], $api_key['user_id'], $api_key['key_type']); + + $api_key['created_on_format'] = format_date($api_key['created_on'], array('day', 'month', 'year')); + $api_key['expired_on_format'] = format_date($api_key['expired_on'], array('day', 'month', 'year')); + $api_key['last_used_on_since'] = + $api_key['last_used_on'] + ? time_since($api_key['last_used_on'], 'day') + : l10n('Never'); + + $expired_on = str2DateTime($api_key['expired_on']); + $now = str2DateTime($now); + + $api_key['is_expired'] = $expired_on < $now; + if ($api_key['is_expired']) + { + $api_key['expiration'] = l10n('Expired'); + } + else + { + $diff = dateDiff($now, $expired_on); + if ($diff->days > 0) + { + $api_key['expiration'] = l10n('%d days', $diff->days); + } + elseif ($diff->h > 0) + { + $api_key['expiration'] = l10n('%d hours', $diff->h); + } + else + { + $api_key['expiration'] = l10n('%d minutes', $diff->i); + } + } + + $api_key['expired_on_since'] = time_since($api_key['expired_on'], 'day'); + + $api_key['revoked_on_since'] = + $api_key['revoked_on'] + ? time_since($api_key['revoked_on'], 'day') + : null; + + $api_key['revoked_on_message'] = + $api_key['revoked_on'] + ? l10n('This API key was manually revoked on %s', format_date($api_key['revoked_on'], array('day', 'month', 'year'))) + : null; + + $api_keys[$i] = $api_key; + } + + return $api_keys; +} ?> diff --git a/include/user.inc.php b/include/user.inc.php index c3be8418f..8bb62e76c 100644 --- a/include/user.inc.php +++ b/include/user.inc.php @@ -56,6 +56,44 @@ if (isset($_GET['auth'])) auth_key_login($_GET['auth']); } +// HTTP_AUTHORIZATION api_key +if ( + defined('IN_WS') + and isset($_SERVER['HTTP_AUTHORIZATION']) + and !empty($_SERVER['HTTP_AUTHORIZATION']) + and isset($_REQUEST['method']) +) +{ + $auth_header = pwg_db_real_escape_string($_SERVER['HTTP_AUTHORIZATION']) ?? null; + + if ($auth_header) + { + $authenticate = auth_key_login($auth_header, true); + if (!$authenticate) + { + include_once(PHPWG_ROOT_PATH.'include/ws_init.inc.php'); + $service->sendResponse(new PwgError(401, 'Invalid api_key')); + exit; + } + define('PWG_API_KEY_REQUEST', true); + + // set pwg_token for api_key request + if (isset($_POST['pwg_token'])) + { + $_POST['pwg_token'] = get_pwg_token(); + } + + if (isset($_GET['pwg_token'])) + { + $_GET['pwg_token'] = get_pwg_token(); + } + + // logger + global $logger; + $logger->info('[api_key][pkid='.explode(':', $auth_header)[0].'][method='.$_REQUEST['method'].']'); + } +} + if ( defined('IN_WS') and isset($_REQUEST['method']) @@ -70,6 +108,7 @@ if ( $service->sendResponse(new PwgError(999, 'Invalid username/password')); exit(); } + $_SESSION['connected_with'] = 'pwg.images.uploadAsync'; } $page['user_use_cache'] = true; diff --git a/include/ws_core.inc.php b/include/ws_core.inc.php index 3fa2bea39..8130035fe 100644 --- a/include/ws_core.inc.php +++ b/include/ws_core.inc.php @@ -517,6 +517,11 @@ Request format: ".@$this->_requestFormat." Response format: ".@$this->_responseF return new PwgError(401, 'Access denied'); } + if (!$this->isAuthorizedMethodForAPIKEY()) + { + return new PwgError(401, 'Access denied'); + } + // parameter check and data correction $signature = $method['signature']; $missing_params = array(); @@ -679,5 +684,27 @@ Request format: ".@$this->_requestFormat." Response format: ".@$this->_responseF } return $res; } + + function isAuthorizedMethodForAPIKEY() + { + global $conf; + + // if the request is made with an API key (via header or session API key), + // we check whether the requested method is on the + // list of prohibited methods ($conf['api_key_forbidden_methods']) for API keys + // if it is, access is refused (false) + if ( + defined('PWG_API_KEY_REQUEST') + OR (isset($_SESSION['connected_with']) AND 'ws_session_login_api_key' === $_SESSION['connected_with']) + ) + { + if (in_array($_REQUEST['method'], $conf['api_key_forbidden_methods'])) + { + return false; + } + } + + return true; + } } ?> diff --git a/include/ws_functions/pwg.php b/include/ws_functions/pwg.php index b155e07a4..c1886b189 100644 --- a/include/ws_functions/pwg.php +++ b/include/ws_functions/pwg.php @@ -347,8 +347,24 @@ DELETE FROM '. RATE_TABLE .' */ function ws_session_login($params, &$service) { - if (try_log_user($params['username'], $params['password'], false)) + if (defined('PWG_API_KEY_REQUEST')) { + return new PwgError(401, 'Cannot use this method with an api key'); + } + + if (preg_match('/^pkid-\d{8}-[a-z0-9]{20}$/i', $params['username'])) + { + $secret = pwg_db_real_escape_string($params['password']); + $authenticate = auth_key_login($params['username'].':'.$secret); + if ($authenticate) + { + $_SESSION['connected_with'] = 'ws_session_login_api_key'; + return true; + } + } + else if (try_log_user($params['username'], $params['password'], false)) + { + $_SESSION['connected_with'] = 'ws_session_login'; return true; } return new PwgError(999, 'Invalid username/password'); @@ -362,6 +378,11 @@ function ws_session_login($params, &$service) */ function ws_session_logout($params, &$service) { + if (defined('PWG_API_KEY_REQUEST')) + { + return new PwgError(401, 'Cannot use this method with an api key'); + } + if (!is_a_guest()) { logout_user(); @@ -390,11 +411,13 @@ function ws_session_getStatus($params, &$service) $res['current_datetime'] = $dbnow; $res['version'] = PHPWG_VERSION; $res['save_visits'] = do_log(); + $res['connected_with'] = $_SESSION['connected_with'] ?? null; // Piwigo Remote Sync does not support receiving the new (version 14) output "save_visits" if (isset($_SERVER['HTTP_USER_AGENT']) and preg_match('/^PiwigoRemoteSync/', $_SERVER['HTTP_USER_AGENT'])) { unset($res['save_visits']); + unset($res['connected_with']); } // Piwigo Remote Sync does not support receiving the available sizes @@ -1151,4 +1174,5 @@ SELECT 'summary' => $search_summary ); } -?> + +?> \ No newline at end of file diff --git a/include/ws_functions/pwg.users.php b/include/ws_functions/pwg.users.php index bb66f6baf..baf44f14d 100644 --- a/include/ws_functions/pwg.users.php +++ b/include/ws_functions/pwg.users.php @@ -629,6 +629,8 @@ SELECT '.$conf['user_fields']['password'].' AS password $params['password'] = $params['new_password']; } + + // Unset admin field also new and conf password unset( $params['new_password'], $params['conf_new_password'], @@ -949,4 +951,158 @@ function ws_set_main_user($params, &$service) conf_update_param('webmaster_id', $params['user_id']); return 'The main user has been changed.'; } + +/** + * API method + * Create a new api key for the current user + * @since 15 + * @param mixed[] $params + */ +function ws_create_api_key($params, &$service) +{ + global $user, $logger; + + if (is_a_guest() OR !can_manage_api_key()) return new PwgError(401, 'Acces Denied'); + + if (get_pwg_token() != $params['pwg_token']) + { + return new PwgError(403, 'Invalid security token'); + } + + if ($params['duration'] < 1 OR $params['duration'] > 999999) + { + return new PwgError(400, 'Invalid duration max days is 999999'); + } + + if (strlen($params['key_name']) > 100) + { + return new PwgError(400, 'Key name is too long'); + } + + $key_name = pwg_db_real_escape_string($params['key_name']); + $duration = 0 == $params['duration'] ? 1 : $params['duration']; + + $secret = create_api_key($user['id'], $duration, $key_name); + + $logger->info('[api_key][user_id='.$user['id'].'][action=create][key_name='.$params['key_name'].']'); + + return $secret; +} + +/** + * API method + * Revoke a api key for the current user + * @since 15 + * @param mixed[] $params + */ +function ws_revoke_api_key($params, &$service) +{ + global $user, $logger; + + if (is_a_guest() OR !can_manage_api_key()) return new PwgError(401, 'Acces Denied'); + + if (get_pwg_token() != $params['pwg_token']) + { + return new PwgError(403, l10n('Invalid security token')); + } + + if (!preg_match('/^pkid-\d{8}-[a-z0-9]{20}$/i', $params['pkid'])) + { + return new PwgError(403, l10n('Invalid pkid format')); + } + + $revoked_key = revoke_api_key($user['id'], $params['pkid']); + + if (true !== $revoked_key) + { + return new PwgError(403, $revoked_key); + } + + $logger->info('[api_key][user_id='.$user['id'].'][action=revoke][pkid='.$params['pkid'].']'); + + return l10n('API Key has been successfully revoked.'); +} + +/** + * API method + * Edit a api key for the current user + * @since 15 + * @param mixed[] $params + */ +function ws_edit_api_key($params, &$service) +{ + global $user, $logger; + + if (is_a_guest()) + { + return new PwgError(401, 'Acces Denied'); + } + + if (!can_manage_api_key()) + { + return new PwgError(401, 'Acces Denied'); + } + + if (get_pwg_token() != $params['pwg_token']) + { + return new PwgError(403, l10n('Invalid security token')); + } + + if (!preg_match('/^pkid-\d{8}-[a-z0-9]{20}$/i', $params['pkid'])) + { + return new PwgError(403, l10n('Invalid pkid format')); + } + + $key_name = pwg_db_real_escape_string($params['key_name']); + $edited_key = edit_api_key($user['id'], $params['pkid'], $key_name); + + if (true !== $edited_key) + { + return new PwgError(403, $edited_key); + } + + $logger->info('[api_key][user_id='.$user['id'].'][action=edit][pkid='.$params['pkid'].'][new_name='.$key_name.']'); + + return l10n('API Key has been successfully edited.'); +} + +/** + * API method + * Get all api key for the current user + * @since 15 + * @param mixed[] $params + */ +function ws_get_api_key($params, &$service) +{ + global $user; + + if (is_a_guest()) + { + return new PwgError(401, 'Acces Denied'); + } + + if (!can_manage_api_key()) + { + return new PwgError(401, 'Acces Denied'); + } + + if (get_pwg_token() != $params['pwg_token']) + { + return new PwgError(403, 'Invalid security token'); + } + + $api_keys = get_api_key($user['id']); + + return $api_keys ?? l10n('No API key found'); +} + +function can_manage_api_key() +{ + // You can manage your api key only if you are connected via identification.php + if (isset($_SESSION['connected_with']) and 'pwg_ui' === $_SESSION['connected_with']) + { + return true; + } + return false; +} ?> diff --git a/install/db/176-database.php b/install/db/176-database.php new file mode 100644 index 000000000..813d96603 --- /dev/null +++ b/install/db/176-database.php @@ -0,0 +1,36 @@ + \ No newline at end of file diff --git a/language/en_UK/common.lang.php b/language/en_UK/common.lang.php index ff6d4360f..fb798a993 100644 --- a/language/en_UK/common.lang.php +++ b/language/en_UK/common.lang.php @@ -477,3 +477,42 @@ $lang['Choose how you want to see your gallery'] = 'Choose how you want to see y $lang['Change your password'] = 'Change your password'; $lang['Options'] = 'Options'; $lang['Your changes have been applied.'] = 'Your changes have been applied.'; +$lang['Create API Keys to secure your acount'] = 'Create API Keys to secure your account'; +$lang['API Keys'] = 'API Keys'; +$lang['Created at'] = 'Created at'; +$lang['Last use'] = 'Last use'; +$lang['Expires in'] = 'Expires in'; +$lang['Expired on'] = 'Expired on'; +$lang['Never'] = 'Never'; +$lang['New API Key'] = 'New API Key'; +$lang['Show expired keys'] = 'Show expired keys'; +$lang['Hide expired keys'] = 'Hide expired keys'; +$lang['Generate API Key'] = 'Generate API Key'; +$lang['Create a new API key to secure your account.'] = 'Create a new API key to secure your account.'; +$lang['API Key name'] = 'API Key name'; +$lang['Duration'] = 'Duration'; +$lang['Custom date'] = 'Custom date'; +$lang['Generate key'] = 'Generate key'; +$lang['Save your secret Key and ID'] = 'Save your secret Key and ID'; +$lang['This will not be displayed again. You must copy it to continue.'] = 'This will not be displayed again. You must copy it to continue.'; +$lang['Done'] = 'Done'; +$lang['Public key copied.'] = 'Public key copied.'; +$lang['Secret key copied. Keep it in a safe place.'] = 'Secret key copied. Keep it in a safe place.'; +$lang['Impossible to copy automatically. Please copy manually.'] = 'Impossible to copy automatically. Please copy manually.'; +$lang['The api key has been successfully created.'] = 'The API key has been successfully created.'; +$lang['API Key not found'] = 'API Key not found'; +$lang['Expired'] = 'Expired'; +$lang['API Key has been successfully revoked.'] = 'API Key has been successfully revoked.'; +$lang['API Key has been successfully edited.'] = 'API Key has been successfully edited.'; +$lang['No expiration'] = 'No expiration'; +$lang['must not be empty'] = 'must not be empty'; +$lang['The secret key can no longer be displayed.'] = 'The secret key can no longer be displayed.'; +$lang['Revoked'] = 'Revoked'; +$lang['Revoke'] = 'Revoke'; +$lang['The email %s will be used to notify you when your API key is about to expire.'] = 'The email %s will be used to notify you when your API key is about to expire.'; +$lang['You have no email address, so you will not be notified when your API key is about to expire.'] = 'You have no email address, so you will not be notified when your API key is about to expire.'; +$lang['you must choose a date'] = 'you must choose a date'; +$lang['This API key was manually revoked on %s'] = 'This API key was manually revoked on %s'; +$lang['Edit API Key'] = 'Edit API Key'; +$lang['Do you really want to revoke the "%s" API key?'] = 'Do you really want to revoke the "%s" API key?'; +$lang['To manage your API keys, please log in with your username/password.'] = 'To manage your API keys, please log in with your username/password.'; diff --git a/language/fr_FR/common.lang.php b/language/fr_FR/common.lang.php index 28e6580d4..a86f22915 100644 --- a/language/fr_FR/common.lang.php +++ b/language/fr_FR/common.lang.php @@ -476,3 +476,42 @@ $lang['Choose how you want to see your gallery'] = 'Choisissez comment vous voul $lang['Change your password'] = 'Changez votre mot de passe'; $lang['Options'] = 'Options'; $lang['Your changes have been applied.'] = 'Vos changements ont été pris en compte.'; +$lang['Create API Keys to secure your acount'] = 'Créez des clés API pour sécuriser votre compte'; +$lang['API Keys'] = 'Clés API'; +$lang['Created at'] = 'Crée le'; +$lang['Last use'] = 'Dernière utilisation'; +$lang['Expires in'] = 'Expire dans'; +$lang['Expired on'] = 'Expiré le'; +$lang['Never'] = 'Jamais'; +$lang['New API Key'] = 'Nouvelle clé API'; +$lang['Show expired keys'] = 'Afficher les clés expirées'; +$lang['Hide expired keys'] = 'Masquer les clés expirées'; +$lang['Generate API Key'] = 'Générer une clé API'; +$lang['Create a new API key to secure your account.'] = 'Créez une nouvelle clé API pour sécuriser votre compte.'; +$lang['API Key name'] = 'Nom de la clé API'; +$lang['Duration'] = 'Durée'; +$lang['Custom date'] = 'Date personnalisée'; +$lang['Generate key'] = 'Générer la clé'; +$lang['Save your secret Key and ID'] = 'Enregistrez votre clé secrète et votre identifiant'; +$lang['This will not be displayed again. You must copy it to continue.'] = 'La clé secrete ne sera plus affichée. Vous devez la copier pour continuer.'; +$lang['Done'] = 'Terminé'; +$lang['Public key copied.'] = 'Clé publique copiée.'; +$lang['Secret key copied. Keep it in a safe place.'] = 'Clé secrète copiée. Gardez-la dans un endroit sûr.'; +$lang['Impossible to copy automatically. Please copy manually.'] = 'Impossible de copier automatiquement. Veuillez copier manuellement.'; +$lang['The api key has been successfully created.'] = 'La clé API a été créée avec succès.'; +$lang['API Key not found'] = 'Clé API non trouvée'; +$lang['Expired'] = 'Expirée'; +$lang['API Key has been successfully revoked.'] = 'La clé API a été révoquée avec succès.'; +$lang['API Key has been successfully edited.'] = 'La clé API a été modifiée avec succès.'; +$lang['No expiration'] = 'Pas d’expiration'; +$lang['must not be empty'] = 'ne doit pas être vide'; +$lang['The secret key can no longer be displayed.'] = 'La clé secrète ne peut plus être affichée.'; +$lang['Revoked'] = 'Révoqué'; +$lang['Revoke'] = 'Révoquer'; +$lang['The email %s will be used to notify you when your API key is about to expire.'] = 'L\'email %s sera utilisé pour vous notifier que votre clé API est sur le point d\'expirer.'; +$lang['You have no email address, so you will not be notified when your API key is about to expire.'] = 'Vous n\'avez pas d\'adresse email, vous ne serez donc pas notifié lorsque votre clé API sera sur le point d\'expirer.'; +$lang['you must choose a date'] = 'vous devez choisir une date'; +$lang['This API key was manually revoked on %s'] = 'Cette clé API a été révoquée manuellement le %s'; +$lang['Edit API Key'] = 'Modifier la clé API'; +$lang['Do you really want to revoke the "%s" API key?'] = 'Voulez-vous vraiment révoquer la clé API "%s" ?'; +$lang['To manage your API keys, please log in with your username/password.'] = 'Pour gérer vos clés API, veuillez vous connecter avec votre nom d\'utilisateur/mot de passe.'; diff --git a/profile.php b/profile.php index 86f182a92..f8e0d4a84 100644 --- a/profile.php +++ b/profile.php @@ -342,7 +342,7 @@ function save_profile_from_post($userdata, &$errors) */ function load_profile_in_template($url_action, $url_redirect, $userdata, $template_prefixe=null) { - global $template, $conf; + global $template, $conf, $user; $template->assign('radio_options', array( @@ -382,6 +382,47 @@ function load_profile_in_template($url_action, $url_redirect, $userdata, $templa $template->assign('SPECIAL_USER', $special_user); $template->assign('IN_ADMIN', defined('IN_ADMIN')); + // api key expiration choice + list($dbnow) = pwg_db_fetch_row(pwg_query('SELECT ADDDATE(NOW(), INTERVAL 1 DAY);')); + $template->assign('API_CURRENT_DATE', explode(' ', $dbnow)[0]); + + $duration = array(); + $display_duration = array(); + $has_custom = false; + foreach ($conf['api_key_duration'] as $day) + { + if ('custom' === $day) + { + $has_custom = true; + continue; + } + $duration[] = 'ADDDATE(NOW(), INTERVAL '.$day.' DAY) as `'.$day.'`'; + } + + $query = ' +SELECT + '.implode(', ', $duration).' +;'; + $result = query2array($query)[0]; + foreach ($result as $day => $date) + { + $display_duration[ $day ] = l10n('%d days', $day) . ' (' . format_date($date, array('day', 'month', 'year')) . ')'; + } + + if ($has_custom) + { + $display_duration['custom'] = l10n('Custom date'); + } + $template->assign('API_EXPIRATION', $display_duration); + $template->assign('API_SELECTED_EXPIRATION', array_key_first($display_duration)); + $template->assign('API_CAN_MANAGE', 'pwg_ui' === ($_SESSION['connected_with'] ?? null)); + + $email_notifications_infos = $user['email'] ? + l10n('The email %s will be used to notify you when your API key is about to expire.', $user['email']) + : l10n('You have no email address, so you will not be notified when your API key is about to expire.'); + $template->assign('API_EMAIL_INFOS', $email_notifications_infos); + + // allow plugins to add their own form data to content trigger_notify( 'load_profile_in_template', $userdata ); diff --git a/themes/standard_pages/js/profile.js b/themes/standard_pages/js/profile.js index a7a2496b3..c8a59e2fb 100644 --- a/themes/standard_pages/js/profile.js +++ b/themes/standard_pages/js/profile.js @@ -10,23 +10,14 @@ $(function() { // close element.style.maxHeight = element.scrollHeight + 'px'; void element.offsetHeight; - element.style.maxHeight = '0px'; + element.style.maxHeight = '1px'; selector.removeClass('open'); $(this).addClass('close'); } else { // open selector.addClass('open'); - element.style.maxHeight = element.scrollHeight + 'px'; + resetSection(display); $(this).removeClass('close'); - if ('account-display' !== display) { - setTimeout(() => { - const el = $(`#${display.split('-')[0]}-section`).get(0); - el.scrollIntoView({ - behavior: 'smooth', - block: 'start' - }); - }, 200); - } } }); @@ -85,7 +76,6 @@ $(function() { }); standardSaveSelector.forEach((selector, i) => { - // console.log(i, selector); $(selector).on('click', function() { const values = {}; $(`#${i}-section`).find('input, textarea, select').each((i, element) => { @@ -126,9 +116,79 @@ $(function() { $('#opt_comment').prop('checked', preferencesDefaultValues.opt_comment); $('#opt_hits').prop('checked', preferencesDefaultValues.opt_hits); }); + + // API KEY BELOW + if (!can_manage_api) { + $('.can-manage').hide(); + $('#cant_manage_api').show(); + return; + }; + $('#new_apikey').on('click', function() { + openApiModal(); + }); + + $('#close_api_modal, #cancel_apikey').on('click', function() { + closeApiModal(); + }); + + $('#close_api_modal_edit').on('click', function() { + closeApiEditModal(); + }); + + $('#close_api_modal_revoke, #cancel_api_revoke').on('click', function() { + closeApiRevokeModal(); + }); + + $('#show_expired_list').on('click', function() { + const api_list_expired = $('#api_key_list_expired'); + const isOpen = $(this).data('show'); + if(!isOpen) { + api_list_expired.get(0).style.maxHeight = 'max-content'; + $(this).text(str_hide_expired); + } else { + api_list_expired.get(0).style.maxHeight = '0'; + $(this).text(str_show_expired); + } + + $(this).data('show', !isOpen); + + resetSection('apikey-display', false, true); + }); + + $(window).on('keydown', function(e) { + const haveApiModal = $('#api_modal').is(':visible'); + const haveApiEditModal = $('#api_modal_edit').is(':visible'); + const haveApiRevokeModal = $('#api_modal_revoke').is(':visible'); + if (haveApiModal && e.key === 'Escape') { + closeApiModal(); + } + if (haveApiEditModal && e.key === 'Escape') { + closeApiEditModal(); + } + if (haveApiRevokeModal && e.key === 'Escape') { + closeApiRevokeModal(); + } + }); + + $('select[name="api_expiration"]').on('change', function() { + const custom_date = $('#api_custom_date'); + const value = $(this).val(); + if ('custom' === value) { + custom_date.css('display', 'flex'); + } else { + custom_date.css('display', 'none'); + } + $('#error_api_key_date').hide(); + }); + + $('#api_expiration_date').on('change', function() { + $('#error_api_key_date').hide(); + }); + + getAllApiKeys(); }); -function setInfos(params, method='pwg.users.setMyInfo') { +function setInfos(params, method='pwg.users.setMyInfo', callback=null, errCallback=null) { // for debug // console.log('setInfos', params); const all_params = { @@ -142,15 +202,359 @@ function setInfos(params, method='pwg.users.setMyInfo') { data: all_params, success: (data) => { if (data.stat == 'ok') { + if (typeof callback === 'function') { + callback(data.result); + return; + }; pwgToaster({ text: data.result, icon: 'success' }); } else if (data.stat == 'fail') { pwgToaster({ text: data.message, icon: 'error' }); } else { - pwgToaster({ text: 'Error try later...', icon: 'error' }); + pwgToaster({ text: str_handle_error, icon: 'error' }); + } + if (typeof callback === 'function') { + errCallback(data); + return; } }, error: function (e) { - pwgToaster({ text: e.responseJSON?.message ?? 'Server Internal Error try later...', icon: 'error' }); + pwgToaster({ text: e.responseJSON?.message ?? str_handle_error, icon: 'error' }); + if (typeof callback === 'function') { + errCallback(e); + return; + } }, }); -} \ No newline at end of file +} + +function getAllApiKeys(reset = false) { + $.ajax({ + url: 'ws.php?format=json&method=pwg.users.api_key.get', + type: "POST", + dataType: 'json', + data: { + pwg_token: PWG_TOKEN + }, + success: function(res) { + if (res.stat == 'ok') { + if (typeof res.result === 'string') { + // No keys + } else { + AddApiLine(res.result, reset); + } + } + }, + error: function(e) { + pwgToaster({ text: e.responseJSON?.message ?? str_handle_error + 'getAllApiKeys', icon: 'error' }); + } + }); +} + +function AddApiLine(lines, reset) { + const api_list = $('#api_key_list'); + const api_list_expired = $('#api_key_list_expired'); + + $('#api_key_list .api-tab-line:not(.template-api), #api_key_list .api-tab-collapse:not(.template-api)').remove(); + $('#api_key_list_expired .api-tab-line:not(.template-api), #api_key_list_expired .api-tab-collapse:not(.template-api)').remove(); + + lines.forEach((line, i) => { + const api_line = $('#api_line').clone(); + const api_collapse = $('#api_collapse').clone(); + const tmp_id = line.auth_key.slice(24, 34); + + api_line.removeClass('template-api').addClass('api-tab'); + api_line.attr('id', `api_${tmp_id}`); + api_line.find('.icon-collapse').data('api', tmp_id); + api_line.find('.api_name').text(line.apikey_name).attr('title', line.apikey_name); + api_line.find('.api_creation').text(line.created_on_format); + api_line.find('.api_last_use').text(line.last_used_on_since).attr('title', line.last_used_on_since); + api_line.find('.api_expiration').text(line.expiration); + api_line.find('.api-icon-action').attr('data-api', `api_${tmp_id}`); + api_line.find('.api-icon-action').attr('data-pkid', line.auth_key); + + api_collapse.attr('id', `api_collapse_${tmp_id}`); + api_collapse.removeClass('template-api'); + api_collapse.find('.api_key').text(line.auth_key); + api_collapse.find('.icon-clone').attr({ + 'data-copy': line.auth_key, + 'data-success': `api_copy_success_${tmp_id}` + }); + api_collapse.find('.api-copy').attr('id', `api_copy_success_${tmp_id}`); + + if (!line.revoked_on && !line.is_expired) { + api_list.append(api_line); + api_line.after(api_collapse); + } else { + api_list_expired.append(api_line); + api_line.after(api_collapse); + api_line.find('.api-icon-action').remove(); + if (line.is_expired) { + api_line.find('.api_expiration').html(` ${line.expired_on_since}`); + } else { + api_line.find('.api_expiration').html(` ${/\d/.test(line.revoked_on_since) ? line.revoked_on_since : no_time_elapsed} `); + } + } + + }); + + apiLineEvent(); + if (reset) { + resetSection('apikey-display'); + } +} + +function apiLineEvent() { + $('.icon-collapse').off('click').on('click', function() { + const api_collapse = $(`#api_collapse_${$(this).data('api')}`); + const api_line = $(`#api_${$(this).data('api')}`); + + if (api_collapse.is(':visible')) { + api_collapse.removeClass('open'); + api_line.removeClass('open'); + api_line.find('.icon-collapse').addClass('close'); + api_collapse.css('display', 'none'); + api_collapse.find('.api-copy').addClass('api-hide'); + } else { + api_collapse.addClass('open'); + api_line.addClass('open'); + api_line.find('.icon-collapse').removeClass('close'); + api_collapse.css('display', 'grid'); + } + + resetSection('apikey-display', false, true); + }); + + $('.api-tab-collapse .icon-clone').off('click').on('click', function() { + const data_to_copy = $(this).data('copy'); + const selector = $(this).data('success'); + copyToClipboard(data_to_copy, str_copy_key_id, `#${selector}`); + }); + + $('.api-tab-line .edit-mode').off('click').on('click', function() { + const selector = $(this).parent().data('api'); + openApiEditModal(`#${selector}`); + }); + + $('.api-tab-line .delete-mode').off('click').on('click', function() { + const selector = $(this).parent().data('api'); + openApiRevokeModal(`#${selector}`); + }); + +} + +function resetSection(selector, scroll = true, maxContent = false) { + const el = $(`#${selector}`); + const element = el.get(0); + const scrollH = maxContent ? 'max-content' : element.scrollHeight + 'px'; + element.style.maxHeight = scrollH; + + if ('account-display' !== selector && scroll) { + setTimeout(() => { + const el = $(`#${selector.split('-')[0]}-section`).get(0); + el.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + }, 200); + } +} + +function openApiModal() { + $('#api_modal').fadeIn(); + $('#api_key_name').trigger('focus'); + saveApiKeyEvent(); +} + +function closeApiModal() { + $('#api_modal').fadeOut(() => { + $('#api_key_name').val(''); + $('select[name="api_expiration"]').val(selected_date).trigger('change'); + $('#api_expiration_date').val(''); + + $('#api_secret_key').val(''); + $('#retrieves_keyapi').hide(); + $('#generate_keyapi').show(); + $('#done_apikey').attr('disabled', true); + $('#api_key_copy_success, #api_id_copy_success').addClass('api-hide'); + }); + unbindApiKeyEvents(); +} + +function successApiModal(secret, id) { + $('#api_secret_key').val(secret); + $('#api_id_key').val(id); + + $('#generate_keyapi').hide(); + $('#retrieves_keyapi').fadeIn(); + + $('#api_secret_copy').off('click').on('click', function() { + const copy = copyToClipboard(secret, str_copy_key_secret, '#api_key_copy_success'); + + $('#done_apikey').removeAttr('disabled'); + $('#done_apikey').on('click', closeApiModal); + }); + + $('#api_id_copy').off('click').on('click', function() { + const copy = copyToClipboard(id, str_copy_key_id, '#api_id_copy_success'); + }); +} + +//api edit modal +function openApiEditModal(selector) { + const value = $(selector).find('.api_name').text(); + const pkid = $(selector).find('.api-icon-action').data('pkid'); + $('#api_key_edit').val(value); + $('#api_modal_edit').fadeIn(); + $('#api_key_edit').trigger('focus'); + saveApiEditEvents(pkid); +} + +function closeApiEditModal() { + $('#api_modal_edit').fadeOut(() => { + $('#api_key_edit').val(''); + unbindApiEditEvents(); + }); +} + +function saveApiEditEvents(pkid) { + $('#save_api_edit').on('click', function() { + const value = $('#api_key_edit').val(); + + if ('' == value) { + $('#error_api_key_edit').show(); + return; + } + setInfos( + { + pkid, + key_name: value, + }, + 'pwg.users.api_key.edit', + (res) => { + pwgToaster({ text: str_api_edited, icon: 'success' }); + getAllApiKeys(true); + closeApiEditModal(); + } + ); + }); +} + +function unbindApiEditEvents() { + $('#save_api_edit').off('click'); +} + +// api revoke modal +function openApiRevokeModal(selector) { + const apiName = $(selector).find('.api_name').text(); + const pkid = $(selector).find('.api-icon-action').data('pkid'); + const text = sprintf(str_revoke_key, apiName); + $('#api_modal_revoke_title').text(text); + + $('#api_modal_revoke').fadeIn(); + saveApiRevokeEvents(pkid); +} + +function closeApiRevokeModal() { + $('#api_modal_revoke').fadeOut(() => { + $('#api_modal_revoke_title').text(''); + unbindApiRevokeEvents(); + }); +} + +function saveApiRevokeEvents(pkid) { + $('#revoke_api_key').on('click', function() { + setInfos( + { + pkid, + }, + 'pwg.users.api_key.revoke', + (res) => { + pwgToaster({ text: str_api_revoked, icon: 'success' }); + getAllApiKeys(true); + closeApiRevokeModal(); + } + ); + }); +} + +function unbindApiRevokeEvents() { + $('#revoke_api_key').off('click'); +} + +function copyToClipboard(copy, message, selector = null) { + if (window.isSecureContext && navigator.clipboard) { + navigator.clipboard.writeText(copy); + if (selector) { + $(selector).removeClass('api-hide'); + // auto hide + // setTimeout(() => { + // $(selector).addClass('api-hide'); + // }, 1000); + } else { + pwgToaster({ text: message, icon: 'success' }); + } + return true; + } else { + pwgToaster({ text: str_cant_copy, icon: 'error' }); + return false; + } +} + +function saveApiKeyEvent() { + const handler = () => { + const api_name = $('#api_key_name').val(); + let api_duration = $('select[name="api_expiration"]').val(); + + if (api_name == '') { + $('#error_api_key_name').show(); + return; + } + + if ('custom' === api_duration && !$('#api_expiration_date').val()) { + $('#error_api_key_date').show(); + return; + } + + + unbindApiKeyEvents(); + + if ('custom' === api_duration) { + const today = new Date(); + const custom_date = new Date($('#api_expiration_date').val()); + const one_day = 1000 * 60 * 60 * 24; + const days = Math.ceil((custom_date.getTime() - today.getTime() ) / (one_day)); + api_duration = days; + } else { + api_duration = Number(api_duration) ?? 1; + } + + setInfos( + { + key_name: api_name, + duration: api_duration + }, + 'pwg.users.api_key.create', + (res) => { + pwgToaster({ text: str_api_added, icon: 'success' }); + getAllApiKeys(true); + successApiModal(res.apikey_secret, res.auth_key); + }, + (err) => { + saveApiKeyEvent(); + } + ); + } + + $('#save_apikey').on('click.apikey', handler); + $(window).on('keydown.apikey', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + handler(); + } + }) +} + +function unbindApiKeyEvents() { + $('#api_modal').find('*').addBack().off('.apikey'); + $(window).off('.apikey'); +} diff --git a/themes/standard_pages/template/profile.tpl b/themes/standard_pages/template/profile.tpl index da9b9658c..e83d2c9c6 100644 --- a/themes/standard_pages/template/profile.tpl +++ b/themes/standard_pages/template/profile.tpl @@ -3,21 +3,39 @@ {combine_css path="admin/themes/default/fontello/css/fontello.css" order=-11} {combine_script id='standard_pages_js' load='async' require='jquery' path='themes/standard_pages/js/standard_pages.js'} {combine_script id='standard_profile_js' load='async' require='jquery' path='themes/standard_pages/js/profile.js'} +{combine_script id='common' load='footer' require='jquery' path='admin/themes/default/js/common.js'} {footer_script} const standardSaveSelector = []; const preferencesDefaultValues = { - nb_image_page: {$DEFAULT_USER_VALUES['nb_image_page']}, - recent_period: {$DEFAULT_USER_VALUES['recent_period']}, - opt_album: {$DEFAULT_USER_VALUES['expand']}, - opt_comment: {$DEFAULT_USER_VALUES['show_nb_comments']}, - opt_hits: {$DEFAULT_USER_VALUES['show_nb_hits']}, +nb_image_page: {$DEFAULT_USER_VALUES['nb_image_page']}, +recent_period: {$DEFAULT_USER_VALUES['recent_period']}, +opt_album: {$DEFAULT_USER_VALUES['expand']}, +opt_comment: {$DEFAULT_USER_VALUES['show_nb_comments']}, +opt_hits: {$DEFAULT_USER_VALUES['show_nb_hits']}, }; +const selected_date = "{$API_SELECTED_EXPIRATION}"; +const can_manage_api = {($API_CAN_MANAGE) ? "true" : "false"}; + +const str_copy_key_id = "{"Public key copied."|translate|escape:javascript}"; +const str_copy_key_secret = "{"Secret key copied. Keep it in a safe place."|translate|escape:javascript}"; +const str_cant_copy = "{"Impossible to copy automatically. Please copy manually."|translate|escape:javascript}"; +const str_api_added = "{"The api key has been successfully created."|translate|escape:javascript}"; +const str_revoked = "{"Revoked"|translate|escape:javascript}"; +const str_show_expired = "{"Show expired keys"|translate|escape:javascript}"; +const str_hide_expired = "{"Hide expired keys"|translate|escape:javascript}"; +const str_handle_error = "{"An error has occured"|translate|escape:javascript}"; +const str_expires_in = "{"Expires in"|translate|escape:javascript}"; +const str_expires_on = "{"Expired on"|translate|escape:javascript}"; +const str_revoke_key = "{'Do you really want to revoke the "%s" API key?'|translate|escape:javascript}"; +const str_api_revoked = "{"API Key has been successfully revoked."|translate|escape:javascript}"; +const str_api_edited = "{"API Key has been successfully edited."|translate|escape:javascript}"; +const no_time_elapsed = "{"right now"|translate|escape:javascript}"; {/footer_script} @@ -28,7 +46,6 @@ const preferencesDefaultValues = {
{'Help'|translate} - {include file='toaster.tpl'}
@@ -49,7 +66,7 @@ const preferencesDefaultValues = {
- +

{$USERNAME}

@@ -57,9 +74,9 @@ const preferencesDefaultValues = {
- +
- +

@@ -94,7 +111,7 @@ const preferencesDefaultValues = {

- +
{html_options name=theme options=$template_options selected=$template_selection} @@ -103,7 +120,7 @@ const preferencesDefaultValues = {
- +
{html_options name=language options=$language_options selected=$language_selection} @@ -212,6 +229,205 @@ const preferencesDefaultValues = { {/if} + {* API KEY *} +
+
+
+

{'API Keys'|translate}

+

{'Create API Keys to secure your acount'|translate}

+
+ +
+ +
+
+

{'To manage your API keys, please log in with your username/password.'|translate|escape:html}

+
+ +
+ +
+
+
+ +

{'API Key name'|translate}

+

{'Created at'|translate}

+

{'Last use'|translate}

+

{'Expires in'|translate}

+ +
+
+ +
+
+ +
+

+

+

+

+
+ + +
+
+ +
+ +
+ +
+
+
+ +
+
+ + {* API KEY MODAL *} +
+
+ + +
+
+

{'Generate API Key'|translate}

+

{'Create a new API key to secure your account.'|translate}

+
+ +
+
+ +
+ + +
+

+ {'must not be empty'|translate}

+
+ +
+
+ +
+ + {html_options name=api_expiration options=$API_EXPIRATION} +
+

+ {'you must choose a date'|translate}

+
+ +
+ +
+ +
+
+
+ +

{$API_EMAIL_INFOS}

+ +
+ + +
+
+
+ +
+
+

{'Generate API Key'|translate}

+

{'Save your secret Key and ID'|translate}

+

+

+ + +
+ + + +
+ + +
+ + + +
+ +
+ +
+
+ +
+
+ + {* API KEY MODAL EDIT *} +
+
+ + +
+
+

{'Edit API Key'|translate}

+
+ +
+ +
+ + +
+

+ {'must not be empty'|translate}

+
+ +
+ +
+
+
+
+ {* API KEY MODAL REVOKE *} +
+
+ + +
+
+

+
+ +
+ + +
+
+
+
+
+ {if isset($PLUGINS_PROFILE)} {foreach from=$PLUGINS_PROFILE item=plugin_block key=k_block}
@@ -225,12 +441,12 @@ const preferencesDefaultValues = {
{include file=$plugin_block.template} {if $plugin_block.standard_show_save} -
- -
- {footer_script} +
+ +
+ {footer_script} standardSaveSelector.push('#save_{$k_block}'); - {/footer_script} + {/footer_script} {/if}
@@ -252,4 +468,5 @@ const preferencesDefaultValues = {
{/if} + {include file='toaster.tpl'} \ No newline at end of file diff --git a/themes/standard_pages/template/toaster.tpl b/themes/standard_pages/template/toaster.tpl index a264802d1..14891d6c9 100644 --- a/themes/standard_pages/template/toaster.tpl +++ b/themes/standard_pages/template/toaster.tpl @@ -5,13 +5,14 @@ } .toaster { - position: absolute; + position: fixed; right: 15px; max-width: 300px; top: 40px; display: flex; flex-direction: column; gap: 10px; + z-index: 9999; } .toast { @@ -29,14 +30,24 @@ font-size: 33px; } -.toast.success { - background-color:#4CA530; - color:#D6FFCF; +.light .toast.success { + background-color: #D6FFCF; + color: #4CA530; } -.toast.error { - background-color:#BE4949; - color:#FFC8C8; +.light .toast.error { + background-color: #F8D7DC; + color: #EB3D33; +} + +.dark .toast.success { + background-color: #4EA590; + color: #AAF6E4; +} + +.dark .toast.error { + background-color: #BE4949; + color: #FFC8C8; } {/html_style}
diff --git a/themes/standard_pages/theme.css b/themes/standard_pages/theme.css index ccee0c2e7..c6635cd4a 100644 --- a/themes/standard_pages/theme.css +++ b/themes/standard_pages/theme.css @@ -9,10 +9,18 @@ html{ #theHeader, #copyright, -.template-section{ +#api_custom_date, +#retrieves_keyapi, +.template-section, +.template-api, +.api_name_edit { display:none; } +.api-hide { + display: none !important; +} + #theIdentificationPage, #theRegisterPage, #thePasswordPage, @@ -95,7 +103,8 @@ h1 i{ bottom:0; } -.input-container{ +.input-container, +.input-modal { border-radius:3px; padding:5px 15px; margin-bottom:25px; @@ -103,6 +112,7 @@ h1 i{ } .input-container input, +.input-modal input, .input-container select, .input-container textarea{ background-color:transparent; @@ -129,6 +139,8 @@ input[type='radio'] { } .input-container input:focus, +.input-modal input:focus, +.profile-section .api-tab-line.edit input:focus, .input-container select:focus, .input-container textarea:focus{ border:none; @@ -139,11 +151,13 @@ select { padding: 5px 0; } -.input-container:focus-within{ +.input-container:focus-within, +.input-modal:focus-within{ border:1px solid #ff7700!important; } -.input-container i { +.input-container i, +.input-modal i { font-size:15px; margin-right:5px; } @@ -226,11 +240,36 @@ p.form-instructions{ color: #3C3C3C!important; } +.btn-cancel, +.btn-link { + background: none; + border: none; + padding: 0; + margin: 0; + font: inherit; + color: inherit; + cursor: pointer; + font-size: 14px; + line-height: 1; + height: fit-content; +} + +.btn-link { + text-decoration: underline; + margin-top: 15px; +} + a.btn-main{ display:block; text-align:center; } +.btn-main:disabled { + background-color:#aaaaaa!important; + color: #3C3C3C !important; + cursor: not-allowed; +} + #return-to-gallery{ margin: 30px auto; display:block; @@ -352,14 +391,19 @@ a.btn-main{ .error-message{ text-align: left; position: absolute; - bottom: 10px; + bottom: 5px; left:0; margin: 0; display:none; } +#error_api_key_date.error-message { + bottom: -20px; +} + .error-message i, -p.error-message{ +p.error-message, +.modal-secret { color: #EB3223!important; } @@ -417,7 +461,7 @@ p.error-message{ margin-top: 0; max-height: 0; overflow: hidden; - transition: max-height 0.2s ease; + transition: max-height 0.6s ease; } .profile-section .form.open { @@ -430,13 +474,22 @@ p.error-message{ /* gap: 15px; */ } -.profile-section .save { +.profile-section .save, +.profile-section .new-apikey, +.profile-section .modal-input-keys { display: flex; gap: 15px; justify-content: flex-end; + align-items: center; } +.profile-section .modal-input-keys { + position: absolute; + right: 30px; +} + .profile-section .save .btn-main, +.profile-section .new-apikey .btn-main, .profile-section .reset .btn-main { padding: 10px 35px; } @@ -445,7 +498,10 @@ p.error-message{ margin-bottom: 10px; } -.gallery-icon-up-open { +.gallery-icon-up-open:not( + .api-list .gallery-icon-up-open, + #api_key_list_expired .gallery-icon-up-open +) { position: absolute; top: 50%; cursor: pointer; @@ -453,10 +509,23 @@ p.error-message{ transition: transform 0.5s ease; } -.gallery-icon-up-open.close { +.gallery-icon-up-open.close, +.profile-section .icon-collapse.close { + position: relative; transform: rotate(180deg); } +.profile-section .api-icon-collapse .icon-collapse.close { + top: 2px; + left: -0.2px; +} + +.profile-section .icon-collapse { + display: inline-block; + transition: transform 0.4s; + vertical-align: middle; +} + .profile-section .username { width: fit-content; cursor: not-allowed; @@ -464,10 +533,17 @@ p.error-message{ border: none !important; } -.profile-section .input-container.radio { +.profile-section .input-container.radio, +.profile-section .section-expiration, +.profile-section .api-icon-action { gap: 10px; } +.profile-section .api-icon-action { + padding-right: 10px; + font-size: 14px; +} + .profile-section .input-container.radio label { display: flex; align-items: center; @@ -487,6 +563,227 @@ p.error-message{ gap: 15px; } +.profile-section .api-tab { + display: grid; + grid-template-columns: 60px 2fr 1fr 1fr 1fr 0.5fr; + /* grid-template-columns: 60px 200px 100px 100px 100px 30px; */ + justify-items: start; + align-items: center; + max-height: 40px; +} + +.profile-section .api-list-head { + padding: 15px 0; + border-radius: 8px; + margin-top: 15px; +} + +.profile-section .api-expiration { + width: fit-content; + margin-bottom: 0; +} + +.profile-section .api-mail-infos { + position: relative; + font-size: 12px; + padding-top: 20px; + margin-bottom: 25px; + text-align: start; +} + +.profile-section .api-icon-collapse { + justify-self: center; +} + +.profile-section .api-icon-action i:hover, +.close-modal:hover, +.profile-section .icon-clone:hover { + color: #ff7700; +} + +.profile-section .api-tab-line, +.profile-section .api-tab-collapse { + padding: 10px 0; + border-radius: 8px; + white-space: nowrap; +} + +.profile-section .api-tab-line.open { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.profile-section .api-tab-collapse.open { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.profile-section .api-tab-line i:not(#api_key_list_expired, .api_expiration .api-skull), +.profile-section .icon-clone { + cursor: pointer; +} + +.profile-section div.api-tab-line:nth-child(even) { + background-color: #303030; +} +.profile-section .api-tab-collapse .key { + gap: 10px; + padding: 5px 0; +} + +.profile-section .api-tab-collapse { + padding-bottom: 20px; + display: grid; + grid-template-columns: 60px auto; +} + +.profile-section .api_name, +.profile-section .api_creation, +.profile-section .api_expiration { + text-overflow: ellipsis; + max-width: 90%; + overflow: hidden; +} + +.profile-section #api_key_list_expired .api_expiration { + text-overflow: unset; + max-width: unset; +} + +.profile-section #api_key_list .border-line { + border: 1px solid transparent !important; +} + +.profile-section #api_key_list .edit { + border: 1px solid #ff7700 !important; +} + +.profile-section .api-list-head > p{ + text-align: start !important; +} + +.profile-section .api_last_use { + max-width: 98%; + overflow: hidden; + text-overflow: ellipsis; +} + +.profile-section .api_name_edit { + width: max-content; + background-color: transparent; + border: none; + border-radius: 3px; + width: 90%; +} + +.profile-section .new-apikey .btn-link{ + color: #9A9A9A !important; + font-weight: 700; +} + +.profile-section .api-copy { + padding: 1px 10px; + width: fit-content; + font-size: 12px; +} + +.profile-section #show_expired_list { + margin: 15px 0; +} + +.profile-section #api_key_list_expired { + max-height: 0; + transition: max-height 0.3s ease; + overflow: hidden; +} + +.profile-section .api-info { + font-size: 12px; +} + +.profile-section .api_key +.profile-section #api_id_key, +.profile-section #api_secret_key { + font-family: monospace !important; +} + +/* Modal */ + +.bg-modal { + display: none; + position: fixed; + z-index: 100; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0,0,0,0.7); +} + +.close-modal { + position: absolute; + right: -40px; + top: -40px; + font-size: 30px; + text-decoration: none !important; + cursor: pointer; +} + +.body-modal { + display: flex; + flex-direction: column; + position: absolute; + border-radius: 10px; + left: 50%; + top: 50%; + transform: translate(-50%, -48%); + text-align: left; + padding: 30px; + max-width: 600px; + width: 100%; +} + +#api_modal_revoke .body-modal { + max-width: 400px; +} + +.body-modal .btn-main { + margin-top: 0; +} + +.title-modal { + font-size: 24px !important; + font-weight: 600 !important; +} + +.subtitle-modal { + font-size: 16px !important; + font-weight: 400; +} + +.head-modal { + display: flex; + flex-direction: column; + align-items: start; + gap: 5px; +} + +#generate_keyapi .head-modal, +#api_modal_edit .head-modal, +#api_modal_revoke .head-modal { + margin-bottom: 25px; +} + +.input-modal-id { + margin-top: 25px; + margin-bottom: 5px; +} + +.input-modal-key { + margin-top: 30px; +} + /* The switch */ .switch { position: relative; @@ -547,6 +844,20 @@ input:checked + .slider:before, input:checked + .slider::after { border-radius: 50%; } +/* Tooltips */ +[data-tooltip]:hover::after { + position: absolute; + content: attr(data-tooltip); + animation: fadeIn 100ms cubic-bezier(0.42, 0, 0.62, 1.32) forwards; + animation-delay: 100ms; + border-radius: 5px; + max-width: 100%; + text-align: center; + font-size: 12px; + padding: 5px 10px; + box-shadow: 0px 10px 33px #3333332e; +} + /* Light */ #theIdentificationPage .light, #theRegisterPage .light, @@ -560,16 +871,19 @@ input:checked + .slider:before, input:checked + .slider::after { .light #password-form, .light #lang-select #other-languages, .light .profile-section, -.light .slider:before { +.light .slider:before, +.light .body-modal, +.light [data-tooltip]:hover::after { background-color:#ffffff; } #theIdentificationPage .light a, #theRegisterPage .light a, #thePasswordPage .light a, -#theProfilePage .light a, +#theProfilePage .light a:not(.close-modal), .light h1, .light .input-container input, +.light .input-modal input, .light .input-container select, .light .input-container textarea, .light .secondary-links, @@ -579,7 +893,9 @@ input:checked + .slider:before, input:checked + .slider::after { .light .profile-section i, .light #password-form p, .light .profile-section p, -.light #lang-select #other-languages span{ +.light #lang-select #other-languages span, +.light .btn-cancel, +.light .btn-link { color:#3C3C3C; } @@ -596,7 +912,9 @@ input:checked + .slider:before, input:checked + .slider::after { color:#ff7700; } -.light .input-container{ +.light .input-container, +.light .input-modal, +.light .api-list-head { background-color:#F0F0F0; border:1px solid #F0F0F0; } @@ -619,7 +937,7 @@ input:checked + .slider:before, input:checked + .slider::after { .light .success-message{ background-color: #DBF6D7; - color: #6DCE5E; + color: #6DCE5E !important; border-left: 4px solid #6DCE5E; } @@ -635,6 +953,35 @@ input:checked + .slider:before, input:checked + .slider::after { background-color: #CCCCCC; } +.light .api-list-body > div:nth-child(4n+1), +.light .api-list-body > div:nth-child(4n+2) { + background-color: #F8F8F8; +} + +.light .api-tab-line p, +.light .api-icon-action i, +.light .keys p:not(.api-copy), +.light .keys i, +.light #api_key_list_expired .api-skull { + color: #656565; +} + +.light .close-modal { + color: #ffffff; +} + +.light input[type="date"] { + color-scheme: light; +} + +.light input[type="date"]::-webkit-calendar-picker-indicator { + filter: invert(0); +} + +.light .btn-revoked { + background-color: #EB3223 !important; +} + /* Dark */ #theIdentificationPage .dark, #theRegisterPage .dark, @@ -646,16 +993,18 @@ input:checked + .slider:before, input:checked + .slider::after { .dark #login-form, .dark #register-form, .dark #password-form, -.dark .profile-section{ +.dark .profile-section, +.dark .body-modal { background-color:#3C3C3C; } #theIdentificationPage .dark a, #theRegisterPage .dark a, #thePasswordPage .dark a, -#theProfilePage .dark a, +#theProfilePage .dark a:not(.close-modal), .dark h1, .dark .input-container input, +.dark .input-modal input, .dark .input-container select, .dark .input-container textarea, .dark .secondary-links, @@ -665,7 +1014,9 @@ input:checked + .slider:before, input:checked + .slider::after { .dark .profile-section i, .dark #password-form p, .dark .profile-section p, -.dark #lang-select #other-languages span{ +.dark #lang-select #other-languages span, +.dark .btn-cancel, +.dark .btn-link { color:#D6D6D6; } @@ -683,7 +1034,8 @@ input:checked + .slider:before, input:checked + .slider::after { color:#FFEBD0; } -.dark .input-container{ +.dark .input-container, +.dark .input-modal { background-color:#303030; border:1px solid #303030; } @@ -712,7 +1064,7 @@ input:checked + .slider:before, input:checked + .slider::after { .dark .success-message{ background-color: #4EA590; - color: #AAF6E4; + color: #AAF6E4 !important; border-left: 4px solid #AAF6E4; } @@ -725,14 +1077,40 @@ input:checked + .slider:before, input:checked + .slider::after { background-color: #FFA646; } -.dark input:focus + .slider { - box-shadow: 0 0 1px #FFA646; -} - .dark .slider:before { background-color: #777777; } +.dark .api-list-head, +.dark [data-tooltip]:hover::after{ + background-color: #2A2A2A; +} + +.dark .api-list-body > div:nth-child(4n+1), +.dark .api-list-body > div:nth-child(4n+2) { + background-color: #333333; +} + +.dark .icon-collapse { + color: white !important; +} + +.dark .close-modal { + color: #3C3C3C; +} + +.dark input[type="date"] { + color-scheme: dark; +} + +.dark input[type="date"]::-webkit-calendar-picker-indicator { + filter: invert(0); +} + +.dark .btn-revoked { + background-color: #BE4949 !important; +} + /*Responsive display*/ @media (max-width: 768px) { #login-form, diff --git a/tools/ws.htm b/tools/ws.htm index e6807cd0f..296f471c7 100644 --- a/tools/ws.htm +++ b/tools/ws.htm @@ -84,6 +84,21 @@