package identityhttp import ( "encoding/json" "errors" "io" "net/http" "strconv" sresponse "git.sendico.io/tech/sxchnge/product/auth/http/v1/response" customer "git.sendico.io/tech/sxchnge/product/customers/model" "git.sendico.io/tech/sxchnge/product/identity" merrors "git.sendico.io/tech/sxchnge/shared/errors" api "git.sendico.io/tech/sxchnge/shared/http" "git.sendico.io/tech/sxchnge/shared/http/response" mlogger "git.sendico.io/tech/sxchnge/shared/logging" mservice "git.sendico.io/tech/sxchnge/shared/service" ) type Registrar interface { AccountHandler(mservice.Type, string, api.HTTPMethod, sresponse.AccountHandlerFunc) } type router struct { service *identity.Service logger mlogger.Logger } func Install(reg Registrar, service *identity.Service, logger mlogger.Logger) { h := &router{service, logger.Named("identity_http")} reg.AccountHandler(mservice.Accounts, "/identity", api.Get, h.get) reg.AccountHandler(mservice.Accounts, "/identity", api.Post, h.submit) reg.AccountHandler(mservice.Accounts, "/limits", api.Get, h.limits) } func (h *router) limits(r *http.Request, a *customer.Account, _ *sresponse.TokenData) http.HandlerFunc { v, err := h.service.Limits(r.Context(), a) return h.result(mapLimits(v), err) } func (h *router) result(data any, err error) http.HandlerFunc { if err == nil { return response.Ok(h.logger, data) } if throttle, ok := merrors.ThrottleDetailsFrom(err); ok { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Retry-After", strconv.Itoa(max(1, int(throttle.RetryAfter.Seconds())))) response.TooManyRequests(h.logger, mservice.Accounts, "Wait before retrying identity submission")(w, r) } } switch { case errors.Is(err, merrors.ErrInvalidArg): return response.BadRequest(h.logger, mservice.Accounts, "invalid_request", "Check profile and use readable JPEG/PNG photos, 600×600 minimum, 10 MiB maximum each") case errors.Is(err, merrors.ErrDataConflict): return response.DataConflict(h.logger, mservice.Accounts, "Identity submission changed; refresh its status") case errors.Is(err, merrors.ErrAccessDenied): return response.AccessDenied(h.logger, mservice.Accounts, "Identity submission is unavailable") default: return response.Error(h.logger, mservice.Accounts, http.StatusServiceUnavailable, "unavailable", "Identity verification is temporarily unavailable; retry") } } func (h *router) get(r *http.Request, a *customer.Account, _ *sresponse.TokenData) http.HandlerFunc { v, err := h.service.Verification(r.Context(), a) return h.result(mapVerification(v), err) } func (h *router) submit(r *http.Request, a *customer.Account, _ *sresponse.TokenData) http.HandlerFunc { // Read multipart as a bounded stream. Never spill identity documents to disk. r.Body = http.MaxBytesReader(nil, r.Body, 2*identity.MaxPhotoBytes+16384) reader, err := r.MultipartReader() if err != nil { return h.result(nil, merrors.InvalidArgument("multipart required")) } fields := map[string][]byte{} for { part, readErr := reader.NextPart() if readErr == io.EOF { break } if readErr != nil { return h.result(nil, merrors.InvalidArgument("invalid multipart")) } name := part.FormName() limit := int64(8192) switch name { case "document", "selfie": limit = identity.MaxPhotoBytes case "profile", "idempotencyKey": default: _ = part.Close() return h.result(nil, merrors.InvalidArgument("unexpected field")) } if _, exists := fields[name]; exists { _ = part.Close() return h.result(nil, merrors.InvalidArgument("duplicate field")) } data, readErr := io.ReadAll(io.LimitReader(part, limit+1)) _ = part.Close() if readErr != nil || int64(len(data)) > limit { return h.result(nil, merrors.InvalidArgument("field too large")) } fields[name] = data } var p profileDTO if err = json.Unmarshal(fields["profile"], &p); err != nil { return h.result(nil, merrors.InvalidArgument("profile required")) } v, err := h.service.SubmitVerification(r.Context(), a, string(fields["idempotencyKey"]), p.model(), fields["document"], fields["selfie"]) return h.result(mapVerification(v), err) }