diff --git a/docs/spec/components/schemas/Balance.yaml b/docs/spec/components/schemas/Balance.yaml index f58b63e..18ab9a5 100644 --- a/docs/spec/components/schemas/Balance.yaml +++ b/docs/spec/components/schemas/Balance.yaml @@ -42,6 +42,11 @@ allOf: description: Referral codes. Returned only for the single user. items: $ref: '#/components/schemas/ReferralCode' + referred_users_count: + type: integer + format: int + description: Number of invited users + example: 13 level: type: integer format: int diff --git a/docs/spec/components/schemas/EventStaticMeta.yaml b/docs/spec/components/schemas/EventStaticMeta.yaml index c59c727..ce48df9 100644 --- a/docs/spec/components/schemas/EventStaticMeta.yaml +++ b/docs/spec/components/schemas/EventStaticMeta.yaml @@ -85,3 +85,8 @@ properties: type: string description: Base64-encoded QR code. Must match the code provided in event type. example: "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABaElEQVR4AWP4//8/AyUYw" + usage_count: + type: integer + format: int + description: Number of uses. Only available to the administrator. + example: 1002 diff --git a/docs/spec/paths/integrations@geo-points-svc@v1@public@event_types@qr.yaml b/docs/spec/paths/integrations@geo-points-svc@v1@public@event_types@qr.yaml new file mode 100644 index 0000000..09880ac --- /dev/null +++ b/docs/spec/paths/integrations@geo-points-svc@v1@public@event_types@qr.yaml @@ -0,0 +1,101 @@ +get: + tags: + - Event types + summary: List QR event types + description: | + Returns configuration of all event types with QR-code. + Basically, it is event static metadata (model `EventStaticMeta`) + for each event type in the system. + Requires **admin** role in JWT. + operationId: getQREventTypes + parameters: + - in: query + name: 'count' + description: Іpecifies whether to return the number of uses of the event + required: false + schema: + type: bool + example: true + - in: query + name: 'filter[name]' + description: Filter by type name. Possible values should be hard-coded in the client. + required: false + schema: + type: array + items: + type: string + example: "passport_scan" + - in: query + name: 'filter[name][not]' + description: | + Inverted filter by type name: excludes provided values + required: false + schema: + type: array + items: + type: string + example: "referral_specific" + - in: query + name: 'filter[flag]' + description: Filter by configuration flags. Values are disjunctive (OR). + required: false + schema: + type: array + items: + type: string + enum: + - active + - not_started + - expired + - disabled + responses: + 200: + description: Success + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/EventType' + 500: + $ref: '#/components/responses/internalError' + +post: + tags: + - Event types + summary: Create event type + description: | + Creates a new event type. Requires **admin** role in JWT. + The type must not be present in the system. + operationId: createEventType + requestBody: + required: true + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/EventType' + responses: + 204: + description: No content + 400: + $ref: '#/components/responses/invalidParameter' + 401: + $ref: '#/components/responses/invalidAuth' + 409: + description: Event type already exists + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Errors' + 500: + $ref: '#/components/responses/internalError' diff --git a/docs/spec/paths/integrations@geo-points-svc@v1@public@events@{id}@qrcode.yaml b/docs/spec/paths/integrations@geo-points-svc@v1@public@events@{id}@qrcode.yaml index 632b381..344e154 100644 --- a/docs/spec/paths/integrations@geo-points-svc@v1@public@events@{id}@qrcode.yaml +++ b/docs/spec/paths/integrations@geo-points-svc@v1@public@events@{id}@qrcode.yaml @@ -11,13 +11,6 @@ patch: schema: type: string example: "059c81dd-2a54-44a8-8142-c15ad8f88949" - - in: header - name: Signature - description: Signature of the request - required: true - schema: - type: string - pattern: '^[a-f0-9]{64}$' requestBody: required: true content: diff --git a/go.sum b/go.sum index 59fee3e..1cd7821 100644 --- a/go.sum +++ b/go.sum @@ -2118,10 +2118,6 @@ github.com/rarimo/geo-auth-svc v0.2.0 h1:yQvcIBNx+Tc1jJdtpWDfyLc0HogU+okA08HEZ55 github.com/rarimo/geo-auth-svc v0.2.0/go.mod h1:SB4bo1xHYDAsBaQGX2+FoEgD3xxqYmcgr4XTTjy4/OM= github.com/rarimo/saver-grpc-lib v1.0.0 h1:MGUVjYg7unmodYczVsLqlqZNkT4CIgKqdo6aQtL1qdE= github.com/rarimo/saver-grpc-lib v1.0.0/go.mod h1:DpugWK5B7Hi0bdC3MPe/9FD2zCxaRwsyykdwxtF1Zgg= -github.com/rarimo/zkverifier-kit v1.0.0 h1:zMW85hyDP3Uk6p9Dk9U4TBzOf0Pry+RNlWpli1tUZ1Q= -github.com/rarimo/zkverifier-kit v1.0.0/go.mod h1:3YDg5dTkDRr4IdfaDHGYetopd6gS/2SuwSeseYTWwNw= -github.com/rarimo/zkverifier-kit v1.1.0-rc.0 h1:5JkObPkEUGwgq4SKJAGInaTBDBILQUHMP4VKZuYPcsM= -github.com/rarimo/zkverifier-kit v1.1.0-rc.0/go.mod h1:3YDg5dTkDRr4IdfaDHGYetopd6gS/2SuwSeseYTWwNw= github.com/rarimo/zkverifier-kit v1.1.0-rc.1 h1:xtmrFEl7eLAE6mi7IQYOOMKFdwXC3gbe39fYQdvKVZg= github.com/rarimo/zkverifier-kit v1.1.0-rc.1/go.mod h1:3YDg5dTkDRr4IdfaDHGYetopd6gS/2SuwSeseYTWwNw= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= diff --git a/internal/data/evtypes/models/event_type.go b/internal/data/evtypes/models/event_type.go index 4b99958..d707f90 100644 --- a/internal/data/evtypes/models/event_type.go +++ b/internal/data/evtypes/models/event_type.go @@ -68,6 +68,7 @@ func (e EventType) Resource() resources.EventStaticMeta { ExpiresAt: e.ExpiresAt, AutoClaim: e.AutoClaim, ActionUrl: e.ActionURL, + Disabled: e.Disabled, Logo: e.Logo, Flag: e.Flag(), } diff --git a/internal/service/handlers/create_balance.go b/internal/service/handlers/create_balance.go index efb61f0..3b31909 100644 --- a/internal/service/handlers/create_balance.go +++ b/internal/service/handlers/create_balance.go @@ -86,7 +86,7 @@ func CreateBalance(w http.ResponseWriter, r *http.Request) { return } - ape.Render(w, newBalanceResponse(*balance, referrals)) + ape.Render(w, newBalanceResponse(*balance, referrals, 0)) } func prepareEventsWithRef(nullifier, refBy string, isGenesisRef bool, r *http.Request) []data.Event { diff --git a/internal/service/handlers/fulfill_qr_event.go b/internal/service/handlers/fulfill_qr_event.go index 3d17b96..32272eb 100644 --- a/internal/service/handlers/fulfill_qr_event.go +++ b/internal/service/handlers/fulfill_qr_event.go @@ -3,7 +3,6 @@ package handlers import ( "net/http" - "github.com/labstack/gommon/log" "github.com/rarimo/geo-auth-svc/pkg/auth" "github.com/rarimo/geo-points-svc/internal/data" "github.com/rarimo/geo-points-svc/internal/data/evtypes" @@ -36,20 +35,6 @@ func FulfillQREvent(w http.ResponseWriter, r *http.Request) { return } - gotSig := r.Header.Get("Signature") - wantSig, err := SigCalculator(r).QREventSignature(event.Nullifier, event.ID, req.Data.Attributes.QrCode) - if err != nil { // must never happen due to preceding validation - Log(r).WithError(err).Error("Failed to calculate HMAC signature") - ape.RenderErr(w, problems.InternalError()) - return - } - - if gotSig != wantSig { - log.Warnf("QR event fulfillment unauthorized access: HMAC signature mismatch: got %s, want %s", gotSig, wantSig) - ape.RenderErr(w, problems.Forbidden()) - return - } - evType := EventTypes(r).Get(event.Type, evtypes.FilterInactive) if evType == nil { Log(r).Infof("Event type %s is inactive", event.Type) diff --git a/internal/service/handlers/get_balance.go b/internal/service/handlers/get_balance.go index 834ec45..4503b63 100644 --- a/internal/service/handlers/get_balance.go +++ b/internal/service/handlers/get_balance.go @@ -42,6 +42,7 @@ func GetBalance(w http.ResponseWriter, r *http.Request) { } var referrals []data.Referral + var referredUsers int if req.ReferralCodes { referrals, err = ReferralsQ(r). FilterByNullifier(req.Nullifier). @@ -52,9 +53,26 @@ func GetBalance(w http.ResponseWriter, r *http.Request) { ape.RenderErr(w, problems.InternalError()) return } + + // Infinite referral codes initially have 0 uses and, + // accordingly, after use, this value will decrease, + // i.e. the number of invited users for this code will + // be an absolute value + // + // A one-time code is considered used if it has 0 uses, + // because the initial value is 1 + for _, ref := range referrals { + if ref.Infinity { + referredUsers += -int(ref.UsageLeft) + continue + } + if ref.UsageLeft == 0 { + referredUsers++ + } + } } - ape.Render(w, newBalanceResponse(*balance, referrals)) + ape.Render(w, newBalanceResponse(*balance, referrals, referredUsers)) } // newBalanceModel forms a balance response without referral fields, which must @@ -75,12 +93,13 @@ func newBalanceModel(balance data.Balance) resources.Balance { } } -func newBalanceResponse(balance data.Balance, referrals []data.Referral) resources.BalanceResponse { +func newBalanceResponse(balance data.Balance, referrals []data.Referral, referredUsers int) resources.BalanceResponse { resp := resources.BalanceResponse{Data: newBalanceModel(balance)} boolP := func(b bool) *bool { return &b } resp.Data.Attributes.IsDisabled = boolP(balance.ReferredBy == nil) resp.Data.Attributes.IsVerified = boolP(balance.IsVerified) + resp.Data.Attributes.ReferredUsersCount = &referredUsers if len(referrals) == 0 { return resp diff --git a/internal/service/handlers/list_qr_event_types.go b/internal/service/handlers/list_qr_event_types.go new file mode 100644 index 0000000..d090f43 --- /dev/null +++ b/internal/service/handlers/list_qr_event_types.go @@ -0,0 +1,61 @@ +package handlers + +import ( + "net/http" + + "github.com/rarimo/geo-auth-svc/pkg/auth" + "github.com/rarimo/geo-points-svc/internal/data" + "github.com/rarimo/geo-points-svc/internal/data/evtypes" + "github.com/rarimo/geo-points-svc/internal/data/evtypes/models" + "github.com/rarimo/geo-points-svc/internal/service/requests" + "github.com/rarimo/geo-points-svc/resources" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func ListQREventTypes(w http.ResponseWriter, r *http.Request) { + req, err := requests.NewListEventTypes(r) + if err != nil { + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + if !auth.Authenticates(UserClaims(r), auth.AdminGrant) { + ape.RenderErr(w, problems.Unauthorized()) + return + } + + types := EventTypes(r).List( + func(ev models.EventType) bool { + return ev.QRCodeValue == nil + }, + evtypes.FilterByNames(req.FilterName...), + evtypes.FilterByFlags(req.FilterFlag...), + func(ev models.EventType) bool { + return len(req.FilterNotName) > 0 && !evtypes.FilterByNames(req.FilterNotName...)(ev) + }, + ) + + resTypes := make([]resources.EventType, len(types)) + for i, t := range types { + resTypes[i] = resources.EventType{ + Key: resources.Key{ + ID: t.Name, + Type: resources.EVENT_TYPE, + }, + Attributes: t.Resource(), + } + resTypes[i].Attributes.QrCodeValue = t.QRCodeValue + if req.Count { + evCount, err := EventsQ(r).FilterByType(t.Name).FilterByStatus(data.EventFulfilled, data.EventClaimed).Count() + if err != nil { + Log(r).WithError(err).Errorf("failed to get %s event usage count", t.Name) + ape.RenderErr(w, problems.InternalError()) + return + } + resTypes[i].Attributes.UsageCount = &evCount + } + } + + ape.Render(w, resources.EventTypeListResponse{Data: resTypes}) +} diff --git a/internal/service/handlers/update_event_type.go b/internal/service/handlers/update_event_type.go index 664bd2d..242709c 100644 --- a/internal/service/handlers/update_event_type.go +++ b/internal/service/handlers/update_event_type.go @@ -53,7 +53,9 @@ func UpdateEventType(w http.ResponseWriter, r *http.Request) { } EventTypes(r).Push(typeModel) - ape.Render(w, newEventTypeResponse(res[0])) + resp := newEventTypeResponse(res[0]) + resp.Data.Attributes.QrCodeValue = typeModel.QRCodeValue + ape.Render(w, resp) } func newEventTypeResponse(evType models.EventType) resources.EventTypeResponse { diff --git a/internal/service/requests/list_event_types.go b/internal/service/requests/list_event_types.go index 1d6ad18..5615b87 100644 --- a/internal/service/requests/list_event_types.go +++ b/internal/service/requests/list_event_types.go @@ -12,6 +12,7 @@ type ListExpiredEvents struct { FilterName []string `filter:"name"` FilterFlag []string `filter:"flag"` FilterNotName []string `url:"filter[name][not]"` + Count bool `url:"count"` } func NewListEventTypes(r *http.Request) (req ListExpiredEvents, err error) { diff --git a/internal/service/router.go b/internal/service/router.go index 65ce03e..d41dbf9 100644 --- a/internal/service/router.go +++ b/internal/service/router.go @@ -48,6 +48,7 @@ func Run(ctx context.Context, cfg config.Config) { }) r.Get("/balances", handlers.Leaderboard) r.Route("/event_types", func(r chi.Router) { + r.With(authMW).Get("/qr", handlers.ListQREventTypes) r.Get("/", handlers.ListEventTypes) r.With(authMW).Post("/", handlers.CreateEventType) r.Get("/{name}", handlers.GetEventType) diff --git a/resources/model_balance_attributes.go b/resources/model_balance_attributes.go index dfd4066..bca5c88 100644 --- a/resources/model_balance_attributes.go +++ b/resources/model_balance_attributes.go @@ -19,6 +19,8 @@ type BalanceAttributes struct { Rank *int `json:"rank,omitempty"` // Referral codes. Returned only for the single user. ReferralCodes *[]ReferralCode `json:"referral_codes,omitempty"` + // Number of invited users + ReferredUsersCount *int `json:"referred_users_count,omitempty"` // Unix timestamp of the last points accruing UpdatedAt int32 `json:"updated_at"` } diff --git a/resources/model_event_static_meta.go b/resources/model_event_static_meta.go index cb8f77d..1008f66 100644 --- a/resources/model_event_static_meta.go +++ b/resources/model_event_static_meta.go @@ -33,4 +33,6 @@ type EventStaticMeta struct { // General event starting date (UTC RFC3339) StartsAt *time.Time `json:"starts_at,omitempty"` Title string `json:"title"` + // Number of uses. Only available to the administrator. + UsageCount *int `json:"usage_count,omitempty"` }