fix: hide internal server errors from clients

This commit is contained in:
mirivlad 2026-07-14 22:00:24 +08:00
parent 66bd53f835
commit 0d0ae92dec
7 changed files with 306 additions and 266 deletions

View File

@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"html"
"log"
"net/http"
"strings"
"time"
@ -81,7 +82,8 @@ func (s *Server) handleAdminUsers(w http.ResponseWriter, r *http.Request) {
}
rows, err := s.db.Query("SELECT id, username, email, confirmed, blocked, created_at FROM server_users ORDER BY created_at DESC")
if err != nil {
http.Error(w, err.Error(), 500)
log.Printf("admin users: query failed: %v", err)
http.Error(w, t(s.locale(), "server.internalError"), http.StatusInternalServerError)
return
}
defer rows.Close()
@ -127,7 +129,8 @@ func (s *Server) handleAdminDevices(w http.ResponseWriter, r *http.Request) {
rows, err := s.db.Query(`SELECT d.id, d.name, d.client_version, COALESCE(d.last_seen,''), COALESCE(d.revoked_at,''), d.created_at
FROM server_devices d ORDER BY d.created_at DESC`)
if err != nil {
http.Error(w, err.Error(), 500)
log.Printf("admin devices: query failed: %v", err)
http.Error(w, t(s.locale(), "server.internalError"), http.StatusInternalServerError)
return
}
defer rows.Close()
@ -214,11 +217,12 @@ func (s *Server) handleAdminSMTPTest(w http.ResponseWriter, r *http.Request) {
to = from
}
if host == "" || port == "" || from == "" {
jsonOK(w, map[string]interface{}{"ok": false, "error": "host, port and from required"})
jsonOK(w, map[string]interface{}{"ok": false, "error": t(s.locale(), "admin.smtpRequired")})
return
}
if err := s.smtpTest(host, port, user, pass, security, from, to); err != nil {
jsonOK(w, map[string]interface{}{"ok": false, "error": err.Error()})
log.Printf("admin SMTP test failed: %v", err)
jsonOK(w, map[string]interface{}{"ok": false, "error": t(s.locale(), "admin.smtpTestFailed")})
return
}
jsonOK(w, map[string]interface{}{"ok": true})
@ -235,7 +239,7 @@ func (s *Server) handleAdminAPIDevices(w http.ResponseWriter, r *http.Request) {
LEFT JOIN server_users u ON u.id = d.user_id
ORDER BY d.created_at DESC`)
if err != nil {
jsonErr(w, 500, err.Error())
jsonInternalError(w, err)
return
}
defer rows.Close()
@ -265,7 +269,7 @@ func (s *Server) handleAdminAPIKeys(w http.ResponseWriter, r *http.Request) {
case "GET":
rows, err := s.db.Query("SELECT id, name, api_key FROM server_devices ORDER BY created_at")
if err != nil {
jsonErr(w, 500, err.Error())
jsonInternalError(w, err)
return
}
defer rows.Close()
@ -313,7 +317,7 @@ func (s *Server) handleAdminAPIKeysDelete(w http.ResponseWriter, r *http.Request
id := strings.TrimPrefix(r.URL.Path, "/admin/api/keys/")
_, err := s.db.Exec("DELETE FROM server_devices WHERE id=?", id)
if err != nil {
jsonErr(w, 500, err.Error())
jsonInternalError(w, err)
return
}
s.db.Exec("DELETE FROM server_user_devices WHERE device_id=?", id)
@ -373,7 +377,7 @@ func (s *Server) handleAdminAPIUsers(w http.ResponseWriter, r *http.Request) {
args = append(args, perPage, offset)
rows, err := s.db.Query(sql, args...)
if err != nil {
jsonErr(w, 500, err.Error())
jsonInternalError(w, err)
return
}
defer rows.Close()
@ -433,7 +437,7 @@ func (s *Server) handleAdminAPIUserActions(w http.ResponseWriter, r *http.Reques
hash, _ := bcrypt.GenerateFromPassword([]byte(newPass), bcrypt.DefaultCost)
_, err := s.db.Exec("UPDATE server_users SET password_hash=? WHERE id=?", string(hash), id)
if err != nil {
jsonErr(w, 500, err.Error())
jsonInternalError(w, err)
return
}
jsonOK(w, map[string]interface{}{"status": "ok", "new_password": newPass})
@ -456,7 +460,7 @@ func (s *Server) handleAdminAPIUserActions(w http.ResponseWriter, r *http.Reques
}
_, err := s.db.Exec("UPDATE server_users SET username=?, email=? WHERE id=?", editReq.Username, strings.ToLower(editReq.Email), id)
if err != nil {
jsonErr(w, 500, err.Error())
jsonInternalError(w, err)
return
}
jsonOK(w, map[string]interface{}{"status": "ok"})
@ -534,8 +538,9 @@ func (s *Server) handleAdminCreateUser(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(409)
w.Write([]byte(errorPageHTML(locale, t(locale, "common.error"), "Username or email already taken", "/admin/create-user")))
} else {
log.Printf("admin create user: failed: %v", err)
w.WriteHeader(500)
w.Write([]byte(errorPageHTML(locale, t(locale, "common.error"), err.Error(), "/admin/create-user")))
w.Write([]byte(errorPageHTML(locale, t(locale, "common.error"), t(locale, "admin.createUserFailed"), "/admin/create-user")))
}
return
}
@ -587,7 +592,7 @@ func (s *Server) handleAdminAPICreateUser(w http.ResponseWriter, r *http.Request
if strings.Contains(err.Error(), "UNIQUE") {
jsonErr(w, 409, "username or email already taken")
} else {
jsonErr(w, 500, err.Error())
jsonInternalError(w, err)
}
return
}

View File

@ -110,7 +110,7 @@ func (s *Server) handleClientPair(w http.ResponseWriter, r *http.Request) {
deviceID, req.DeviceName, hex.EncodeToString(apiKey), tokenHash, prefix, suffix,
userID, req.VaultID, req.ClientVersion, ip, now, now)
if err != nil {
jsonErr(w, 500, err.Error())
jsonInternalError(w, err)
return
}
s.db.Exec("INSERT OR IGNORE INTO server_user_devices (user_id, device_id) VALUES (?, ?)", userID, deviceID)
@ -335,7 +335,7 @@ func (s *Server) handleDeviceRegister(w http.ResponseWriter, r *http.Request) {
deviceID, req.Name, apiKey, userID, req.VaultID, now, now,
)
if err != nil {
jsonErr(w, 500, err.Error())
jsonInternalError(w, err)
return
}
s.db.Exec("INSERT OR IGNORE INTO server_user_devices (user_id, device_id) VALUES (?, ?)", userID, deviceID)
@ -369,7 +369,7 @@ func (s *Server) handleSyncPush(w http.ResponseWriter, r *http.Request) {
} `json:"ops"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonErr(w, 400, "invalid JSON: "+err.Error())
jsonErr(w, 400, "invalid JSON")
return
}
if req.IdempotencyKey != "" {
@ -479,7 +479,7 @@ func (s *Server) handleSyncPull(w http.ResponseWriter, r *http.Request) {
WHERE user_id=? AND vault_id=? AND server_sequence > ? AND server_sequence IS NOT NULL
ORDER BY server_sequence`, scope.UserID, scope.VaultID, req.SinceSequence)
if err != nil {
jsonErr(w, 500, err.Error())
jsonInternalError(w, err)
return
}
defer rows.Close()
@ -515,7 +515,7 @@ func (s *Server) handleBlobs(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "POST":
if err := r.ParseMultipartForm(200 << 20); err != nil {
jsonErr(w, 400, "multipart error: "+err.Error())
jsonErr(w, 400, "invalid multipart request")
return
}
file, header, err := r.FormFile("file")

View File

@ -56,7 +56,7 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
jsonErr(w, 409, "username or email already taken")
return
}
jsonErr(w, 500, err.Error())
jsonInternalError(w, err)
return
}
tok := make([]byte, 24)

View File

@ -78,8 +78,9 @@ func (s *Server) handleUserWebRegister(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(409)
w.Write([]byte(errorPageHTML(locale, t(locale, "common.error"), "Username or email already taken", "/register")))
} else {
log.Printf("register web: create user failed: %v", err)
w.WriteHeader(500)
w.Write([]byte(errorPageHTML(locale, t(locale, "common.error"), err.Error(), "/register")))
w.Write([]byte(errorPageHTML(locale, t(locale, "common.error"), t(locale, "server.registrationFailed"), "/register")))
}
return
}
@ -352,7 +353,7 @@ func (s *Server) handleUserDevices(w http.ResponseWriter, r *http.Request) {
WHERE ud.user_id = ?
ORDER BY d.created_at DESC`, userID)
if err != nil {
jsonErr(w, 500, err.Error())
jsonInternalError(w, err)
return
}
defer rows.Close()

View File

@ -4,6 +4,7 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"log"
"net/http"
)
@ -18,6 +19,11 @@ func jsonErr(w http.ResponseWriter, code int, msg string) {
json.NewEncoder(w).Encode(map[string]string{"error": msg})
}
func jsonInternalError(w http.ResponseWriter, err error) {
log.Printf("request failed: %v", err)
jsonErr(w, http.StatusInternalServerError, "internal error")
}
func sha256Hex(s string) string {
h := sha256.Sum256([]byte(s))
return hex.EncodeToString(h[:])

View File

@ -16,253 +16,263 @@ func t(locale, key string) string {
var _translations = map[string]map[string]string{
"ru": {
"server.registerTitle": "Регистрация",
"server.register": "Регистрация",
"server.username": "Имя пользователя",
"server.email": "Email",
"server.password": "Пароль",
"server.registerBtn": "Зарегистрироваться",
"server.alreadyHaveAccount": "Уже есть аккаунт?",
"server.loginBtn": "Войти",
"server.loginTitle": "Вход",
"server.usernameOrEmail": "Имя пользователя или email",
"server.forgotPassword": "Забыли пароль?",
"server.adminLink": "Админ",
"server.dashboard": "Панель управления",
"server.allFieldsRequired": "Все поля обязательны",
"server.back": "Назад",
"server.emailConfirmBody": "Подтвердите регистрацию: %s",
"server.emailConfirmSubject": "Verstak — подтверждение email",
"server.registrationSuccess": "Регистрация успешна",
"server.registrationEmailSent": "Письмо с ссылкой подтверждения отправлено.",
"server.registrationCheckEmail":"Проверьте почту и подтвердите аккаунт.",
"server.registrationAutoMessage":"Email не подтверждается. Токен подтверждения выведен в лог сервера.",
"server.resetPasswordTitle": "Сброс пароля",
"server.resetPassword": "Сброс пароля",
"server.resetInstruction": "Введите email для получения ссылки сброса.",
"server.sendLink": "Отправить ссылку",
"server.backToLogin": "Вернуться к входу",
"server.emailSentTitle": "Письмо отправлено",
"server.emailSent": "Письмо отправлено",
"server.emailSentMessage": "Если аккаунт существует, ссылка для сброса пароля отправлена на почту.",
"server.goHome": "На главную",
"server.newPasswordTitle": "Новый пароль",
"server.newPassword": "Новый пароль",
"server.passwordConfirm": "Подтвердите пароль",
"server.save": "Сохранить",
"server.passwordChanged": "Пароль изменён",
"server.passwordChangedMessage":"Пароль успешно изменён. Теперь можно войти.",
"server.emailConfirmed": "Email подтверждён",
"server.emailConfirmedMessage": "Аккаунт активирован. Теперь можно войти.",
"server.needEmail": "Введите email",
"server.passwordsDoNotMatch": "Пароли не совпадают",
"server.newPasswordResult": "Новый пароль для %s:\n",
"server.logout": "Выйти",
"server.error": "Ошибка",
"userDashboard.devices": "Устройства",
"userDashboard.device": "Устройство",
"userDashboard.status": "Статус",
"userDashboard.connected": "Подключено",
"userDashboard.lastSeen": "Последний раз",
"userDashboard.version": "Версия",
"userDashboard.connectNew": "Подключить новое устройство",
"userDashboard.connectNewHint": "Установите Верстак на новом устройстве и выполните регистрацию.",
"userDashboard.revokeConfirm": "Отозвать устройство?",
"userDashboard.revokePrompt": "Введите пароль для подтверждения:",
"userDashboard.noDevices": "Нет устройств",
"userDashboard.active": "Активно",
"userDashboard.revoked": "Отозвано",
"userDashboard.revoke": "Отозвать",
"admin.login": "Вход администратора",
"admin.username": "Имя пользователя",
"admin.password": "Пароль",
"admin.loginBtn": "Войти",
"admin.dashboard": "Панель управления",
"admin.deviceCount": "Устройств",
"admin.opsCount": "Операций",
"admin.devices": "Устройства",
"admin.noDevices": "Нет устройств",
"admin.device": "Устройство",
"admin.user": "Пользователь",
"admin.version": "Версия",
"admin.status": "Статус",
"admin.lastSeen": "Последний раз",
"admin.active": "Активно",
"admin.revoked": "Отозвано",
"admin.revoke": "Отозвать",
"admin.smtp": "SMTP",
"admin.users": "Пользователи",
"admin.usersHeading": "Пользователи",
"admin.healthCheck": "Проверка здоровья",
"admin.smtpServer": "SMTP сервер",
"admin.smtpPort": "SMTP порт",
"admin.smtpType": "Тип шифрования",
"admin.smtpNoEncryption": "Без шифрования",
"admin.smtpUsername": "Имя пользователя SMTP",
"admin.smtpPassword": "Пароль SMTP",
"admin.smtpFrom": "Отправитель",
"admin.smtpServerURL": "URL сервера",
"admin.smtpSave": "Сохранить",
"admin.smtpTest": "Тест",
"admin.smtpTitle": "Настройки SMTP",
"admin.smtpTesting": "Проверка...",
"admin.smtpPassed": "✓ Тест пройден",
"admin.revokeConfirm": "Вы уверены?",
"common.loading": "Загрузка...",
"common.ok": "OK",
"common.error": "Ошибка",
"admin.filterPlaceholder": "Поиск...",
"admin.email": "Email",
"admin.actions": "Действия",
"admin.confirmTitle": "Подтверждение",
"admin.modalCancel": "Отмена",
"admin.modalConfirm": "Подтвердить",
"admin.editUser": "Редактировать пользователя",
"admin.editBtn": "Сохранить",
"admin.resultTitle": "Результат",
"admin.confirmed": "Подтверждён",
"admin.unconfirmed": "Не подтверждён",
"admin.blocked": "Заблокирован",
"admin.unblock": "Разблокировать",
"admin.block": "Заблокировать",
"admin.resetPassword": "Сбросить пароль",
"admin.noUsers": "Нет пользователей",
"admin.resetPasswordConfirm": "Сбросить пароль",
"admin.resetPasswordMessage": "Новый пароль: ",
"admin.resetBtn": "Сбросить",
"admin.deleteUser": "Удалить",
"admin.deleteUserMessage": "Удалить пользователя %s?",
"admin.deleteBtn": "Удалить",
"admin.unblockUserTitle": "Разблокировать",
"admin.blockUserTitle": "Заблокировать",
"admin.unblockUserMessage": "Разблокировать пользователя?",
"admin.blockUserMessage": "Заблокировать пользователя?",
"admin.createUser": "Создать пользователя",
"admin.createUserBtn": "Создать",
"server.registerTitle": "Регистрация",
"server.register": "Регистрация",
"server.username": "Имя пользователя",
"server.email": "Email",
"server.password": "Пароль",
"server.registerBtn": "Зарегистрироваться",
"server.alreadyHaveAccount": "Уже есть аккаунт?",
"server.loginBtn": "Войти",
"server.loginTitle": "Вход",
"server.usernameOrEmail": "Имя пользователя или email",
"server.forgotPassword": "Забыли пароль?",
"server.adminLink": "Админ",
"server.dashboard": "Панель управления",
"server.allFieldsRequired": "Все поля обязательны",
"server.back": "Назад",
"server.emailConfirmBody": "Подтвердите регистрацию: %s",
"server.emailConfirmSubject": "Verstak — подтверждение email",
"server.registrationSuccess": "Регистрация успешна",
"server.registrationEmailSent": "Письмо с ссылкой подтверждения отправлено.",
"server.registrationCheckEmail": "Проверьте почту и подтвердите аккаунт.",
"server.registrationAutoMessage": "Email не подтверждается. Токен подтверждения выведен в лог сервера.",
"server.resetPasswordTitle": "Сброс пароля",
"server.resetPassword": "Сброс пароля",
"server.resetInstruction": "Введите email для получения ссылки сброса.",
"server.sendLink": "Отправить ссылку",
"server.backToLogin": "Вернуться к входу",
"server.emailSentTitle": "Письмо отправлено",
"server.emailSent": "Письмо отправлено",
"server.emailSentMessage": "Если аккаунт существует, ссылка для сброса пароля отправлена на почту.",
"server.goHome": "На главную",
"server.newPasswordTitle": "Новый пароль",
"server.newPassword": "Новый пароль",
"server.passwordConfirm": "Подтвердите пароль",
"server.save": "Сохранить",
"server.passwordChanged": "Пароль изменён",
"server.passwordChangedMessage": "Пароль успешно изменён. Теперь можно войти.",
"server.emailConfirmed": "Email подтверждён",
"server.emailConfirmedMessage": "Аккаунт активирован. Теперь можно войти.",
"server.needEmail": "Введите email",
"server.passwordsDoNotMatch": "Пароли не совпадают",
"server.newPasswordResult": "Новый пароль для %s:\n",
"server.logout": "Выйти",
"server.error": "Ошибка",
"server.internalError": "Что-то пошло не так. Повторите попытку.",
"server.registrationFailed": "Не удалось зарегистрировать аккаунт. Повторите попытку.",
"userDashboard.devices": "Устройства",
"userDashboard.device": "Устройство",
"userDashboard.status": "Статус",
"userDashboard.connected": "Подключено",
"userDashboard.lastSeen": "Последний раз",
"userDashboard.version": "Версия",
"userDashboard.connectNew": "Подключить новое устройство",
"userDashboard.connectNewHint": "Установите Верстак на новом устройстве и выполните регистрацию.",
"userDashboard.revokeConfirm": "Отозвать устройство?",
"userDashboard.revokePrompt": "Введите пароль для подтверждения:",
"userDashboard.noDevices": "Нет устройств",
"userDashboard.active": "Активно",
"userDashboard.revoked": "Отозвано",
"userDashboard.revoke": "Отозвать",
"admin.login": "Вход администратора",
"admin.username": "Имя пользователя",
"admin.password": "Пароль",
"admin.loginBtn": "Войти",
"admin.dashboard": "Панель управления",
"admin.deviceCount": "Устройств",
"admin.opsCount": "Операций",
"admin.devices": "Устройства",
"admin.noDevices": "Нет устройств",
"admin.device": "Устройство",
"admin.user": "Пользователь",
"admin.version": "Версия",
"admin.status": "Статус",
"admin.lastSeen": "Последний раз",
"admin.active": "Активно",
"admin.revoked": "Отозвано",
"admin.revoke": "Отозвать",
"admin.smtp": "SMTP",
"admin.users": "Пользователи",
"admin.usersHeading": "Пользователи",
"admin.healthCheck": "Проверка здоровья",
"admin.smtpServer": "SMTP сервер",
"admin.smtpPort": "SMTP порт",
"admin.smtpType": "Тип шифрования",
"admin.smtpNoEncryption": "Без шифрования",
"admin.smtpUsername": "Имя пользователя SMTP",
"admin.smtpPassword": "Пароль SMTP",
"admin.smtpFrom": "Отправитель",
"admin.smtpServerURL": "URL сервера",
"admin.smtpSave": "Сохранить",
"admin.smtpTest": "Тест",
"admin.smtpTitle": "Настройки SMTP",
"admin.smtpTesting": "Проверка...",
"admin.smtpPassed": "✓ Тест пройден",
"admin.smtpRequired": "Укажите SMTP-сервер, порт и отправителя.",
"admin.smtpTestFailed": "Не удалось проверить настройки SMTP. Повторите попытку.",
"admin.revokeConfirm": "Вы уверены?",
"common.loading": "Загрузка...",
"common.ok": "OK",
"common.error": "Ошибка",
"admin.filterPlaceholder": "Поиск...",
"admin.email": "Email",
"admin.actions": "Действия",
"admin.confirmTitle": "Подтверждение",
"admin.modalCancel": "Отмена",
"admin.modalConfirm": "Подтвердить",
"admin.editUser": "Редактировать пользователя",
"admin.editBtn": "Сохранить",
"admin.resultTitle": "Результат",
"admin.confirmed": "Подтверждён",
"admin.unconfirmed": "Не подтверждён",
"admin.blocked": "Заблокирован",
"admin.unblock": "Разблокировать",
"admin.block": "Заблокировать",
"admin.resetPassword": "Сбросить пароль",
"admin.noUsers": "Нет пользователей",
"admin.resetPasswordConfirm": "Сбросить пароль",
"admin.resetPasswordMessage": "Новый пароль: ",
"admin.resetBtn": "Сбросить",
"admin.deleteUser": "Удалить",
"admin.deleteUserMessage": "Удалить пользователя %s?",
"admin.deleteBtn": "Удалить",
"admin.unblockUserTitle": "Разблокировать",
"admin.blockUserTitle": "Заблокировать",
"admin.unblockUserMessage": "Разблокировать пользователя?",
"admin.blockUserMessage": "Заблокировать пользователя?",
"admin.createUser": "Создать пользователя",
"admin.createUserBtn": "Создать",
"admin.createUserFailed": "Не удалось создать пользователя. Повторите попытку.",
},
"en": {
"server.registerTitle": "Registration",
"server.register": "Register",
"server.username": "Username",
"server.email": "Email",
"server.password": "Password",
"server.registerBtn": "Register",
"server.alreadyHaveAccount": "Already have an account?",
"server.loginBtn": "Login",
"server.loginTitle": "Login",
"server.usernameOrEmail": "Username or email",
"server.forgotPassword": "Forgot password?",
"server.adminLink": "Admin",
"server.dashboard": "Dashboard",
"server.allFieldsRequired": "All fields are required",
"server.back": "Back",
"server.emailConfirmBody": "Confirm registration: %s",
"server.emailConfirmSubject": "Verstak — email confirmation",
"server.registrationSuccess": "Registration successful",
"server.registrationEmailSent": "Confirmation link has been sent.",
"server.registrationCheckEmail":"Check your email and confirm your account.",
"server.registrationAutoMessage":"Email is not confirmed. Confirmation token logged to server.",
"server.resetPasswordTitle": "Reset Password",
"server.resetPassword": "Reset Password",
"server.resetInstruction": "Enter your email to receive a reset link.",
"server.sendLink": "Send link",
"server.backToLogin": "Back to login",
"server.emailSentTitle": "Email sent",
"server.emailSent": "Email sent",
"server.emailSentMessage": "If the account exists, a reset link has been sent.",
"server.goHome": "Go home",
"server.newPasswordTitle": "New Password",
"server.newPassword": "New Password",
"server.passwordConfirm": "Confirm Password",
"server.save": "Save",
"server.passwordChanged": "Password changed",
"server.passwordChangedMessage":"Password has been changed. You can now login.",
"server.emailConfirmed": "Email confirmed",
"server.emailConfirmedMessage": "Account activated. You can now login.",
"server.needEmail": "Enter email",
"server.passwordsDoNotMatch": "Passwords do not match",
"server.newPasswordResult": "New password for %s:\n",
"server.logout": "Logout",
"server.error": "Error",
"userDashboard.devices": "Devices",
"userDashboard.device": "Device",
"userDashboard.status": "Status",
"userDashboard.connected": "Connected",
"userDashboard.lastSeen": "Last seen",
"userDashboard.version": "Version",
"userDashboard.connectNew": "Connect new device",
"userDashboard.connectNewHint": "Install Verstak on a new device and register it.",
"userDashboard.revokeConfirm": "Revoke device?",
"userDashboard.revokePrompt": "Enter password to confirm:",
"userDashboard.noDevices": "No devices",
"userDashboard.active": "Active",
"userDashboard.revoked": "Revoked",
"userDashboard.revoke": "Revoke",
"admin.login": "Admin Login",
"admin.username": "Username",
"admin.password": "Password",
"admin.loginBtn": "Login",
"admin.dashboard": "Admin Dashboard",
"admin.deviceCount": "Devices",
"admin.opsCount": "Operations",
"admin.devices": "Devices",
"admin.noDevices": "No devices",
"admin.device": "Device",
"admin.user": "User",
"admin.version": "Version",
"admin.status": "Status",
"admin.lastSeen": "Last seen",
"admin.active": "Active",
"admin.revoked": "Revoked",
"admin.revoke": "Revoke",
"admin.smtp": "SMTP",
"admin.users": "Users",
"admin.usersHeading": "Users",
"admin.healthCheck": "Health Check",
"admin.smtpServer": "SMTP Server",
"admin.smtpPort": "SMTP Port",
"admin.smtpType": "Encryption",
"admin.smtpNoEncryption": "None",
"admin.smtpUsername": "SMTP Username",
"admin.smtpPassword": "SMTP Password",
"admin.smtpFrom": "From",
"admin.smtpServerURL": "Server URL",
"admin.smtpSave": "Save",
"admin.smtpTest": "Test",
"admin.smtpTitle": "SMTP Settings",
"admin.smtpTesting": "Testing...",
"admin.smtpPassed": "✓ Test passed",
"admin.revokeConfirm": "Are you sure?",
"common.loading": "Loading...",
"common.ok": "OK",
"common.error": "Error",
"admin.filterPlaceholder": "Search...",
"admin.email": "Email",
"admin.actions": "Actions",
"admin.confirmTitle": "Confirm",
"admin.modalCancel": "Cancel",
"admin.modalConfirm": "Confirm",
"admin.editUser": "Edit User",
"admin.editBtn": "Save",
"admin.resultTitle": "Result",
"admin.confirmed": "Confirmed",
"admin.unconfirmed": "Unconfirmed",
"admin.blocked": "Blocked",
"admin.unblock": "Unblock",
"admin.block": "Block",
"admin.resetPassword": "Reset Password",
"admin.noUsers": "No users",
"admin.resetPasswordConfirm": "Reset Password",
"admin.resetPasswordMessage": "New password: ",
"admin.resetBtn": "Reset",
"admin.deleteUser": "Delete",
"admin.deleteUserMessage": "Delete user %s?",
"admin.deleteBtn": "Delete",
"admin.unblockUserTitle": "Unblock",
"admin.blockUserTitle": "Block",
"admin.unblockUserMessage": "Unblock user?",
"admin.blockUserMessage": "Block user?",
"admin.createUser": "Create User",
"admin.createUserBtn": "Create",
"server.registerTitle": "Registration",
"server.register": "Register",
"server.username": "Username",
"server.email": "Email",
"server.password": "Password",
"server.registerBtn": "Register",
"server.alreadyHaveAccount": "Already have an account?",
"server.loginBtn": "Login",
"server.loginTitle": "Login",
"server.usernameOrEmail": "Username or email",
"server.forgotPassword": "Forgot password?",
"server.adminLink": "Admin",
"server.dashboard": "Dashboard",
"server.allFieldsRequired": "All fields are required",
"server.back": "Back",
"server.emailConfirmBody": "Confirm registration: %s",
"server.emailConfirmSubject": "Verstak — email confirmation",
"server.registrationSuccess": "Registration successful",
"server.registrationEmailSent": "Confirmation link has been sent.",
"server.registrationCheckEmail": "Check your email and confirm your account.",
"server.registrationAutoMessage": "Email is not confirmed. Confirmation token logged to server.",
"server.resetPasswordTitle": "Reset Password",
"server.resetPassword": "Reset Password",
"server.resetInstruction": "Enter your email to receive a reset link.",
"server.sendLink": "Send link",
"server.backToLogin": "Back to login",
"server.emailSentTitle": "Email sent",
"server.emailSent": "Email sent",
"server.emailSentMessage": "If the account exists, a reset link has been sent.",
"server.goHome": "Go home",
"server.newPasswordTitle": "New Password",
"server.newPassword": "New Password",
"server.passwordConfirm": "Confirm Password",
"server.save": "Save",
"server.passwordChanged": "Password changed",
"server.passwordChangedMessage": "Password has been changed. You can now login.",
"server.emailConfirmed": "Email confirmed",
"server.emailConfirmedMessage": "Account activated. You can now login.",
"server.needEmail": "Enter email",
"server.passwordsDoNotMatch": "Passwords do not match",
"server.newPasswordResult": "New password for %s:\n",
"server.logout": "Logout",
"server.error": "Error",
"server.internalError": "Something went wrong. Please try again.",
"server.registrationFailed": "Could not register the account. Please try again.",
"userDashboard.devices": "Devices",
"userDashboard.device": "Device",
"userDashboard.status": "Status",
"userDashboard.connected": "Connected",
"userDashboard.lastSeen": "Last seen",
"userDashboard.version": "Version",
"userDashboard.connectNew": "Connect new device",
"userDashboard.connectNewHint": "Install Verstak on a new device and register it.",
"userDashboard.revokeConfirm": "Revoke device?",
"userDashboard.revokePrompt": "Enter password to confirm:",
"userDashboard.noDevices": "No devices",
"userDashboard.active": "Active",
"userDashboard.revoked": "Revoked",
"userDashboard.revoke": "Revoke",
"admin.login": "Admin Login",
"admin.username": "Username",
"admin.password": "Password",
"admin.loginBtn": "Login",
"admin.dashboard": "Admin Dashboard",
"admin.deviceCount": "Devices",
"admin.opsCount": "Operations",
"admin.devices": "Devices",
"admin.noDevices": "No devices",
"admin.device": "Device",
"admin.user": "User",
"admin.version": "Version",
"admin.status": "Status",
"admin.lastSeen": "Last seen",
"admin.active": "Active",
"admin.revoked": "Revoked",
"admin.revoke": "Revoke",
"admin.smtp": "SMTP",
"admin.users": "Users",
"admin.usersHeading": "Users",
"admin.healthCheck": "Health Check",
"admin.smtpServer": "SMTP Server",
"admin.smtpPort": "SMTP Port",
"admin.smtpType": "Encryption",
"admin.smtpNoEncryption": "None",
"admin.smtpUsername": "SMTP Username",
"admin.smtpPassword": "SMTP Password",
"admin.smtpFrom": "From",
"admin.smtpServerURL": "Server URL",
"admin.smtpSave": "Save",
"admin.smtpTest": "Test",
"admin.smtpTitle": "SMTP Settings",
"admin.smtpTesting": "Testing...",
"admin.smtpPassed": "✓ Test passed",
"admin.smtpRequired": "Enter the SMTP server, port, and sender.",
"admin.smtpTestFailed": "Could not test the SMTP settings. Please try again.",
"admin.revokeConfirm": "Are you sure?",
"common.loading": "Loading...",
"common.ok": "OK",
"common.error": "Error",
"admin.filterPlaceholder": "Search...",
"admin.email": "Email",
"admin.actions": "Actions",
"admin.confirmTitle": "Confirm",
"admin.modalCancel": "Cancel",
"admin.modalConfirm": "Confirm",
"admin.editUser": "Edit User",
"admin.editBtn": "Save",
"admin.resultTitle": "Result",
"admin.confirmed": "Confirmed",
"admin.unconfirmed": "Unconfirmed",
"admin.blocked": "Blocked",
"admin.unblock": "Unblock",
"admin.block": "Block",
"admin.resetPassword": "Reset Password",
"admin.noUsers": "No users",
"admin.resetPasswordConfirm": "Reset Password",
"admin.resetPasswordMessage": "New password: ",
"admin.resetBtn": "Reset",
"admin.deleteUser": "Delete",
"admin.deleteUserMessage": "Delete user %s?",
"admin.deleteBtn": "Delete",
"admin.unblockUserTitle": "Unblock",
"admin.blockUserTitle": "Block",
"admin.unblockUserMessage": "Unblock user?",
"admin.blockUserMessage": "Block user?",
"admin.createUser": "Create User",
"admin.createUserBtn": "Create",
"admin.createUserFailed": "Could not create the user. Please try again.",
},
}

View File

@ -81,6 +81,24 @@ func TestAdminDashboardSMTPSecuritySelectUsesApplicationStyles(t *testing.T) {
}
}
func TestUserFacingServerErrorsDoNotExposeInternalDetails(t *testing.T) {
for _, name := range []string{"handlers_auth.go", "handlers_api.go", "handlers_admin.go", "handlers_web_user.go"} {
source, err := os.ReadFile(name)
if err != nil {
t.Fatalf("read %s: %v", name, err)
}
for _, forbidden := range []string{
"jsonErr(w, 500, err.Error())",
"http.Error(w, err.Error(), 500)",
"errorPageHTML(locale, t(locale, \"common.error\"), err.Error()",
} {
if strings.Contains(string(source), forbidden) {
t.Errorf("%s exposes an internal error with %q", name, forbidden)
}
}
}
}
func TestSyncPushPullStoresSequencedOps(t *testing.T) {
dir := t.TempDir()
s, err := NewServer(filepath.Join(dir, "test.db"), filepath.Join(dir, "data"), &Config{Port: 47732})