Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 63 additions & 31 deletions backend/cmd/map/handlers/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
htmltemplate "html/template"
"io"
"net/http"
"net/mail"
Expand Down Expand Up @@ -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()),
}
}

Expand Down Expand Up @@ -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()),
Expand All @@ -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()),
Expand Down Expand Up @@ -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()),
Expand All @@ -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()),
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -4977,17 +5000,20 @@ 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
}
// 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()),
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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()),
Expand Down
6 changes: 3 additions & 3 deletions backend/cmd/map/handlers/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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);`)
Expand Down Expand Up @@ -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`)
Expand Down
45 changes: 45 additions & 0 deletions backend/cmd/map/handlers/login_i18n_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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{`<html lang="es">`, "Administración del juego", "Gestión de usuarios", "Añadir usuario", `window.MCTF_LANG = "es"`} {
require.Contains(t, body, want)
}
}
Loading
Loading