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
+420 -16
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');
}
+234 -17
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>
+18 -7
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">
+403 -25
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,