Русский и английский языки интерфейса
CI / test (push) Successful in 22s
Release / release (push) Successful in 29s

Язык выбирается в шапке панели, а до первого выбора берётся из настроек
браузера. Переведены интерфейс, подписи и описания всех параметров конфигов,
ошибки API и служебные сообщения менеджера в консоли.

Подписи параметров вынесены из app.js в hints-<язык>.json: их больше четырёхсот
на язык, и грузить чужой язык незачем. Сообщения менеджера переводятся на
выходе из API по словарю, где ключом служит русская фраза из кода, — вывод
самого игрового сервера через него проходит без изменений.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 21:14:21 +03:00
co-authored by Claude Opus 5
parent d5a308ac8d
commit 9a224bc070
19 changed files with 5417 additions and 857 deletions
+10 -10
View File
@@ -34,7 +34,7 @@ func (s *Server) handleSession(w http.ResponseWriter, r *http.Request) {
// handleSetup создаёт первого администратора. Доступен, только пока учёток нет.
func (s *Server) handleSetup(w http.ResponseWriter, r *http.Request) {
if !s.users.IsEmpty() {
writeError(w, http.StatusConflict, "панель уже настроена")
writeError(w, r, http.StatusConflict, "панель уже настроена")
return
}
var creds credentials
@@ -44,16 +44,16 @@ func (s *Server) handleSetup(w http.ResponseWriter, r *http.Request) {
// Токен печатается в журнал менеджера при старте: без него первым
// администратором стал бы любой, кто успел открыть страницу.
if s.setupToken == "" || subtle.ConstantTimeCompare([]byte(creds.Token), []byte(s.setupToken)) != 1 {
writeError(w, http.StatusForbidden, "неверный код первичной настройки")
writeError(w, r, http.StatusForbidden, "неверный код первичной настройки")
return
}
if err := s.users.AddUser(creds.Login, creds.Password); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeError(w, r, http.StatusBadRequest, err.Error())
return
}
id, err := s.users.Login(creds.Login, creds.Password)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
return
}
// Администратор создан — код больше не нужен и не должен лежать на диске.
@@ -72,10 +72,10 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
id, err := s.users.Login(creds.Login, creds.Password)
if err != nil {
if errors.Is(err, auth.ErrBadCredentials) {
writeError(w, http.StatusUnauthorized, err.Error())
writeError(w, r, http.StatusUnauthorized, err.Error())
return
}
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
return
}
s.setSessionCookie(w, id, sessionTTL)
@@ -100,7 +100,7 @@ func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) {
return
}
if err := s.users.AddUser(creds.Login, creds.Password); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeError(w, r, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusCreated, map[string]any{"login": creds.Login})
@@ -110,11 +110,11 @@ func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
login := r.PathValue("login")
current, _ := s.users.SessionFromRequest(r)
if login == current {
writeError(w, http.StatusBadRequest, "нельзя удалить учётную запись, под которой вы вошли")
writeError(w, r, http.StatusBadRequest, "нельзя удалить учётную запись, под которой вы вошли")
return
}
if err := s.users.DeleteUser(login); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeError(w, r, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{"deleted": login})
@@ -128,7 +128,7 @@ func (s *Server) handleChangePassword(w http.ResponseWriter, r *http.Request) {
return
}
if err := s.users.SetPassword(r.PathValue("login"), body.Password); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeError(w, r, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
+13 -12
View File
@@ -7,6 +7,7 @@ import (
"time"
"github.com/mrleo1nid/pzmanager/internal/config"
"github.com/mrleo1nid/pzmanager/internal/i18n"
"github.com/mrleo1nid/pzmanager/internal/pzconfig"
)
@@ -19,12 +20,12 @@ func (s *Server) handleGetServerConfig(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{
"exists": false,
"path": path,
"hint": "Файл создаётся при первом запуске сервера",
"hint": i18n.T(i18n.FromRequest(r), "Файл создаётся при первом запуске сервера"),
})
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{
@@ -50,14 +51,14 @@ func (s *Server) handlePutServerConfig(w http.ResponseWriter, r *http.Request) {
path := s.cfg.ServerConfigPath()
ini, err := pzconfig.LoadINI(path)
if err != nil {
writeError(w, http.StatusBadRequest, "конфиг сервера недоступен: "+err.Error())
writeError(w, r, http.StatusBadRequest, "конфиг сервера недоступен: "+err.Error())
return
}
for key, value := range body.Values {
ini.Set(key, value)
}
if err := pzconfig.SaveINI(path, ini); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
return
}
s.sup.AppendLog("manager", "Конфиг сервера сохранён (изменений: "+strconv.Itoa(len(body.Values))+"). Изменения вступят в силу после перезапуска.")
@@ -71,12 +72,12 @@ func (s *Server) handleGetSandbox(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{
"exists": false,
"path": path,
"hint": "Файл создаётся при первом запуске сервера",
"hint": i18n.T(i18n.FromRequest(r), "Файл создаётся при первом запуске сервера"),
})
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{
@@ -96,17 +97,17 @@ func (s *Server) handlePutSandbox(w http.ResponseWriter, r *http.Request) {
path := s.cfg.SandboxConfigPath()
sb, err := pzconfig.LoadSandbox(path)
if err != nil {
writeError(w, http.StatusBadRequest, "настройки песочницы недоступны: "+err.Error())
writeError(w, r, http.StatusBadRequest, "настройки песочницы недоступны: "+err.Error())
return
}
for key, value := range body.Values {
if err := sb.Set(key, value); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeError(w, r, http.StatusBadRequest, err.Error())
return
}
}
if err := pzconfig.SaveSandbox(path, sb); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
return
}
s.sup.AppendLog("manager", "Настройки песочницы сохранены. Часть параметров применяется только к новому миру.")
@@ -177,16 +178,16 @@ func (s *Server) handlePutManagerConfig(w http.ResponseWriter, r *http.Request)
c.Backup.Schedule = config.Duration(time.Duration(dto.BackupScheduleMin) * time.Minute)
if err := c.Save(); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
return
}
if err := c.EnsureDirs(); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
return
}
s.sup.AppendLog("manager", "Настройки менеджера сохранены")
writeJSON(w, http.StatusOK, map[string]any{
"saved": true,
"note": "Адрес прослушивания применится после перезапуска pzmanager",
"note": i18n.T(i18n.FromRequest(r), "Адрес прослушивания применится после перезапуска pzmanager"),
})
}
+8 -7
View File
@@ -6,6 +6,7 @@ import (
"path/filepath"
"regexp"
"github.com/mrleo1nid/pzmanager/internal/i18n"
"github.com/mrleo1nid/pzmanager/internal/pzconfig"
)
@@ -24,12 +25,12 @@ func (s *Server) handleGetMods(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{
"exists": false,
"path": path,
"hint": "Файл конфигурации появится после первого запуска сервера",
"hint": i18n.T(i18n.FromRequest(r), "Файл конфигурации появится после первого запуска сервера"),
})
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
return
}
@@ -67,7 +68,7 @@ func (s *Server) handlePutMods(w http.ResponseWriter, r *http.Request) {
path := s.cfg.ServerConfigPath()
ini, err := pzconfig.LoadINI(path)
if err != nil {
writeError(w, http.StatusBadRequest, "конфиг сервера недоступен: "+err.Error())
writeError(w, r, http.StatusBadRequest, "конфиг сервера недоступен: "+err.Error())
return
}
ini.SetMods(body.Mods, body.WorkshopItems)
@@ -75,7 +76,7 @@ func (s *Server) handlePutMods(w http.ResponseWriter, r *http.Request) {
ini.Set(pzconfig.KeyMap, pzconfig.JoinList(body.Map))
}
if err := pzconfig.SaveINI(path, ini); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
return
}
s.sup.AppendLog("manager", "Список модов обновлён. Сервер докачает моды из мастерской при следующем запуске.")
@@ -95,7 +96,7 @@ func (s *Server) handleLookupMods(w http.ResponseWriter, r *http.Request) {
return
}
if len(body.IDs) > 200 {
writeError(w, http.StatusBadRequest, "слишком много идентификаторов за один запрос")
writeError(w, r, http.StatusBadRequest, "слишком много идентификаторов за один запрос")
return
}
items := s.workshop.Details(r.Context(), body.IDs)
@@ -114,13 +115,13 @@ func (s *Server) handleExpandCollection(w http.ResponseWriter, r *http.Request)
match := idFromInput.FindString(body.ID)
if match == "" {
writeError(w, http.StatusBadRequest, "не похоже на идентификатор или ссылку на коллекцию")
writeError(w, r, http.StatusBadRequest, "не похоже на идентификатор или ссылку на коллекцию")
return
}
items, err := s.workshop.Collection(r.Context(), match)
if err != nil {
writeError(w, http.StatusBadGateway, err.Error())
writeError(w, r, http.StatusBadGateway, err.Error())
return
}
// Названия подтягиваем сразу: иначе панель показала бы десяток голых
+13 -13
View File
@@ -84,7 +84,7 @@ func (s *Server) handleCreateProfile(w http.ResponseWriter, r *http.Request) {
return
}
if err := validateProfileMemory(dto); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeError(w, r, http.StatusBadRequest, err.Error())
return
}
@@ -97,7 +97,7 @@ func (s *Server) handleCreateProfile(w http.ResponseWriter, r *http.Request) {
AdminPassword: dto.AdminPassword,
}
if err := s.cfg.AddProfile(profile); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeError(w, r, http.StatusBadRequest, err.Error())
return
}
@@ -106,13 +106,13 @@ func (s *Server) handleCreateProfile(w http.ResponseWriter, r *http.Request) {
// Профиль уже добавлен в память — откатываем, чтобы не остался
// наполовину настроенный.
_ = s.cfg.DeleteProfile(profile.ID)
writeError(w, http.StatusBadRequest, err.Error())
writeError(w, r, http.StatusBadRequest, err.Error())
return
}
}
if err := s.cfg.Save(); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
return
}
s.sup.AppendLog("manager", fmt.Sprintf("Создан профиль %q", profile.Name()))
@@ -152,7 +152,7 @@ func (s *Server) handleUpdateProfile(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
current, ok := s.cfg.Profile(id)
if !ok {
writeError(w, http.StatusNotFound, "профиль не найден")
writeError(w, r, http.StatusNotFound, "профиль не найден")
return
}
@@ -161,7 +161,7 @@ func (s *Server) handleUpdateProfile(w http.ResponseWriter, r *http.Request) {
return
}
if err := validateProfileMemory(dto); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeError(w, r, http.StatusBadRequest, err.Error())
return
}
@@ -180,11 +180,11 @@ func (s *Server) handleUpdateProfile(w http.ResponseWriter, r *http.Request) {
}
if err := s.cfg.UpdateProfile(id, updated); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeError(w, r, http.StatusBadRequest, err.Error())
return
}
if err := s.cfg.Save(); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
return
}
@@ -198,11 +198,11 @@ func (s *Server) handleUpdateProfile(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleDeleteProfile(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if err := s.cfg.DeleteProfile(id); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeError(w, r, http.StatusBadRequest, err.Error())
return
}
if err := s.cfg.Save(); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
return
}
s.sup.AppendLog("manager", fmt.Sprintf(
@@ -215,16 +215,16 @@ func (s *Server) handleDeleteProfile(w http.ResponseWriter, r *http.Request) {
// сервере нельзя: профили делят игровые порты и файлы запуска.
func (s *Server) handleActivateProfile(w http.ResponseWriter, r *http.Request) {
if s.sup.State() != pzserver.StateStopped {
writeError(w, http.StatusConflict, "сначала остановите игровой сервер")
writeError(w, r, http.StatusConflict, "сначала остановите игровой сервер")
return
}
id := r.PathValue("id")
if err := s.cfg.SetActive(id); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeError(w, r, http.StatusBadRequest, err.Error())
return
}
if err := s.cfg.Save(); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
return
}
s.sup.AppendLog("manager", fmt.Sprintf("Активный профиль: %s", s.cfg.ActiveProfile().Name()))
+37 -11
View File
@@ -8,19 +8,26 @@ import (
"strconv"
"time"
"github.com/mrleo1nid/pzmanager/internal/i18n"
"github.com/mrleo1nid/pzmanager/internal/pzserver"
)
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{
"server": s.sup.Status(),
"task": s.jobs.Status(),
})
lang := i18n.FromRequest(r)
status := s.sup.Status()
status.LastExit = i18n.T(lang, status.LastExit)
// Имя задачи придумано менеджером и висит в панели, пока задача идёт.
task := s.jobs.Status()
task.Name = i18n.T(lang, task.Name)
task.Error = i18n.T(lang, task.Error)
writeJSON(w, http.StatusOK, map[string]any{"server": status, "task": task})
}
func (s *Server) handleStart(w http.ResponseWriter, r *http.Request) {
if err := s.sup.Start(); err != nil {
writeOpError(w, err)
writeOpError(w, r, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"state": s.sup.State()})
@@ -31,7 +38,7 @@ func (s *Server) handleStart(w http.ResponseWriter, r *http.Request) {
// видит прогресс по состоянию и логам.
func (s *Server) handleStop(w http.ResponseWriter, r *http.Request) {
if s.sup.State() == pzserver.StateStopped {
writeOpError(w, pzserver.ErrNotRunning)
writeOpError(w, r, pzserver.ErrNotRunning)
return
}
go func() {
@@ -44,7 +51,7 @@ func (s *Server) handleStop(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleRestart(w http.ResponseWriter, r *http.Request) {
if !s.sup.Installed() {
writeOpError(w, pzserver.ErrNotInstalled)
writeOpError(w, r, pzserver.ErrNotInstalled)
return
}
go func() {
@@ -63,7 +70,7 @@ func (s *Server) handleConsole(w http.ResponseWriter, r *http.Request) {
return
}
if err := s.sup.Send(body.Command); err != nil {
writeOpError(w, err)
writeOpError(w, r, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"sent": body.Command})
@@ -72,7 +79,23 @@ func (s *Server) handleConsole(w http.ResponseWriter, r *http.Request) {
// handleLogs отдаёт историю консоли начиная с указанного номера строки.
func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) {
since, _ := strconv.ParseInt(r.URL.Query().Get("since"), 10, 64)
writeJSON(w, http.StatusOK, map[string]any{"lines": s.sup.Lines(since)})
writeJSON(w, http.StatusOK, map[string]any{"lines": translateLines(r, s.sup.Lines(since))})
}
// translateLines переводит строки консоли на язык читающего. Буфер один на
// всех, поэтому перевод делается на выходе, у каждого подключения свой. Вывод
// самого игрового сервера через словарь проходит без изменений: он английский.
func translateLines(r *http.Request, lines []pzserver.LogLine) []pzserver.LogLine {
lang := i18n.FromRequest(r)
if lang == i18n.Default {
return lines
}
out := make([]pzserver.LogLine, len(lines))
for i, line := range lines {
line.Text = i18n.T(lang, line.Text)
out[i] = line
}
return out
}
// handleLogStream — живая консоль через Server-Sent Events. SSE выбран вместо
@@ -80,7 +103,7 @@ func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleLogStream(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
writeError(w, http.StatusInternalServerError, "потоковая отдача не поддерживается")
writeError(w, r, http.StatusInternalServerError, "потоковая отдача не поддерживается")
return
}
@@ -94,9 +117,11 @@ func (s *Server) handleLogStream(w http.ResponseWriter, r *http.Request) {
lines, unsubscribe := s.sup.Subscribe()
defer unsubscribe()
lang := i18n.FromRequest(r)
// Сначала отдаём хвост истории, чтобы вкладка не открывалась пустой.
since, _ := strconv.ParseInt(r.URL.Query().Get("since"), 10, 64)
for _, line := range s.sup.Lines(since) {
for _, line := range translateLines(r, s.sup.Lines(since)) {
if !writeSSE(w, line) {
return
}
@@ -115,6 +140,7 @@ func (s *Server) handleLogStream(w http.ResponseWriter, r *http.Request) {
if !ok {
return
}
line.Text = i18n.T(lang, line.Text)
if !writeSSE(w, line) {
return
}
+13 -13
View File
@@ -19,7 +19,7 @@ func (s *Server) handleTaskStatus(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleTaskCancel(w http.ResponseWriter, r *http.Request) {
if !s.jobs.Busy() {
writeError(w, http.StatusConflict, "нет активной задачи")
writeError(w, r, http.StatusConflict, "нет активной задачи")
return
}
s.jobs.Cancel()
@@ -31,7 +31,7 @@ func (s *Server) handleTaskCancel(w http.ResponseWriter, r *http.Request) {
// JVM, и сервер упадёт непредсказуемо.
func (s *Server) handleInstall(w http.ResponseWriter, r *http.Request) {
if s.sup.State() != pzserver.StateStopped {
writeError(w, http.StatusConflict, "сначала остановите игровой сервер")
writeError(w, r, http.StatusConflict, "сначала остановите игровой сервер")
return
}
@@ -46,7 +46,7 @@ func (s *Server) handleInstall(w http.ResponseWriter, r *http.Request) {
return nil
})
if err != nil {
writeOpError(w, err)
writeOpError(w, r, err)
return
}
writeJSON(w, http.StatusAccepted, map[string]any{"task": name})
@@ -55,7 +55,7 @@ func (s *Server) handleInstall(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleListBackups(w http.ResponseWriter, r *http.Request) {
archives, err := s.backups.List()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
return
}
// Фильтр по профилю необязателен: панель показывает архивы всех профилей,
@@ -88,7 +88,7 @@ func (s *Server) handleCreateBackup(w http.ResponseWriter, r *http.Request) {
body.Profile = s.cfg.Active
}
if _, ok := s.cfg.Profile(body.Profile); !ok {
writeError(w, http.StatusBadRequest, "профиль не найден")
writeError(w, r, http.StatusBadRequest, "профиль не найден")
return
}
// Живой сервер держит мир в памяти, и снимок чужого профиля тут ни при
@@ -119,7 +119,7 @@ func (s *Server) handleCreateBackup(w http.ResponseWriter, r *http.Request) {
return err
})
if err != nil {
writeOpError(w, err)
writeOpError(w, r, err)
return
}
writeJSON(w, http.StatusAccepted, map[string]any{"task": name})
@@ -127,7 +127,7 @@ func (s *Server) handleCreateBackup(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleDeleteBackup(w http.ResponseWriter, r *http.Request) {
if err := s.backups.Delete(r.PathValue("name")); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeError(w, r, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{"deleted": r.PathValue("name")})
@@ -137,18 +137,18 @@ func (s *Server) handleDownloadBackup(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
path, err := s.backups.Path(name)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeError(w, r, http.StatusBadRequest, err.Error())
return
}
f, err := os.Open(path)
if err != nil {
writeError(w, http.StatusNotFound, "архив не найден")
writeError(w, r, http.StatusNotFound, "архив не найден")
return
}
defer f.Close()
info, err := f.Stat()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
return
}
@@ -162,12 +162,12 @@ func (s *Server) handleDownloadBackup(w http.ResponseWriter, r *http.Request) {
// своим состоянием при следующем сохранении.
func (s *Server) handleRestoreBackup(w http.ResponseWriter, r *http.Request) {
if s.sup.State() != pzserver.StateStopped {
writeError(w, http.StatusConflict, "сначала остановите игровой сервер")
writeError(w, r, http.StatusConflict, "сначала остановите игровой сервер")
return
}
name := r.PathValue("name")
if _, err := s.backups.Path(name); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeError(w, r, http.StatusBadRequest, err.Error())
return
}
// Страховочную копию снимаем с того профиля, поверх которого ляжет архив;
@@ -188,7 +188,7 @@ func (s *Server) handleRestoreBackup(w http.ResponseWriter, r *http.Request) {
return s.backups.Restore(ctx, log, name)
})
if err != nil {
writeOpError(w, err)
writeOpError(w, r, err)
return
}
writeJSON(w, http.StatusAccepted, map[string]any{"task": taskName})
+4 -4
View File
@@ -26,7 +26,7 @@ func (s *Server) handleApplyUpdate(w http.ResponseWriter, r *http.Request) {
}
if !s.updater.CanApply(r.Context()) {
writeError(w, http.StatusPreconditionFailed, updater.ErrNoAdmin.Error())
writeError(w, r, http.StatusPreconditionFailed, updater.ErrNoAdmin.Error())
return
}
@@ -40,7 +40,7 @@ func (s *Server) handleApplyUpdate(w http.ResponseWriter, r *http.Request) {
return s.updater.Apply(ctx, log, body.Version)
})
if err != nil {
writeOpError(w, err)
writeOpError(w, r, err)
return
}
writeJSON(w, http.StatusAccepted, map[string]any{"task": name})
@@ -49,7 +49,7 @@ func (s *Server) handleApplyUpdate(w http.ResponseWriter, r *http.Request) {
// handleRestartManager перезапускает саму панель.
func (s *Server) handleRestartManager(w http.ResponseWriter, r *http.Request) {
if !s.updater.CanApply(r.Context()) {
writeError(w, http.StatusPreconditionFailed, updater.ErrNoAdmin.Error())
writeError(w, r, http.StatusPreconditionFailed, updater.ErrNoAdmin.Error())
return
}
@@ -60,7 +60,7 @@ func (s *Server) handleRestartManager(w http.ResponseWriter, r *http.Request) {
return s.updater.Restart(ctx, log)
})
if err != nil {
writeOpError(w, err)
writeOpError(w, r, err)
return
}
writeJSON(w, http.StatusAccepted, map[string]any{"task": name})
+11 -9
View File
@@ -13,6 +13,7 @@ import (
"github.com/mrleo1nid/pzmanager/internal/auth"
"github.com/mrleo1nid/pzmanager/internal/backup"
"github.com/mrleo1nid/pzmanager/internal/config"
"github.com/mrleo1nid/pzmanager/internal/i18n"
"github.com/mrleo1nid/pzmanager/internal/job"
"github.com/mrleo1nid/pzmanager/internal/pzconfig"
"github.com/mrleo1nid/pzmanager/internal/pzserver"
@@ -175,7 +176,7 @@ func (s *Server) withCommonHeaders(next http.Handler) http.Handler {
if r.Method != http.MethodGet && r.Method != http.MethodHead &&
strings.HasPrefix(r.URL.Path, "/api/") && r.Header.Get(csrfHeader) == "" {
writeError(w, http.StatusForbidden, "запрос без заголовка "+csrfHeader)
writeError(w, r, http.StatusForbidden, "запрос без заголовка "+csrfHeader)
return
}
next.ServeHTTP(w, r)
@@ -186,7 +187,7 @@ func (s *Server) withCommonHeaders(next http.Handler) http.Handler {
func (s *Server) requireAuth(next http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, ok := s.users.SessionFromRequest(r); !ok {
writeError(w, http.StatusUnauthorized, "требуется вход")
writeError(w, r, http.StatusUnauthorized, "требуется вход")
return
}
next(w, r)
@@ -228,20 +229,21 @@ func writeJSON(w http.ResponseWriter, status int, v any) {
}
// writeError отправляет ошибку в том же формате, что и остальные ответы.
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{"error": message})
// Текст переводится на язык панели: в коде сообщения написаны по-русски.
func writeError(w http.ResponseWriter, r *http.Request, status int, message string) {
writeJSON(w, status, map[string]string{"error": i18n.T(i18n.FromRequest(r), message)})
}
// writeOpError переводит ошибку операции в подходящий HTTP-код.
func writeOpError(w http.ResponseWriter, err error) {
func writeOpError(w http.ResponseWriter, r *http.Request, err error) {
switch {
case errors.Is(err, pzserver.ErrAlreadyRunning), errors.Is(err, pzserver.ErrNotRunning),
errors.Is(err, job.ErrBusy):
writeError(w, http.StatusConflict, err.Error())
writeError(w, r, http.StatusConflict, err.Error())
case errors.Is(err, pzserver.ErrNotInstalled):
writeError(w, http.StatusPreconditionFailed, err.Error())
writeError(w, r, http.StatusPreconditionFailed, err.Error())
default:
writeError(w, http.StatusInternalServerError, err.Error())
writeError(w, r, http.StatusInternalServerError, err.Error())
}
}
@@ -251,7 +253,7 @@ func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool {
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(dst); err != nil {
writeError(w, http.StatusBadRequest, "некорректный JSON: "+err.Error())
writeError(w, r, http.StatusBadRequest, "некорректный JSON: "+err.Error())
return false
}
return true