feat: 로그인 기능 추가 및 사용자 관리 페이지 전환 구현

Co-authored-by: aider (gemini/gemini-2.5-flash) <aider@aider.chat>
This commit is contained in:
2026-04-13 16:25:16 +09:00
parent 79d1e561d3
commit dcd58a9c53
2 changed files with 154 additions and 6 deletions
+111 -6
View File
@@ -12,10 +12,42 @@
</style>
</head>
<body class="font-sans antialiased text-gray-900">
<div class="min-h-screen flex flex-col items-center justify-center p-4">
<!-- 로그인 컨테이너 -->
<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">
<h1 class="text-3xl font-bold text-center text-gray-800">VaultKeeper</h1>
<p class="text-center text-gray-600">Nginx .htpasswd 파일 관리</p>
<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">
@@ -55,6 +87,16 @@
<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');
@@ -63,6 +105,10 @@
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 = '';
@@ -164,10 +210,69 @@
});
}
addUserForm.addEventListener('submit', addUser);
async function handleLogin(event) {
event.preventDefault();
loginMessageDiv.textContent = ''; // 이전 메시지 지우기
loginMessageDiv.className = 'mt-3 text-center font-medium'; // 클래스 초기화
// 페이지 로드 시 사용자 목록 가져오기
fetchUsers();
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>
+43
View File
@@ -73,6 +73,7 @@ func main() {
// API 엔드포인트
mux.HandleFunc("/api/users", handleUsers)
mux.HandleFunc("/api/users/", handleDeleteUser) // DELETE 요청 처리
mux.HandleFunc("/api/login", handleLogin) // 로그인 요청 처리
log.Printf("VaultKeeper 서버가 %d 포트에서 시작됩니다.\n", appPort)
log.Printf("HTPASSWD_PATH: %s\n", htpasswdPath)
@@ -217,3 +218,45 @@ func handleDeleteUser(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"message": fmt.Sprintf("사용자 '%s'가 성공적으로 삭제되었습니다.", username)})
}
// handleLogin은 사용자 로그인을 처리합니다.
func handleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "허용되지 않는 메소드입니다.", http.StatusMethodNotAllowed)
return
}
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "유효하지 않은 요청 본문입니다.", http.StatusBadRequest)
return
}
// 사용자 이름 유효성 검사 (로그인도 동일하게 적용)
if !usernameRegex.MatchString(req.Username) {
http.Error(w, "유효하지 않은 사용자 이름 형식입니다.", http.StatusBadRequest)
return
}
if req.Username == "" || req.Password == "" {
http.Error(w, "사용자 이름과 비밀번호는 필수입니다.", http.StatusBadRequest)
return
}
htpasswdMutex.Lock()
defer htpasswdMutex.Unlock()
// htpasswd -v <file> <username> <password> 명령어를 사용하여 비밀번호 확인
// exit code 0이면 성공, 0이 아니면 실패
cmd := exec.Command("htpasswd", "-v", htpasswdPath, req.Username, req.Password)
output, err := cmd.CombinedOutput()
if err != nil {
// htpasswd -v는 비밀번호가 일치하지 않거나 사용자 이름을 찾을 수 없으면 에러를 반환합니다.
log.Printf("Login verification failed for user '%s': %v, Output: %s", req.Username, err, output)
http.Error(w, "사용자 이름 또는 비밀번호가 올바르지 않습니다.", http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"message": "로그인 성공"})
}