Admin/Public: add admin basic auth and async comment feedback
This commit is contained in:
parent
034feed282
commit
529df24013
13
admin.php
13
admin.php
|
|
@ -17,6 +17,19 @@ if (!is_file($configPath)) {
|
||||||
exit('deploy-config.php not found');
|
exit('deploy-config.php not found');
|
||||||
}
|
}
|
||||||
$config = require $configPath;
|
$config = require $configPath;
|
||||||
|
$basicUser = (string)($config['basic_auth_user'] ?? '');
|
||||||
|
$basicPass = (string)($config['basic_auth_pass'] ?? '');
|
||||||
|
|
||||||
|
if ($basicUser !== '' || $basicPass !== '') {
|
||||||
|
$authUser = (string)($_SERVER['PHP_AUTH_USER'] ?? '');
|
||||||
|
$authPass = (string)($_SERVER['PHP_AUTH_PW'] ?? '');
|
||||||
|
if (!hash_equals($basicUser, $authUser) || !hash_equals($basicPass, $authPass)) {
|
||||||
|
header('WWW-Authenticate: Basic realm="Admin"');
|
||||||
|
http_response_code(401);
|
||||||
|
exit('Unauthorized');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$tokenExpected = (string)($config['token'] ?? '');
|
$tokenExpected = (string)($config['token'] ?? '');
|
||||||
$tokenIncoming = (string)($_REQUEST['token'] ?? '');
|
$tokenIncoming = (string)($_REQUEST['token'] ?? '');
|
||||||
if ($tokenExpected === '' || !hash_equals($tokenExpected, $tokenIncoming)) {
|
if ($tokenExpected === '' || !hash_equals($tokenExpected, $tokenIncoming)) {
|
||||||
|
|
|
||||||
120
index.php
120
index.php
|
|
@ -22,12 +22,35 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') ==
|
||||||
$sectionId = (int)($_POST['section_id'] ?? 0);
|
$sectionId = (int)($_POST['section_id'] ?? 0);
|
||||||
$topicId = (int)($_POST['topic_id'] ?? 0);
|
$topicId = (int)($_POST['topic_id'] ?? 0);
|
||||||
$text = trim((string)($_POST['comment_text'] ?? ''));
|
$text = trim((string)($_POST['comment_text'] ?? ''));
|
||||||
|
$isAjax = (string)($_POST['ajax'] ?? '') === '1'
|
||||||
|
|| strcasecmp((string)($_SERVER['HTTP_X_REQUESTED_WITH'] ?? ''), 'XMLHttpRequest') === 0
|
||||||
|
|| str_contains((string)($_SERVER['HTTP_ACCEPT'] ?? ''), 'application/json');
|
||||||
|
|
||||||
|
$commentSaved = false;
|
||||||
|
$errorMessage = '';
|
||||||
|
|
||||||
if ($token !== '' && $photoId > 0 && $text !== '') {
|
if ($token !== '' && $photoId > 0 && $text !== '') {
|
||||||
$u = commenterByToken($token);
|
$u = commenterByToken($token);
|
||||||
if ($u) {
|
if ($u) {
|
||||||
commentAdd($photoId, (int)$u['id'], limitText($text, 1000));
|
commentAdd($photoId, (int)$u['id'], limitText($text, 1000));
|
||||||
|
$commentSaved = true;
|
||||||
|
} else {
|
||||||
|
$errorMessage = 'Ссылка для комментариев недействительна.';
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
$errorMessage = 'Заполни текст комментария.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isAjax) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
if ($commentSaved) {
|
||||||
|
echo json_encode(['ok' => true, 'message' => 'Ваш комментарий отправлен.'], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(422);
|
||||||
|
echo json_encode(['ok' => false, 'message' => $errorMessage !== '' ? $errorMessage : 'Не удалось отправить комментарий.'], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$redirect = './?photo_id=' . $photoId;
|
$redirect = './?photo_id=' . $photoId;
|
||||||
|
|
@ -435,6 +458,8 @@ function outputWatermarked(string $path, string $mime): never
|
||||||
.comment-input{width:100%;min-height:94px;border:1px solid #d1d5db;border-radius:10px;padding:10px;line-height:1.45;resize:vertical}
|
.comment-input{width:100%;min-height:94px;border:1px solid #d1d5db;border-radius:10px;padding:10px;line-height:1.45;resize:vertical}
|
||||||
.comment-input:focus{outline:0;border-color:#1f6feb;box-shadow:0 0 0 3px rgba(31,111,235,.16)}
|
.comment-input:focus{outline:0;border-color:#1f6feb;box-shadow:0 0 0 3px rgba(31,111,235,.16)}
|
||||||
.comment-actions{margin:0}
|
.comment-actions{margin:0}
|
||||||
|
.comment-feedback{margin:0;padding:9px 11px;border:1px solid #d1fae5;border-radius:10px;background:#ecfdf5;color:#065f46;font-size:13px;line-height:1.35}
|
||||||
|
.comment-feedback.is-error{border-color:#fecaca;background:#fef2f2;color:#991b1b}
|
||||||
.cmt{border-top:1px solid #e8edf5;padding-top:10px;margin-top:10px;line-height:1.45}
|
.cmt{border-top:1px solid #e8edf5;padding-top:10px;margin-top:10px;line-height:1.45}
|
||||||
.detail .cmt:first-of-type{border-top:0;margin-top:0;padding-top:0}
|
.detail .cmt:first-of-type{border-top:0;margin-top:0;padding-top:0}
|
||||||
.muted{color:#6b7280;font-size:13px}
|
.muted{color:#6b7280;font-size:13px}
|
||||||
|
|
@ -576,6 +601,7 @@ function outputWatermarked(string $path, string $mime): never
|
||||||
<input type="hidden" name="viewer" value="<?= h($viewerToken) ?>">
|
<input type="hidden" name="viewer" value="<?= h($viewerToken) ?>">
|
||||||
<textarea class="js-comment-textarea comment-input" name="comment_text" required></textarea>
|
<textarea class="js-comment-textarea comment-input" name="comment_text" required></textarea>
|
||||||
<p class="comment-actions"><button class="btn" type="submit">Отправить</button></p>
|
<p class="comment-actions"><button class="btn" type="submit">Отправить</button></p>
|
||||||
|
<p class="comment-feedback js-comment-feedback" role="status" aria-live="polite" hidden></p>
|
||||||
</form>
|
</form>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<p class="muted">Комментарии может оставлять только пользователь с персональной ссылкой.</p>
|
<p class="muted">Комментарии может оставлять только пользователь с персональной ссылкой.</p>
|
||||||
|
|
@ -774,6 +800,94 @@ function outputWatermarked(string $path, string $mime): never
|
||||||
(() => {
|
(() => {
|
||||||
const commentTextarea = document.querySelector('.js-comment-textarea');
|
const commentTextarea = document.querySelector('.js-comment-textarea');
|
||||||
const commentForm = commentTextarea ? commentTextarea.closest('.js-comment-form') : null;
|
const commentForm = commentTextarea ? commentTextarea.closest('.js-comment-form') : null;
|
||||||
|
const commentFeedback = commentForm ? commentForm.querySelector('.js-comment-feedback') : null;
|
||||||
|
const commentSubmitButton = commentForm ? commentForm.querySelector('button[type="submit"]') : null;
|
||||||
|
|
||||||
|
const setCommentFeedback = (message, isError) => {
|
||||||
|
if (!commentFeedback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!message) {
|
||||||
|
commentFeedback.hidden = true;
|
||||||
|
commentFeedback.textContent = '';
|
||||||
|
commentFeedback.classList.remove('is-error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
commentFeedback.hidden = false;
|
||||||
|
commentFeedback.textContent = message;
|
||||||
|
commentFeedback.classList.toggle('is-error', !!isError);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitCommentForm = () => {
|
||||||
|
if (!commentForm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof commentForm.requestSubmit === 'function') {
|
||||||
|
commentForm.requestSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (commentSubmitButton) {
|
||||||
|
commentSubmitButton.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
commentForm.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (commentForm && commentTextarea) {
|
||||||
|
commentForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (commentForm.dataset.sending === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = commentTextarea.value.trim();
|
||||||
|
if (text === '') {
|
||||||
|
setCommentFeedback('Заполни текст комментария.', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData(commentForm);
|
||||||
|
formData.set('ajax', '1');
|
||||||
|
|
||||||
|
commentForm.dataset.sending = '1';
|
||||||
|
if (commentSubmitButton) {
|
||||||
|
commentSubmitButton.disabled = true;
|
||||||
|
}
|
||||||
|
setCommentFeedback('', false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(commentForm.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = await response.json().catch(() => null);
|
||||||
|
if (!response.ok || !payload || payload.ok !== true) {
|
||||||
|
throw new Error(payload && payload.message ? String(payload.message) : 'Не удалось отправить комментарий.');
|
||||||
|
}
|
||||||
|
|
||||||
|
setCommentFeedback(payload.message || 'Ваш комментарий отправлен.', false);
|
||||||
|
commentTextarea.value = '';
|
||||||
|
commentTextarea.focus();
|
||||||
|
} catch (error) {
|
||||||
|
const fallbackMessage = 'Не удалось отправить комментарий.';
|
||||||
|
const message = error instanceof Error && error.message !== '' ? error.message : fallbackMessage;
|
||||||
|
setCommentFeedback(message, true);
|
||||||
|
} finally {
|
||||||
|
delete commentForm.dataset.sending;
|
||||||
|
if (commentSubmitButton) {
|
||||||
|
commentSubmitButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (commentTextarea) {
|
if (commentTextarea) {
|
||||||
commentTextarea.addEventListener('keydown', (e) => {
|
commentTextarea.addEventListener('keydown', (e) => {
|
||||||
if (!e.shiftKey || e.key !== 'Enter' || e.isComposing) {
|
if (!e.shiftKey || e.key !== 'Enter' || e.isComposing) {
|
||||||
|
|
@ -783,11 +897,7 @@ function outputWatermarked(string $path, string $mime): never
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (typeof commentForm.requestSubmit === 'function') {
|
submitCommentForm();
|
||||||
commentForm.requestSubmit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
commentForm.submit();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user