/* * SPDX-FileCopyrightText: 2023 jordi fita mas * SPDX-License-Identifier: AGPL-3.0-only */ package app import ( "context" "net/http" "path" "strings" "golang.org/x/text/language" "dev.tandem.ws/tandem/camper/pkg/database" httplib "dev.tandem.ws/tandem/camper/pkg/http" "dev.tandem.ws/tandem/camper/pkg/locale" "dev.tandem.ws/tandem/camper/pkg/template" ) func shiftPath(p string) (head, tail string) { p = path.Clean("/" + p) if i := strings.IndexByte(p[1:], '/') + 1; i <= 0 { return p[1:], "/" } else { return p[1:i], p[i:] } } func methodNotAllowed(w http.ResponseWriter, _ *http.Request, allowed ...string) { w.Header().Set("Allow", strings.Join(allowed, ", ")) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } type App struct { db *database.DB fileHandler http.Handler locales locale.Locales defaultLocale *locale.Locale languageMatcher language.Matcher } func New(db *database.DB) http.Handler { locales := locale.MustGetAll(db) app := &App{ db: db, fileHandler: http.FileServer(http.Dir("web/static")), locales: locales, defaultLocale: locales[language.Catalan], languageMatcher: language.NewMatcher(locales.Tags()), } var handler http.Handler = app handler = httplib.RecoverPanic(handler) handler = httplib.LogRequest(handler) return handler } func (h *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { requestURL := r.URL.Path var head string head, r.URL.Path = shiftPath(r.URL.Path) if head == "static" { h.fileHandler.ServeHTTP(w, r) } else { conn, err := h.db.Acquire(r.Context()) if err != nil { panic(err) } defer conn.Release() cookie := getSessionCookie(r) user, err := h.getUser(r.Context(), conn, cookie) if err != nil { panic(err) } l := h.matchLocale(r, user) if head == "login" { switch r.Method { case http.MethodPost: h.handleLogin(w, r, l, conn) default: methodNotAllowed(w, r, http.MethodPost) } } else { if !user.LoggedIn { h.serveLoginForm(w, r, l, requestURL) return } switch head { case "": switch r.Method { case http.MethodGet: h.serveDashboard(w, r, l) default: methodNotAllowed(w, r, http.MethodGet) } default: http.NotFound(w, r) } } } } type User struct { Email string LoggedIn bool Role string Language language.Tag CsrfToken string } func (h *App) getUser(ctx context.Context, conn *database.Conn, cookie string) (*User, error) { if _, err := conn.Exec(ctx, "select set_cookie($1)", cookie); err != nil { conn.Release() return nil, err } user := &User{ Email: "", LoggedIn: false, Role: "guest", } row := conn.QueryRow(ctx, "select coalesce(email, ''), email is not null, role, lang_tag, csrf_token from user_profile") var langTag string if err := row.Scan(&user.Email, &user.LoggedIn, &user.Role, &langTag, &user.CsrfToken); err != nil { return nil, err } if lang, err := language.Parse(langTag); err == nil { user.Language = lang } else { return nil, err } return user, nil } func (h *App) matchLocale(r *http.Request, user *User) *locale.Locale { l := h.locales[user.Language] if l == nil { l = locale.Match(r.Header.Get("Accept-Language"), h.locales, h.languageMatcher) if l == nil { l = h.defaultLocale } } return l } func (h *App) serveDashboard(w http.ResponseWriter, _ *http.Request, l *locale.Locale) { template.MustRender(w, l, "dashboard.gohtml", nil) }