Auto-submit four digit launcher PIN
This commit is contained in:
@@ -17,7 +17,7 @@
|
|||||||
- 차량 모니터 이동
|
- 차량 모니터 이동
|
||||||
- 공통 암호 로그인
|
- 공통 암호 로그인
|
||||||
- 공통 암호 변경
|
- 공통 암호 변경
|
||||||
- 숫자 전용 PIN 입력과 커스텀 숫자 키패드
|
- 4자리 숫자 PIN 입력과 커스텀 숫자 키패드
|
||||||
- 사이트 관리자 로그인 상태에서만 관리 버튼 표시
|
- 사이트 관리자 로그인 상태에서만 관리 버튼 표시
|
||||||
- 사이트 관리자 로그인 상태에서는 미니앱 실행 시 공통 암호 생략
|
- 사이트 관리자 로그인 상태에서는 미니앱 실행 시 공통 암호 생략
|
||||||
- 사이트 관리자 세션으로 미니앱 진입 시 공통 접근 쿠키 자동 발급
|
- 사이트 관리자 세션으로 미니앱 진입 시 공통 접근 쿠키 자동 발급
|
||||||
@@ -39,6 +39,8 @@
|
|||||||
|
|
||||||
메인 버튼은 기존 4개를 유지합니다. Rhymix 사이트 관리자 로그인 상태에서는 미니앱 실행 시 공통 암호를 묻지 않습니다. 이때 `/custom/launcher/api.php?adminSession=1` 또는 `session=1` 응답에서 공통 접근 쿠키를 함께 발급해, 이동한 미니앱 내부 세션 검사에서도 바로 통과합니다.
|
메인 버튼은 기존 4개를 유지합니다. Rhymix 사이트 관리자 로그인 상태에서는 미니앱 실행 시 공통 암호를 묻지 않습니다. 이때 `/custom/launcher/api.php?adminSession=1` 또는 `session=1` 응답에서 공통 접근 쿠키를 함께 발급해, 이동한 미니앱 내부 세션 검사에서도 바로 통과합니다.
|
||||||
|
|
||||||
암호는 숫자만 허용합니다. 로그인과 암호 변경 입력창은 `inputmode=numeric`, `[0-9]*` 패턴, 커스텀 숫자 키패드를 사용하며 Android/iPhone에서 문자 키보드가 뜨지 않도록 `readonly` 입력창에 화면 키패드로 값을 넣습니다. 서버 API도 숫자가 아닌 암호를 거부합니다.
|
암호는 숫자 4자리만 허용합니다. 로그인과 암호 변경 입력창은 `inputmode=numeric`, `[0-9]*` 패턴, 커스텀 숫자 키패드를 사용하며 Android/iPhone에서 문자 키보드가 뜨지 않도록 `readonly` 입력창에 화면 키패드로 값을 넣습니다. 서버 API도 4자리 숫자가 아닌 암호를 거부합니다.
|
||||||
|
|
||||||
|
로그인은 확인 버튼 없이 4자리가 입력되면 즉시 검증하고 성공 시 원래 선택한 미니앱으로 이동합니다. 암호 변경 화면도 변경/확인 버튼 없이 현재 암호, 새 암호, 새 암호 확인 순서로 4자리 입력이 끝날 때마다 다음 칸으로 이동하며, 마지막 칸까지 입력되면 바로 변경을 검증합니다. 이미 값이 들어간 입력창을 누르면 해당 칸의 값을 지우고 다시 입력받습니다. 현재 입력 중인 칸은 상태 문구와 파란 테두리로 표시합니다.
|
||||||
|
|
||||||
암호 변경은 배너 우하단의 작은 `관리` 링크에서 처리하며, 이 링크는 사이트 관리자 로그인 상태에서만 표시합니다. 암호 변경 시에는 현재 공통 암호 입력을 계속 요구합니다.
|
암호 변경은 배너 우하단의 작은 `관리` 링크에서 처리하며, 이 링크는 사이트 관리자 로그인 상태에서만 표시합니다. 암호 변경 시에는 현재 공통 암호 입력을 계속 요구합니다.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ $config = custom_config();
|
|||||||
|
|
||||||
function launcher_is_digit_password(string $password): bool
|
function launcher_is_digit_password(string $password): bool
|
||||||
{
|
{
|
||||||
return $password !== '' && ctype_digit($password);
|
return preg_match('/^\d{4}$/', $password) === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
@@ -48,10 +48,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
custom_json(['result' => 'fail', 'message' => 'current_password_invalid'], 401);
|
custom_json(['result' => 'fail', 'message' => 'current_password_invalid'], 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mb_strlen($next) < 4) {
|
|
||||||
custom_json(['result' => 'fail', 'message' => 'password_too_short'], 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!custom_save_password_hash(password_hash($next, PASSWORD_DEFAULT))) {
|
if (!custom_save_password_hash(password_hash($next, PASSWORD_DEFAULT))) {
|
||||||
custom_json(['result' => 'fail', 'message' => 'password_save_failed'], 500);
|
custom_json(['result' => 'fail', 'message' => 'password_save_failed'], 500);
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-31
@@ -2,6 +2,7 @@ const shortcut = document.getElementById('findMyShortcut');
|
|||||||
let failIndex = 0;
|
let failIndex = 0;
|
||||||
|
|
||||||
const apiUrl = '/custom/launcher/api.php';
|
const apiUrl = '/custom/launcher/api.php';
|
||||||
|
const PIN_LENGTH = 4;
|
||||||
|
|
||||||
const iconItems = [
|
const iconItems = [
|
||||||
{
|
{
|
||||||
@@ -98,14 +99,12 @@ function showPasswordForm(onSuccess) {
|
|||||||
|
|
||||||
const title = createFormHeader('비밀번호', renderIcons);
|
const title = createFormHeader('비밀번호', renderIcons);
|
||||||
const input = createPinInput('blog_secret', '비밀번호');
|
const input = createPinInput('blog_secret', '비밀번호');
|
||||||
const button = createButton('확인');
|
|
||||||
const msg = document.createElement('div');
|
const msg = document.createElement('div');
|
||||||
msg.id = 'failMsg';
|
msg.id = 'failMsg';
|
||||||
|
|
||||||
const inputRow = document.createElement('div');
|
const inputRow = document.createElement('div');
|
||||||
inputRow.className = 'launcher-input-row';
|
inputRow.className = 'launcher-input-row';
|
||||||
inputRow.appendChild(input);
|
inputRow.appendChild(input);
|
||||||
inputRow.appendChild(button);
|
|
||||||
|
|
||||||
const column = document.createElement('div');
|
const column = document.createElement('div');
|
||||||
column.className = 'launcher-form-column';
|
column.className = 'launcher-form-column';
|
||||||
@@ -116,6 +115,7 @@ function showPasswordForm(onSuccess) {
|
|||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
const password = onlyDigits(input.value);
|
const password = onlyDigits(input.value);
|
||||||
input.value = password;
|
input.value = password;
|
||||||
|
if (password.length !== PIN_LENGTH) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const messageRes = await fetch(`${apiUrl}?failMessages=1`);
|
const messageRes = await fetch(`${apiUrl}?failMessages=1`);
|
||||||
@@ -152,7 +152,6 @@ function showPasswordForm(onSuccess) {
|
|||||||
|
|
||||||
const keypad = createPinKeypad([input], submit);
|
const keypad = createPinKeypad([input], submit);
|
||||||
column.appendChild(keypad);
|
column.appendChild(keypad);
|
||||||
button.addEventListener('click', submit);
|
|
||||||
attachPinInputEvents(input, submit);
|
attachPinInputEvents(input, submit);
|
||||||
input.focus();
|
input.focus();
|
||||||
}
|
}
|
||||||
@@ -165,17 +164,20 @@ function showPasswordChangeForm() {
|
|||||||
const currentInput = createPinInput('current_password', '현재 암호');
|
const currentInput = createPinInput('current_password', '현재 암호');
|
||||||
const newInput = createPinInput('new_password', '새 암호');
|
const newInput = createPinInput('new_password', '새 암호');
|
||||||
const confirmInput = createPinInput('confirm_password', '새 암호 확인');
|
const confirmInput = createPinInput('confirm_password', '새 암호 확인');
|
||||||
const button = createButton('변경');
|
|
||||||
const msg = document.createElement('div');
|
const msg = document.createElement('div');
|
||||||
msg.id = 'failMsg';
|
msg.id = 'failMsg';
|
||||||
|
|
||||||
|
const status = document.createElement('div');
|
||||||
|
status.className = 'launcher-pin-status';
|
||||||
|
status.textContent = '현재 암호 입력';
|
||||||
|
|
||||||
const column = document.createElement('div');
|
const column = document.createElement('div');
|
||||||
column.className = 'launcher-form-column';
|
column.className = 'launcher-form-column';
|
||||||
column.appendChild(title);
|
column.appendChild(title);
|
||||||
|
column.appendChild(status);
|
||||||
column.appendChild(currentInput);
|
column.appendChild(currentInput);
|
||||||
column.appendChild(newInput);
|
column.appendChild(newInput);
|
||||||
column.appendChild(confirmInput);
|
column.appendChild(confirmInput);
|
||||||
column.appendChild(button);
|
|
||||||
column.appendChild(msg);
|
column.appendChild(msg);
|
||||||
shortcut.appendChild(column);
|
shortcut.appendChild(column);
|
||||||
|
|
||||||
@@ -187,8 +189,13 @@ function showPasswordChangeForm() {
|
|||||||
newInput.value = newPassword;
|
newInput.value = newPassword;
|
||||||
confirmInput.value = confirmPassword;
|
confirmInput.value = confirmPassword;
|
||||||
|
|
||||||
|
if ([currentPassword, newPassword, confirmPassword].some(value => value.length !== PIN_LENGTH)) return;
|
||||||
|
|
||||||
if (newPassword !== confirmPassword) {
|
if (newPassword !== confirmPassword) {
|
||||||
msg.textContent = '새 암호가 일치하지 않습니다.';
|
msg.textContent = '새 암호가 일치하지 않습니다.';
|
||||||
|
newInput.value = '';
|
||||||
|
confirmInput.value = '';
|
||||||
|
newInput.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isDigitPin(currentPassword) || !isDigitPin(newPassword)) {
|
if (!isDigitPin(currentPassword) || !isDigitPin(newPassword)) {
|
||||||
@@ -213,22 +220,34 @@ function showPasswordChangeForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
msg.style.color = 'red';
|
msg.style.color = 'red';
|
||||||
msg.textContent = data.message === 'password_too_short'
|
msg.textContent = data.message === 'password_digits_only' ? '암호는 숫자 4자리여야 합니다.' : '현재 암호가 틀립니다.';
|
||||||
? '새 암호는 4자리 이상이어야 합니다.'
|
if (data.message === 'current_password_invalid') {
|
||||||
: (data.message === 'password_digits_only' ? '암호는 숫자만 입력할 수 있습니다.' : '현재 암호가 틀립니다.');
|
currentInput.value = '';
|
||||||
|
currentInput.focus();
|
||||||
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
msg.style.color = 'red';
|
msg.style.color = 'red';
|
||||||
msg.textContent = '요청을 처리하지 못했습니다.';
|
msg.textContent = '요청을 처리하지 못했습니다.';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const keypad = createPinKeypad([currentInput, newInput, confirmInput], submit);
|
const labels = new Map([
|
||||||
|
[currentInput, '현재 암호 입력'],
|
||||||
|
[newInput, '새 암호 입력'],
|
||||||
|
[confirmInput, '새 암호 확인 입력'],
|
||||||
|
]);
|
||||||
|
const syncStatus = () => {
|
||||||
|
const active = document.activeElement && labels.has(document.activeElement) ? document.activeElement : currentInput;
|
||||||
|
status.textContent = labels.get(active);
|
||||||
|
};
|
||||||
|
const keypad = createPinKeypad([currentInput, newInput, confirmInput], submit, syncStatus);
|
||||||
column.insertBefore(keypad, msg);
|
column.insertBefore(keypad, msg);
|
||||||
button.addEventListener('click', submit);
|
|
||||||
[currentInput, newInput, confirmInput].forEach(input => {
|
[currentInput, newInput, confirmInput].forEach(input => {
|
||||||
attachPinInputEvents(input, submit);
|
attachPinInputEvents(input, submit);
|
||||||
|
input.addEventListener('focus', syncStatus);
|
||||||
});
|
});
|
||||||
currentInput.focus();
|
currentInput.focus();
|
||||||
|
syncStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function wakeComputer() {
|
async function wakeComputer() {
|
||||||
@@ -365,7 +384,7 @@ function onlyDigits(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isDigitPin(value) {
|
function isDigitPin(value) {
|
||||||
return /^\d+$/.test(String(value || ''));
|
return new RegExp(`^\\d{${PIN_LENGTH}}$`).test(String(value || ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPinInput(id, placeholder) {
|
function createPinInput(id, placeholder) {
|
||||||
@@ -377,6 +396,7 @@ function createPinInput(id, placeholder) {
|
|||||||
input.pattern = '[0-9]*';
|
input.pattern = '[0-9]*';
|
||||||
input.autocomplete = 'off';
|
input.autocomplete = 'off';
|
||||||
input.enterKeyHint = 'done';
|
input.enterKeyHint = 'done';
|
||||||
|
input.maxLength = PIN_LENGTH;
|
||||||
input.readOnly = true;
|
input.readOnly = true;
|
||||||
input.className = 'launcher-pin-input';
|
input.className = 'launcher-pin-input';
|
||||||
input.setAttribute('aria-label', placeholder);
|
input.setAttribute('aria-label', placeholder);
|
||||||
@@ -392,12 +412,14 @@ function attachPinInputEvents(input, onEnter) {
|
|||||||
});
|
});
|
||||||
input.addEventListener('paste', e => {
|
input.addEventListener('paste', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
input.value += onlyDigits(e.clipboardData?.getData('text') || '');
|
input.value = (input.value + onlyDigits(e.clipboardData?.getData('text') || '')).slice(0, PIN_LENGTH);
|
||||||
|
if (input.value.length === PIN_LENGTH) onEnter();
|
||||||
});
|
});
|
||||||
input.addEventListener('keydown', e => {
|
input.addEventListener('keydown', e => {
|
||||||
if (/^\d$/.test(e.key)) {
|
if (/^\d$/.test(e.key)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
input.value += e.key;
|
input.value = (input.value + e.key).slice(0, PIN_LENGTH);
|
||||||
|
if (input.value.length === PIN_LENGTH) onEnter();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === 'Backspace') {
|
if (e.key === 'Backspace') {
|
||||||
@@ -417,17 +439,24 @@ function attachPinInputEvents(input, onEnter) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPinKeypad(inputs, onEnter) {
|
function createPinKeypad(inputs, onEnter, onActiveChange = null) {
|
||||||
let activeInput = inputs[0];
|
let activeInput = inputs[0];
|
||||||
const keypad = document.createElement('div');
|
const keypad = document.createElement('div');
|
||||||
keypad.className = 'launcher-pin-keypad';
|
keypad.className = 'launcher-pin-keypad';
|
||||||
|
const setActiveInput = (input, clear = false) => {
|
||||||
|
activeInput = input;
|
||||||
|
if (clear) activeInput.value = '';
|
||||||
|
activeInput.focus();
|
||||||
|
document.querySelectorAll('.launcher-pin-input').forEach(el => el.classList.toggle('active', el === activeInput));
|
||||||
|
if (typeof onActiveChange === 'function') onActiveChange(activeInput);
|
||||||
|
};
|
||||||
inputs.forEach(input => {
|
inputs.forEach(input => {
|
||||||
input.addEventListener('focus', () => {
|
input.addEventListener('focus', () => {
|
||||||
activeInput = input;
|
activeInput = input;
|
||||||
|
if (typeof onActiveChange === 'function') onActiveChange(activeInput);
|
||||||
});
|
});
|
||||||
input.addEventListener('click', () => {
|
input.addEventListener('click', () => {
|
||||||
activeInput = input;
|
setActiveInput(input, input.value.length > 0);
|
||||||
input.focus();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -444,29 +473,28 @@ function createPinKeypad(inputs, onEnter) {
|
|||||||
} else if (key === 'back') {
|
} else if (key === 'back') {
|
||||||
activeInput.value = activeInput.value.slice(0, -1);
|
activeInput.value = activeInput.value.slice(0, -1);
|
||||||
} else {
|
} else {
|
||||||
activeInput.value += key;
|
activeInput.value = (activeInput.value + key).slice(0, PIN_LENGTH);
|
||||||
|
if (activeInput.value.length === PIN_LENGTH) {
|
||||||
|
if (inputs.length === 1) {
|
||||||
|
onEnter();
|
||||||
|
} else {
|
||||||
|
const idx = inputs.indexOf(activeInput);
|
||||||
|
if (idx >= 0 && idx < inputs.length - 1) {
|
||||||
|
const nextInput = inputs[idx + 1];
|
||||||
|
setActiveInput(nextInput, nextInput.value.length > 0);
|
||||||
|
} else {
|
||||||
|
onEnter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
keypad.appendChild(btn);
|
keypad.appendChild(btn);
|
||||||
});
|
});
|
||||||
|
|
||||||
const done = document.createElement('button');
|
|
||||||
done.type = 'button';
|
|
||||||
done.className = 'launcher-pin-key launcher-pin-done';
|
|
||||||
done.textContent = '확인';
|
|
||||||
done.addEventListener('click', onEnter);
|
|
||||||
keypad.appendChild(done);
|
|
||||||
|
|
||||||
return keypad;
|
return keypad;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createButton(text) {
|
|
||||||
const button = document.createElement('button');
|
|
||||||
button.type = 'button';
|
|
||||||
button.textContent = text;
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
function launcherAlert(name) {
|
function launcherAlert(name) {
|
||||||
if (typeof showAlert === 'function') {
|
if (typeof showAlert === 'function') {
|
||||||
showAlert(name);
|
showAlert(name);
|
||||||
|
|||||||
Reference in New Issue
Block a user