diff --git a/backend/cmd/map/handlers/admin.go b/backend/cmd/map/handlers/admin.go index 5532843..5c60bf9 100644 --- a/backend/cmd/map/handlers/admin.go +++ b/backend/cmd/map/handlers/admin.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + htmltemplate "html/template" "io" "net/http" "net/mail" @@ -269,11 +270,12 @@ func logoFilePath(custom bool, logo string) string { return "/static/svg/icons/badges/badge-" + slug + ".svg" } -func adminTemplateFuncs() template.FuncMap { +func (h *HandlersMap) adminTemplateFuncs(r *http.Request) template.FuncMap { return template.FuncMap{ "logoFilePath": logoFilePath, "logoIsImage": isCustomLogoAssetPath, "logoSymbol": normalizeLogoSymbolName, + "T": h.T(r.Context()), } } @@ -478,16 +480,19 @@ func (h *HandlersMap) AdminTemplateHandler(w http.ResponseWriter, r *http.Reques return } // Prepare template - t, err := template.ParseFiles( - h.Config.Map.TemplatesDir + "/admin/index.html") + t, err := template.New("index.html").Funcs(h.adminTemplateFuncs(r)).ParseFiles(h.Config.Map.TemplatesDir + "/admin/index.html") if err != nil { log.Err(err).Msg("error getting admin template") return } // Prepare template data authenticated := h.IsAuthenticated(r.Context()) + tr := h.T(r.Context()) + i18nJSON, _ := json.Marshal(h.LocaleMessages(r.Context())) templateData := AdminTemplateData{ - Title: "MapCTF Admin: Dashboard", + Title: tr("admin.title.dashboard"), + Lang: h.Locale(r.Context()).String(), + I18NJSON: htmltemplate.JS(i18nJSON), UUID: uuid, Authenticated: authenticated, Admin: h.IsAdmin(r.Context()), @@ -513,16 +518,19 @@ func (h *HandlersMap) AdminSettingsTemplateHandler(w http.ResponseWriter, r *htt return } // Prepare template - t, err := template.ParseFiles( - h.Config.Map.TemplatesDir + "/admin/settings.html") + t, err := template.New("settings.html").Funcs(h.adminTemplateFuncs(r)).ParseFiles(h.Config.Map.TemplatesDir + "/admin/settings.html") if err != nil { log.Err(err).Msg("error getting admin template") return } // Prepare template data authenticated := h.IsAuthenticated(r.Context()) + tr := h.T(r.Context()) + i18nJSON, _ := json.Marshal(h.LocaleMessages(r.Context())) templateData := AdminSettingsTemplateData{ - Title: "MapCTF Admin: Settings", + Title: tr("admin.title.settings"), + Lang: h.Locale(r.Context()).String(), + I18NJSON: htmltemplate.JS(i18nJSON), UUID: uuid, Authenticated: authenticated, Admin: h.IsAdmin(r.Context()), @@ -1700,16 +1708,19 @@ func (h *HandlersMap) AdminControlsTemplateHandler(w http.ResponseWriter, r *htt return } // Prepare template - t, err := template.ParseFiles( - h.Config.Map.TemplatesDir + "/admin/controls.html") + t, err := template.New("controls.html").Funcs(h.adminTemplateFuncs(r)).ParseFiles(h.Config.Map.TemplatesDir + "/admin/controls.html") if err != nil { log.Err(err).Msg("error getting admin template") return } // Prepare template data authenticated := h.IsAuthenticated(r.Context()) + tr := h.T(r.Context()) + i18nJSON, _ := json.Marshal(h.LocaleMessages(r.Context())) templateData := AdminControlsTemplateData{ - Title: "MapCTF Admin: Controls", + Title: tr("admin.title.controls"), + Lang: h.Locale(r.Context()).String(), + I18NJSON: htmltemplate.JS(i18nJSON), UUID: uuid, Authenticated: authenticated, Admin: h.IsAdmin(r.Context()), @@ -1735,16 +1746,19 @@ func (h *HandlersMap) AdminTeamsTemplateHandler(w http.ResponseWriter, r *http.R return } // Prepare template - t, err := template.New("teams.html").Funcs(adminTemplateFuncs()).ParseFiles( - h.Config.Map.TemplatesDir + "/admin/teams.html") + t, err := template.New("teams.html").Funcs(h.adminTemplateFuncs(r)).ParseFiles(h.Config.Map.TemplatesDir + "/admin/teams.html") if err != nil { log.Err(err).Msg("error getting admin template") return } // Prepare template data authenticated := h.IsAuthenticated(r.Context()) + tr := h.T(r.Context()) + i18nJSON, _ := json.Marshal(h.LocaleMessages(r.Context())) templateData := AdminTeamsTemplateData{ - Title: "MapCTF Admin: Teams", + Title: tr("admin.title.teams"), + Lang: h.Locale(r.Context()).String(), + I18NJSON: htmltemplate.JS(i18nJSON), UUID: uuid, Authenticated: authenticated, Admin: h.IsAdmin(r.Context()), @@ -1797,15 +1811,18 @@ func (h *HandlersMap) AdminTeamLogosTemplateHandler(w http.ResponseWriter, r *ht h.ErrorInvalidUUID(w, r) return } - t, err := template.New("team-logos.html").Funcs(adminTemplateFuncs()).ParseFiles( - h.Config.Map.TemplatesDir + "/admin/team-logos.html") + t, err := template.New("team-logos.html").Funcs(h.adminTemplateFuncs(r)).ParseFiles(h.Config.Map.TemplatesDir + "/admin/team-logos.html") if err != nil { log.Err(err).Msg("error getting admin team-logos template") return } authenticated := h.IsAuthenticated(r.Context()) + tr := h.T(r.Context()) + i18nJSON, _ := json.Marshal(h.LocaleMessages(r.Context())) templateData := AdminTeamLogosTemplateData{ - Title: "MapCTF Admin: Team Logos", + Title: tr("admin.title.team_logos"), + Lang: h.Locale(r.Context()).String(), + I18NJSON: htmltemplate.JS(i18nJSON), UUID: uuid, Authenticated: authenticated, Admin: h.IsAdmin(r.Context()), @@ -3231,16 +3248,19 @@ func (h *HandlersMap) AdminUsersTemplateHandler(w http.ResponseWriter, r *http.R return } // Prepare template - t, err := template.ParseFiles( - h.Config.Map.TemplatesDir + "/admin/users.html") + t, err := template.New("users.html").Funcs(h.adminTemplateFuncs(r)).ParseFiles(h.Config.Map.TemplatesDir + "/admin/users.html") if err != nil { log.Err(err).Msg("error getting admin template") return } // Prepare template data authenticated := h.IsAuthenticated(r.Context()) + tr := h.T(r.Context()) + i18nJSON, _ := json.Marshal(h.LocaleMessages(r.Context())) templateData := AdminUsersTemplateData{ - Title: "MapCTF Admin: Users", + Title: tr("admin.title.users"), + Lang: h.Locale(r.Context()).String(), + I18NJSON: htmltemplate.JS(i18nJSON), UUID: uuid, Authenticated: authenticated, Admin: h.IsAdmin(r.Context()), @@ -4143,16 +4163,19 @@ func (h *HandlersMap) AdminChallengesTemplateHandler(w http.ResponseWriter, r *h return } // Prepare template - t, err := template.ParseFiles( - h.Config.Map.TemplatesDir + "/admin/challenges.html") + t, err := template.New("challenges.html").Funcs(h.adminTemplateFuncs(r)).ParseFiles(h.Config.Map.TemplatesDir + "/admin/challenges.html") if err != nil { log.Err(err).Msg("error getting admin template") return } // Prepare template data authenticated := h.IsAuthenticated(r.Context()) + tr := h.T(r.Context()) + i18nJSON, _ := json.Marshal(h.LocaleMessages(r.Context())) templateData := AdminChallengesTemplateData{ - Title: "MapCTF Admin: Challenges", + Title: tr("admin.title.challenges"), + Lang: h.Locale(r.Context()).String(), + I18NJSON: htmltemplate.JS(i18nJSON), UUID: uuid, Authenticated: authenticated, Admin: h.IsAdmin(r.Context()), @@ -4977,8 +5000,7 @@ func (h *HandlersMap) AdminActivityTemplateHandler(w http.ResponseWriter, r *htt return } // Prepare template - t, err := template.ParseFiles( - h.Config.Map.TemplatesDir + "/admin/activity.html") + t, err := template.New("activity.html").Funcs(h.adminTemplateFuncs(r)).ParseFiles(h.Config.Map.TemplatesDir + "/admin/activity.html") if err != nil { log.Err(err).Msg("error getting admin template") return @@ -4986,8 +5008,12 @@ func (h *HandlersMap) AdminActivityTemplateHandler(w http.ResponseWriter, r *htt // Prepare template data authenticated := h.IsAuthenticated(r.Context()) currentUsername := strings.TrimSpace(h.Sessions.GetString(r.Context(), string(ContextKeyUser))) + tr := h.T(r.Context()) + i18nJSON, _ := json.Marshal(h.LocaleMessages(r.Context())) templateData := AdminActivityTemplateData{ - Title: "MapCTF Admin: Activity", + Title: tr("admin.title.activity"), + Lang: h.Locale(r.Context()).String(), + I18NJSON: htmltemplate.JS(i18nJSON), UUID: uuid, Authenticated: authenticated, Admin: h.IsAdmin(r.Context()), @@ -5132,14 +5158,17 @@ func (h *HandlersMap) AdminChatTemplateHandler(w http.ResponseWriter, r *http.Re h.ErrorInvalidUUID(w, r) return } - t, err := template.ParseFiles( - h.Config.Map.TemplatesDir + "/admin/chat.html") + t, err := template.New("chat.html").Funcs(h.adminTemplateFuncs(r)).ParseFiles(h.Config.Map.TemplatesDir + "/admin/chat.html") if err != nil { log.Err(err).Msg("error getting admin chat template") return } + tr := h.T(r.Context()) + i18nJSON, _ := json.Marshal(h.LocaleMessages(r.Context())) templateData := AdminChatTemplateData{ - Title: "MapCTF Admin: Chat", + Title: tr("admin.title.chat"), + Lang: h.Locale(r.Context()).String(), + I18NJSON: htmltemplate.JS(i18nJSON), UUID: uuid, Authenticated: h.IsAuthenticated(r.Context()), Admin: h.IsAdmin(r.Context()), @@ -5336,16 +5365,19 @@ func (h *HandlersMap) AdminCountriesTemplateHandler(w http.ResponseWriter, r *ht return } // Prepare template - t, err := template.ParseFiles( - h.Config.Map.TemplatesDir + "/admin/countries.html") + t, err := template.New("countries.html").Funcs(h.adminTemplateFuncs(r)).ParseFiles(h.Config.Map.TemplatesDir + "/admin/countries.html") if err != nil { log.Err(err).Msg("error getting admin template") return } // Prepare template data authenticated := h.IsAuthenticated(r.Context()) + tr := h.T(r.Context()) + i18nJSON, _ := json.Marshal(h.LocaleMessages(r.Context())) templateData := AdminCountriesTemplateData{ - Title: "MapCTF Admin: Countries", + Title: tr("admin.title.countries"), + Lang: h.Locale(r.Context()).String(), + I18NJSON: htmltemplate.JS(i18nJSON), UUID: uuid, Authenticated: authenticated, Admin: h.IsAdmin(r.Context()), diff --git a/backend/cmd/map/handlers/admin_test.go b/backend/cmd/map/handlers/admin_test.go index 76c7c44..39cd2ee 100644 --- a/backend/cmd/map/handlers/admin_test.go +++ b/backend/cmd/map/handlers/admin_test.go @@ -693,7 +693,7 @@ func TestAdminAddLogoModalAllowsRasterImageUploads(t *testing.T) { require.Contains(t, string(adminJS), `var ADMIN_LOGO_UPLOAD_MAX_LABEL = "512 KB";`) require.Contains(t, string(adminJS), `var ADMIN_LOGO_UPLOAD_RECOMMENDED_SIZE = "64 x 48 px";`) require.Contains(t, string(adminJS), `function formatAdminLogoFileSize(bytes)`) - require.Contains(t, string(adminJS), `Selected file: " + logoFile.name + " (" + formatAdminLogoFileSize(logoFile.size) + ")"`) + require.Contains(t, string(adminJS), `t("admin.js.selected_file_prefix", "Selected file: ") + logoFile.name + " (" + formatAdminLogoFileSize(logoFile.size) + ")"`) require.Contains(t, string(adminJS), `logoFile.size > ADMIN_LOGO_UPLOAD_MAX_BYTES`) require.Contains(t, string(adminJS), `function ensureAdminLogoUploadFormats(form)`) require.Contains(t, string(adminJS), `ensureAdminLogoUploadFormats(form);`) @@ -1360,8 +1360,8 @@ func TestAdminChallengesEditorGroupsTaxonomyAndScoringRows(t *testing.T) { css := string(mustReadFile(t, filepath.Join("..", "templates", "static", "css", "mapctf.css"))) require.Contains(t, template, `admin-challenge-grid-row admin-challenge-grid-row--taxonomy`) - require.Contains(t, template, `for="challenge-category-{{ .ID }}">Category`) - require.Contains(t, template, `for="challenge-country-{{ .ID }}">Country`) + require.Contains(t, template, `for="challenge-category-{{ .ID }}">{{ T "admin.challenges.category" }}`) + require.Contains(t, template, `for="challenge-country-{{ .ID }}">{{ T "admin.challenges.country" }}`) require.Contains(t, css, `.admin-box .admin-challenge-grid-row--taxonomy`) require.Contains(t, css, `grid-template-columns: repeat(2, minmax(0, 1fr));`) require.Contains(t, css, `.admin-box .admin-score-group`) diff --git a/backend/cmd/map/handlers/login_i18n_test.go b/backend/cmd/map/handlers/login_i18n_test.go index 6137dc7..891232e 100644 --- a/backend/cmd/map/handlers/login_i18n_test.go +++ b/backend/cmd/map/handlers/login_i18n_test.go @@ -11,6 +11,8 @@ import ( "github.com/jmpsec/mapctf/pkg/config" "github.com/jmpsec/mapctf/pkg/i18n" "github.com/jmpsec/mapctf/pkg/settings" + "github.com/jmpsec/mapctf/pkg/teams" + "github.com/jmpsec/mapctf/pkg/users" "github.com/stretchr/testify/require" ) @@ -159,3 +161,46 @@ func TestGameboardHandlerRendersConfiguredLocale(t *testing.T) { } require.Contains(t, body, `window.MCTF_LANG = "es"`) } + +func TestAdminUsersHandlerRendersConfiguredLocale(t *testing.T) { + if _, err := os.Stat(filepath.Join("..", "templates", "admin", "users.html")); err != nil { + t.Skipf("admin users template not available: %v", err) + } + + db := newJSONTestDB(t) + settingsManager, err := settings.CreateSettingsManager(db, "test-service") + require.NoError(t, err) + require.NoError(t, settingsManager.Initialization(jsonTestUUID)) + require.NoError(t, settingsManager.SetLanguage("es", jsonSettingsAuthor, jsonTestUUID)) + teamManager, err := teams.CreateTeams(db) + require.NoError(t, err) + userManager, err := users.CreateUserManager(db, &config.ConfigurationJWT{Secret: "test-secret", HoursToExpire: 24}) + require.NoError(t, err) + catalog, err := i18n.New() + require.NoError(t, err) + sessions := scs.New() + handler := CreateHandlersMap( + WithConfig(config.MapCTFConfiguration{Map: config.ConfigurationMap{UUID: jsonTestUUID, TemplatesDir: filepath.Join("..", "templates")}}), + WithSettings(settingsManager), + WithTeams(teamManager), + WithUsers(userManager), + WithSessions(sessions), + WithI18N(catalog), + ) + + req := newTemplateRequestWithUUID(http.MethodGet, "/"+jsonTestUUID+"/admin/users", jsonTestUUID) + ctx, err := sessions.Load(req.Context(), "") + require.NoError(t, err) + sessions.Put(ctx, string(ContextKeyUser), "admin") + sessions.Put(ctx, string(ContextKeyAdmin), true) + req = req.WithContext(ctx) + + rr := httptest.NewRecorder() + handler.LocaleMiddleware(http.HandlerFunc(handler.AdminUsersTemplateHandler)).ServeHTTP(rr, req) + + require.Equal(t, http.StatusOK, rr.Code, "body: %s", rr.Body.String()) + body := rr.Body.String() + for _, want := range []string{``, "Administración del juego", "Gestión de usuarios", "Añadir usuario", `window.MCTF_LANG = "es"`} { + require.Contains(t, body, want) + } +} diff --git a/backend/cmd/map/handlers/types-templates.go b/backend/cmd/map/handlers/types-templates.go index 37670eb..38da704 100644 --- a/backend/cmd/map/handlers/types-templates.go +++ b/backend/cmd/map/handlers/types-templates.go @@ -114,6 +114,8 @@ type RegistrationTemplateData struct { // AdminSettingsTemplateData for passing data to the admin settings template type AdminSettingsTemplateData struct { Title string + Lang string + I18NJSON template.JS UUID string Authenticated bool Admin bool @@ -143,6 +145,8 @@ type AdminSettingsTemplateData struct { // AdminTemplateData for passing data to the admin template type AdminTemplateData struct { Title string + Lang string + I18NJSON template.JS UUID string Authenticated bool Admin bool @@ -153,6 +157,8 @@ type AdminTemplateData struct { // AdminControlsTemplateData for passing data to the admin controls template type AdminControlsTemplateData struct { Title string + Lang string + I18NJSON template.JS UUID string Authenticated bool Admin bool @@ -163,6 +169,8 @@ type AdminControlsTemplateData struct { // AdminUsersTemplateData for passing data to the admin users template type AdminUsersTemplateData struct { Title string + Lang string + I18NJSON template.JS UUID string Authenticated bool Admin bool @@ -176,6 +184,8 @@ type AdminUsersTemplateData struct { // AdminTeamsTemplateData for passing data to the admin teams template type AdminTeamsTemplateData struct { Title string + Lang string + I18NJSON template.JS UUID string Authenticated bool Admin bool @@ -190,6 +200,8 @@ type AdminTeamsTemplateData struct { // AdminTeamLogosTemplateData for passing data to the admin team-logos template type AdminTeamLogosTemplateData struct { Title string + Lang string + I18NJSON template.JS UUID string Authenticated bool Admin bool @@ -203,6 +215,8 @@ type AdminTeamLogosTemplateData struct { // AdminChallengesTemplateData for passing data to the admin challenges template type AdminChallengesTemplateData struct { Title string + Lang string + I18NJSON template.JS UUID string Authenticated bool Admin bool @@ -229,6 +243,8 @@ type AdminChallengeActivityEntry struct { // AdminActivityTemplateData for passing data to the admin activity template type AdminActivityTemplateData struct { Title string + Lang string + I18NJSON template.JS UUID string Authenticated bool Admin bool @@ -241,6 +257,8 @@ type AdminActivityTemplateData struct { // AdminChatTemplateData for passing data to the admin chat template type AdminChatTemplateData struct { Title string + Lang string + I18NJSON template.JS UUID string Authenticated bool Admin bool @@ -253,6 +271,8 @@ type AdminChatTemplateData struct { // AdminCountriesTemplateData for passing data to the admin countries template type AdminCountriesTemplateData struct { Title string + Lang string + I18NJSON template.JS UUID string Authenticated bool Admin bool diff --git a/backend/cmd/map/templates/admin/activity.html b/backend/cmd/map/templates/admin/activity.html index c6755ca..2d3f803 100644 --- a/backend/cmd/map/templates/admin/activity.html +++ b/backend/cmd/map/templates/admin/activity.html @@ -1,5 +1,5 @@ - +
@@ -238,48 +238,48 @@ -| Country | -Country Code | -Assigned | -Challenge | -SVG | -Active | +{{ T "admin.countries.th_country" }} | +{{ T "admin.countries.th_code" }} | +{{ T "admin.countries.th_assigned" }} | +{{ T "admin.countries.th_challenge" }} | +{{ T "admin.countries.th_svg" }} | +{{ T "admin.countries.th_active" }} |
|---|