Submit customer identity proofs and show reviewed limits
This commit is contained in:
1 parent
7bf8b8087a
commit
1354551ae8
50 files changed
+1777
-67
No files matched your search
@@ -0,0 +1,107 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user