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
This commit is contained in:
Linty 2025-06-09 20:35:57 +02:00
parent 2624be1c90
commit ae740ba3af
20 changed files with 1937 additions and 102 deletions

View File

@ -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()

View File

@ -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 |
// +-----------------------------------------------------------------------+

View File

@ -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)

View File

@ -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;
}
?>

View File

@ -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;

View File

@ -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;
}
}
?>

View File

@ -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
);
}
?>

View File

@ -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;
}
?>

View File

@ -0,0 +1,36 @@
<?php
// +-----------------------------------------------------------------------+
// | This file is part of Piwigo. |
// | |
// | For copyright and license information, please view the COPYING.txt |
// | file that was distributed with this source code. |
// +-----------------------------------------------------------------------+
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
$upgrade_description = 'Modification to the user_auth_key table to match the api keys';
// we are modifying the "auth_key" table structure to support the new API key system.
// the existing structure was too limited for our needs, this update ensures better
// flexibility and security for managing API access tokens in the future.
pwg_query(
'ALTER TABLE `'.PREFIX_TABLE.'user_auth_keys`
ADD COLUMN `apikey_secret` VARCHAR(255) DEFAULT NULL AFTER auth_key,
ADD COLUMN `apikey_name` VARCHAR(100) DEFAULT NULL,
ADD COLUMN `key_type` VARCHAR(40) DEFAULT NULL,
ADD COLUMN `revoked_on` datetime DEFAULT NULL,
ADD COLUMN `last_used_on` datetime DEFAULT NULL
;');
// For rows that already exist in the table, we add a key_type
pwg_query(
'UPDATE `'.PREFIX_TABLE.'user_auth_keys`
SET `key_type` = \'auth_key\'
WHERE `key_type` IS NULL
;');
echo "\n".$upgrade_description."\n";
?>

View File

@ -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.';

View File

@ -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 dexpiration';
$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 <em>%s</em> will be used to notify you when your API key is about to expire.'] = 'L\'email <em>%s</em> 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.';

View File

@ -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 <em>%s</em> 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 );

View File

@ -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;
}
},
});
}
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(`<i class="gallery-icon-skull api-skull"></i> <span data-tooltip="${line.expired_on_format}">${line.expired_on_since}</span>`);
} else {
api_line.find('.api_expiration').html(`<i class="gallery-icon-skull api-skull"></i> <span>${/\d/.test(line.revoked_on_since) ? line.revoked_on_since : no_time_elapsed}</span> <i data-tooltip="${line.revoked_on_message}" class="icon-info-circled-1 api-info"></i>`);
}
}
});
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');
}

View File

