From 122e16b8dec5f895bda01e3b5998d5feac0bbe3c Mon Sep 17 00:00:00 2001 From: violog <51th.apprent1ce.f0rce@gmail.com> Date: Tue, 2 Jul 2024 11:02:25 +0300 Subject: [PATCH] Add QR code scan event --- LICENSE | 21 +++++ config.yaml | 9 ++ ...ventState.yaml => EventClaimingState.yaml} | 2 +- ...ateKey.yaml => EventClaimingStateKey.yaml} | 2 +- .../components/schemas/EventStaticMeta.yaml | 4 + .../components/schemas/FulfillQREvent.yaml | 16 ++++ .../components/schemas/FulfillQREventKey.yaml | 12 +++ ...lic@balances@{nullifier}@join_program.yaml | 2 +- ...c@balances@{nullifier}@verifypassport.yaml | 2 +- ...ints-svc@v1@public@events@{id}@qrcode.yaml | 57 +++++++++++ internal/data/evtypes/main.go | 7 ++ internal/service/handlers/fulfill_qr_event.go | 94 +++++++++++++++++++ internal/service/handlers/verify_passport.go | 10 +- internal/service/requests/fulfill_qr_event.go | 25 +++++ internal/service/router.go | 1 + .../service/workers/nooneisforgotten/main.go | 5 +- resources/model_event_claiming_state.go | 43 +++++++++ .../model_event_claiming_state_attributes.go | 10 ++ resources/model_event_static_meta.go | 2 + resources/model_fulfill_qr_event.go | 43 +++++++++ .../model_fulfill_qr_event_attributes.go | 10 ++ resources/model_resource_type.go | 3 +- verification_key.json | 0 23 files changed, 366 insertions(+), 14 deletions(-) create mode 100644 LICENSE rename docs/spec/components/schemas/{PassportEventState.yaml => EventClaimingState.yaml} (84%) rename docs/spec/components/schemas/{PassportEventStateKey.yaml => EventClaimingStateKey.yaml} (85%) create mode 100644 docs/spec/components/schemas/FulfillQREvent.yaml create mode 100644 docs/spec/components/schemas/FulfillQREventKey.yaml create mode 100644 docs/spec/paths/integrations@geo-points-svc@v1@public@events@{id}@qrcode.yaml create mode 100644 internal/service/handlers/fulfill_qr_event.go create mode 100644 internal/service/requests/fulfill_qr_event.go create mode 100644 resources/model_event_claiming_state.go create mode 100644 resources/model_event_claiming_state_attributes.go create mode 100644 resources/model_fulfill_qr_event.go create mode 100644 resources/model_fulfill_qr_event_attributes.go create mode 100644 verification_key.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3a000f3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 Zero Block Global Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/config.yaml b/config.yaml index 604c251..d8a53ee 100644 --- a/config.yaml +++ b/config.yaml @@ -46,6 +46,15 @@ event_types: short_description: Short description no_auto_open: true auto_claim: true + - name: meetup_participation + title: Prove your participation by scanning QR code + reward: 5 + frequency: unlimited + description: Prove your participation by scanning QR code + short_description: Short description + no_auto_open: true + auto_claim: true + qr_code_value: "qr_code_base64_string" levels: levels: diff --git a/docs/spec/components/schemas/PassportEventState.yaml b/docs/spec/components/schemas/EventClaimingState.yaml similarity index 84% rename from docs/spec/components/schemas/PassportEventState.yaml rename to docs/spec/components/schemas/EventClaimingState.yaml index 4fd519d..ae8892f 100644 --- a/docs/spec/components/schemas/PassportEventState.yaml +++ b/docs/spec/components/schemas/EventClaimingState.yaml @@ -1,5 +1,5 @@ allOf: - - $ref: '#/components/schemas/PassportEventStateKey' + - $ref: '#/components/schemas/EventClaimingStateKey' - type: object required: - attributes diff --git a/docs/spec/components/schemas/PassportEventStateKey.yaml b/docs/spec/components/schemas/EventClaimingStateKey.yaml similarity index 85% rename from docs/spec/components/schemas/PassportEventStateKey.yaml rename to docs/spec/components/schemas/EventClaimingStateKey.yaml index d630d08..0e85597 100644 --- a/docs/spec/components/schemas/PassportEventStateKey.yaml +++ b/docs/spec/components/schemas/EventClaimingStateKey.yaml @@ -10,4 +10,4 @@ properties: pattern: '^0x[0-9a-fA-F]{64}$' type: type: string - enum: [ passport_event_state ] + enum: [ event_claiming_state ] diff --git a/docs/spec/components/schemas/EventStaticMeta.yaml b/docs/spec/components/schemas/EventStaticMeta.yaml index 18fc263..a4fd8c8 100644 --- a/docs/spec/components/schemas/EventStaticMeta.yaml +++ b/docs/spec/components/schemas/EventStaticMeta.yaml @@ -69,3 +69,7 @@ properties: - not_started - expired - disabled + qr_code_value: + type: string + description: Base64-encoded QR code. Must match the code provided in event type. + example: "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABaElEQVR4AWP4//8/AyUYw" diff --git a/docs/spec/components/schemas/FulfillQREvent.yaml b/docs/spec/components/schemas/FulfillQREvent.yaml new file mode 100644 index 0000000..d585d87 --- /dev/null +++ b/docs/spec/components/schemas/FulfillQREvent.yaml @@ -0,0 +1,16 @@ +allOf: + - $ref: '#/components/schemas/FulfillQREventKey' + - type: object + x-go-is-request: true + required: + - attributes + properties: + attributes: + required: + - qr_code + type: object + properties: + qr_code: + type: string + description: Base64-encoded QR code + example: "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABaElEQVR4AWP4//8/AyUYw" diff --git a/docs/spec/components/schemas/FulfillQREventKey.yaml b/docs/spec/components/schemas/FulfillQREventKey.yaml new file mode 100644 index 0000000..7f5efd6 --- /dev/null +++ b/docs/spec/components/schemas/FulfillQREventKey.yaml @@ -0,0 +1,12 @@ +type: object +required: + - id + - type +properties: + id: + type: string + description: Event ID + example: "059c81dd-2a54-44a8-8142-c15ad8f88949" + type: + type: string + enum: [ fulfill_qr_event ] diff --git a/docs/spec/paths/integrations@geo-points-svc@v1@public@balances@{nullifier}@join_program.yaml b/docs/spec/paths/integrations@geo-points-svc@v1@public@balances@{nullifier}@join_program.yaml index 4b1040f..3635b66 100644 --- a/docs/spec/paths/integrations@geo-points-svc@v1@public@balances@{nullifier}@join_program.yaml +++ b/docs/spec/paths/integrations@geo-points-svc@v1@public@balances@{nullifier}@join_program.yaml @@ -35,7 +35,7 @@ post: - data properties: data: - $ref: '#/components/schemas/PassportEventState' + $ref: '#/components/schemas/EventClaimingState' 400: $ref: '#/components/responses/invalidParameter' 401: diff --git a/docs/spec/paths/integrations@geo-points-svc@v1@public@balances@{nullifier}@verifypassport.yaml b/docs/spec/paths/integrations@geo-points-svc@v1@public@balances@{nullifier}@verifypassport.yaml index 62955d5..e51f45d 100644 --- a/docs/spec/paths/integrations@geo-points-svc@v1@public@balances@{nullifier}@verifypassport.yaml +++ b/docs/spec/paths/integrations@geo-points-svc@v1@public@balances@{nullifier}@verifypassport.yaml @@ -37,7 +37,7 @@ post: - data properties: data: - $ref: '#/components/schemas/PassportEventState' + $ref: '#/components/schemas/EventClaimingState' 400: $ref: '#/components/responses/invalidParameter' 401: 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 new file mode 100644 index 0000000..cb66bb6 --- /dev/null +++ b/docs/spec/paths/integrations@geo-points-svc@v1@public@events@{id}@qrcode.yaml @@ -0,0 +1,57 @@ +patch: + tags: + - Events + summary: Fulfill QR code event + description: Fulfill QR code event + operationId: fulfillQREvent + parameters: + - in: path + name: 'id' + required: true + 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: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/ClaimEventKey' + responses: + 200: + description: Success + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/EventClaimingState' + 400: + $ref: '#/components/responses/invalidParameter' + 401: + $ref: '#/components/responses/invalidAuth' + 403: + description: This event type was disabled and cannot be fulfilled + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Errors' + 404: + $ref: '#/components/responses/notFound' + 500: + $ref: '#/components/responses/internalError' diff --git a/internal/data/evtypes/main.go b/internal/data/evtypes/main.go index e14ae53..3acecf9 100644 --- a/internal/data/evtypes/main.go +++ b/internal/data/evtypes/main.go @@ -49,6 +49,7 @@ type EventConfig struct { Disabled bool `fig:"disabled"` ActionURL *url.URL `fig:"action_url"` Logo *url.URL `fig:"logo"` + QRCodeValue string `fig:"qr_code_value"` } func (e EventConfig) Flag() string { @@ -73,6 +74,11 @@ func (e EventConfig) Resource() resources.EventStaticMeta { return &s } + var qrValue *string + if e.QRCodeValue != "" { + qrValue = &e.QRCodeValue + } + return resources.EventStaticMeta{ Name: e.Name, Description: e.Description, @@ -85,6 +91,7 @@ func (e EventConfig) Resource() resources.EventStaticMeta { ActionUrl: safeConv(e.ActionURL), Logo: safeConv(e.Logo), Flag: e.Flag(), + QrCodeValue: qrValue, } } diff --git a/internal/service/handlers/fulfill_qr_event.go b/internal/service/handlers/fulfill_qr_event.go new file mode 100644 index 0000000..e9325f9 --- /dev/null +++ b/internal/service/handlers/fulfill_qr_event.go @@ -0,0 +1,94 @@ +package handlers + +import ( + "net/http" + + "github.com/rarimo/decentralized-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/service/requests" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func FulfillQREvent(w http.ResponseWriter, r *http.Request) { + req, err := requests.NewFulfillQREvent(r) + if err != nil { + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + event, err := EventsQ(r).FilterByID(req.Data.ID).FilterByStatus(data.EventOpen).Get() + if err != nil { + Log(r).WithError(err).Error("Failed to get event by ID") + ape.RenderErr(w, problems.InternalError()) + return + } + if event == nil { + Log(r).Debugf("Event not found for id=%s status=%s", req.Data.ID, data.EventOpen) + ape.RenderErr(w, problems.NotFound()) + return + } + + if !auth.Authenticates(UserClaims(r), auth.UserGrant(event.Nullifier)) { + ape.RenderErr(w, problems.Unauthorized()) + return + } + + evType := EventTypes(r).Get(event.Type, evtypes.FilterInactive) + if evType == nil { + Log(r).Infof("Event type %s is inactive", event.Type) + ape.RenderErr(w, problems.Forbidden()) + return + } + if evType.QRCodeValue != req.Data.Attributes.QrCode { + Log(r).Debugf("QR code for event %s doesn't match: got %s, want %s", event.Type, req.Data.Attributes.QrCode, evType.QRCodeValue) + ape.RenderErr(w, problems.Forbidden()) + return + } + + balance, err := BalancesQ(r).FilterByNullifier(event.Nullifier).FilterDisabled().Get() + if err != nil { + Log(r).WithError(err).Error("Failed to get balance by nullifier") + ape.RenderErr(w, problems.InternalError()) + return + } + if balance == nil { + Log(r).Infof("Balance nullifier=%s is disabled", event.Nullifier) + ape.RenderErr(w, problems.Forbidden()) + return + } + + if !evType.AutoClaim { + _, err = EventsQ(r).FilterByID(event.ID).Update(data.EventFulfilled, nil, nil) + if err != nil { + Log(r).WithError(err).Error("Failed to update event status") + ape.RenderErr(w, problems.InternalError()) + return + } + + ape.Render(w, newEventClaimingStateResponse(balance.Nullifier, false)) + return + } + + err = EventsQ(r).Transaction(func() error { + event, err = claimEvent(r, event, balance) + return err + }) + if err != nil { + Log(r).WithError(err).Errorf("Failed to claim event %s and accrue %d points to the balance %s", + event.ID, evType.Reward, event.Nullifier) + ape.RenderErr(w, problems.InternalError()) + return + } + + // balance should exist cause of previous logic + balance, err = BalancesQ(r).GetWithRank(event.Nullifier) + if err != nil { + Log(r).WithError(err).Error("Failed to get balance by nullifier with rank") + ape.RenderErr(w, problems.InternalError()) + return + } + + ape.Render(w, newClaimEventResponse(*event, evType.Resource(), *balance)) +} diff --git a/internal/service/handlers/verify_passport.go b/internal/service/handlers/verify_passport.go index 16634f0..b223af4 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -122,7 +122,7 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { return } - ape.Render(w, newPassportEventStateResponse(req.Data.ID, nil)) + ape.Render(w, newEventClaimingStateResponse(req.Data.ID, true)) return } @@ -145,14 +145,14 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { return } - ape.Render(w, newPassportEventStateResponse(req.Data.ID, event)) + ape.Render(w, newEventClaimingStateResponse(req.Data.ID, event != nil)) } -func newPassportEventStateResponse(id string, event *data.Event) resources.PassportEventStateResponse { +func newEventClaimingStateResponse(id string, isClaimed bool) resources.PassportEventStateResponse { var res resources.PassportEventStateResponse res.Data.ID = id - res.Data.Type = resources.PASSPORT_EVENT_STATE - res.Data.Attributes.Claimed = event != nil + res.Data.Type = resources.EVENT_CLAIMING_STATE + res.Data.Attributes.Claimed = isClaimed return res } diff --git a/internal/service/requests/fulfill_qr_event.go b/internal/service/requests/fulfill_qr_event.go new file mode 100644 index 0000000..5e1c020 --- /dev/null +++ b/internal/service/requests/fulfill_qr_event.go @@ -0,0 +1,25 @@ +package requests + +import ( + "encoding/json" + "net/http" + + "github.com/go-chi/chi" + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/rarimo/geo-points-svc/resources" +) + +func NewFulfillQREvent(r *http.Request) (req resources.FulfillQrEventRequest, err error) { + id := chi.URLParam(r, "id") + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { + err = newDecodeError("body", err) + return + } + + return req, validation.Errors{ + "data/id": validation.Validate(req.Data.ID, validation.Required, validation.In(id)), + "data/type": validation.Validate(req.Data.Type, validation.Required, validation.In(resources.FULFILL_QR_EVENT)), + "data/attributes/qr_code": validation.Validate(req.Data.Attributes.QrCode, validation.Required, is.Base64), + }.Filter() +} diff --git a/internal/service/router.go b/internal/service/router.go index 332a586..ad8cbf4 100644 --- a/internal/service/router.go +++ b/internal/service/router.go @@ -40,6 +40,7 @@ func Run(ctx context.Context, cfg config.Config) { r.Use(handlers.AuthMiddleware(cfg.Auth(), cfg.Log())) r.Get("/", handlers.ListEvents) r.Get("/{id}", handlers.GetEvent) + r.Patch("/{id}/qrcode", handlers.FulfillQREvent) r.Patch("/{id}", handlers.ClaimEvent) }) r.Get("/balances", handlers.Leaderboard) diff --git a/internal/service/workers/nooneisforgotten/main.go b/internal/service/workers/nooneisforgotten/main.go index 7c33c32..02a2cf2 100644 --- a/internal/service/workers/nooneisforgotten/main.go +++ b/internal/service/workers/nooneisforgotten/main.go @@ -133,10 +133,7 @@ func updateReferralUserEvents(db *pgdb.DB, types evtypes.Types) error { // friends which have passport scanned, if it possible func claimReferralSpecificEvents(db *pgdb.DB, types evtypes.Types, levels config.Levels) error { evType := types.Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive) - if evType == nil { - return nil - } - if !evType.AutoClaim { + if evType == nil || !evType.AutoClaim { return nil } diff --git a/resources/model_event_claiming_state.go b/resources/model_event_claiming_state.go new file mode 100644 index 0000000..690c90c --- /dev/null +++ b/resources/model_event_claiming_state.go @@ -0,0 +1,43 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +import "encoding/json" + +type EventClaimingState struct { + Key + Attributes EventClaimingStateAttributes `json:"attributes"` +} +type EventClaimingStateResponse struct { + Data EventClaimingState `json:"data"` + Included Included `json:"included"` +} + +type EventClaimingStateListResponse struct { + Data []EventClaimingState `json:"data"` + Included Included `json:"included"` + Links *Links `json:"links"` + Meta json.RawMessage `json:"meta,omitempty"` +} + +func (r *EventClaimingStateListResponse) PutMeta(v interface{}) (err error) { + r.Meta, err = json.Marshal(v) + return err +} + +func (r *EventClaimingStateListResponse) GetMeta(out interface{}) error { + return json.Unmarshal(r.Meta, out) +} + +// MustEventClaimingState - returns EventClaimingState from include collection. +// if entry with specified key does not exist - returns nil +// if entry with specified key exists but type or ID mismatches - panics +func (c *Included) MustEventClaimingState(key Key) *EventClaimingState { + var eventClaimingState EventClaimingState + if c.tryFindEntry(key, &eventClaimingState) { + return &eventClaimingState + } + return nil +} diff --git a/resources/model_event_claiming_state_attributes.go b/resources/model_event_claiming_state_attributes.go new file mode 100644 index 0000000..ac59b3a --- /dev/null +++ b/resources/model_event_claiming_state_attributes.go @@ -0,0 +1,10 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +type EventClaimingStateAttributes struct { + // If passport scan event was automatically claimed + Claimed bool `json:"claimed"` +} diff --git a/resources/model_event_static_meta.go b/resources/model_event_static_meta.go index c9d2cc4..9968934 100644 --- a/resources/model_event_static_meta.go +++ b/resources/model_event_static_meta.go @@ -21,6 +21,8 @@ type EventStaticMeta struct { Logo *string `json:"logo,omitempty"` // Unique event code name Name string `json:"name"` + // Base64-encoded QR code. Must match the code provided in event type. + QrCodeValue *string `json:"qr_code_value,omitempty"` // Reward amount in points Reward int64 `json:"reward"` ShortDescription string `json:"short_description"` diff --git a/resources/model_fulfill_qr_event.go b/resources/model_fulfill_qr_event.go new file mode 100644 index 0000000..2eb379c --- /dev/null +++ b/resources/model_fulfill_qr_event.go @@ -0,0 +1,43 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +import "encoding/json" + +type FulfillQrEvent struct { + Key + Attributes FulfillQrEventAttributes `json:"attributes"` +} +type FulfillQrEventRequest struct { + Data FulfillQrEvent `json:"data"` + Included Included `json:"included"` +} + +type FulfillQrEventListRequest struct { + Data []FulfillQrEvent `json:"data"` + Included Included `json:"included"` + Links *Links `json:"links"` + Meta json.RawMessage `json:"meta,omitempty"` +} + +func (r *FulfillQrEventListRequest) PutMeta(v interface{}) (err error) { + r.Meta, err = json.Marshal(v) + return err +} + +func (r *FulfillQrEventListRequest) GetMeta(out interface{}) error { + return json.Unmarshal(r.Meta, out) +} + +// MustFulfillQrEvent - returns FulfillQrEvent from include collection. +// if entry with specified key does not exist - returns nil +// if entry with specified key exists but type or ID mismatches - panics +func (c *Included) MustFulfillQrEvent(key Key) *FulfillQrEvent { + var fulfillQREvent FulfillQrEvent + if c.tryFindEntry(key, &fulfillQREvent) { + return &fulfillQREvent + } + return nil +} diff --git a/resources/model_fulfill_qr_event_attributes.go b/resources/model_fulfill_qr_event_attributes.go new file mode 100644 index 0000000..83a256e --- /dev/null +++ b/resources/model_fulfill_qr_event_attributes.go @@ -0,0 +1,10 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +type FulfillQrEventAttributes struct { + // Base64-encoded QR code + QrCode string `json:"qr_code"` +} diff --git a/resources/model_resource_type.go b/resources/model_resource_type.go index 13cf998..0a668a6 100644 --- a/resources/model_resource_type.go +++ b/resources/model_resource_type.go @@ -12,9 +12,10 @@ const ( CLAIM_EVENT ResourceType = "claim_event" CREATE_BALANCE ResourceType = "create_balance" UPDATE_BALANCE ResourceType = "update_balance" + EVENT_CLAIMING_STATE ResourceType = "event_claiming_state" EVENT ResourceType = "event" EVENT_TYPE ResourceType = "event_type" + FULFILL_QR_EVENT ResourceType = "fulfill_qr_event" JOIN_PROGRAM ResourceType = "join_program" - PASSPORT_EVENT_STATE ResourceType = "passport_event_state" VERIFY_PASSPORT ResourceType = "verify_passport" ) diff --git a/verification_key.json b/verification_key.json new file mode 100644 index 0000000..e69de29