dcd58a9c53
Co-authored-by: aider (gemini/gemini-2.5-flash) <aider@aider.chat>
280 lines
14 KiB
HTML
280 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>VaultKeeper - Nginx .htpasswd 관리</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
|
<style>
|
|
body {
|
|
background-color: #f3f4f6;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="font-sans antialiased text-gray-900">
|
|
<!-- 로그인 컨테이너 -->
|
|
<div id="login-wrapper" class="min-h-screen flex flex-col items-center justify-center p-4">
|
|
<div class="w-full max-w-md bg-white rounded-lg shadow-xl p-8 space-y-8">
|
|
<h1 class="text-3xl font-bold text-center text-gray-800">VaultKeeper 로그인</h1>
|
|
<p class="text-center text-gray-600">Nginx .htpasswd 관리 도구</p>
|
|
<form id="loginForm" class="space-y-4">
|
|
<div>
|
|
<label for="loginUsername" class="block text-sm font-medium text-gray-700">사용자 이름</label>
|
|
<input type="text" id="loginUsername" name="username" required
|
|
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
|
</div>
|
|
<div>
|
|
<label for="loginPassword" class="block text-sm font-medium text-gray-700">비밀번호</label>
|
|
<input type="password" id="loginPassword" name="password" required
|
|
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
|
</div>
|
|
<button type="submit"
|
|
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
|
로그인
|
|
</button>
|
|
</form>
|
|
<div id="loginMessage" class="mt-3 text-center font-medium"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 앱 컨테이너 (초기에는 숨김) -->
|
|
<div id="app-wrapper" class="min-h-screen flex flex-col items-center justify-center p-4 hidden">
|
|
<div class="w-full max-w-2xl bg-white rounded-lg shadow-xl p-8 space-y-8">
|
|
<div class="flex justify-between items-center">
|
|
<h1 class="text-3xl font-bold text-gray-800">VaultKeeper</h1>
|
|
<button id="logoutBtn"
|
|
class="py-1 px-3 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-500 hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
|
|
로그아웃
|
|
</button>
|
|
</div>
|
|
<p class="text-gray-600">Nginx .htpasswd 파일 관리</p>
|
|
|
|
<!-- 사용자 추가 폼 -->
|
|
<div class="border-t border-gray-200 pt-8">
|
|
<h2 class="text-2xl font-semibold text-gray-800 mb-4">새 사용자 추가</h2>
|
|
<form id="addUserForm" class="space-y-4">
|
|
<div>
|
|
<label for="username" class="block text-sm font-medium text-gray-700">사용자 이름</label>
|
|
<input type="text" id="username" name="username" required
|
|
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
|
</div>
|
|
<div>
|
|
<label for="password" class="block text-sm font-medium text-gray-700">비밀번호</label>
|
|
<input type="password" id="password" name="password" required
|
|
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
|
</div>
|
|
<button type="submit"
|
|
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
|
사용자 추가
|
|
</button>
|
|
</form>
|
|
<div id="addMessage" class="mt-3 text-center font-medium"></div>
|
|
</div>
|
|
|
|
<!-- 사용자 목록 -->
|
|
<div class="border-t border-gray-200 pt-8">
|
|
<h2 class="text-2xl font-semibold text-gray-800 mb-4">현재 사용자 목록</h2>
|
|
<div id="userListContainer">
|
|
<ul id="userList" class="divide-y divide-gray-200 bg-white rounded-md shadow-sm">
|
|
<!-- 사용자는 여기에 동적으로 로드됩니다. -->
|
|
</ul>
|
|
<p id="noUsersMessage" class="text-center text-gray-500 mt-4 hidden">등록된 사용자가 없습니다.</p>
|
|
</div>
|
|
<div id="listMessage" class="mt-3 text-center font-medium"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// UI elements for login
|
|
const loginWrapper = document.getElementById('login-wrapper');
|
|
const loginForm = document.getElementById('loginForm');
|
|
const loginUsernameInput = document.getElementById('loginUsername');
|
|
const loginPasswordInput = document.getElementById('loginPassword');
|
|
const loginMessageDiv = document.getElementById('loginMessage');
|
|
|
|
// UI elements for app (user management)
|
|
const appWrapper = document.getElementById('app-wrapper');
|
|
const logoutBtn = document.getElementById('logoutBtn');
|
|
const addUserForm = document.getElementById('addUserForm');
|
|
const usernameInput = document.getElementById('username');
|
|
const passwordInput = document.getElementById('password');
|
|
const addMessageDiv = document.getElementById('addMessage');
|
|
const userList = document.getElementById('userList');
|
|
const noUsersMessage = document.getElementById('noUsersMessage');
|
|
const listMessageDiv = document.getElementById('listMessage');
|
|
|
|
// Initial state: show login, hide app
|
|
loginWrapper.classList.remove('hidden');
|
|
appWrapper.classList.add('hidden');
|
|
|
|
async function fetchUsers() {
|
|
listMessageDiv.textContent = '';
|
|
userList.innerHTML = '';
|
|
try {
|
|
const response = await fetch('/api/users');
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const users = await response.json();
|
|
if (users.length === 0) {
|
|
noUsersMessage.classList.remove('hidden');
|
|
} else {
|
|
noUsersMessage.classList.add('hidden');
|
|
users.forEach(user => {
|
|
const li = document.createElement('li');
|
|
li.className = 'flex items-center justify-between p-4 hover:bg-gray-50';
|
|
li.innerHTML = `
|
|
<span class="text-lg font-medium">${user.username}</span>
|
|
<button data-username="${user.username}"
|
|
class="delete-btn bg-red-500 hover:bg-red-600 text-white font-bold py-1 px-3 rounded text-sm">
|
|
삭제
|
|
</button>
|
|
`;
|
|
userList.appendChild(li);
|
|
});
|
|
addDeleteEventListeners();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching users:', error);
|
|
listMessageDiv.textContent = `사용자 목록을 불러오는 데 실패했습니다: ${error.message}`;
|
|
listMessageDiv.className = 'mt-3 text-center font-medium text-red-600';
|
|
}
|
|
}
|
|
|
|
async function addUser(event) {
|
|
event.preventDefault();
|
|
addMessageDiv.textContent = '';
|
|
|
|
const username = usernameInput.value;
|
|
const password = passwordInput.value;
|
|
|
|
try {
|
|
const response = await fetch('/api/users', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ username, password }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.message || '사용자 추가 실패');
|
|
}
|
|
|
|
addMessageDiv.textContent = data.message;
|
|
addMessageDiv.className = 'mt-3 text-center font-medium text-green-600';
|
|
usernameInput.value = '';
|
|
passwordInput.value = '';
|
|
fetchUsers(); // 사용자 목록 새로 고침
|
|
} catch (error) {
|
|
console.error('Error adding user:', error);
|
|
addMessageDiv.textContent = `사용자 추가 실패: ${error.message}`;
|
|
addMessageDiv.className = 'mt-3 text-center font-medium text-red-600';
|
|
}
|
|
}
|
|
|
|
async function deleteUser(username) {
|
|
listMessageDiv.textContent = '';
|
|
if (!confirm(`사용자 '${username}'을(를) 정말 삭제하시겠습니까?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/users/${username}`, {
|
|
method: 'DELETE',
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.message || '사용자 삭제 실패');
|
|
}
|
|
|
|
listMessageDiv.textContent = data.message;
|
|
listMessageDiv.className = 'mt-3 text-center font-medium text-green-600';
|
|
fetchUsers(); // 사용자 목록 새로 고침
|
|
} catch (error) {
|
|
console.error('Error deleting user:', error);
|
|
listMessageDiv.textContent = `사용자 삭제 실패: ${error.message}`;
|
|
listMessageDiv.className = 'mt-3 text-center font-medium text-red-600';
|
|
}
|
|
}
|
|
|
|
function addDeleteEventListeners() {
|
|
document.querySelectorAll('.delete-btn').forEach(button => {
|
|
button.onclick = () => deleteUser(button.dataset.username);
|
|
});
|
|
}
|
|
|
|
async function handleLogin(event) {
|
|
event.preventDefault();
|
|
loginMessageDiv.textContent = ''; // 이전 메시지 지우기
|
|
loginMessageDiv.className = 'mt-3 text-center font-medium'; // 클래스 초기화
|
|
|
|
const username = loginUsernameInput.value;
|
|
const password = loginPasswordInput.value;
|
|
|
|
try {
|
|
const response = await fetch('/api/login', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ username, password }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.message || '로그인 실패');
|
|
}
|
|
|
|
loginMessageDiv.textContent = data.message;
|
|
loginMessageDiv.className = 'mt-3 text-center font-medium text-green-600';
|
|
|
|
// 로그인 성공 시 UI 전환
|
|
loginWrapper.classList.add('hidden');
|
|
appWrapper.classList.remove('hidden');
|
|
fetchUsers(); // 사용자 목록 불러오기
|
|
|
|
loginUsernameInput.value = ''; // 입력 필드 초기화
|
|
loginPasswordInput.value = ''; // 입력 필드 초기화
|
|
|
|
} catch (error) {
|
|
console.error('Error logging in:', error);
|
|
loginMessageDiv.textContent = `로그인 실패: ${error.message}`;
|
|
loginMessageDiv.className = 'mt-3 text-center font-medium text-red-600';
|
|
}
|
|
}
|
|
|
|
function handleLogout() {
|
|
// UI 전환: 앱 화면 숨기고 로그인 화면 표시
|
|
appWrapper.classList.add('hidden');
|
|
loginWrapper.classList.remove('hidden');
|
|
|
|
// 메시지 초기화
|
|
addMessageDiv.textContent = '';
|
|
listMessageDiv.textContent = '';
|
|
loginMessageDiv.textContent = '';
|
|
|
|
// 사용자 목록 비우기 (선택 사항)
|
|
userList.innerHTML = '';
|
|
noUsersMessage.classList.remove('hidden');
|
|
}
|
|
|
|
// 이벤트 리스너 등록
|
|
loginForm.addEventListener('submit', handleLogin);
|
|
addUserForm.addEventListener('submit', addUser);
|
|
logoutBtn.addEventListener('click', handleLogout);
|
|
|
|
// 페이지 로드 시 사용자 목록 가져오기는 로그인 성공 후 호출되므로 주석 처리 또는 제거
|
|
// fetchUsers(); // 이 부분은 이제 로그인 성공 시 호출됩니다.
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|