diff --git a/internal/server/handlers_admin.go b/internal/server/handlers_admin.go index a151b76..517eb6e 100644 --- a/internal/server/handlers_admin.go +++ b/internal/server/handlers_admin.go @@ -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 } diff --git a/internal/server/handlers_api.go b/internal/server/handlers_api.go index ab13d10..f3e1a7b 100644 --- a/internal/server/handlers_api.go +++ b/internal/server/handlers_api.go @@ -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") diff --git a/internal/server/handlers_auth.go b/internal/server/handlers_auth.go index 47cc4f3..05957ea 100644 --- a/internal/server/handlers_auth.go +++ b/internal/server/handlers_auth.go @@ -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) diff --git a/internal/server/handlers_web_user.go b/internal/server/handlers_web_user.go index 8d6c591..5629c57 100644 --- a/internal/server/handlers_web_user.go +++ b/internal/server/handlers_web_user.go @@ -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() diff --git a/internal/server/helpers.go b/internal/server/helpers.go index 8fcda22..fab3447 100644 --- a/internal/server/helpers.go +++ b/internal/server/helpers.go @@ -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[:]) diff --git a/internal/server/locale.go b/internal/server/locale.go index 4dd4b12..6c68222 100644 --- a/internal/server/locale.go +++ b/internal/server/locale.go @@ -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.", }, } diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 19d2506..3bb56a2 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -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})