Compare commits
3 Commits
79d1e561d3
...
78518645aa
| Author | SHA1 | Date | |
|---|---|---|---|
| 78518645aa | |||
| 7d6d36d401 | |||
| dcd58a9c53 |
@@ -0,0 +1,19 @@
|
|||||||
|
@echo off
|
||||||
|
set GOOS=linux
|
||||||
|
set GOARCH=amd64
|
||||||
|
|
||||||
|
echo VaultKeeper 리눅스 빌드를 시작합니다...
|
||||||
|
|
||||||
|
go build -o vaultkeeper -ldflags "-s -w" main.go
|
||||||
|
|
||||||
|
if %ERRORLEVEL% equ 0 (
|
||||||
|
echo.
|
||||||
|
echo -----------------------------------
|
||||||
|
echo 빌드 성공: vaultkeeper 파일이 생성되었습니다.
|
||||||
|
echo -----------------------------------
|
||||||
|
) else (
|
||||||
|
echo.
|
||||||
|
echo 빌드에 실패했습니다. 오류를 확인하세요.
|
||||||
|
)
|
||||||
|
|
||||||
|
pause
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
o# [1] HTTP (80) -> HTTPS (443) 자동 리다이렉트 (통합 관리)
|
||||||
|
server {
|
||||||
|
if ($host = white-smith.duckdns.org) {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
} # managed by Certbot
|
||||||
|
|
||||||
|
|
||||||
|
if ($host = git-white-smith.duckdns.org) {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
} # managed by Certbot
|
||||||
|
|
||||||
|
|
||||||
|
if ($host = kui-white-smith.duckdns.org) {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
} # managed by Certbot
|
||||||
|
|
||||||
|
|
||||||
|
if ($host = git.white-smith.shop) {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
} # managed by Certbot
|
||||||
|
|
||||||
|
|
||||||
|
if ($host = white-smith.shop) {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
} # managed by Certbot
|
||||||
|
|
||||||
|
|
||||||
|
listen 80;
|
||||||
|
# 서비스되는 모든 도메인을 한곳에 적습니다.
|
||||||
|
server_name white-smith.shop kui-white-smith.duckdns.org white-smith.duckdns.org
|
||||||
|
git.white-smith.shop git-white-smith.duckdns.org 34.19.79.94;
|
||||||
|
|
||||||
|
# 모든 HTTP 접속을 각 호스트의 HTTPS 주소로 보냅니다.
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# [2] Quartz 블로그 설정 (HTTPS)
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name kui-white-smith.duckdns.org white-smith.shop white-smith.duckdns.org;
|
||||||
|
|
||||||
|
# Certbot 인증서 경로 (가장 최근에 발급받은 대표 경로 확인 필요)
|
||||||
|
ssl_certificate /etc/letsencrypt/live/git-white-smith.duckdns.org/fullchain.pem; # managed by Certbot
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/git-white-smith.duckdns.org/privkey.pem; # managed by Certbot
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||||
|
|
||||||
|
root /var/www/white-smith-blog/public;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
auth_basic "Please Login New Account";
|
||||||
|
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||||
|
|
||||||
|
# 인증 관련 정보가 캐시되지 않도록 강제
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||||
|
add_header Pragma "no-cache";
|
||||||
|
expires off;
|
||||||
|
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# 정적 파일 서빙을 위한 최적화 설정
|
||||||
|
# try_files $uri $uri.html $uri/ =404;
|
||||||
|
try_files $uri $uri/ $uri.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 추가된 부분 ---
|
||||||
|
# [A] VaultKeeper 프론트엔드 페이지 접속 경로
|
||||||
|
location /auth/ {
|
||||||
|
# Nginx 인증 팝업을 띄우지 않음
|
||||||
|
auth_basic off;
|
||||||
|
|
||||||
|
# 뒤에 /를 붙이면 Go 서버의 "/" 경로로 전달됩니다.
|
||||||
|
proxy_pass http://127.0.0.1:8486/;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_redirect off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# [B] VaultKeeper API 통신 경로
|
||||||
|
location /api/ {
|
||||||
|
# 이 부분이 누락되어 Nginx 인증창이 계속 떴던 것입니다.
|
||||||
|
auth_basic off;
|
||||||
|
|
||||||
|
proxy_pass http://localhost:8486;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
# ------------------
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# [3] Gitea 설정 (HTTPS)
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name git.white-smith.shop git-white-smith.duckdns.org;
|
||||||
|
|
||||||
|
client_max_body_size 50M;
|
||||||
|
|
||||||
|
# Gitea 전용 인증서 경로
|
||||||
|
ssl_certificate /etc/letsencrypt/live/git-white-smith.duckdns.org/fullchain.pem; # managed by Certbot
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/git-white-smith.duckdns.org/privkey.pem; # managed by Certbot
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
+111
-6
@@ -12,10 +12,42 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="font-sans antialiased text-gray-900">
|
<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">
|
<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>
|
<div class="flex justify-between items-center">
|
||||||
<p class="text-center text-gray-600">Nginx .htpasswd 파일 관리</p>
|
<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">
|
<div class="border-t border-gray-200 pt-8">
|
||||||
@@ -55,6 +87,16 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
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 addUserForm = document.getElementById('addUserForm');
|
||||||
const usernameInput = document.getElementById('username');
|
const usernameInput = document.getElementById('username');
|
||||||
const passwordInput = document.getElementById('password');
|
const passwordInput = document.getElementById('password');
|
||||||
@@ -63,6 +105,10 @@
|
|||||||
const noUsersMessage = document.getElementById('noUsersMessage');
|
const noUsersMessage = document.getElementById('noUsersMessage');
|
||||||
const listMessageDiv = document.getElementById('listMessage');
|
const listMessageDiv = document.getElementById('listMessage');
|
||||||
|
|
||||||
|
// Initial state: show login, hide app
|
||||||
|
loginWrapper.classList.remove('hidden');
|
||||||
|
appWrapper.classList.add('hidden');
|
||||||
|
|
||||||
async function fetchUsers() {
|
async function fetchUsers() {
|
||||||
listMessageDiv.textContent = '';
|
listMessageDiv.textContent = '';
|
||||||
userList.innerHTML = '';
|
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'; // 클래스 초기화
|
||||||
|
|
||||||
// 페이지 로드 시 사용자 목록 가져오기
|
const username = loginUsernameInput.value;
|
||||||
fetchUsers();
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ type AddUserRequest struct {
|
|||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoginRequest는 로그인 요청에 사용됩니다.
|
||||||
|
type LoginRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
// Config는 애플리케이션 설정을 나타냅니다.
|
// Config는 애플리케이션 설정을 나타냅니다.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
HtpasswdPath string `json:"htpasswdPath"`
|
HtpasswdPath string `json:"htpasswdPath"`
|
||||||
@@ -73,6 +79,7 @@ func main() {
|
|||||||
// API 엔드포인트
|
// API 엔드포인트
|
||||||
mux.HandleFunc("/api/users", handleUsers)
|
mux.HandleFunc("/api/users", handleUsers)
|
||||||
mux.HandleFunc("/api/users/", handleDeleteUser) // DELETE 요청 처리
|
mux.HandleFunc("/api/users/", handleDeleteUser) // DELETE 요청 처리
|
||||||
|
mux.HandleFunc("/api/login", handleLogin) // 로그인 요청 처리
|
||||||
|
|
||||||
log.Printf("VaultKeeper 서버가 %d 포트에서 시작됩니다.\n", appPort)
|
log.Printf("VaultKeeper 서버가 %d 포트에서 시작됩니다.\n", appPort)
|
||||||
log.Printf("HTPASSWD_PATH: %s\n", htpasswdPath)
|
log.Printf("HTPASSWD_PATH: %s\n", htpasswdPath)
|
||||||
@@ -217,3 +224,45 @@ func handleDeleteUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"message": fmt.Sprintf("사용자 '%s'가 성공적으로 삭제되었습니다.", username)})
|
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", "-vb", 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": "로그인 성공"})
|
||||||
|
}
|
||||||
|
|||||||
BIN
Binary file not shown.
Reference in New Issue
Block a user