108 lines
4.1 KiB
Go
108 lines
4.1 KiB
Go
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)
|
||
}
|