package api import ( "errors" "net/http" "time" "github.com/jackc/pgx/v5" ) // Issuing the code that claims a shop PC. // // This existed only as a provisioning command, which made every replacement PC // a support ticket and an SSH session - and a shop PC is exactly the kind of // machine that gets replaced, reimaged and swapped between branches. The // command remains the bootstrap, because a brand new customer has nobody to // sign in as yet; this is for every time after that. // // Manager and above, never staff: the code is redeemed for the site's broker // password, so it is a credential in its own right, not a convenience. func (s *Server) handleIssueEnrolmentCode(w http.ResponseWriter, r *http.Request) { p := PrincipalFrom(r.Context()) if !p.CanManageSites() { writeErr(w, http.StatusForbidden, "forbidden", "Your account cannot set up shop computers. Ask a manager or the owner.") return } site, ok := s.resolveSite(w, r, r.PathValue("site")) if !ok { return } var in NewEnrolmentCodeInput if err := decodeOptional(w, r, &in); err != nil { badRequest(w, err.Error()) return } // A week by default, and a month at the very most. The code is read aloud, // photographed and pasted into chat on its way to a shop; a long-lived one // is a broker credential lying about in a WhatsApp thread. days := in.Days if days <= 0 { days = 7 } if days > 30 { days = 30 } out, err := s.Store.IssueEnrolmentCode(r.Context(), p.ClientID, site, p.UserID, clip(trim(in.Label), 120), time.Duration(days)*24*time.Hour) if err != nil { if errors.Is(err, pgx.ErrNoRows) { writeErr(w, http.StatusNotFound, "not_found", "No such shop.") return } s.serverError(w, "issue enrolment code", err) return } // A code hands out a site's broker password, so who minted one and when is // worth a row - the same reason every read of a face image writes one. s.Store.Audit(r.Context(), AuditEntry{ ClientID: p.ClientID, ActorID: p.UserID, ActorKind: "user", Action: "enrolment_code.issued", Entity: "site", EntityID: site, Detail: map[string]any{"label": out.Label, "expires_at": out.ExpiresAt}, }) // 201: a credential was created. The body is the only time it is readable. writeJSON(w, http.StatusCreated, out) }