@ -3,21 +3,39 @@
{combine_css path="admin/themes/default/fontello/css/fontello.css" order=-11}
<script>
var selected_language = `{$language_options[$current_language]}`;
var selected_language = `{$language_options[$current_language]}`;
var url_logo_light = `{$ROOT_URL}themes/standard_pages/images/piwigo_logo.svg`;
var url_logo_dark = `{$ROOT_URL}themes/standard_pages/images/piwigo_logo_dark.svg`;
</script>
{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}
<container id="mode" class="light">
@ -28,7 +46,6 @@ const preferencesDefaultValues = {
</div>
<div>
<a href="{$HELP_LINK}" target="_blank">{'Help'|translate}</a>
{include file='toaster.tpl'}
</div>
</section>
@ -49,7 +66,7 @@ const preferencesDefaultValues = {
</div>
<div class="form" id="account-display">
<div class="column-flex first">
<label for="username">{'Username'|translate}</label>
<label>{'Username'|translate}</label>
<div class="row-flex input-container username">
<i class="gallery-icon-user"></i>
<p>{$USERNAME}</p>
@ -57,9 +74,9 @@ const preferencesDefaultValues = {
</div>
</div>
<div class="column-flex">
<label for="mail_address">{'Email address'|translate}</label>
<label for="email">{'Email address'|translate}</label>
<div class="row-flex input-container">
<i class="gallery-icon-user"></i>
<i class="icon-mail-alt"></i>
<input type="email" name="mail_address" id="email" value="{$EMAIL}" />
</div>
<p id="email_error" class="error-message"><i class="gallery-icon-attention-circled"></i>
@ -94,7 +111,7 @@ const preferencesDefaultValues = {
</div>
<div class="column-flex">
<label for="theme">{'Theme'|translate}</label>
<label>{'Theme'|translate}</label>
<div class="row-flex input-container">
<i class="icon-brush"></i>
{html_options name=theme options=$template_options selected=$template_selection}
@ -103,7 +120,7 @@ const preferencesDefaultValues = {
</div>
<div class="column-flex">
<label for="language">{'Language'|translate}</label>
<label>{'Language'|translate}</label>
<div class="row-flex input-container">
<i class="icon-language"></i>
{html_options name=language options=$language_options selected=$language_selection}
@ -212,6 +229,205 @@ const preferencesDefaultValues = {
</section>
{/if}
{* API KEY *}
<section id="apikey-section" class="profile-section">
<div class="title">
<div class="column-flex">
<h1>{'API Keys'|translate}</h1>
<p>{'Create API Keys to secure your acount'|translate}</p>
</div>
<i class="gallery-icon-up-open display-btn close" data-display="apikey-display"></i>
</div>
<div class="form" id="apikey-display">
<div class="api-cant-manage" id="cant_manage_api">
<p>{'To manage your API keys, please log in with your username/password.'|translate|escape:html}</p>
</div>
<div class="new-apikey can-manage">
<button class="btn btn-main" id="new_apikey">{'New API Key'|translate}</button>
</div>
<div class="api-list can-manage">
<div class="api-list-head api-tab">
<div aria-hidden="true"></div>
<p>{'API Key name'|translate}</p>
<p>{'Created at'|translate}</p>
<p>{'Last use'|translate}</p>
<p id="api_expires_in">{'Expires in'|translate}</p>
<div aria-hidden="true"></div>
</div>
<div class="api-list-body" id="api_key_list">
<div class="api-tab-line border-line template-api" id="api_line">
<div class="api-icon-collapse">
<i class="gallery-icon-up-open icon-collapse close" data-api=""></i>
</div>
<p class="api_name"></p>
<p class="api_creation"></p>
<p class="api_last_use"></p>
<p class="api_expiration"></p>
<div class="api-icon-action row-flex" data-api="" data-pkid="">
<i class="icon-pencil edit-mode"></i>
<i class="icon-trash-1 delete-mode"></i>
</div>
</div>
<div class="api-tab-collapse border-line template-api" style="display: none;" id="api_collapse">
<div aria-hidden="true"></div>
<div class="keys">
<div class="row-flex key">
<i class="gallery-icon-hash"></i>
<p class="api_key"></p>
<i class="icon-clone" data-copy="" data-success=""></i>
<p id="" class="api-copy api-hide success-message">{"Public key copied."|translate|escape:html}</p>
</div>
<div class="row-flex key">
<i class="icon-key"></i>
<p>{"The secret key can no longer be displayed."|translate}</p>
</div>
</div>
</div>
</div>
<div class="new-apikey">
<button class="btn btn-link" id="show_expired_list" data-show="false">{'Show expired keys'|translate}</button>
</div>
<div class="api-list-body" id="api_key_list_expired">
</div>
</div>
</div>
{* API KEY MODAL *}
<div class="bg-modal" id="api_modal">
<div class="body-modal">
<a class="icon-cancel close-modal" id="close_api_modal"></a>
<div id="generate_keyapi">
<div class="head-modal">
<p class="title-modal">{'Generate API Key'|translate}</p>
<p class="subtitle-modal">{'Create a new API key to secure your account.'|translate}</p>
</div>
<div>
<div class="column-flex first">
<label for="api_key_name">{'API Key name'|translate}</label>
<div class="row-flex input-container">
<i class="icon-key"></i>
<input type="text" id="api_key_name" />
</div>
<p id="error_api_key_name" class="error-message"><i class="gallery-icon-attention-circled"></i>
{'must not be empty'|translate}</p>
</div>
<div class="row-flex section-expiration">
<div class="column-flex">
<label>{'Duration'|translate}</label>
<div class="row-flex input-container api-expiration">
<i class="gallery-icon-calendar"></i>
{html_options name=api_expiration options=$API_EXPIRATION}
</div>
<p id="error_api_key_date" class="error-message"><i class="gallery-icon-attention-circled"></i>
{'you must choose a date'|translate}</p>
</div>
<div class="column-flex" id="api_custom_date">
<label for="api_expiration_date">{'Custom date'|translate}</label>
<div class="row-flex input-container api-expiration">
<input type="date" id="api_expiration_date" name="api_expiration_custom" min="{$API_CURRENT_DATE}" />
</div>
</div>
</div>
<p class="api-mail-infos">{$API_EMAIL_INFOS}</p>
<div class="save">
<button class="btn btn-cancel" id="cancel_apikey">{'Cancel'|translate}</button>
<button class="btn btn-main" id="save_apikey">{'Generate key'|translate}</button>
</div>
</div>
</div>
<div id="retrieves_keyapi">
<div class="head-modal">
<p class="title-modal">{'Generate API Key'|translate}</p>
<p class="subtitle-modal">{'Save your secret Key and ID'|translate}</p>
<p class="modal-secret">{'This will not be displayed again. You must copy it to continue.'|translate}
<p>
</div>
<div class="modal-input-keys">
<p id="api_id_copy_success" class="api-copy api-hide success-message">
{"Public key copied."|translate|escape:html}</p>
</div>
<div class="input-modal input-modal-id row-flex">
<i class="gallery-icon-hash"></i>
<input type="text" id="api_id_key" />
<i class="icon-clone" id="api_id_copy"></i>
</div>
<div class="modal-input-keys">
<p id="api_key_copy_success" class="modal-input-key api-copy api-hide success-message">
{"Secret key copied. Keep it in a safe place."|translate|escape:html}</p>
</div>
<div class="input-modal input-modal-key row-flex">
<i class="icon-key"></i>
<input type="text" id="api_secret_key" />
<i class="icon-clone" id="api_secret_copy"></i>
</div>
<div class="save">
<button class="btn btn-main" id="done_apikey" disabled>{'Done'|translate}</button>
</div>
</div>
</div>
</div>
{* API KEY MODAL EDIT *}
<div class="bg-modal" id="api_modal_edit">
<div class="body-modal">
<a class="icon-cancel close-modal" id="close_api_modal_edit"></a>
<div>
<div class="head-modal">
<p class="title-modal">{'Edit API Key'|translate}</p>
</div>
<div class="column-flex first">
<label for="api_key_edit">{'API Key name'|translate}</label>
<div class="row-flex input-container">
<i class="icon-key"></i>
<input type="text" id="api_key_edit" />
</div>
<p id="error_api_key_edit" class="error-message"><i class="gallery-icon-attention-circled"></i>
{'must not be empty'|translate}</p>
</div>
<div class="save">
<button class="btn btn-main" id="save_api_edit">{'Save'|translate}</button>
</div>
</div>
</div>
</div>
{* API KEY MODAL REVOKE *}
<div class="bg-modal" id="api_modal_revoke">
<div class="body-modal">
<a class="icon-cancel close-modal" id="close_api_modal_revoke"></a>
<div>
<div class="head-modal">
<p class="title-modal" id="api_modal_revoke_title"></p>
</div>
<div class="save">
<button class="btn btn-cancel" id="cancel_api_revoke">{'Cancel'|translate}</button>
<button class="btn btn-main btn-revoked" id="revoke_api_key">{'Revoke'|translate}</button>
</div>
</div>
</div>
</div>
</section>
{if isset($PLUGINS_PROFILE)}
{foreach from=$PLUGINS_PROFILE item=plugin_block key=k_block}
<section id="{$k_block}-section" class="profile-section">
@ -225,12 +441,12 @@ const preferencesDefaultValues = {
<div class="form plugins" id="{$k_block}-display">
{include file=$plugin_block.template}
{if $plugin_block.standard_show_save}
<div class="save">
<button class="btn btn-main" id="save_{$k_block}">{'Submit'|translate}</button>
</div>
{footer_script}
<div class="save">
<button class="btn btn-main" id="save_{$k_block}">{'Submit'|translate}</button>
</div>
{footer_script}
standardSaveSelector.push('#save_{$k_block}');
{/footer_script}
{/footer_script}
{/if}
</div>
</section>
@ -252,4 +468,5 @@ const preferencesDefaultValues = {
</div>
</section>
{/if}
{include file='toaster.tpl'}
</container>

View File

@ -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}
<div class="toaster" id="pwg_toaster">

View File

@ -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,

View File

@ -84,6 +84,21 @@
<form id="methodWrapper" style="display:none;">
<div class="card">
<h3 class="card-title"><i class="icon-cog-alt"></i>Authenticate with API Key (Header)</h3>
<div class="card-content">
<p class="header-description"> Introduced in Piwigo 16, you can now use an API key in the HTTP header
to perform authenticated requests without a user session.
For more details, check out our <a href="https://github.com/Piwigo/Piwigo/wiki/Piwigo-Web-API#api-key-authentication" target="_blank">documentation</a>.
</p>
<p class="header-warning">Doesn't work when you use "INVOKE (new window)"</p>
<div class="header-setting">
<p>Authorization:</p>
<input type="text" id="apiKey" placeholder="pkid-xxxxxxxx-xxxxxxxxxxxxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" />
</div>
</div>
</div> <!-- methodHeader -->
<div class="card" id="methodDescription" style="display:none;">
<h3 class="card-title"><i class="icon-book"></i>Description</h3>
<blockquote>
@ -134,7 +149,7 @@
<div class="select">
<select id="responseFormat">
<option value="json" selected>JSON</option>
<option value="rest" selected>REST (xml)</option>
<option value="rest">REST (xml)</option>
<option value="php">PHP serial</option>
<option value="xmlrpc">XML RPC</option>
</select>
@ -187,7 +202,7 @@
</div> <!-- the_container -->
<div id="the_footer">
Copyright &copy; 2002-2021 <a href="http://piwigo.org">Piwigo Team</a>
Copyright &copy; 2002-2025 <a href="http://piwigo.org">Piwigo Team</a>
</div> <!-- the_footer -->
</div>

View File

@ -349,6 +349,29 @@ input[type="text"] {
position: relative;
}
.header-setting {
display: flex;
align-items: center;
gap: 10px;
}
.header-setting input {
max-width: 580px;
width: 100%;
}
.header-description {
margin: 0;
margin-bottom: 10px;
}
.header-warning {
margin: 0;
color: red;
font-size: 12px;
font-style: italic;
}
/* #requestResultDisplay {
background: white;
} */

View File

@ -93,7 +93,8 @@ $(() => {
url: ws_url,
data: { format: "json", method: "reflection.getMethodList" }
}).done(function (result) {
console.log(result);
// for debug
//console.log(result);
result = parsePwgJSON(result);
if (result != null) {
@ -328,6 +329,8 @@ $(() => {
// invoke method
function invokeMethod(methodName, newWindow) {
$('#json-viewer').jsonViewer({});
$('#requestURLDisplay').show();
$('#requestResultDisplay').show();
@ -335,6 +338,18 @@ $(() => {
let reqUrl = ws_url + "?format=" + $("#responseFormat").val();
const isJson = $("#responseFormat").val() === 'json';
const authorization = $('#apiKey').val();
const useCookie = '' === authorization;
let fetchOption = {};
if (!useCookie) {
fetchOption.credentials = 'omit';
fetchOption.headers = {
Authorization: authorization
}
}
// GET
if ($("#requestFormat").val() == 'get') {
reqUrl += "&method=" + methodName;
@ -361,19 +376,11 @@ $(() => {
window.open(reqUrl);
}
else {
if ($("#responseFormat").val() === 'json') {
$("#invokeFrame").hide();
$('#json-viewer').show();
fetch(reqUrl)
.then(data => data.json())
.then(json => {
$('#json-viewer').jsonViewer(json);
})
} else {
$("#invokeFrame").show();
$('#json-viewer').hide();
$("#invokeFrame").attr('src', reqUrl);
}
fetch(reqUrl, fetchOption)
.then(data => data.text())
.then(data => {
showResponseData(data);
})
}
$('#requestURLDisplay').find('.url').html(reqUrl).end()
@ -411,22 +418,30 @@ $(() => {
}
}
if (!newWindow && $("#responseFormat").val() === 'json') {
if (!newWindow) {
$("#invokeFrame").hide();
$('#json-viewer').show();
jQuery.ajax({
url: reqUrl,
type: 'POST',
dataType: 'json',
data: {
"method": methodName,
...params
},
success : function(data) {
$('#json-viewer').jsonViewer(data);
}
const formData = new URLSearchParams();
formData.append('method', methodName);
for (const key in params) {
formData.append(key, params[key]);
}
fetchOption.headers ??= {};
fetchOption.headers['Content-Type'] = 'application/x-www-form-urlencoded';
fetch(reqUrl, {
...fetchOption,
method: 'POST',
body: formData
})
.then(data => {
return data.text();
})
.then(data => {
showResponseData(data);
});
} else {
$("#invokeFrame").show();
$('#json-viewer').hide();
@ -491,3 +506,35 @@ $(() => {
}
})
})
function showResponseData(data) {
const isJson = $("#responseFormat").val() === 'json';
if (isJson) {
try {
const json = JSON.parse(data);
$('#json-viewer').jsonViewer(json);
$("#invokeFrame").hide();
$('#json-viewer').show();
} catch (error) {
const iframe = $('#invokeFrame');
const iframeDoc = iframe[0].contentDocument || iframe[0].contentWindow.document;
iframeDoc.open();
iframeDoc.write(`<pre>${data}</pre>`);
iframeDoc.close();
$("#invokeFrame").show();
$('#json-viewer').hide();
}
return;
}
const iframe = $('#invokeFrame');
const iframeDoc = iframe[0].contentDocument || iframe[0].contentWindow.document;
iframeDoc.open();
iframeDoc.write(data);
iframeDoc.close();
$("#invokeFrame").show();
$('#json-viewer').hide();
}

52
ws.php
View File

@ -1567,6 +1567,58 @@ enabled_high, registration_date, registration_date_string, registration_date_sin
$ws_functions_root . 'pwg.users.php',
array('admin_only'=>true, 'post_only'=>true)
);
$service->addMethod(
'pwg.users.api_key.create',
'ws_create_api_key',
array(
'key_name' => array(),
'duration' => array(
'type' => WS_TYPE_INT|WS_TYPE_POSITIVE,
'info' => 'Number of days',
),
'pwg_token' => array(),
),
'Create a new api key for the user in the current session',
$ws_functions_root . 'pwg.users.php',
array('admin_only'=>false, 'post_only'=>true)
);
$service->addMethod(
'pwg.users.api_key.revoke',
'ws_revoke_api_key',
array(
'pkid' => array(),
'pwg_token' => array(),
),
'Revoke a api key for the user in the current session',
$ws_functions_root . 'pwg.users.php',
array('admin_only'=>false, 'post_only'=>true)
);
$service->addMethod(
'pwg.users.api_key.edit',
'ws_edit_api_key',
array(
'key_name' => array(),
'pkid' => array(),
'pwg_token' => array(),
),
'Edit a api key for the user in the current session',
$ws_functions_root . 'pwg.users.php',
array('admin_only'=>false, 'post_only'=>true)
);
$service->addMethod(
'pwg.users.api_key.get',
'ws_get_api_key',
array(
'pwg_token' => array(),
),
'Get all api key for the user in the current session',
$ws_functions_root . 'pwg.users.php',
array('admin_only'=>false, 'post_only'=>true)
);
}
?>