Merge pull request 'Thumbnail Fixes + Bypass Submit Button' (#161) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #161
This commit was merged in pull request #161.
This commit is contained in:
@@ -29,7 +29,7 @@ Prerequisite: bun installed
|
||||
The environment variables `API_HOST` and `AUTH_HOST` will need to be set for the middleware.
|
||||
Example `.env` in web's root:
|
||||
```
|
||||
API_HOST="http://localhost:8082/v1/"
|
||||
API_HOST="http://localhost:8082/"
|
||||
AUTH_HOST="http://localhost:8083/"
|
||||
```
|
||||
|
||||
|
||||
34
openapi.yaml
34
openapi.yaml
@@ -384,6 +384,23 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/mapfixes/{MapfixID}/status/bypass-submit:
|
||||
post:
|
||||
summary: Role Reviewer changes status from ChangesRequested -> Submitted
|
||||
operationId: actionMapfixBypassSubmit
|
||||
tags:
|
||||
- Mapfixes
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/MapfixID'
|
||||
responses:
|
||||
"204":
|
||||
description: Successful response
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/mapfixes/{MapfixID}/status/reset-submitting:
|
||||
post:
|
||||
summary: Role Submitter manually resets submitting softlock and changes status from Submitting -> UnderConstruction
|
||||
@@ -816,6 +833,23 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/submissions/{SubmissionID}/status/bypass-submit:
|
||||
post:
|
||||
summary: Role Reviewer changes status from ChangesRequested -> Submitted
|
||||
operationId: actionSubmissionBypassSubmit
|
||||
tags:
|
||||
- Submissions
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/SubmissionID'
|
||||
responses:
|
||||
"204":
|
||||
description: Successful response
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/submissions/{SubmissionID}/status/reset-submitting:
|
||||
post:
|
||||
summary: Role Submitter manually resets submitting softlock and changes status from Submitting -> UnderConstruction
|
||||
|
||||
@@ -35,6 +35,12 @@ type Invoker interface {
|
||||
//
|
||||
// POST /mapfixes/{MapfixID}/status/reset-validating
|
||||
ActionMapfixAccepted(ctx context.Context, params ActionMapfixAcceptedParams) error
|
||||
// ActionMapfixBypassSubmit invokes actionMapfixBypassSubmit operation.
|
||||
//
|
||||
// Role Reviewer changes status from ChangesRequested -> Submitted.
|
||||
//
|
||||
// POST /mapfixes/{MapfixID}/status/bypass-submit
|
||||
ActionMapfixBypassSubmit(ctx context.Context, params ActionMapfixBypassSubmitParams) error
|
||||
// ActionMapfixReject invokes actionMapfixReject operation.
|
||||
//
|
||||
// Role Reviewer changes status from Submitted -> Rejected.
|
||||
@@ -96,6 +102,12 @@ type Invoker interface {
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/status/reset-validating
|
||||
ActionSubmissionAccepted(ctx context.Context, params ActionSubmissionAcceptedParams) error
|
||||
// ActionSubmissionBypassSubmit invokes actionSubmissionBypassSubmit operation.
|
||||
//
|
||||
// Role Reviewer changes status from ChangesRequested -> Submitted.
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/status/bypass-submit
|
||||
ActionSubmissionBypassSubmit(ctx context.Context, params ActionSubmissionBypassSubmitParams) error
|
||||
// ActionSubmissionReject invokes actionSubmissionReject operation.
|
||||
//
|
||||
// Role Reviewer changes status from Submitted -> Rejected.
|
||||
@@ -518,6 +530,130 @@ func (c *Client) sendActionMapfixAccepted(ctx context.Context, params ActionMapf
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ActionMapfixBypassSubmit invokes actionMapfixBypassSubmit operation.
|
||||
//
|
||||
// Role Reviewer changes status from ChangesRequested -> Submitted.
|
||||
//
|
||||
// POST /mapfixes/{MapfixID}/status/bypass-submit
|
||||
func (c *Client) ActionMapfixBypassSubmit(ctx context.Context, params ActionMapfixBypassSubmitParams) error {
|
||||
_, err := c.sendActionMapfixBypassSubmit(ctx, params)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) sendActionMapfixBypassSubmit(ctx context.Context, params ActionMapfixBypassSubmitParams) (res *ActionMapfixBypassSubmitNoContent, err error) {
|
||||
otelAttrs := []attribute.KeyValue{
|
||||
otelogen.OperationID("actionMapfixBypassSubmit"),
|
||||
semconv.HTTPRequestMethodKey.String("POST"),
|
||||
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/bypass-submit"),
|
||||
}
|
||||
|
||||
// Run stopwatch.
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
// Use floating point division here for higher precision (instead of Millisecond method).
|
||||
elapsedDuration := time.Since(startTime)
|
||||
c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...))
|
||||
}()
|
||||
|
||||
// Increment request counter.
|
||||
c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
|
||||
|
||||
// Start a span for this request.
|
||||
ctx, span := c.cfg.Tracer.Start(ctx, ActionMapfixBypassSubmitOperation,
|
||||
trace.WithAttributes(otelAttrs...),
|
||||
clientSpanKind,
|
||||
)
|
||||
// Track stage for error reporting.
|
||||
var stage string
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, stage)
|
||||
c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
stage = "BuildURL"
|
||||
u := uri.Clone(c.requestURL(ctx))
|
||||
var pathParts [3]string
|
||||
pathParts[0] = "/mapfixes/"
|
||||
{
|
||||
// Encode "MapfixID" parameter.
|
||||
e := uri.NewPathEncoder(uri.PathEncoderConfig{
|
||||
Param: "MapfixID",
|
||||
Style: uri.PathStyleSimple,
|
||||
Explode: false,
|
||||
})
|
||||
if err := func() error {
|
||||
return e.EncodeValue(conv.Int64ToString(params.MapfixID))
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "encode path")
|
||||
}
|
||||
encoded, err := e.Result()
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "encode path")
|
||||
}
|
||||
pathParts[1] = encoded
|
||||
}
|
||||
pathParts[2] = "/status/bypass-submit"
|
||||
uri.AddPathParts(u, pathParts[:]...)
|
||||
|
||||
stage = "EncodeRequest"
|
||||
r, err := ht.NewRequest(ctx, "POST", u)
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "create request")
|
||||
}
|
||||
|
||||
{
|
||||
type bitset = [1]uint8
|
||||
var satisfied bitset
|
||||
{
|
||||
stage = "Security:CookieAuth"
|
||||
switch err := c.securityCookieAuth(ctx, ActionMapfixBypassSubmitOperation, r); {
|
||||
case err == nil: // if NO error
|
||||
satisfied[0] |= 1 << 0
|
||||
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
|
||||
// Skip this security.
|
||||
default:
|
||||
return res, errors.Wrap(err, "security \"CookieAuth\"")
|
||||
}
|
||||
}
|
||||
|
||||
if ok := func() bool {
|
||||
nextRequirement:
|
||||
for _, requirement := range []bitset{
|
||||
{0b00000001},
|
||||
} {
|
||||
for i, mask := range requirement {
|
||||
if satisfied[i]&mask != mask {
|
||||
continue nextRequirement
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}(); !ok {
|
||||
return res, ogenerrors.ErrSecurityRequirementIsNotSatisfied
|
||||
}
|
||||
}
|
||||
|
||||
stage = "SendRequest"
|
||||
resp, err := c.cfg.Client.Do(r)
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "do request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
stage = "DecodeResponse"
|
||||
result, err := decodeActionMapfixBypassSubmitResponse(resp)
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "decode response")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ActionMapfixReject invokes actionMapfixReject operation.
|
||||
//
|
||||
// Role Reviewer changes status from Submitted -> Rejected.
|
||||
@@ -1759,6 +1895,130 @@ func (c *Client) sendActionSubmissionAccepted(ctx context.Context, params Action
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ActionSubmissionBypassSubmit invokes actionSubmissionBypassSubmit operation.
|
||||
//
|
||||
// Role Reviewer changes status from ChangesRequested -> Submitted.
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/status/bypass-submit
|
||||
func (c *Client) ActionSubmissionBypassSubmit(ctx context.Context, params ActionSubmissionBypassSubmitParams) error {
|
||||
_, err := c.sendActionSubmissionBypassSubmit(ctx, params)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) sendActionSubmissionBypassSubmit(ctx context.Context, params ActionSubmissionBypassSubmitParams) (res *ActionSubmissionBypassSubmitNoContent, err error) {
|
||||
otelAttrs := []attribute.KeyValue{
|
||||
otelogen.OperationID("actionSubmissionBypassSubmit"),
|
||||
semconv.HTTPRequestMethodKey.String("POST"),
|
||||
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/bypass-submit"),
|
||||
}
|
||||
|
||||
// Run stopwatch.
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
// Use floating point division here for higher precision (instead of Millisecond method).
|
||||
elapsedDuration := time.Since(startTime)
|
||||
c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...))
|
||||
}()
|
||||
|
||||
// Increment request counter.
|
||||
c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
|
||||
|
||||
// Start a span for this request.
|
||||
ctx, span := c.cfg.Tracer.Start(ctx, ActionSubmissionBypassSubmitOperation,
|
||||
trace.WithAttributes(otelAttrs...),
|
||||
clientSpanKind,
|
||||
)
|
||||
// Track stage for error reporting.
|
||||
var stage string
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, stage)
|
||||
c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
stage = "BuildURL"
|
||||
u := uri.Clone(c.requestURL(ctx))
|
||||
var pathParts [3]string
|
||||
pathParts[0] = "/submissions/"
|
||||
{
|
||||
// Encode "SubmissionID" parameter.
|
||||
e := uri.NewPathEncoder(uri.PathEncoderConfig{
|
||||
Param: "SubmissionID",
|
||||
Style: uri.PathStyleSimple,
|
||||
Explode: false,
|
||||
})
|
||||
if err := func() error {
|
||||
return e.EncodeValue(conv.Int64ToString(params.SubmissionID))
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "encode path")
|
||||
}
|
||||
encoded, err := e.Result()
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "encode path")
|
||||
}
|
||||
pathParts[1] = encoded
|
||||
}
|
||||
pathParts[2] = "/status/bypass-submit"
|
||||
uri.AddPathParts(u, pathParts[:]...)
|
||||
|
||||
stage = "EncodeRequest"
|
||||
r, err := ht.NewRequest(ctx, "POST", u)
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "create request")
|
||||
}
|
||||
|
||||
{
|
||||
type bitset = [1]uint8
|
||||
var satisfied bitset
|
||||
{
|
||||
stage = "Security:CookieAuth"
|
||||
switch err := c.securityCookieAuth(ctx, ActionSubmissionBypassSubmitOperation, r); {
|
||||
case err == nil: // if NO error
|
||||
satisfied[0] |= 1 << 0
|
||||
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
|
||||
// Skip this security.
|
||||
default:
|
||||
return res, errors.Wrap(err, "security \"CookieAuth\"")
|
||||
}
|
||||
}
|
||||
|
||||
if ok := func() bool {
|
||||
nextRequirement:
|
||||
for _, requirement := range []bitset{
|
||||
{0b00000001},
|
||||
} {
|
||||
for i, mask := range requirement {
|
||||
if satisfied[i]&mask != mask {
|
||||
continue nextRequirement
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}(); !ok {
|
||||
return res, ogenerrors.ErrSecurityRequirementIsNotSatisfied
|
||||
}
|
||||
}
|
||||
|
||||
stage = "SendRequest"
|
||||
resp, err := c.cfg.Client.Do(r)
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "do request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
stage = "DecodeResponse"
|
||||
result, err := decodeActionSubmissionBypassSubmitResponse(resp)
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "decode response")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ActionSubmissionReject invokes actionSubmissionReject operation.
|
||||
//
|
||||
// Role Reviewer changes status from Submitted -> Rejected.
|
||||
|
||||
@@ -225,6 +225,201 @@ func (s *Server) handleActionMapfixAcceptedRequest(args [1]string, argsEscaped b
|
||||
}
|
||||
}
|
||||
|
||||
// handleActionMapfixBypassSubmitRequest handles actionMapfixBypassSubmit operation.
|
||||
//
|
||||
// Role Reviewer changes status from ChangesRequested -> Submitted.
|
||||
//
|
||||
// POST /mapfixes/{MapfixID}/status/bypass-submit
|
||||
func (s *Server) handleActionMapfixBypassSubmitRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) {
|
||||
statusWriter := &codeRecorder{ResponseWriter: w}
|
||||
w = statusWriter
|
||||
otelAttrs := []attribute.KeyValue{
|
||||
otelogen.OperationID("actionMapfixBypassSubmit"),
|
||||
semconv.HTTPRequestMethodKey.String("POST"),
|
||||
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/bypass-submit"),
|
||||
}
|
||||
|
||||
// Start a span for this request.
|
||||
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixBypassSubmitOperation,
|
||||
trace.WithAttributes(otelAttrs...),
|
||||
serverSpanKind,
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
// Add Labeler to context.
|
||||
labeler := &Labeler{attrs: otelAttrs}
|
||||
ctx = contextWithLabeler(ctx, labeler)
|
||||
|
||||
// Run stopwatch.
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
elapsedDuration := time.Since(startTime)
|
||||
|
||||
attrSet := labeler.AttributeSet()
|
||||
attrs := attrSet.ToSlice()
|
||||
code := statusWriter.status
|
||||
if code != 0 {
|
||||
codeAttr := semconv.HTTPResponseStatusCode(code)
|
||||
attrs = append(attrs, codeAttr)
|
||||
span.SetAttributes(codeAttr)
|
||||
}
|
||||
attrOpt := metric.WithAttributes(attrs...)
|
||||
|
||||
// Increment request counter.
|
||||
s.requests.Add(ctx, 1, attrOpt)
|
||||
|
||||
// Use floating point division here for higher precision (instead of Millisecond method).
|
||||
s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt)
|
||||
}()
|
||||
|
||||
var (
|
||||
recordError = func(stage string, err error) {
|
||||
span.RecordError(err)
|
||||
|
||||
// https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status
|
||||
// Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges,
|
||||
// unless there was another error (e.g., network error receiving the response body; or 3xx codes with
|
||||
// max redirects exceeded), in which case status MUST be set to Error.
|
||||
code := statusWriter.status
|
||||
if code >= 100 && code < 500 {
|
||||
span.SetStatus(codes.Error, stage)
|
||||
}
|
||||
|
||||
attrSet := labeler.AttributeSet()
|
||||
attrs := attrSet.ToSlice()
|
||||
if code != 0 {
|
||||
attrs = append(attrs, semconv.HTTPResponseStatusCode(code))
|
||||
}
|
||||
|
||||
s.errors.Add(ctx, 1, metric.WithAttributes(attrs...))
|
||||
}
|
||||
err error
|
||||
opErrContext = ogenerrors.OperationContext{
|
||||
Name: ActionMapfixBypassSubmitOperation,
|
||||
ID: "actionMapfixBypassSubmit",
|
||||
}
|
||||
)
|
||||
{
|
||||
type bitset = [1]uint8
|
||||
var satisfied bitset
|
||||
{
|
||||
sctx, ok, err := s.securityCookieAuth(ctx, ActionMapfixBypassSubmitOperation, r)
|
||||
if err != nil {
|
||||
err = &ogenerrors.SecurityError{
|
||||
OperationContext: opErrContext,
|
||||
Security: "CookieAuth",
|
||||
Err: err,
|
||||
}
|
||||
if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil {
|
||||
defer recordError("Security:CookieAuth", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
satisfied[0] |= 1 << 0
|
||||
ctx = sctx
|
||||
}
|
||||
}
|
||||
|
||||
if ok := func() bool {
|
||||
nextRequirement:
|
||||
for _, requirement := range []bitset{
|
||||
{0b00000001},
|
||||
} {
|
||||
for i, mask := range requirement {
|
||||
if satisfied[i]&mask != mask {
|
||||
continue nextRequirement
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}(); !ok {
|
||||
err = &ogenerrors.SecurityError{
|
||||
OperationContext: opErrContext,
|
||||
Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied,
|
||||
}
|
||||
if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil {
|
||||
defer recordError("Security", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
params, err := decodeActionMapfixBypassSubmitParams(args, argsEscaped, r)
|
||||
if err != nil {
|
||||
err = &ogenerrors.DecodeParamsError{
|
||||
OperationContext: opErrContext,
|
||||
Err: err,
|
||||
}
|
||||
defer recordError("DecodeParams", err)
|
||||
s.cfg.ErrorHandler(ctx, w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var response *ActionMapfixBypassSubmitNoContent
|
||||
if m := s.cfg.Middleware; m != nil {
|
||||
mreq := middleware.Request{
|
||||
Context: ctx,
|
||||
OperationName: ActionMapfixBypassSubmitOperation,
|
||||
OperationSummary: "Role Reviewer changes status from ChangesRequested -> Submitted",
|
||||
OperationID: "actionMapfixBypassSubmit",
|
||||
Body: nil,
|
||||
Params: middleware.Parameters{
|
||||
{
|
||||
Name: "MapfixID",
|
||||
In: "path",
|
||||
}: params.MapfixID,
|
||||
},
|
||||
Raw: r,
|
||||
}
|
||||
|
||||
type (
|
||||
Request = struct{}
|
||||
Params = ActionMapfixBypassSubmitParams
|
||||
Response = *ActionMapfixBypassSubmitNoContent
|
||||
)
|
||||
response, err = middleware.HookMiddleware[
|
||||
Request,
|
||||
Params,
|
||||
Response,
|
||||
](
|
||||
m,
|
||||
mreq,
|
||||
unpackActionMapfixBypassSubmitParams,
|
||||
func(ctx context.Context, request Request, params Params) (response Response, err error) {
|
||||
err = s.h.ActionMapfixBypassSubmit(ctx, params)
|
||||
return response, err
|
||||
},
|
||||
)
|
||||
} else {
|
||||
err = s.h.ActionMapfixBypassSubmit(ctx, params)
|
||||
}
|
||||
if err != nil {
|
||||
if errRes, ok := errors.Into[*ErrorStatusCode](err); ok {
|
||||
if err := encodeErrorResponse(errRes, w, span); err != nil {
|
||||
defer recordError("Internal", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ht.ErrNotImplemented) {
|
||||
s.cfg.ErrorHandler(ctx, w, r, err)
|
||||
return
|
||||
}
|
||||
if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil {
|
||||
defer recordError("Internal", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := encodeActionMapfixBypassSubmitResponse(response, w, span); err != nil {
|
||||
defer recordError("EncodeResponse", err)
|
||||
if !errors.Is(err, ht.ErrInternalServerErrorResponse) {
|
||||
s.cfg.ErrorHandler(ctx, w, r, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleActionMapfixRejectRequest handles actionMapfixReject operation.
|
||||
//
|
||||
// Role Reviewer changes status from Submitted -> Rejected.
|
||||
@@ -2176,6 +2371,201 @@ func (s *Server) handleActionSubmissionAcceptedRequest(args [1]string, argsEscap
|
||||
}
|
||||
}
|
||||
|
||||
// handleActionSubmissionBypassSubmitRequest handles actionSubmissionBypassSubmit operation.
|
||||
//
|
||||
// Role Reviewer changes status from ChangesRequested -> Submitted.
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/status/bypass-submit
|
||||
func (s *Server) handleActionSubmissionBypassSubmitRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) {
|
||||
statusWriter := &codeRecorder{ResponseWriter: w}
|
||||
w = statusWriter
|
||||
otelAttrs := []attribute.KeyValue{
|
||||
otelogen.OperationID("actionSubmissionBypassSubmit"),
|
||||
semconv.HTTPRequestMethodKey.String("POST"),
|
||||
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/bypass-submit"),
|
||||
}
|
||||
|
||||
// Start a span for this request.
|
||||
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionSubmissionBypassSubmitOperation,
|
||||
trace.WithAttributes(otelAttrs...),
|
||||
serverSpanKind,
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
// Add Labeler to context.
|
||||
labeler := &Labeler{attrs: otelAttrs}
|
||||
ctx = contextWithLabeler(ctx, labeler)
|
||||
|
||||
// Run stopwatch.
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
elapsedDuration := time.Since(startTime)
|
||||
|
||||
attrSet := labeler.AttributeSet()
|
||||
attrs := attrSet.ToSlice()
|
||||
code := statusWriter.status
|
||||
if code != 0 {
|
||||
codeAttr := semconv.HTTPResponseStatusCode(code)
|
||||
attrs = append(attrs, codeAttr)
|
||||
span.SetAttributes(codeAttr)
|
||||
}
|
||||
attrOpt := metric.WithAttributes(attrs...)
|
||||
|
||||
// Increment request counter.
|
||||
s.requests.Add(ctx, 1, attrOpt)
|
||||
|
||||
// Use floating point division here for higher precision (instead of Millisecond method).
|
||||
s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt)
|
||||
}()
|
||||
|
||||
var (
|
||||
recordError = func(stage string, err error) {
|
||||
span.RecordError(err)
|
||||
|
||||
// https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status
|
||||
// Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges,
|
||||
// unless there was another error (e.g., network error receiving the response body; or 3xx codes with
|
||||
// max redirects exceeded), in which case status MUST be set to Error.
|
||||
code := statusWriter.status
|
||||
if code >= 100 && code < 500 {
|
||||
span.SetStatus(codes.Error, stage)
|
||||
}
|
||||
|
||||
attrSet := labeler.AttributeSet()
|
||||
attrs := attrSet.ToSlice()
|
||||
if code != 0 {
|
||||
attrs = append(attrs, semconv.HTTPResponseStatusCode(code))
|
||||
}
|
||||
|
||||
s.errors.Add(ctx, 1, metric.WithAttributes(attrs...))
|
||||
}
|
||||
err error
|
||||
opErrContext = ogenerrors.OperationContext{
|
||||
Name: ActionSubmissionBypassSubmitOperation,
|
||||
ID: "actionSubmissionBypassSubmit",
|
||||
}
|
||||
)
|
||||
{
|
||||
type bitset = [1]uint8
|
||||
var satisfied bitset
|
||||
{
|
||||
sctx, ok, err := s.securityCookieAuth(ctx, ActionSubmissionBypassSubmitOperation, r)
|
||||
if err != nil {
|
||||
err = &ogenerrors.SecurityError{
|
||||
OperationContext: opErrContext,
|
||||
Security: "CookieAuth",
|
||||
Err: err,
|
||||
}
|
||||
if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil {
|
||||
defer recordError("Security:CookieAuth", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
satisfied[0] |= 1 << 0
|
||||
ctx = sctx
|
||||
}
|
||||
}
|
||||
|
||||
if ok := func() bool {
|
||||
nextRequirement:
|
||||
for _, requirement := range []bitset{
|
||||
{0b00000001},
|
||||
} {
|
||||
for i, mask := range requirement {
|
||||
if satisfied[i]&mask != mask {
|
||||
continue nextRequirement
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}(); !ok {
|
||||
err = &ogenerrors.SecurityError{
|
||||
OperationContext: opErrContext,
|
||||
Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied,
|
||||
}
|
||||
if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil {
|
||||
defer recordError("Security", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
params, err := decodeActionSubmissionBypassSubmitParams(args, argsEscaped, r)
|
||||
if err != nil {
|
||||
err = &ogenerrors.DecodeParamsError{
|
||||
OperationContext: opErrContext,
|
||||
Err: err,
|
||||
}
|
||||
defer recordError("DecodeParams", err)
|
||||
s.cfg.ErrorHandler(ctx, w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var response *ActionSubmissionBypassSubmitNoContent
|
||||
if m := s.cfg.Middleware; m != nil {
|
||||
mreq := middleware.Request{
|
||||
Context: ctx,
|
||||
OperationName: ActionSubmissionBypassSubmitOperation,
|
||||
OperationSummary: "Role Reviewer changes status from ChangesRequested -> Submitted",
|
||||
OperationID: "actionSubmissionBypassSubmit",
|
||||
Body: nil,
|
||||
Params: middleware.Parameters{
|
||||
{
|
||||
Name: "SubmissionID",
|
||||
In: "path",
|
||||
}: params.SubmissionID,
|
||||
},
|
||||
Raw: r,
|
||||
}
|
||||
|
||||
type (
|
||||
Request = struct{}
|
||||
Params = ActionSubmissionBypassSubmitParams
|
||||
Response = *ActionSubmissionBypassSubmitNoContent
|
||||
)
|
||||
response, err = middleware.HookMiddleware[
|
||||
Request,
|
||||
Params,
|
||||
Response,
|
||||
](
|
||||
m,
|
||||
mreq,
|
||||
unpackActionSubmissionBypassSubmitParams,
|
||||
func(ctx context.Context, request Request, params Params) (response Response, err error) {
|
||||
err = s.h.ActionSubmissionBypassSubmit(ctx, params)
|
||||
return response, err
|
||||
},
|
||||
)
|
||||
} else {
|
||||
err = s.h.ActionSubmissionBypassSubmit(ctx, params)
|
||||
}
|
||||
if err != nil {
|
||||
if errRes, ok := errors.Into[*ErrorStatusCode](err); ok {
|
||||
if err := encodeErrorResponse(errRes, w, span); err != nil {
|
||||
defer recordError("Internal", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ht.ErrNotImplemented) {
|
||||
s.cfg.ErrorHandler(ctx, w, r, err)
|
||||
return
|
||||
}
|
||||
if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil {
|
||||
defer recordError("Internal", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := encodeActionSubmissionBypassSubmitResponse(response, w, span); err != nil {
|
||||
defer recordError("EncodeResponse", err)
|
||||
if !errors.Is(err, ht.ErrInternalServerErrorResponse) {
|
||||
s.cfg.ErrorHandler(ctx, w, r, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleActionSubmissionRejectRequest handles actionSubmissionReject operation.
|
||||
//
|
||||
// Role Reviewer changes status from Submitted -> Rejected.
|
||||
|
||||
@@ -7,6 +7,7 @@ type OperationName = string
|
||||
|
||||
const (
|
||||
ActionMapfixAcceptedOperation OperationName = "ActionMapfixAccepted"
|
||||
ActionMapfixBypassSubmitOperation OperationName = "ActionMapfixBypassSubmit"
|
||||
ActionMapfixRejectOperation OperationName = "ActionMapfixReject"
|
||||
ActionMapfixRequestChangesOperation OperationName = "ActionMapfixRequestChanges"
|
||||
ActionMapfixResetSubmittingOperation OperationName = "ActionMapfixResetSubmitting"
|
||||
@@ -17,6 +18,7 @@ const (
|
||||
ActionMapfixTriggerValidateOperation OperationName = "ActionMapfixTriggerValidate"
|
||||
ActionMapfixValidatedOperation OperationName = "ActionMapfixValidated"
|
||||
ActionSubmissionAcceptedOperation OperationName = "ActionSubmissionAccepted"
|
||||
ActionSubmissionBypassSubmitOperation OperationName = "ActionSubmissionBypassSubmit"
|
||||
ActionSubmissionRejectOperation OperationName = "ActionSubmissionReject"
|
||||
ActionSubmissionRequestChangesOperation OperationName = "ActionSubmissionRequestChanges"
|
||||
ActionSubmissionResetSubmittingOperation OperationName = "ActionSubmissionResetSubmitting"
|
||||
|
||||
@@ -98,6 +98,89 @@ func decodeActionMapfixAcceptedParams(args [1]string, argsEscaped bool, r *http.
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// ActionMapfixBypassSubmitParams is parameters of actionMapfixBypassSubmit operation.
|
||||
type ActionMapfixBypassSubmitParams struct {
|
||||
// The unique identifier for a mapfix.
|
||||
MapfixID int64
|
||||
}
|
||||
|
||||
func unpackActionMapfixBypassSubmitParams(packed middleware.Parameters) (params ActionMapfixBypassSubmitParams) {
|
||||
{
|
||||
key := middleware.ParameterKey{
|
||||
Name: "MapfixID",
|
||||
In: "path",
|
||||
}
|
||||
params.MapfixID = packed[key].(int64)
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func decodeActionMapfixBypassSubmitParams(args [1]string, argsEscaped bool, r *http.Request) (params ActionMapfixBypassSubmitParams, _ error) {
|
||||
// Decode path: MapfixID.
|
||||
if err := func() error {
|
||||
param := args[0]
|
||||
if argsEscaped {
|
||||
unescaped, err := url.PathUnescape(args[0])
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unescape path")
|
||||
}
|
||||
param = unescaped
|
||||
}
|
||||
if len(param) > 0 {
|
||||
d := uri.NewPathDecoder(uri.PathDecoderConfig{
|
||||
Param: "MapfixID",
|
||||
Value: param,
|
||||
Style: uri.PathStyleSimple,
|
||||
Explode: false,
|
||||
})
|
||||
|
||||
if err := func() error {
|
||||
val, err := d.DecodeValue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := conv.ToInt64(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params.MapfixID = c
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 0,
|
||||
MaxSet: false,
|
||||
Max: 0,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
}).Validate(int64(params.MapfixID)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return validate.ErrFieldRequired
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return params, &ogenerrors.DecodeParamError{
|
||||
Name: "MapfixID",
|
||||
In: "path",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// ActionMapfixRejectParams is parameters of actionMapfixReject operation.
|
||||
type ActionMapfixRejectParams struct {
|
||||
// The unique identifier for a mapfix.
|
||||
@@ -928,6 +1011,89 @@ func decodeActionSubmissionAcceptedParams(args [1]string, argsEscaped bool, r *h
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// ActionSubmissionBypassSubmitParams is parameters of actionSubmissionBypassSubmit operation.
|
||||
type ActionSubmissionBypassSubmitParams struct {
|
||||
// The unique identifier for a submission.
|
||||
SubmissionID int64
|
||||
}
|
||||
|
||||
func unpackActionSubmissionBypassSubmitParams(packed middleware.Parameters) (params ActionSubmissionBypassSubmitParams) {
|
||||
{
|
||||
key := middleware.ParameterKey{
|
||||
Name: "SubmissionID",
|
||||
In: "path",
|
||||
}
|
||||
params.SubmissionID = packed[key].(int64)
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func decodeActionSubmissionBypassSubmitParams(args [1]string, argsEscaped bool, r *http.Request) (params ActionSubmissionBypassSubmitParams, _ error) {
|
||||
// Decode path: SubmissionID.
|
||||
if err := func() error {
|
||||
param := args[0]
|
||||
if argsEscaped {
|
||||
unescaped, err := url.PathUnescape(args[0])
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unescape path")
|
||||
}
|
||||
param = unescaped
|
||||
}
|
||||
if len(param) > 0 {
|
||||
d := uri.NewPathDecoder(uri.PathDecoderConfig{
|
||||
Param: "SubmissionID",
|
||||
Value: param,
|
||||
Style: uri.PathStyleSimple,
|
||||
Explode: false,
|
||||
})
|
||||
|
||||
if err := func() error {
|
||||
val, err := d.DecodeValue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := conv.ToInt64(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params.SubmissionID = c
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 0,
|
||||
MaxSet: false,
|
||||
Max: 0,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
}).Validate(int64(params.SubmissionID)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return validate.ErrFieldRequired
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return params, &ogenerrors.DecodeParamError{
|
||||
Name: "SubmissionID",
|
||||
In: "path",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// ActionSubmissionRejectParams is parameters of actionSubmissionReject operation.
|
||||
type ActionSubmissionRejectParams struct {
|
||||
// The unique identifier for a submission.
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
"github.com/go-faster/jx"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
"github.com/ogen-go/ogen/ogenerrors"
|
||||
"github.com/ogen-go/ogen/validate"
|
||||
@@ -27,13 +26,13 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = multierr.Append(merr, c())
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = multierr.Append(rerr, close())
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
@@ -98,13 +97,13 @@ func (s *Server) decodeCreateMapfixAuditCommentRequest(r *http.Request) (
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = multierr.Append(merr, c())
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = multierr.Append(rerr, close())
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
@@ -132,13 +131,13 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = multierr.Append(merr, c())
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = multierr.Append(rerr, close())
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
@@ -203,13 +202,13 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = multierr.Append(merr, c())
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = multierr.Append(rerr, close())
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
@@ -274,13 +273,13 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = multierr.Append(merr, c())
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = multierr.Append(rerr, close())
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
@@ -345,13 +344,13 @@ func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = multierr.Append(merr, c())
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = multierr.Append(rerr, close())
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
@@ -416,13 +415,13 @@ func (s *Server) decodeCreateSubmissionAuditCommentRequest(r *http.Request) (
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = multierr.Append(merr, c())
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = multierr.Append(rerr, close())
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
@@ -450,13 +449,13 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = multierr.Append(merr, c())
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = multierr.Append(rerr, close())
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
@@ -554,13 +553,13 @@ func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = multierr.Append(merr, c())
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = multierr.Append(rerr, close())
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
@@ -625,13 +624,13 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = multierr.Append(merr, c())
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = multierr.Append(rerr, close())
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
|
||||
@@ -75,6 +75,66 @@ func decodeActionMapfixAcceptedResponse(resp *http.Response) (res *ActionMapfixA
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeActionMapfixBypassSubmitResponse(resp *http.Response) (res *ActionMapfixBypassSubmitNoContent, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 204:
|
||||
// Code 204.
|
||||
return &ActionMapfixBypassSubmitNoContent{}, nil
|
||||
}
|
||||
// Convenient error response.
|
||||
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response Error
|
||||
if err := func() error {
|
||||
if err := response.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if err := response.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &ErrorStatusCode{
|
||||
StatusCode: resp.StatusCode,
|
||||
Response: response,
|
||||
}, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||
}
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeActionMapfixRejectResponse(resp *http.Response) (res *ActionMapfixRejectNoContent, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 204:
|
||||
@@ -675,6 +735,66 @@ func decodeActionSubmissionAcceptedResponse(resp *http.Response) (res *ActionSub
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeActionSubmissionBypassSubmitResponse(resp *http.Response) (res *ActionSubmissionBypassSubmitNoContent, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 204:
|
||||
// Code 204.
|
||||
return &ActionSubmissionBypassSubmitNoContent{}, nil
|
||||
}
|
||||
// Convenient error response.
|
||||
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response Error
|
||||
if err := func() error {
|
||||
if err := response.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if err := response.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &ErrorStatusCode{
|
||||
StatusCode: resp.StatusCode,
|
||||
Response: response,
|
||||
}, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||
}
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeActionSubmissionRejectResponse(resp *http.Response) (res *ActionSubmissionRejectNoContent, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 204:
|
||||
|
||||
@@ -20,6 +20,13 @@ func encodeActionMapfixAcceptedResponse(response *ActionMapfixAcceptedNoContent,
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeActionMapfixBypassSubmitResponse(response *ActionMapfixBypassSubmitNoContent, w http.ResponseWriter, span trace.Span) error {
|
||||
w.WriteHeader(204)
|
||||
span.SetStatus(codes.Ok, http.StatusText(204))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeActionMapfixRejectResponse(response *ActionMapfixRejectNoContent, w http.ResponseWriter, span trace.Span) error {
|
||||
w.WriteHeader(204)
|
||||
span.SetStatus(codes.Ok, http.StatusText(204))
|
||||
@@ -90,6 +97,13 @@ func encodeActionSubmissionAcceptedResponse(response *ActionSubmissionAcceptedNo
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeActionSubmissionBypassSubmitResponse(response *ActionSubmissionBypassSubmitNoContent, w http.ResponseWriter, span trace.Span) error {
|
||||
w.WriteHeader(204)
|
||||
span.SetStatus(codes.Ok, http.StatusText(204))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeActionSubmissionRejectResponse(response *ActionSubmissionRejectNoContent, w http.ResponseWriter, span trace.Span) error {
|
||||
w.WriteHeader(204)
|
||||
span.SetStatus(codes.Ok, http.StatusText(204))
|
||||
|
||||
@@ -250,6 +250,28 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
break
|
||||
}
|
||||
switch elem[0] {
|
||||
case 'b': // Prefix: "bypass-submit"
|
||||
|
||||
if l := len("bypass-submit"); len(elem) >= l && elem[0:l] == "bypass-submit" {
|
||||
elem = elem[l:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
if len(elem) == 0 {
|
||||
// Leaf node.
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
s.handleActionMapfixBypassSubmitRequest([1]string{
|
||||
args[0],
|
||||
}, elemIsEscaped, w, r)
|
||||
default:
|
||||
s.notAllowed(w, r, "POST")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
case 'r': // Prefix: "re"
|
||||
|
||||
if l := len("re"); len(elem) >= l && elem[0:l] == "re" {
|
||||
@@ -1046,6 +1068,28 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
break
|
||||
}
|
||||
switch elem[0] {
|
||||
case 'b': // Prefix: "bypass-submit"
|
||||
|
||||
if l := len("bypass-submit"); len(elem) >= l && elem[0:l] == "bypass-submit" {
|
||||
elem = elem[l:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
if len(elem) == 0 {
|
||||
// Leaf node.
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
s.handleActionSubmissionBypassSubmitRequest([1]string{
|
||||
args[0],
|
||||
}, elemIsEscaped, w, r)
|
||||
default:
|
||||
s.notAllowed(w, r, "POST")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
case 'r': // Prefix: "re"
|
||||
|
||||
if l := len("re"); len(elem) >= l && elem[0:l] == "re" {
|
||||
@@ -1621,6 +1665,30 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
break
|
||||
}
|
||||
switch elem[0] {
|
||||
case 'b': // Prefix: "bypass-submit"
|
||||
|
||||
if l := len("bypass-submit"); len(elem) >= l && elem[0:l] == "bypass-submit" {
|
||||
elem = elem[l:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
if len(elem) == 0 {
|
||||
// Leaf node.
|
||||
switch method {
|
||||
case "POST":
|
||||
r.name = ActionMapfixBypassSubmitOperation
|
||||
r.summary = "Role Reviewer changes status from ChangesRequested -> Submitted"
|
||||
r.operationID = "actionMapfixBypassSubmit"
|
||||
r.pathPattern = "/mapfixes/{MapfixID}/status/bypass-submit"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
return r, true
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
case 'r': // Prefix: "re"
|
||||
|
||||
if l := len("re"); len(elem) >= l && elem[0:l] == "re" {
|
||||
@@ -2525,6 +2593,30 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
||||
break
|
||||
}
|
||||
switch elem[0] {
|
||||
case 'b': // Prefix: "bypass-submit"
|
||||
|
||||
if l := len("bypass-submit"); len(elem) >= l && elem[0:l] == "bypass-submit" {
|
||||
elem = elem[l:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
if len(elem) == 0 {
|
||||
// Leaf node.
|
||||
switch method {
|
||||
case "POST":
|
||||
r.name = ActionSubmissionBypassSubmitOperation
|
||||
r.summary = "Role Reviewer changes status from ChangesRequested -> Submitted"
|
||||
r.operationID = "actionSubmissionBypassSubmit"
|
||||
r.pathPattern = "/submissions/{SubmissionID}/status/bypass-submit"
|
||||
r.args = args
|
||||
r.count = 1
|
||||
return r, true
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
case 'r': // Prefix: "re"
|
||||
|
||||
if l := len("re"); len(elem) >= l && elem[0:l] == "re" {
|
||||
|
||||
@@ -17,6 +17,9 @@ func (s *ErrorStatusCode) Error() string {
|
||||
// ActionMapfixAcceptedNoContent is response for ActionMapfixAccepted operation.
|
||||
type ActionMapfixAcceptedNoContent struct{}
|
||||
|
||||
// ActionMapfixBypassSubmitNoContent is response for ActionMapfixBypassSubmit operation.
|
||||
type ActionMapfixBypassSubmitNoContent struct{}
|
||||
|
||||
// ActionMapfixRejectNoContent is response for ActionMapfixReject operation.
|
||||
type ActionMapfixRejectNoContent struct{}
|
||||
|
||||
@@ -47,6 +50,9 @@ type ActionMapfixValidatedNoContent struct{}
|
||||
// ActionSubmissionAcceptedNoContent is response for ActionSubmissionAccepted operation.
|
||||
type ActionSubmissionAcceptedNoContent struct{}
|
||||
|
||||
// ActionSubmissionBypassSubmitNoContent is response for ActionSubmissionBypassSubmit operation.
|
||||
type ActionSubmissionBypassSubmitNoContent struct{}
|
||||
|
||||
// ActionSubmissionRejectNoContent is response for ActionSubmissionReject operation.
|
||||
type ActionSubmissionRejectNoContent struct{}
|
||||
|
||||
@@ -182,6 +188,7 @@ func (s *AuditEventEventData) init() AuditEventEventData {
|
||||
|
||||
type CookieAuth struct {
|
||||
APIKey string
|
||||
Roles []string
|
||||
}
|
||||
|
||||
// GetAPIKey returns the value of APIKey.
|
||||
@@ -189,11 +196,21 @@ func (s *CookieAuth) GetAPIKey() string {
|
||||
return s.APIKey
|
||||
}
|
||||
|
||||
// GetRoles returns the value of Roles.
|
||||
func (s *CookieAuth) GetRoles() []string {
|
||||
return s.Roles
|
||||
}
|
||||
|
||||
// SetAPIKey sets the value of APIKey.
|
||||
func (s *CookieAuth) SetAPIKey(val string) {
|
||||
s.APIKey = val
|
||||
}
|
||||
|
||||
// SetRoles sets the value of Roles.
|
||||
func (s *CookieAuth) SetRoles(val []string) {
|
||||
s.Roles = val
|
||||
}
|
||||
|
||||
// CreateMapfixAuditCommentNoContent is response for CreateMapfixAuditComment operation.
|
||||
type CreateMapfixAuditCommentNoContent struct{}
|
||||
|
||||
|
||||
@@ -33,6 +33,51 @@ func findAuthorization(h http.Header, prefix string) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
var operationRolesCookieAuth = map[string][]string{
|
||||
ActionMapfixAcceptedOperation: []string{},
|
||||
ActionMapfixBypassSubmitOperation: []string{},
|
||||
ActionMapfixRejectOperation: []string{},
|
||||
ActionMapfixRequestChangesOperation: []string{},
|
||||
ActionMapfixResetSubmittingOperation: []string{},
|
||||
ActionMapfixRetryValidateOperation: []string{},
|
||||
ActionMapfixRevokeOperation: []string{},
|
||||
ActionMapfixTriggerSubmitOperation: []string{},
|
||||
ActionMapfixTriggerUploadOperation: []string{},
|
||||
ActionMapfixTriggerValidateOperation: []string{},
|
||||
ActionMapfixValidatedOperation: []string{},
|
||||
ActionSubmissionAcceptedOperation: []string{},
|
||||
ActionSubmissionBypassSubmitOperation: []string{},
|
||||
ActionSubmissionRejectOperation: []string{},
|
||||
ActionSubmissionRequestChangesOperation: []string{},
|
||||
ActionSubmissionResetSubmittingOperation: []string{},
|
||||
ActionSubmissionRetryValidateOperation: []string{},
|
||||
ActionSubmissionRevokeOperation: []string{},
|
||||
ActionSubmissionTriggerSubmitOperation: []string{},
|
||||
ActionSubmissionTriggerUploadOperation: []string{},
|
||||
ActionSubmissionTriggerValidateOperation: []string{},
|
||||
ActionSubmissionValidatedOperation: []string{},
|
||||
CreateMapfixOperation: []string{},
|
||||
CreateMapfixAuditCommentOperation: []string{},
|
||||
CreateScriptOperation: []string{},
|
||||
CreateScriptPolicyOperation: []string{},
|
||||
CreateSubmissionOperation: []string{},
|
||||
CreateSubmissionAdminOperation: []string{},
|
||||
CreateSubmissionAuditCommentOperation: []string{},
|
||||
DeleteScriptOperation: []string{},
|
||||
DeleteScriptPolicyOperation: []string{},
|
||||
GetOperationOperation: []string{},
|
||||
ReleaseSubmissionsOperation: []string{},
|
||||
SessionRolesOperation: []string{},
|
||||
SessionUserOperation: []string{},
|
||||
SessionValidateOperation: []string{},
|
||||
SetMapfixCompletedOperation: []string{},
|
||||
SetSubmissionCompletedOperation: []string{},
|
||||
UpdateMapfixModelOperation: []string{},
|
||||
UpdateScriptOperation: []string{},
|
||||
UpdateScriptPolicyOperation: []string{},
|
||||
UpdateSubmissionModelOperation: []string{},
|
||||
}
|
||||
|
||||
func (s *Server) securityCookieAuth(ctx context.Context, operationName OperationName, req *http.Request) (context.Context, bool, error) {
|
||||
var t CookieAuth
|
||||
const parameterName = "session_id"
|
||||
@@ -46,6 +91,7 @@ func (s *Server) securityCookieAuth(ctx context.Context, operationName Operation
|
||||
return nil, false, errors.Wrap(err, "get cookie value")
|
||||
}
|
||||
t.APIKey = value
|
||||
t.Roles = operationRolesCookieAuth[operationName]
|
||||
rctx, err := s.sec.HandleCookieAuth(ctx, operationName, t)
|
||||
if errors.Is(err, ogenerrors.ErrSkipServerSecurity) {
|
||||
return nil, false, nil
|
||||
|
||||
@@ -14,6 +14,12 @@ type Handler interface {
|
||||
//
|
||||
// POST /mapfixes/{MapfixID}/status/reset-validating
|
||||
ActionMapfixAccepted(ctx context.Context, params ActionMapfixAcceptedParams) error
|
||||
// ActionMapfixBypassSubmit implements actionMapfixBypassSubmit operation.
|
||||
//
|
||||
// Role Reviewer changes status from ChangesRequested -> Submitted.
|
||||
//
|
||||
// POST /mapfixes/{MapfixID}/status/bypass-submit
|
||||
ActionMapfixBypassSubmit(ctx context.Context, params ActionMapfixBypassSubmitParams) error
|
||||
// ActionMapfixReject implements actionMapfixReject operation.
|
||||
//
|
||||
// Role Reviewer changes status from Submitted -> Rejected.
|
||||
@@ -75,6 +81,12 @@ type Handler interface {
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/status/reset-validating
|
||||
ActionSubmissionAccepted(ctx context.Context, params ActionSubmissionAcceptedParams) error
|
||||
// ActionSubmissionBypassSubmit implements actionSubmissionBypassSubmit operation.
|
||||
//
|
||||
// Role Reviewer changes status from ChangesRequested -> Submitted.
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/status/bypass-submit
|
||||
ActionSubmissionBypassSubmit(ctx context.Context, params ActionSubmissionBypassSubmitParams) error
|
||||
// ActionSubmissionReject implements actionSubmissionReject operation.
|
||||
//
|
||||
// Role Reviewer changes status from Submitted -> Rejected.
|
||||
|
||||
@@ -22,6 +22,15 @@ func (UnimplementedHandler) ActionMapfixAccepted(ctx context.Context, params Act
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// ActionMapfixBypassSubmit implements actionMapfixBypassSubmit operation.
|
||||
//
|
||||
// Role Reviewer changes status from ChangesRequested -> Submitted.
|
||||
//
|
||||
// POST /mapfixes/{MapfixID}/status/bypass-submit
|
||||
func (UnimplementedHandler) ActionMapfixBypassSubmit(ctx context.Context, params ActionMapfixBypassSubmitParams) error {
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// ActionMapfixReject implements actionMapfixReject operation.
|
||||
//
|
||||
// Role Reviewer changes status from Submitted -> Rejected.
|
||||
@@ -113,6 +122,15 @@ func (UnimplementedHandler) ActionSubmissionAccepted(ctx context.Context, params
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// ActionSubmissionBypassSubmit implements actionSubmissionBypassSubmit operation.
|
||||
//
|
||||
// Role Reviewer changes status from ChangesRequested -> Submitted.
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/status/bypass-submit
|
||||
func (UnimplementedHandler) ActionSubmissionBypassSubmit(ctx context.Context, params ActionSubmissionBypassSubmitParams) error {
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// ActionSubmissionReject implements actionSubmissionReject operation.
|
||||
//
|
||||
// Role Reviewer changes status from Submitted -> Rejected.
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
"github.com/go-faster/jx"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
"github.com/ogen-go/ogen/ogenerrors"
|
||||
"github.com/ogen-go/ogen/validate"
|
||||
@@ -26,13 +25,13 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = multierr.Append(merr, c())
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = multierr.Append(rerr, close())
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
@@ -97,13 +96,13 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = multierr.Append(merr, c())
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = multierr.Append(rerr, close())
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
@@ -168,13 +167,13 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = multierr.Append(merr, c())
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = multierr.Append(rerr, close())
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
@@ -239,13 +238,13 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = multierr.Append(merr, c())
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = multierr.Append(rerr, close())
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
|
||||
@@ -25,15 +25,24 @@ func (svc *Service) CreateMapfixAuditComment(ctx context.Context, req api.Create
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has_role {
|
||||
return ErrPermissionDeniedNeedRoleMapfixReview
|
||||
}
|
||||
|
||||
userId, err := userInfo.GetUserID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !has_role {
|
||||
// Submitter has special permission to comment on their mapfix
|
||||
mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if mapfix.Submitter != userId {
|
||||
return ErrPermissionDeniedNeedRoleMapfixReview
|
||||
}
|
||||
}
|
||||
|
||||
data := []byte{}
|
||||
_, err = req.Read(data)
|
||||
if err != nil {
|
||||
@@ -146,15 +155,24 @@ func (svc *Service) CreateSubmissionAuditComment(ctx context.Context, req api.Cr
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has_role {
|
||||
return ErrPermissionDeniedNeedRoleSubmissionReview
|
||||
}
|
||||
|
||||
userId, err := userInfo.GetUserID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !has_role {
|
||||
// Submitter has special permission to comment on their submission
|
||||
submission, err := svc.DB.Submissions().Get(ctx, params.SubmissionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if submission.Submitter != userId {
|
||||
return ErrPermissionDeniedNeedRoleSubmissionReview
|
||||
}
|
||||
}
|
||||
|
||||
data := []byte{}
|
||||
_, err = req.Read(data)
|
||||
if err != nil {
|
||||
|
||||
@@ -539,6 +539,76 @@ func (svc *Service) ActionMapfixTriggerSubmit(ctx context.Context, params api.Ac
|
||||
return nil
|
||||
}
|
||||
|
||||
// ActionMapfixBypassSubmit invokes actionMapfixBypassSubmit operation.
|
||||
//
|
||||
// Role Reviewer changes status from ChangesRequested -> Submitted.
|
||||
//
|
||||
// POST /mapfixes/{MapfixID}/status/bypass-submit
|
||||
func (svc *Service) ActionMapfixBypassSubmit(ctx context.Context, params api.ActionMapfixBypassSubmitParams) error {
|
||||
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
||||
if !ok {
|
||||
return ErrUserInfo
|
||||
}
|
||||
|
||||
// read mapfix (this could be done with a transaction WHERE clause)
|
||||
mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userId, err := userInfo.GetUserID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if caller is the submitter
|
||||
is_submitter := userId == mapfix.Submitter
|
||||
if is_submitter {
|
||||
return ErrAcceptOwnMapfix
|
||||
}
|
||||
|
||||
has_mapfix_review, err := userInfo.HasRoleMapfixReview()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !has_mapfix_review {
|
||||
return ErrPermissionDeniedNeedRoleMapfixReview
|
||||
}
|
||||
|
||||
// transaction
|
||||
target_status := model.MapfixStatusSubmitted
|
||||
smap := datastore.Optional()
|
||||
smap.Add("status_id", target_status)
|
||||
err = svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusChangesRequested}, smap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
event_data := model.AuditEventDataAction{
|
||||
TargetStatus: uint32(target_status),
|
||||
}
|
||||
|
||||
EventData, err := json.Marshal(event_data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = svc.DB.AuditEvents().Create(ctx, model.AuditEvent{
|
||||
ID: 0,
|
||||
User: userId,
|
||||
ResourceType: model.ResourceMapfix,
|
||||
ResourceID: params.MapfixID,
|
||||
EventType: model.AuditEventTypeAction,
|
||||
EventData: EventData,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ActionMapfixResetSubmitting implements actionMapfixResetSubmitting operation.
|
||||
//
|
||||
// Role MapfixReview changes status from Submitting -> UnderConstruction.
|
||||
|
||||
@@ -20,7 +20,7 @@ var(
|
||||
model.SubmissionStatusSubmitted,
|
||||
model.SubmissionStatusUnderConstruction,
|
||||
}
|
||||
// limit mapfixes in the pipeline to one per target map
|
||||
// limit submissions in the pipeline to one per target map
|
||||
ActiveAcceptedSubmissionStatuses = []model.SubmissionStatus{
|
||||
model.SubmissionStatusUploading,
|
||||
model.SubmissionStatusValidated,
|
||||
@@ -584,16 +584,18 @@ func (svc *Service) ActionSubmissionTriggerSubmit(ctx context.Context, params ap
|
||||
return err
|
||||
}
|
||||
|
||||
has_submission_review, err := userInfo.HasRoleSubmissionReview()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if caller is the submitter
|
||||
is_submitter := userId == submission.Submitter
|
||||
// neither = deny
|
||||
if !is_submitter && !has_submission_review {
|
||||
return ErrPermissionDeniedNotSubmitter
|
||||
if !is_submitter {
|
||||
has_submission_review, err := userInfo.HasRoleSubmissionReview()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !has_submission_review {
|
||||
return ErrPermissionDeniedNotSubmitter
|
||||
}
|
||||
}
|
||||
|
||||
// transaction
|
||||
@@ -644,6 +646,76 @@ func (svc *Service) ActionSubmissionTriggerSubmit(ctx context.Context, params ap
|
||||
return nil
|
||||
}
|
||||
|
||||
// ActionSubmissionBypassSubmit invokes actionSubmissionBypassSubmit operation.
|
||||
//
|
||||
// Role Reviewer changes status from ChangesRequested -> Submitted.
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/status/bypass-submit
|
||||
func (svc *Service) ActionSubmissionBypassSubmit(ctx context.Context, params api.ActionSubmissionBypassSubmitParams) error {
|
||||
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
||||
if !ok {
|
||||
return ErrUserInfo
|
||||
}
|
||||
|
||||
// read submission (this could be done with a transaction WHERE clause)
|
||||
submission, err := svc.DB.Submissions().Get(ctx, params.SubmissionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userId, err := userInfo.GetUserID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if caller is the submitter
|
||||
is_submitter := userId == submission.Submitter
|
||||
if is_submitter {
|
||||
return ErrAcceptOwnSubmission
|
||||
}
|
||||
|
||||
has_submission_review, err := userInfo.HasRoleSubmissionReview()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !has_submission_review {
|
||||
return ErrPermissionDeniedNeedRoleSubmissionReview
|
||||
}
|
||||
|
||||
// transaction
|
||||
target_status := model.SubmissionStatusSubmitted
|
||||
smap := datastore.Optional()
|
||||
smap.Add("status_id", target_status)
|
||||
err = svc.DB.Submissions().IfStatusThenUpdate(ctx, params.SubmissionID, []model.SubmissionStatus{model.SubmissionStatusChangesRequested}, smap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
event_data := model.AuditEventDataAction{
|
||||
TargetStatus: uint32(target_status),
|
||||
}
|
||||
|
||||
EventData, err := json.Marshal(event_data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = svc.DB.AuditEvents().Create(ctx, model.AuditEvent{
|
||||
ID: 0,
|
||||
User: userId,
|
||||
ResourceType: model.ResourceSubmission,
|
||||
ResourceID: params.SubmissionID,
|
||||
EventType: model.AuditEventTypeAction,
|
||||
EventData: EventData,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ActionSubmissionResetSubmitting implements actionSubmissionResetSubmitting operation.
|
||||
//
|
||||
// Role SubmissionReview changes status from Submitting -> UnderConstruction.
|
||||
|
||||
@@ -12,6 +12,7 @@ pub enum Error{
|
||||
Download(crate::download::Error),
|
||||
ModelFileDecode(ReadDomError),
|
||||
GetRootInstance(GetRootInstanceError),
|
||||
ToJsonValue(serde_json::Error),
|
||||
}
|
||||
impl std::fmt::Display for Error{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
@@ -47,93 +48,157 @@ impl ModeID{
|
||||
const BONUS:Self=Self(1);
|
||||
}
|
||||
enum Zone{
|
||||
Start(ModeID),
|
||||
Finish(ModeID),
|
||||
Anticheat(ModeID),
|
||||
Start,
|
||||
Finish,
|
||||
Anticheat,
|
||||
}
|
||||
struct ModeElement{
|
||||
zone:Zone,
|
||||
mode_id:ModeID,
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub enum IDParseError{
|
||||
NoCaptures,
|
||||
ParseInt(core::num::ParseIntError)
|
||||
ParseInt(core::num::ParseIntError),
|
||||
}
|
||||
// Parse a Zone from a part name
|
||||
impl std::str::FromStr for Zone{
|
||||
impl std::str::FromStr for ModeElement{
|
||||
type Err=IDParseError;
|
||||
fn from_str(s:&str)->Result<Self,Self::Err>{
|
||||
match s{
|
||||
"MapStart"=>Ok(Self::Start(ModeID::MAIN)),
|
||||
"MapFinish"=>Ok(Self::Finish(ModeID::MAIN)),
|
||||
"MapAnticheat"=>Ok(Self::Anticheat(ModeID::MAIN)),
|
||||
"BonusStart"=>Ok(Self::Start(ModeID::BONUS)),
|
||||
"BonusFinish"=>Ok(Self::Finish(ModeID::BONUS)),
|
||||
"BonusAnticheat"=>Ok(Self::Anticheat(ModeID::BONUS)),
|
||||
"MapStart"=>Ok(Self{zone:Zone::Start,mode_id:ModeID::MAIN}),
|
||||
"MapFinish"=>Ok(Self{zone:Zone::Finish,mode_id:ModeID::MAIN}),
|
||||
"MapAnticheat"=>Ok(Self{zone:Zone::Anticheat,mode_id:ModeID::MAIN}),
|
||||
"BonusStart"=>Ok(Self{zone:Zone::Start,mode_id:ModeID::BONUS}),
|
||||
"BonusFinish"=>Ok(Self{zone:Zone::Finish,mode_id:ModeID::BONUS}),
|
||||
"BonusAnticheat"=>Ok(Self{zone:Zone::Anticheat,mode_id:ModeID::BONUS}),
|
||||
other=>{
|
||||
let bonus_start_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Start$|^BonusStart(\d+)$");
|
||||
if let Some(captures)=bonus_start_pattern.captures(other){
|
||||
return Ok(Self::Start(ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?)));
|
||||
return Ok(Self{
|
||||
zone:Zone::Start,
|
||||
mode_id:ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?),
|
||||
});
|
||||
}
|
||||
let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Finish$|^BonusFinish(\d+)$");
|
||||
if let Some(captures)=bonus_finish_pattern.captures(other){
|
||||
return Ok(Self::Finish(ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?)));
|
||||
return Ok(Self{
|
||||
zone:Zone::Finish,
|
||||
mode_id:ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?),
|
||||
});
|
||||
}
|
||||
let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Anticheat$|^BonusAnticheat(\d+)$");
|
||||
if let Some(captures)=bonus_finish_pattern.captures(other){
|
||||
return Ok(Self::Anticheat(ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?)));
|
||||
return Ok(Self{
|
||||
zone:Zone::Anticheat,
|
||||
mode_id:ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?),
|
||||
});
|
||||
}
|
||||
Err(IDParseError::NoCaptures)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for ModeElement{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
match self{
|
||||
ModeElement{zone:Zone::Start,mode_id:ModeID::MAIN}=>write!(f,"MapStart"),
|
||||
ModeElement{zone:Zone::Start,mode_id:ModeID::BONUS}=>write!(f,"BonusStart"),
|
||||
ModeElement{zone:Zone::Start,mode_id:ModeID(mode_id)}=>write!(f,"Bonus{mode_id}Start"),
|
||||
ModeElement{zone:Zone::Finish,mode_id:ModeID::MAIN}=>write!(f,"MapFinish"),
|
||||
ModeElement{zone:Zone::Finish,mode_id:ModeID::BONUS}=>write!(f,"BonusFinish"),
|
||||
ModeElement{zone:Zone::Finish,mode_id:ModeID(mode_id)}=>write!(f,"Bonus{mode_id}Finish"),
|
||||
ModeElement{zone:Zone::Anticheat,mode_id:ModeID::MAIN}=>write!(f,"MapAnticheat"),
|
||||
ModeElement{zone:Zone::Anticheat,mode_id:ModeID::BONUS}=>write!(f,"BonusAnticheat"),
|
||||
ModeElement{zone:Zone::Anticheat,mode_id:ModeID(mode_id)}=>write!(f,"Bonus{mode_id}Anticheat"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
|
||||
struct SpawnID(u64);
|
||||
impl SpawnID{
|
||||
struct StageID(u64);
|
||||
impl StageID{
|
||||
const FIRST:Self=Self(1);
|
||||
}
|
||||
enum SpawnTeleport{
|
||||
Teleport(SpawnID),
|
||||
Spawn(SpawnID),
|
||||
enum StageElementBehaviour{
|
||||
Teleport,
|
||||
Spawn,
|
||||
}
|
||||
struct StageElement{
|
||||
stage_id:StageID,
|
||||
behaviour:StageElementBehaviour,
|
||||
}
|
||||
// Parse a SpawnTeleport from a part name
|
||||
impl std::str::FromStr for SpawnTeleport{
|
||||
impl std::str::FromStr for StageElement{
|
||||
type Err=IDParseError;
|
||||
fn from_str(s:&str)->Result<Self,Self::Err>{
|
||||
// Trigger ForceTrigger Teleport ForceTeleport SpawnAt ForceSpawnAt
|
||||
let bonus_start_pattern=lazy_regex::lazy_regex!(r"^(?:Force)?(Teleport|SpawnAt|Trigger)(\d+)$");
|
||||
if let Some(captures)=bonus_start_pattern.captures(s){
|
||||
return Ok(Self::Teleport(SpawnID(captures[1].parse().map_err(IDParseError::ParseInt)?)));
|
||||
return Ok(StageElement{
|
||||
behaviour:StageElementBehaviour::Teleport,
|
||||
stage_id:StageID(captures[1].parse().map_err(IDParseError::ParseInt)?),
|
||||
});
|
||||
}
|
||||
// Spawn
|
||||
let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Spawn(\d+)$");
|
||||
if let Some(captures)=bonus_finish_pattern.captures(s){
|
||||
return Ok(Self::Spawn(SpawnID(captures[1].parse().map_err(IDParseError::ParseInt)?)));
|
||||
return Ok(StageElement{
|
||||
behaviour:StageElementBehaviour::Spawn,
|
||||
stage_id:StageID(captures[1].parse().map_err(IDParseError::ParseInt)?),
|
||||
});
|
||||
}
|
||||
Err(IDParseError::NoCaptures)
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for StageElement{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
match self{
|
||||
StageElement{behaviour:StageElementBehaviour::Spawn,stage_id:StageID(stage_id)}=>write!(f,"Spawn{stage_id}"),
|
||||
StageElement{behaviour:StageElementBehaviour::Teleport,stage_id:StageID(stage_id)}=>write!(f,"Teleport{stage_id}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
|
||||
struct WormholeID(u64);
|
||||
enum Wormhole{
|
||||
In(WormholeID),
|
||||
Out(WormholeID),
|
||||
enum WormholeBehaviour{
|
||||
In,
|
||||
Out,
|
||||
}
|
||||
struct WormholeElement{
|
||||
behaviour:WormholeBehaviour,
|
||||
wormhole_id:WormholeID,
|
||||
}
|
||||
// Parse a Wormhole from a part name
|
||||
impl std::str::FromStr for Wormhole{
|
||||
impl std::str::FromStr for WormholeElement{
|
||||
type Err=IDParseError;
|
||||
fn from_str(s:&str)->Result<Self,Self::Err>{
|
||||
let bonus_start_pattern=lazy_regex::lazy_regex!(r"^WormholeIn(\d+)$");
|
||||
if let Some(captures)=bonus_start_pattern.captures(s){
|
||||
return Ok(Self::In(WormholeID(captures[1].parse().map_err(IDParseError::ParseInt)?)));
|
||||
return Ok(Self{
|
||||
behaviour:WormholeBehaviour::In,
|
||||
wormhole_id:WormholeID(captures[1].parse().map_err(IDParseError::ParseInt)?),
|
||||
});
|
||||
}
|
||||
let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^WormholeOut(\d+)$");
|
||||
if let Some(captures)=bonus_finish_pattern.captures(s){
|
||||
return Ok(Self::Out(WormholeID(captures[1].parse().map_err(IDParseError::ParseInt)?)));
|
||||
return Ok(Self{
|
||||
behaviour:WormholeBehaviour::Out,
|
||||
wormhole_id:WormholeID(captures[1].parse().map_err(IDParseError::ParseInt)?),
|
||||
});
|
||||
}
|
||||
Err(IDParseError::NoCaptures)
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for WormholeElement{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
match self{
|
||||
WormholeElement{behaviour:WormholeBehaviour::In,wormhole_id:WormholeID(wormhole_id)}=>write!(f,"WormholeIn{wormhole_id}"),
|
||||
WormholeElement{behaviour:WormholeBehaviour::Out,wormhole_id:WormholeID(wormhole_id)}=>write!(f,"WormholeOut{wormhole_id}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Count various map elements
|
||||
#[derive(Default)]
|
||||
@@ -141,8 +206,8 @@ struct Counts<'a>{
|
||||
mode_start_counts:HashMap<ModeID,Vec<&'a str>>,
|
||||
mode_finish_counts:HashMap<ModeID,Vec<&'a str>>,
|
||||
mode_anticheat_counts:HashMap<ModeID,Vec<&'a str>>,
|
||||
teleport_counts:HashMap<SpawnID,Vec<&'a str>>,
|
||||
spawn_counts:HashMap<SpawnID,u64>,
|
||||
teleport_counts:HashMap<StageID,Vec<&'a str>>,
|
||||
spawn_counts:HashMap<StageID,u64>,
|
||||
wormhole_in_counts:HashMap<WormholeID,u64>,
|
||||
wormhole_out_counts:HashMap<WormholeID,u64>,
|
||||
}
|
||||
@@ -164,21 +229,21 @@ pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_d
|
||||
if class_is_a(instance.class.as_str(),"BasePart"){
|
||||
// Zones
|
||||
match instance.name.parse(){
|
||||
Ok(Zone::Start(mode_id))=>counts.mode_start_counts.entry(mode_id).or_default().push(instance.name.as_str()),
|
||||
Ok(Zone::Finish(mode_id))=>counts.mode_finish_counts.entry(mode_id).or_default().push(instance.name.as_str()),
|
||||
Ok(Zone::Anticheat(mode_id))=>counts.mode_anticheat_counts.entry(mode_id).or_default().push(instance.name.as_str()),
|
||||
Ok(ModeElement{zone:Zone::Start,mode_id})=>counts.mode_start_counts.entry(mode_id).or_default().push(instance.name.as_str()),
|
||||
Ok(ModeElement{zone:Zone::Finish,mode_id})=>counts.mode_finish_counts.entry(mode_id).or_default().push(instance.name.as_str()),
|
||||
Ok(ModeElement{zone:Zone::Anticheat,mode_id})=>counts.mode_anticheat_counts.entry(mode_id).or_default().push(instance.name.as_str()),
|
||||
Err(_)=>(),
|
||||
}
|
||||
// Spawns & Teleports
|
||||
match instance.name.parse(){
|
||||
Ok(SpawnTeleport::Teleport(spawn_id))=>counts.teleport_counts.entry(spawn_id).or_default().push(instance.name.as_str()),
|
||||
Ok(SpawnTeleport::Spawn(spawn_id))=>*counts.spawn_counts.entry(spawn_id).or_insert(0)+=1,
|
||||
Ok(StageElement{behaviour:StageElementBehaviour::Teleport,stage_id})=>counts.teleport_counts.entry(stage_id).or_default().push(instance.name.as_str()),
|
||||
Ok(StageElement{behaviour:StageElementBehaviour::Spawn,stage_id})=>*counts.spawn_counts.entry(stage_id).or_insert(0)+=1,
|
||||
Err(_)=>(),
|
||||
}
|
||||
// Wormholes
|
||||
match instance.name.parse(){
|
||||
Ok(Wormhole::In(wormhole_id))=>*counts.wormhole_in_counts.entry(wormhole_id).or_insert(0)+=1,
|
||||
Ok(Wormhole::Out(wormhole_id))=>*counts.wormhole_out_counts.entry(wormhole_id).or_insert(0)+=1,
|
||||
Ok(WormholeElement{behaviour:WormholeBehaviour::In,wormhole_id})=>*counts.wormhole_in_counts.entry(wormhole_id).or_insert(0)+=1,
|
||||
Ok(WormholeElement{behaviour:WormholeBehaviour::Out,wormhole_id})=>*counts.wormhole_out_counts.entry(wormhole_id).or_insert(0)+=1,
|
||||
Err(_)=>(),
|
||||
}
|
||||
}
|
||||
@@ -345,9 +410,9 @@ struct MapCheck<'a>{
|
||||
// Spawn1 must exist
|
||||
spawn1:Result<Exists,Absent>,
|
||||
// Check for dangling Teleport# (no associated Spawn#)
|
||||
teleport_counts:SetDifferenceCheck<SetDifferenceCheckContextAllowNone<SpawnID,Vec<&'a str>>>,
|
||||
teleport_counts:SetDifferenceCheck<SetDifferenceCheckContextAllowNone<StageID,Vec<&'a str>>>,
|
||||
// No duplicate Spawn#
|
||||
spawn_counts:DuplicateCheck<SpawnID,u64>,
|
||||
spawn_counts:DuplicateCheck<StageID,u64>,
|
||||
// Check for dangling WormholeIn# (no associated WormholeOut#)
|
||||
wormhole_in_counts:SetDifferenceCheck<SetDifferenceCheckContextAtLeastOne<WormholeID,u64>>,
|
||||
// No duplicate WormholeOut# (duplicate WormholeIn# ok)
|
||||
@@ -391,7 +456,7 @@ impl<'a> ModelInfo<'a>{
|
||||
};
|
||||
|
||||
// Spawn1 must exist
|
||||
let spawn1=if self.counts.spawn_counts.contains_key(&SpawnID::FIRST){
|
||||
let spawn1=if self.counts.spawn_counts.contains_key(&StageID::FIRST){
|
||||
Ok(Exists)
|
||||
}else{
|
||||
Err(Absent)
|
||||
@@ -446,7 +511,7 @@ impl<'a> ModelInfo<'a>{
|
||||
}
|
||||
|
||||
impl MapCheck<'_>{
|
||||
fn result(self)->Result<MapInfoOwned,String>{
|
||||
fn result(self)->Result<MapInfoOwned,Result<MapCheckList,serde_json::Error>>{
|
||||
match self{
|
||||
MapCheck{
|
||||
model_class:StringCheck(Ok(())),
|
||||
@@ -470,156 +535,269 @@ impl MapCheck<'_>{
|
||||
game_id,
|
||||
})
|
||||
},
|
||||
other=>Err(other.to_string()),
|
||||
other=>Err(other.itemize()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_comma_separated<T>(
|
||||
f:&mut std::fmt::Formatter<'_>,
|
||||
mut it:impl Iterator<Item=T>,
|
||||
custom_write:impl Fn(&mut std::fmt::Formatter<'_>,T)->std::fmt::Result
|
||||
)->std::fmt::Result{
|
||||
if let Some(t)=it.next(){
|
||||
custom_write(f,t)?;
|
||||
for t in it{
|
||||
write!(f,", ")?;
|
||||
custom_write(f,t)?;
|
||||
}
|
||||
struct Separated<F>{
|
||||
f:F,
|
||||
separator:&'static str,
|
||||
}
|
||||
impl<F> Separated<F>{
|
||||
fn new(separator:&'static str,f:F)->Self{
|
||||
Self{separator,f}
|
||||
}
|
||||
}
|
||||
impl<F,I,D> std::fmt::Display for Separated<F>
|
||||
where
|
||||
D:std::fmt::Display,
|
||||
I:IntoIterator<Item=D>,
|
||||
F:Fn()->I,
|
||||
{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
let mut it=(self.f)().into_iter();
|
||||
if let Some(first)=it.next(){
|
||||
write!(f,"{first}")?;
|
||||
for item in it{
|
||||
write!(f,"{}{item}",self.separator)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a zone string such as BonusStart
|
||||
macro_rules! write_zone{
|
||||
($f:expr,$mode:expr,$zone:expr)=>{
|
||||
match $mode{
|
||||
ModeID(0)=>write!($f,concat!("Map",$zone)),
|
||||
ModeID(1)=>write!($f,concat!("Bonus",$zone)),
|
||||
ModeID(other)=>write!($f,concat!("Bonus{}",$zone),other),
|
||||
struct Duplicates<D>{
|
||||
display:D,
|
||||
duplicates:usize,
|
||||
}
|
||||
impl<D> Duplicates<D>{
|
||||
fn new(display:D,duplicates:usize)->Self{
|
||||
Self{
|
||||
display,
|
||||
duplicates,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<D:std::fmt::Display> std::fmt::Display for Duplicates<D>{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{} ({} duplicates)",self.display,self.duplicates)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct CheckSummary{
|
||||
name:&'static str,
|
||||
summary:String,
|
||||
passed:bool,
|
||||
details:serde_json::Value,
|
||||
}
|
||||
impl CheckSummary{
|
||||
const fn passed(name:&'static str)->Self{
|
||||
Self{
|
||||
name,
|
||||
summary:String::new(),
|
||||
passed:true,
|
||||
details:serde_json::Value::Null,
|
||||
}
|
||||
}
|
||||
}
|
||||
macro_rules! summary{
|
||||
($name:literal,$summary:expr,$details:expr)=>{
|
||||
CheckSummary{
|
||||
name:$name,
|
||||
summary:$summary,
|
||||
passed:false,
|
||||
details:serde_json::to_value($details)?,
|
||||
}
|
||||
};
|
||||
}
|
||||
macro_rules! summary_format{
|
||||
($name:literal,$fmt:literal,$details:expr)=>{
|
||||
CheckSummary{
|
||||
name:$name,
|
||||
summary:format!($fmt),
|
||||
passed:false,
|
||||
details:serde_json::to_value($details)?,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Generate an error message for each observed issue separated by newlines.
|
||||
// This defines MapCheck.to_string() which is used in MapCheck.result()
|
||||
impl std::fmt::Display for MapCheck<'_>{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
if let StringCheck(Err(context))=&self.model_class{
|
||||
writeln!(f,"Invalid model class: {context}")?;
|
||||
}
|
||||
if let StringCheck(Err(context))=&self.model_name{
|
||||
writeln!(f,"Model name must have snake_case: {context}")?;
|
||||
}
|
||||
match &self.display_name{
|
||||
Ok(Ok(StringCheck(Ok(_))))=>(),
|
||||
Ok(Ok(StringCheck(Err(context))))=>writeln!(f,"DisplayName must have Title Case: {context}")?,
|
||||
Ok(Err(context))=>writeln!(f,"Invalid DisplayName: {context}")?,
|
||||
Err(StringValueError::ObjectNotFound)=>writeln!(f,"Missing DisplayName StringValue")?,
|
||||
Err(StringValueError::ValueNotSet)=>writeln!(f,"DisplayName Value not set")?,
|
||||
Err(StringValueError::NonStringValue)=>writeln!(f,"DisplayName Value is not a String")?,
|
||||
}
|
||||
match &self.creator{
|
||||
Ok(Ok(_))=>(),
|
||||
Ok(Err(context))=>writeln!(f,"Invalid Creator: {context}")?,
|
||||
Err(StringValueError::ObjectNotFound)=>writeln!(f,"Missing Creator StringValue")?,
|
||||
Err(StringValueError::ValueNotSet)=>writeln!(f,"Creator Value not set")?,
|
||||
Err(StringValueError::NonStringValue)=>writeln!(f,"Creator Value is not a String")?,
|
||||
}
|
||||
if let Err(_parse_game_id_error)=&self.game_id{
|
||||
writeln!(f,"Model name must be prefixed with bhop_ surf_ or flytrials_")?;
|
||||
}
|
||||
if let Err(Absent)=&self.mapstart{
|
||||
writeln!(f,"Model has no MapStart")?;
|
||||
}
|
||||
if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.mode_start_counts{
|
||||
write!(f,"Duplicate start zones: ")?;
|
||||
write_comma_separated(f,context.iter(),|f,(mode_id,names)|{
|
||||
write_zone!(f,mode_id,"Start")?;
|
||||
write!(f," ({} duplicates)",names.len())?;
|
||||
Ok(())
|
||||
})?;
|
||||
writeln!(f)?;
|
||||
}
|
||||
if let SetDifferenceCheck(Err(context))=&self.mode_finish_counts{
|
||||
if !context.extra.is_empty(){
|
||||
let plural=if context.extra.len()==1{"zone"}else{"zones"};
|
||||
write!(f,"No matching start zone for finish {plural}: ")?;
|
||||
write_comma_separated(f,context.extra.iter(),|f,(mode_id,_names)|
|
||||
write_zone!(f,mode_id,"Finish")
|
||||
)?;
|
||||
writeln!(f)?;
|
||||
impl MapCheck<'_>{
|
||||
fn itemize(&self)->Result<MapCheckList,serde_json::Error>{
|
||||
let model_class=match &self.model_class{
|
||||
StringCheck(Ok(()))=>CheckSummary::passed("ModelClass"),
|
||||
StringCheck(Err(context))=>summary_format!("ModelClass","Invalid model class: {context}",()),
|
||||
};
|
||||
let model_name=match &self.model_name{
|
||||
StringCheck(Ok(()))=>CheckSummary::passed("ModelName"),
|
||||
StringCheck(Err(context))=>summary_format!("ModelName","Model name must have snake_case: {context}",()),
|
||||
};
|
||||
let display_name=match &self.display_name{
|
||||
Ok(Ok(StringCheck(Ok(_))))=>CheckSummary::passed("DisplayName"),
|
||||
Ok(Ok(StringCheck(Err(context))))=>summary_format!("DisplayName","DisplayName must have Title Case: {context}",()),
|
||||
Ok(Err(context))=>summary_format!("DisplayName","Invalid DisplayName: {context}",()),
|
||||
Err(StringValueError::ObjectNotFound)=>summary!("DisplayName","Missing DisplayName StringValue".to_owned(),()),
|
||||
Err(StringValueError::ValueNotSet)=>summary!("DisplayName","DisplayName Value not set".to_owned(),()),
|
||||
Err(StringValueError::NonStringValue)=>summary!("DisplayName","DisplayName Value is not a String".to_owned(),()),
|
||||
};
|
||||
let creator=match &self.creator{
|
||||
Ok(Ok(_))=>CheckSummary::passed("Creator"),
|
||||
Ok(Err(context))=>summary_format!("Creator","Invalid Creator: {context}",()),
|
||||
Err(StringValueError::ObjectNotFound)=>summary!("Creator","Missing Creator StringValue".to_owned(),()),
|
||||
Err(StringValueError::ValueNotSet)=>summary!("Creator","Creator Value not set".to_owned(),()),
|
||||
Err(StringValueError::NonStringValue)=>summary!("Creator","Creator Value is not a String".to_owned(),()),
|
||||
};
|
||||
let game_id=match &self.game_id{
|
||||
Ok(_)=>CheckSummary::passed("GameID"),
|
||||
Err(ParseGameIDError)=>summary!("GameID","Model name must be prefixed with bhop_ surf_ or flytrials_".to_owned(),()),
|
||||
};
|
||||
let mapstart=match &self.mapstart{
|
||||
Ok(Exists)=>CheckSummary::passed("MapStart"),
|
||||
Err(Absent)=>summary_format!("MapStart","Model has no MapStart",()),
|
||||
};
|
||||
let duplicate_start=match &self.mode_start_counts{
|
||||
DuplicateCheck(Ok(()))=>CheckSummary::passed("DuplicateStart"),
|
||||
DuplicateCheck(Err(DuplicateCheckContext(context)))=>{
|
||||
let context=Separated::new(", ",||context.iter().map(|(&mode_id,names)|
|
||||
Duplicates::new(ModeElement{zone:Zone::Start,mode_id},names.len())
|
||||
));
|
||||
summary_format!("DuplicateStart","Duplicate start zones: {context}",())
|
||||
}
|
||||
if !context.missing.is_empty(){
|
||||
let plural=if context.missing.len()==1{"zone"}else{"zones"};
|
||||
write!(f,"Missing finish {plural}: ")?;
|
||||
write_comma_separated(f,context.missing.iter(),|f,mode_id|
|
||||
write_zone!(f,mode_id,"Finish")
|
||||
)?;
|
||||
writeln!(f)?;
|
||||
};
|
||||
let (extra_finish,missing_finish)=match &self.mode_finish_counts{
|
||||
SetDifferenceCheck(Ok(()))=>(CheckSummary::passed("ExtraFinish"),CheckSummary::passed("MissingFinish")),
|
||||
SetDifferenceCheck(Err(context))=>(
|
||||
if context.extra.is_empty(){
|
||||
CheckSummary::passed("ExtraFinish")
|
||||
}else{
|
||||
let plural=if context.extra.len()==1{"zone"}else{"zones"};
|
||||
let context=Separated::new(", ",||context.extra.iter().map(|(&mode_id,_names)|
|
||||
ModeElement{zone:Zone::Finish,mode_id}
|
||||
));
|
||||
summary_format!("ExtraFinish","No matching start zone for finish {plural}: {context}",())
|
||||
},
|
||||
if context.missing.is_empty(){
|
||||
CheckSummary::passed("MissingFinish")
|
||||
}else{
|
||||
let plural=if context.missing.len()==1{"zone"}else{"zones"};
|
||||
let context=Separated::new(", ",||context.missing.iter().map(|&mode_id|
|
||||
ModeElement{zone:Zone::Finish,mode_id}
|
||||
));
|
||||
summary_format!("MissingFinish","Missing finish {plural}: {context}",())
|
||||
}
|
||||
),
|
||||
};
|
||||
let dangling_anticheat=match &self.mode_anticheat_counts{
|
||||
SetDifferenceCheck(Ok(()))=>CheckSummary::passed("DanglingAnticheat"),
|
||||
SetDifferenceCheck(Err(context))=>{
|
||||
if context.extra.is_empty(){
|
||||
CheckSummary::passed("DanglingAnticheat")
|
||||
}else{
|
||||
let plural=if context.extra.len()==1{"zone"}else{"zones"};
|
||||
let context=Separated::new(", ",||context.extra.iter().map(|(&mode_id,_names)|
|
||||
ModeElement{zone:Zone::Anticheat,mode_id}
|
||||
));
|
||||
summary_format!("DanglingAnticheat","No matching start zone for anticheat {plural}: {context}",())
|
||||
}
|
||||
}
|
||||
}
|
||||
if let SetDifferenceCheck(Err(context))=&self.mode_anticheat_counts{
|
||||
if !context.extra.is_empty(){
|
||||
let plural=if context.extra.len()==1{"zone"}else{"zones"};
|
||||
write!(f,"No matching start zone for anticheat {plural}: ")?;
|
||||
write_comma_separated(f,context.extra.iter(),|f,(mode_id,_names)|
|
||||
write_zone!(f,mode_id,"Anticheat")
|
||||
)?;
|
||||
writeln!(f)?;
|
||||
};
|
||||
let spawn1=match &self.spawn1{
|
||||
Ok(Exists)=>CheckSummary::passed("Spawn1"),
|
||||
Err(Absent)=>summary_format!("Spawn1","Model has no Spawn1",()),
|
||||
};
|
||||
let dangling_teleport=match &self.teleport_counts{
|
||||
SetDifferenceCheck(Ok(()))=>CheckSummary::passed("DanglingTeleport"),
|
||||
SetDifferenceCheck(Err(context))=>{
|
||||
let unique_names:HashSet<_>=context.extra.values().flat_map(|names|names.iter().copied()).collect();
|
||||
let plural=if unique_names.len()==1{"object"}else{"objects"};
|
||||
let context=Separated::new(", ",||&unique_names);
|
||||
summary_format!("DanglingTeleport","No matching Spawn for {plural}: {context}",())
|
||||
}
|
||||
}
|
||||
if let Err(Absent)=&self.spawn1{
|
||||
writeln!(f,"Model has no Spawn1")?;
|
||||
}
|
||||
if let SetDifferenceCheck(Err(context))=&self.teleport_counts{
|
||||
for names in context.extra.values(){
|
||||
let plural=if names.len()==1{"object"}else{"objects"};
|
||||
write!(f,"No matching Spawn for {plural}: ")?;
|
||||
write_comma_separated(f,names.iter(),|f,&name|{
|
||||
write!(f,"{name}")
|
||||
})?;
|
||||
writeln!(f)?;
|
||||
};
|
||||
let duplicate_spawns=match &self.spawn_counts{
|
||||
DuplicateCheck(Ok(()))=>CheckSummary::passed("DuplicateSpawn"),
|
||||
DuplicateCheck(Err(DuplicateCheckContext(context)))=>{
|
||||
let context=Separated::new(", ",||context.iter().map(|(&stage_id,&names)|
|
||||
Duplicates::new(StageElement{behaviour:StageElementBehaviour::Spawn,stage_id},names as usize)
|
||||
));
|
||||
summary_format!("DuplicateSpawn","Duplicate Spawn: {context}",())
|
||||
}
|
||||
}
|
||||
if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.spawn_counts{
|
||||
write!(f,"Duplicate Spawn: ")?;
|
||||
write_comma_separated(f,context.iter(),|f,(SpawnID(spawn_id),count)|
|
||||
write!(f,"Spawn{spawn_id} ({count} duplicates)")
|
||||
)?;
|
||||
writeln!(f)?;
|
||||
}
|
||||
if let SetDifferenceCheck(Err(context))=&self.wormhole_in_counts{
|
||||
if !context.extra.is_empty(){
|
||||
write!(f,"WormholeIn with no matching WormholeOut: ")?;
|
||||
write_comma_separated(f,context.extra.iter(),|f,(WormholeID(wormhole_id),_count)|
|
||||
write!(f,"WormholeIn{wormhole_id}")
|
||||
)?;
|
||||
writeln!(f)?;
|
||||
};
|
||||
let (extra_wormhole_in,missing_wormhole_in)=match &self.wormhole_in_counts{
|
||||
SetDifferenceCheck(Ok(()))=>(CheckSummary::passed("ExtraWormholeIn"),CheckSummary::passed("MissingWormholeIn")),
|
||||
SetDifferenceCheck(Err(context))=>(
|
||||
if context.extra.is_empty(){
|
||||
CheckSummary::passed("ExtraWormholeIn")
|
||||
}else{
|
||||
let context=Separated::new(", ",||context.extra.iter().map(|(&wormhole_id,_names)|
|
||||
WormholeElement{behaviour:WormholeBehaviour::In,wormhole_id}
|
||||
));
|
||||
summary_format!("ExtraWormholeIn","WormholeIn with no matching WormholeOut: {context}",())
|
||||
},
|
||||
if context.missing.is_empty(){
|
||||
CheckSummary::passed("MissingWormholeIn")
|
||||
}else{
|
||||
// This counts WormholeIn objects, but
|
||||
// flipped logic is easier to understand
|
||||
let context=Separated::new(", ",||context.missing.iter().map(|&wormhole_id|
|
||||
WormholeElement{behaviour:WormholeBehaviour::Out,wormhole_id}
|
||||
));
|
||||
summary_format!("MissingWormholeIn","WormholeOut with no matching WormholeIn: {context}",())
|
||||
}
|
||||
)
|
||||
};
|
||||
let duplicate_wormhole_out=match &self.wormhole_out_counts{
|
||||
DuplicateCheck(Ok(()))=>CheckSummary::passed("DuplicateWormholeOut"),
|
||||
DuplicateCheck(Err(DuplicateCheckContext(context)))=>{
|
||||
let context=Separated::new(", ",||context.iter().map(|(&wormhole_id,&names)|
|
||||
Duplicates::new(WormholeElement{behaviour:WormholeBehaviour::Out,wormhole_id},names as usize)
|
||||
));
|
||||
summary_format!("DuplicateWormholeOut","Duplicate WormholeOut: {context}",())
|
||||
}
|
||||
if !context.missing.is_empty(){
|
||||
// This counts WormholeIn objects, but
|
||||
// flipped logic is easier to understand
|
||||
write!(f,"WormholeOut with no matching WormholeIn: ")?;
|
||||
write_comma_separated(f,context.missing.iter(),|f,WormholeID(wormhole_id)|
|
||||
write!(f,"WormholeOut{wormhole_id}")
|
||||
)?;
|
||||
writeln!(f)?;
|
||||
}
|
||||
}
|
||||
if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.wormhole_out_counts{
|
||||
write!(f,"Duplicate WormholeOut: ")?;
|
||||
write_comma_separated(f,context.iter(),|f,(WormholeID(wormhole_id),count)|
|
||||
write!(f,"WormholeOut{wormhole_id} ({count} duplicates)")
|
||||
)?;
|
||||
writeln!(f)?;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
Ok(MapCheckList{checks:Box::new([
|
||||
model_class,
|
||||
model_name,
|
||||
display_name,
|
||||
creator,
|
||||
game_id,
|
||||
mapstart,
|
||||
duplicate_start,
|
||||
extra_finish,
|
||||
missing_finish,
|
||||
dangling_anticheat,
|
||||
spawn1,
|
||||
dangling_teleport,
|
||||
duplicate_spawns,
|
||||
extra_wormhole_in,
|
||||
missing_wormhole_in,
|
||||
duplicate_wormhole_out,
|
||||
])})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct MapCheckList{
|
||||
checks:Box<[CheckSummary;16]>,
|
||||
}
|
||||
impl MapCheckList{
|
||||
fn summary(&self)->String{
|
||||
Separated::new("; ",||self.checks.iter().filter_map(|check|
|
||||
(!check.passed).then_some(check.summary.as_str())
|
||||
)).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Summary{
|
||||
pub summary:String,
|
||||
pub json:serde_json::Value,
|
||||
}
|
||||
|
||||
pub struct CheckReportAndVersion{
|
||||
pub status:Result<MapInfoOwned,String>,
|
||||
pub status:Result<MapInfoOwned,Summary>,
|
||||
pub version:u64,
|
||||
}
|
||||
|
||||
@@ -656,7 +834,14 @@ impl crate::message_handler::MessageHandler{
|
||||
let map_check=model_info.check();
|
||||
|
||||
// check the report, generate an error message if it fails the check
|
||||
let status=map_check.result();
|
||||
let status=match map_check.result(){
|
||||
Ok(map_info)=>Ok(map_info),
|
||||
Err(Ok(summary))=>Err(Summary{
|
||||
summary:summary.summary(),
|
||||
json:serde_json::to_value(&summary).map_err(Error::ToJsonValue)?,
|
||||
}),
|
||||
Err(Err(e))=>return Err(Error::ToJsonValue(e)),
|
||||
};
|
||||
|
||||
Ok(CheckReportAndVersion{status,version})
|
||||
}
|
||||
|
||||
@@ -34,17 +34,21 @@ impl crate::message_handler::MessageHandler{
|
||||
Ok(CheckReportAndVersion{status:Err(report),..})=>self.api.action_mapfix_request_changes(
|
||||
submissions_api::types::ActionMapfixRequestChangesRequest{
|
||||
MapfixID:mapfix_id,
|
||||
ErrorMessage:report,
|
||||
ErrorMessage:report.summary,
|
||||
}
|
||||
).await.map_err(Error::ApiActionMapfixCheck)?,
|
||||
// TODO: report the error
|
||||
// update the mapfix model status to request changes
|
||||
Err(e)=>self.api.action_mapfix_request_changes(
|
||||
submissions_api::types::ActionMapfixRequestChangesRequest{
|
||||
MapfixID:mapfix_id,
|
||||
ErrorMessage:e.to_string(),
|
||||
}
|
||||
).await.map_err(Error::ApiActionMapfixCheck)?,
|
||||
Err(e)=>{
|
||||
// log error
|
||||
println!("[check_mapfix] Error: {e}");
|
||||
|
||||
self.api.action_mapfix_request_changes(
|
||||
submissions_api::types::ActionMapfixRequestChangesRequest{
|
||||
MapfixID:mapfix_id,
|
||||
ErrorMessage:e.to_string(),
|
||||
}
|
||||
).await.map_err(Error::ApiActionMapfixCheck)?;
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -35,17 +35,21 @@ impl crate::message_handler::MessageHandler{
|
||||
Ok(CheckReportAndVersion{status:Err(report),..})=>self.api.action_submission_request_changes(
|
||||
submissions_api::types::ActionSubmissionRequestChangesRequest{
|
||||
SubmissionID:submission_id,
|
||||
ErrorMessage:report,
|
||||
ErrorMessage:report.summary,
|
||||
}
|
||||
).await.map_err(Error::ApiActionSubmissionCheck)?,
|
||||
// TODO: report the error
|
||||
// update the submission model status to request changes
|
||||
Err(e)=>self.api.action_submission_request_changes(
|
||||
submissions_api::types::ActionSubmissionRequestChangesRequest{
|
||||
SubmissionID:submission_id,
|
||||
ErrorMessage:e.to_string(),
|
||||
}
|
||||
).await.map_err(Error::ApiActionSubmissionCheck)?,
|
||||
Err(e)=>{
|
||||
// log error
|
||||
println!("[check_submission] Error: {e}");
|
||||
|
||||
self.api.action_submission_request_changes(
|
||||
submissions_api::types::ActionSubmissionRequestChangesRequest{
|
||||
SubmissionID:submission_id,
|
||||
ErrorMessage:e.to_string(),
|
||||
}
|
||||
).await.map_err(Error::ApiActionSubmissionCheck)?;
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -43,6 +43,9 @@ impl crate::message_handler::MessageHandler{
|
||||
let create_result=self.create_mapfix_inner(create_info).await;
|
||||
|
||||
if let Err(e)=create_result{
|
||||
// log error
|
||||
println!("[create_mapfix] Error: {e}");
|
||||
|
||||
self.api.action_operation_failed(submissions_api::types::ActionOperationFailedRequest{
|
||||
OperationID:operation_id,
|
||||
StatusMessage:e.to_string(),
|
||||
|
||||
@@ -57,6 +57,9 @@ impl crate::message_handler::MessageHandler{
|
||||
let create_result=self.create_submission_inner(create_info).await;
|
||||
|
||||
if let Err(e)=create_result{
|
||||
// log error
|
||||
println!("[create_submission] Error: {e}");
|
||||
|
||||
self.api.action_operation_failed(submissions_api::types::ActionOperationFailedRequest{
|
||||
OperationID:operation_id,
|
||||
StatusMessage:e.to_string(),
|
||||
|
||||
@@ -26,6 +26,9 @@ impl crate::message_handler::MessageHandler{
|
||||
).await.map_err(Error::ApiActionMapfixValidate)?;
|
||||
},
|
||||
Err(e)=>{
|
||||
// log error
|
||||
println!("[validate_mapfix] Error: {e}");
|
||||
|
||||
// update the mapfix model status to accepted
|
||||
self.api.action_mapfix_accepted(submissions_api::types::ActionMapfixAcceptedRequest{
|
||||
MapfixID:mapfix_id,
|
||||
|
||||
@@ -26,6 +26,9 @@ impl crate::message_handler::MessageHandler{
|
||||
).await.map_err(Error::ApiActionSubmissionValidate)?;
|
||||
},
|
||||
Err(e)=>{
|
||||
// log error
|
||||
println!("[validate_submission] Error: {e}");
|
||||
|
||||
// update the submission model status to accepted
|
||||
self.api.action_submission_accepted(submissions_api::types::ActionSubmissionAcceptedRequest{
|
||||
SubmissionID:submission_id,
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function SubmissionInfoPage() {
|
||||
DisplayName: (formData.get("display-name") as string) ?? "unknown", // TEMPORARY! TODO: Change
|
||||
Creator: (formData.get("creator") as string) ?? "unknown", // TEMPORARY! TODO: Change
|
||||
GameID: game,
|
||||
AssetID: Number((formData.get("asset-id") as string) ?? "0"),
|
||||
AssetID: Number((formData.get("asset-id") as string) ?? "-1"),
|
||||
};
|
||||
|
||||
console.log(payload)
|
||||
|
||||
BIN
web/src/app/favicon.ico
Normal file
BIN
web/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -10,6 +10,7 @@ interface ReviewAction {
|
||||
|
||||
const ReviewActions = {
|
||||
Submit: {name:"Submit",action:"trigger-submit"} as ReviewAction,
|
||||
BypassSubmit: {name:"Bypass Submit",action:"bypass-submit"} as ReviewAction,
|
||||
ResetSubmitting: {name:"Reset Submitting (fix softlocked status)",action:"reset-submitting"} as ReviewAction,
|
||||
Revoke: {name:"Revoke",action:"revoke"} as ReviewAction,
|
||||
Accept: {name:"Accept",action:"trigger-validate"} as ReviewAction,
|
||||
@@ -119,6 +120,10 @@ export default function ReviewButtons(props: ReviewId) {
|
||||
}
|
||||
|
||||
if (roles&RolesConstants.MapfixReview) {
|
||||
// you can force submit a map in ChangesRequested status
|
||||
if (!is_submitter && mapfixStatus === MapfixStatus.ChangesRequested) {
|
||||
visibleButtons.push({ action: ReviewActions.BypassSubmit, color: "error", mapfixId });
|
||||
}
|
||||
// you can't review your own mapfix!
|
||||
// note that this means there needs to be more than one person with MapfixReview
|
||||
if (!is_submitter && mapfixStatus === MapfixStatus.Submitted) {
|
||||
|
||||
@@ -11,29 +11,10 @@ import "./(styles)/page.scss";
|
||||
import { ListSortConstants } from "../ts/Sort";
|
||||
|
||||
export default function MapfixInfoPage() {
|
||||
const [mapfixes, setMapfixes] = useState<MapfixList>({Total:0,Mapfixes:[]})
|
||||
const [mapfixes, setMapfixes] = useState<MapfixList|null>(null)
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const cardsPerPage = 24; // built to fit on a 1920x1080 monitor
|
||||
|
||||
const totalPages = Math.ceil(mapfixes.Total / cardsPerPage);
|
||||
|
||||
const currentCards = mapfixes.Mapfixes.slice(
|
||||
(currentPage - 1) * cardsPerPage,
|
||||
currentPage * cardsPerPage
|
||||
);
|
||||
|
||||
const nextPage = () => {
|
||||
if (currentPage < totalPages) {
|
||||
setCurrentPage(currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const prevPage = () => {
|
||||
if (currentPage > 1) {
|
||||
setCurrentPage(currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchMapfixes() {
|
||||
const res = await fetch(`/api/mapfixes?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`)
|
||||
@@ -55,7 +36,26 @@ export default function MapfixInfoPage() {
|
||||
</Webpage>
|
||||
}
|
||||
|
||||
if (mapfixes && mapfixes.Total == 0) {
|
||||
const totalPages = Math.ceil(mapfixes.Total / cardsPerPage);
|
||||
|
||||
const currentCards = mapfixes.Mapfixes.slice(
|
||||
(currentPage - 1) * cardsPerPage,
|
||||
currentPage * cardsPerPage
|
||||
);
|
||||
|
||||
const nextPage = () => {
|
||||
if (currentPage < totalPages) {
|
||||
setCurrentPage(currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const prevPage = () => {
|
||||
if (currentPage > 1) {
|
||||
setCurrentPage(currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
if (mapfixes.Total == 0) {
|
||||
return <Webpage>
|
||||
<main>
|
||||
Mapfixes list is empty.
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function MapfixInfoPage() {
|
||||
const formData = new FormData(form);
|
||||
|
||||
const payload: MapfixPayload = {
|
||||
AssetID: Number((formData.get("asset-id") as string) ?? "0"),
|
||||
AssetID: Number((formData.get("asset-id") as string) ?? "-1"),
|
||||
TargetAssetID: Number(dynamicId.mapId),
|
||||
Description: (formData.get("description") as string) ?? "unknown", // TEMPORARY! TODO: Change
|
||||
};
|
||||
|
||||
@@ -10,7 +10,8 @@ interface ReviewAction {
|
||||
|
||||
const ReviewActions = {
|
||||
Submit: {name:"Submit",action:"trigger-submit"} as ReviewAction,
|
||||
ForceSubmit: {name:"Force Submit",action:"trigger-submit"} as ReviewAction,
|
||||
AdminSubmit: {name:"Admin Submit",action:"trigger-submit"} as ReviewAction,
|
||||
BypassSubmit: {name:"Bypass Submit",action:"bypass-submit"} as ReviewAction,
|
||||
ResetSubmitting: {name:"Reset Submitting (fix softlocked status)",action:"reset-submitting"} as ReviewAction,
|
||||
Revoke: {name:"Revoke",action:"revoke"} as ReviewAction,
|
||||
Accept: {name:"Accept",action:"trigger-validate"} as ReviewAction,
|
||||
@@ -122,7 +123,8 @@ export default function ReviewButtons(props: ReviewId) {
|
||||
if (roles&RolesConstants.SubmissionReview) {
|
||||
// you can force submit a map in ChangesRequested status
|
||||
if (!is_submitter && submissionStatus === SubmissionStatus.ChangesRequested) {
|
||||
visibleButtons.push({ action: ReviewActions.ForceSubmit, color: "error", submissionId });
|
||||
visibleButtons.push({ action: ReviewActions.AdminSubmit, color: "error", submissionId });
|
||||
visibleButtons.push({ action: ReviewActions.BypassSubmit, color: "error", submissionId });
|
||||
}
|
||||
// you can't review your own submission!
|
||||
// note that this means there needs to be more than one person with SubmissionReview
|
||||
|
||||
@@ -9,29 +9,10 @@ import "./(styles)/page.scss";
|
||||
import { ListSortConstants } from "../ts/Sort";
|
||||
|
||||
export default function SubmissionInfoPage() {
|
||||
const [submissions, setSubmissions] = useState<SubmissionList>({Total:0,Submissions:[]})
|
||||
const [submissions, setSubmissions] = useState<SubmissionList|null>(null)
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const cardsPerPage = 24; // built to fit on a 1920x1080 monitor
|
||||
|
||||
const totalPages = Math.ceil(submissions.Total / cardsPerPage);
|
||||
|
||||
const currentCards = submissions.Submissions.slice(
|
||||
(currentPage - 1) * cardsPerPage,
|
||||
currentPage * cardsPerPage
|
||||
);
|
||||
|
||||
const nextPage = () => {
|
||||
if (currentPage < totalPages) {
|
||||
setCurrentPage(currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const prevPage = () => {
|
||||
if (currentPage > 1) {
|
||||
setCurrentPage(currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchSubmissions() {
|
||||
const res = await fetch(`/api/submissions?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`)
|
||||
@@ -53,7 +34,26 @@ export default function SubmissionInfoPage() {
|
||||
</Webpage>
|
||||
}
|
||||
|
||||
if (submissions && submissions.Total == 0) {
|
||||
const totalPages = Math.ceil(submissions.Total / cardsPerPage);
|
||||
|
||||
const currentCards = submissions.Submissions.slice(
|
||||
(currentPage - 1) * cardsPerPage,
|
||||
currentPage * cardsPerPage
|
||||
);
|
||||
|
||||
const nextPage = () => {
|
||||
if (currentPage < totalPages) {
|
||||
setCurrentPage(currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const prevPage = () => {
|
||||
if (currentPage > 1) {
|
||||
setCurrentPage(currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
if (submissions.Total == 0) {
|
||||
return <Webpage>
|
||||
<main>
|
||||
Submissions list is empty.
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function SubmissionInfoPage() {
|
||||
DisplayName: (formData.get("display-name") as string) ?? "unknown", // TEMPORARY! TODO: Change
|
||||
Creator: (formData.get("creator") as string) ?? "unknown", // TEMPORARY! TODO: Change
|
||||
GameID: game,
|
||||
AssetID: Number((formData.get("asset-id") as string) ?? "0"),
|
||||
AssetID: Number((formData.get("asset-id") as string) ?? "-1"),
|
||||
};
|
||||
|
||||
console.log(payload)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const cache = new Map<number, { buffer: Buffer; expires: number }>();
|
||||
const CACHE_TTL = 15 * 60 * 1000;
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ assetId: number }> }
|
||||
@@ -27,6 +30,19 @@ export async function GET(
|
||||
}
|
||||
} catch { }
|
||||
|
||||
const now = Date.now();
|
||||
const cached = cache.get(finalAssetId);
|
||||
|
||||
if (cached && cached.expires > now) {
|
||||
return new NextResponse(cached.buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': cached.buffer.length.toString(),
|
||||
'Cache-Control': `public, max-age=${CACHE_TTL / 1000}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://thumbnails.roblox.com/v1/assets?format=png&size=512x512&assetIds=${finalAssetId}`
|
||||
@@ -54,10 +70,13 @@ export async function GET(
|
||||
const arrayBuffer = await imageResponse.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
cache.set(finalAssetId, { buffer, expires: now + CACHE_TTL });
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': buffer.length.toString(),
|
||||
'Cache-Control': `public, max-age=${CACHE_TTL / 1000}`,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
|
||||
@@ -5,9 +5,12 @@ export async function GET(
|
||||
context: { params: Promise<{ mapId: string }> }
|
||||
): Promise<NextResponse> {
|
||||
// TODO: implement this, we need a cdn for in-game map thumbnails...
|
||||
|
||||
|
||||
const { mapId } = await context.params;
|
||||
const baseUrl = request.nextUrl.origin; // Gets the current base URL
|
||||
|
||||
return NextResponse.redirect(`${baseUrl}/thumbnails/asset/${mapId}`);
|
||||
const protocol = request.headers.get("x-forwarded-proto") || "https";
|
||||
const host = request.headers.get("host");
|
||||
const origin = `${protocol}://${host}`;
|
||||
|
||||
return NextResponse.redirect(`${origin}/thumbnails/asset/${mapId}`);
|
||||
}
|
||||
@@ -1,29 +1,33 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
|
||||
export const config = {
|
||||
matcher: ["/api/:path*", "/auth/:path*"],
|
||||
matcher: ["/api/:path*", "/auth/:path*"],
|
||||
}
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const { pathname, search } = request.nextUrl
|
||||
const { pathname, search } = request.nextUrl
|
||||
|
||||
if (pathname.startsWith("/api")) {
|
||||
if (!process.env.API_HOST) {
|
||||
throw new Error('env variable "API_HOST" is not set')
|
||||
}
|
||||
const apiUrl = new URL(process.env.API_HOST + pathname.replace(/^\/api/, '') + search)
|
||||
return NextResponse.rewrite(apiUrl, { request })
|
||||
} else if (pathname.startsWith("/auth")) {
|
||||
if (pathname.startsWith("/api")) {
|
||||
if (!process.env.API_HOST) {
|
||||
throw new Error('env variable "API_HOST" is not set')
|
||||
}
|
||||
|
||||
const baseUrl = process.env.API_HOST.replace(/\/$/, "");
|
||||
const path = pathname.replace(/^\/api/, "");
|
||||
const apiUrl = new URL(baseUrl + path + search);
|
||||
|
||||
return NextResponse.rewrite(apiUrl, { request });
|
||||
} else if (pathname.startsWith("/auth")) {
|
||||
if (!process.env.AUTH_HOST) {
|
||||
throw new Error('env variable "AUTH_HOST" is not set')
|
||||
}
|
||||
|
||||
const authHost = process.env.AUTH_HOST.replace(/\/$/, "")
|
||||
const path = pathname.replace(/^\/auth/, "")
|
||||
const redirectUrl = new URL(authHost + path + search)
|
||||
|
||||
return NextResponse.redirect(redirectUrl, 302)
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
const authHost = process.env.AUTH_HOST.replace(/\/$/, "");
|
||||
const path = pathname.replace(/^\/auth/, "");
|
||||
const redirectUrl = new URL(authHost + path + search);
|
||||
|
||||
return NextResponse.redirect(redirectUrl, 302);
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user