package controllers import ( "errors" "net/http" "strconv" "time" "git.golem15.com/golem15/sm-user-plugin/classes" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bouncer" ) // backendLinks returns the published link service, or one over app. func backendLinks(app *backpack.App) *classes.BackendLinks { if app != nil { if links, ok := app.Lookup[*classes.BackendLinks](); ok && links != nil { return links } } return &classes.BackendLinks{App: app} } // linkNotFound is the one answer for an unknown, used or expired code and // for an administrator who is no longer available: the page cannot tell // them apart. func linkNotFound(w http.ResponseWriter) { writeJSON(w, http.StatusNotFound, map[string]any{"error": "not_found"}) } // BackendLinkShow serves GET /_user/api/v1/backend-link/{code}, the site's // "link this account to the administrator?" page: the administrator's login // and display name and when the code expires, nothing else. Anyone holding // the code may read it; a code that is unknown, used or expired, or whose // administrator was deactivated, answers 404. func BackendLinkShow(app *backpack.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { req, admin, err := backendLinks(app).LinkRequestByCode(r.Context(), r.PathValue("code")) if err != nil { if isLinkGone(err) { linkNotFound(w) return } writeOpaque500(w) return } writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{ "admin": map[string]any{"login": admin.Login, "name": classes.BackendDisplayName(admin)}, "expires_at": req.ExpiresAt.UTC().Format(time.RFC3339), }}) } } // BackendLinkConfirm serves POST /_user/api/v1/backend-link/confirm behind // jwt.auth: body {"code": ...}. The signed-in frontend user is linked to the // administrator who created the code, only when the session token was // issued at or after the request (the user signed in fresh for it): an // older session answers 409 with the stale-session error, an account already linked on // either side 409 already_linked, an unknown, used or expired code 404. func BackendLinkConfirm(app *backpack.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { principal, ok := bouncer.User(r.Context()) if !ok || principal == nil || principal.ID == 0 { writeUnauthorized(w) return } secret, err := classes.JWTSecret(app) if err != nil { writeOpaque500(w) return } // The token jwt.auth accepted (bearer header, then the session // cookies), read again for its iat. sub, iat, _, _, err := bouncer.VerifyClaims(classes.SessionToken(r), secret) if err != nil || sub != strconv.FormatUint(uint64(principal.ID), 10) { writeUnauthorized(w) return } fields, err := readFields(r) if err != nil { linkNotFound(w) return } links := backendLinks(app) _, err = links.ConfirmLinkRequest(r.Context(), strField(fields, "code"), principal.ID, iat) switch { case err == nil: case errors.Is(err, classes.ErrStaleSession): writeJSON(w, http.StatusConflict, map[string]any{"error": "stale_session"}) return case errors.Is(err, classes.ErrAlreadyLinked): writeJSON(w, http.StatusConflict, map[string]any{"error": "already_linked"}) return case isLinkGone(err): linkNotFound(w) return default: writeOpaque500(w) return } linked, found, err := links.LinkedBackendUser(r.Context(), principal.ID) if err != nil || !found { writeOpaque500(w) return } writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{ "backend_user": map[string]any{"login": linked.Login, "name": linked.DisplayName}, }}) } } func isLinkGone(err error) bool { return errors.Is(err, classes.ErrLinkRequestNotFound) || errors.Is(err, classes.ErrLinkRequestUsed) || errors.Is(err, classes.ErrLinkRequestExpired) || errors.Is(err, classes.ErrBackendUserUnavailable) || errors.Is(err, classes.ErrFrontendUserUnavailable) }