diff --git a/.github/workflows/actions_branch.yaml b/.github/workflows/actions_branch.yaml deleted file mode 100644 index f85552b..0000000 --- a/.github/workflows/actions_branch.yaml +++ /dev/null @@ -1,33 +0,0 @@ -on: - push: - branches: - - 'main' - -jobs: - converge: - name: Converge - runs-on: ubuntu-latest - steps: - - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Install werf - uses: werf/actions/install@v1.2 - - - name: Log in to registry - # This is where you will update the personal access token to GITHUB_TOKEN - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin - - - name: Run echo - run: | - werf version - docker version - echo $GITHUB_REPOSITORY - echo $GITHUB_SHA - - name: Run Build - run: | - . $(werf ci-env github --as-file) - werf export service --tag ghcr.io/$GITHUB_REPOSITORY:$GITHUB_SHA diff --git a/docs/spec/components/schemas/EventStaticMeta.yaml b/docs/spec/components/schemas/EventStaticMeta.yaml index a4fd8c8..c59c727 100644 --- a/docs/spec/components/schemas/EventStaticMeta.yaml +++ b/docs/spec/components/schemas/EventStaticMeta.yaml @@ -2,6 +2,8 @@ type: object description: | Primary event metadata in plain JSON. This is a template to be filled by `dynamic` when it's present. + + This structure is also reused as request body to event type creation and update. required: - name - reward @@ -10,6 +12,8 @@ required: - short_description - frequency - flag + - auto_claim + - disabled properties: name: type: string @@ -64,11 +68,19 @@ properties: If event is disabled, it doesn't matter if it's expired or not started: it has `disabled` flag. + + Do not specify this field on creation: this structure is reused for request body too. enum: - active - not_started - expired - disabled + auto_claim: + type: boolean + description: Whether the event is automatically claimed on fulfillment, or requires manual claim + disabled: + type: boolean + description: Whether the event is disabled in the system. Disabled events can only be retrieved. qr_code_value: type: string description: Base64-encoded QR code. Must match the code provided in event type. diff --git a/docs/spec/components/schemas/JoinProgram.yaml b/docs/spec/components/schemas/JoinProgram.yaml deleted file mode 100644 index 93e61f7..0000000 --- a/docs/spec/components/schemas/JoinProgram.yaml +++ /dev/null @@ -1,15 +0,0 @@ -allOf: - - $ref: '#/components/schemas/JoinProgramKey' - - type: object - x-go-is-request: true - required: - - attributes - properties: - attributes: - type: object - required: - - country - properties: - country: - type: string - example: "5589842" \ No newline at end of file diff --git a/docs/spec/components/schemas/JoinProgramKey.yaml b/docs/spec/components/schemas/JoinProgramKey.yaml deleted file mode 100644 index a25b10b..0000000 --- a/docs/spec/components/schemas/JoinProgramKey.yaml +++ /dev/null @@ -1,13 +0,0 @@ -type: object -required: - - id - - type -properties: - id: - type: string - description: Nullifier of the points owner - example: "0x123...abc" - pattern: '^0x[0-9a-fA-F]{64}$' - type: - type: string - enum: [ join_program ] diff --git a/docs/spec/components/schemas/VerifyPassport.yaml b/docs/spec/components/schemas/VerifyPassport.yaml index c0db2d7..2dd352e 100644 --- a/docs/spec/components/schemas/VerifyPassport.yaml +++ b/docs/spec/components/schemas/VerifyPassport.yaml @@ -8,18 +8,12 @@ allOf: attributes: required: - anonymous_id - - country type: object properties: anonymous_id: type: string description: Unique identifier of the passport. example: "2bd3a2532096fee10a45a40e444a11b4d00a707f3459376087747de05996fbf5" - country: - type: string - description: | - ISO 3166-1 alpha-3 country code, must match the one provided in `proof`. - example: "UKR" proof: type: object format: types.ZKProof diff --git a/docs/spec/paths/integrations@geo-points-svc@v1@public@event_types.yaml b/docs/spec/paths/integrations@geo-points-svc@v1@public@event_types.yaml index e3805ab..1dcc66e 100644 --- a/docs/spec/paths/integrations@geo-points-svc@v1@public@event_types.yaml +++ b/docs/spec/paths/integrations@geo-points-svc@v1@public@event_types.yaml @@ -1,6 +1,6 @@ get: tags: - - Events + - Event types summary: List event types description: | Returns public configuration of all event types. @@ -56,3 +56,38 @@ get: $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@event_types@{name}.yaml b/docs/spec/paths/integrations@geo-points-svc@v1@public@event_types@{name}.yaml new file mode 100644 index 0000000..ab20afd --- /dev/null +++ b/docs/spec/paths/integrations@geo-points-svc@v1@public@event_types@{name}.yaml @@ -0,0 +1,69 @@ +get: + tags: + - Event types + summary: Get event type + description: Returns public configuration of event type by its unique name + operationId: getEventType + responses: + 200: + description: Success + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/EventType' + 404: + $ref: '#/components/responses/notFound' + 500: + $ref: '#/components/responses/internalError' + +patch: + tags: + - Event types + summary: Update event type + description: | + Update an existing event type. Requires **admin** role in JWT. + **All attributes** are updated, ensure to pass every existing field too. + Although this is not JSON:API compliant, it is much easier to work with + in Go, because differentiating between `{}` and `{"field": null}` + requires custom unmarshalling implementation. + operationId: updateEventType + requestBody: + required: true + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/EventType' + responses: + 200: + description: Success + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/EventType' + 400: + $ref: '#/components/responses/invalidParameter' + 401: + $ref: '#/components/responses/invalidAuth' + 404: + $ref: '#/components/responses/notFound' + 500: + $ref: '#/components/responses/internalError' diff --git a/generate.sh b/generate.sh index 10c2b93..52caa62 100755 --- a/generate.sh +++ b/generate.sh @@ -51,6 +51,11 @@ function parseArgs { function generate { (cd docs && npm run build) + if [[ ! -d "${GENERATED}" ]]; then + mkdir -p "${GENERATED}" + else + rm -rf "${GENERATED}"/* + fi docker run --rm -v "${OPENAPI_DIR}":/openapi -v "${GENERATED}":/generated "${GENERATOR_IMAGE}" \ generate -pkg "${PACKAGE_NAME}" --raw-formats-as-types --meta-for-lists goimports -w ${GENERATED} diff --git a/go.mod b/go.mod index 6ca3363..a544c33 100644 --- a/go.mod +++ b/go.mod @@ -11,14 +11,12 @@ require ( github.com/go-co-op/gocron/v2 v2.2.2 github.com/go-ozzo/ozzo-validation/v4 v4.3.0 github.com/google/jsonapi v1.0.0 - github.com/google/uuid v1.6.0 github.com/iden3/go-rapidsnark/types v0.0.3 github.com/labstack/gommon v0.4.0 - github.com/rarimo/decentralized-auth-svc v0.0.0-20240522134350-2694eafa9509 + github.com/rarimo/geo-auth-svc v0.2.0 github.com/rarimo/saver-grpc-lib v1.0.0 github.com/rarimo/zkverifier-kit v1.0.0 github.com/rubenv/sql-migrate v1.6.1 - github.com/stretchr/testify v1.9.0 gitlab.com/distributed_lab/ape v1.7.1 gitlab.com/distributed_lab/figure/v3 v3.1.4 gitlab.com/distributed_lab/kit v1.11.3 @@ -83,6 +81,7 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/btree v1.1.2 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect @@ -136,6 +135,7 @@ require ( github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.18.2 // indirect + github.com/stretchr/testify v1.9.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/supranational/blst v0.3.11 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect diff --git a/go.sum b/go.sum index e696ff5..5cda72d 100644 --- a/go.sum +++ b/go.sum @@ -2114,8 +2114,8 @@ github.com/rarimo/broadcaster-svc v1.0.2 h1:ExQcjjWCRP5+POLDlZHrTD1ffUsBH+Dgv5FA github.com/rarimo/broadcaster-svc v1.0.2/go.mod h1:lYIHy+X4IqQt4eBdtMN/V352H3EV0/gO8G+32SFwUWI= github.com/rarimo/cosmos-sdk v0.46.7 h1:jU2PiWzc+19SF02cXM0O0puKPeH1C6Q6t2lzJ9s1ejc= github.com/rarimo/cosmos-sdk v0.46.7/go.mod h1:fqKqz39U5IlEFb4nbQ72951myztsDzFKKDtffYJ63nk= -github.com/rarimo/decentralized-auth-svc v0.0.0-20240522134350-2694eafa9509 h1:U3gu/Z61tVIVEVoWL1YdwiNzkaXlkgd7cSeEslVfsLI= -github.com/rarimo/decentralized-auth-svc v0.0.0-20240522134350-2694eafa9509/go.mod h1:V9XSqZSBN/YmLdI6PW6GL2xNeJ94IXAnhcuvyQfVBL8= +github.com/rarimo/geo-auth-svc v0.2.0 h1:yQvcIBNx+Tc1jJdtpWDfyLc0HogU+okA08HEZ55wv5U= +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= diff --git a/internal/assets/migrations/001_initial.sql b/internal/assets/migrations/001_initial.sql index c3d74a4..f227975 100644 --- a/internal/assets/migrations/001_initial.sql +++ b/internal/assets/migrations/001_initial.sql @@ -34,15 +34,12 @@ CREATE TABLE IF NOT EXISTS referrals CREATE INDEX IF NOT EXISTS referrals_nullifier_index ON referrals (nullifier); -DROP TYPE IF EXISTS event_status; -CREATE TYPE event_status AS ENUM ('open', 'fulfilled', 'claimed'); - CREATE TABLE IF NOT EXISTS events ( id uuid PRIMARY KEY NOT NULL default gen_random_uuid(), nullifier TEXT NOT NULL REFERENCES balances (nullifier), type text NOT NULL, - status event_status NOT NULL, + status text NOT NULL, created_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()), updated_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()), meta jsonb, @@ -60,10 +57,28 @@ CREATE TRIGGER set_updated_at FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at(); +CREATE TABLE IF NOT EXISTS event_types +( + name text PRIMARY KEY NOT NULL, + short_description text NOT NULL, + description text NOT NULL, + reward integer NOT NULL, + title text NOT NULL, + frequency text NOT NULL, + starts_at timestamp, + expires_at timestamp, + no_auto_open BOOLEAN NOT NULL DEFAULT FALSE, + auto_claim BOOLEAN NOT NULL DEFAULT FALSE, + disabled BOOLEAN NOT NULL DEFAULT FALSE, + action_url text, + logo text, + qr_code_value text +); + -- +migrate Down +DROP TABLE IF EXISTS event_types; DROP TABLE IF EXISTS events; DROP TABLE IF EXISTS referrals; DROP TABLE IF EXISTS balances; -DROP TYPE IF EXISTS event_status; DROP FUNCTION IF EXISTS trigger_set_updated_at(); diff --git a/internal/cli/workers.go b/internal/cli/workers.go index 02e7666..c8ece47 100644 --- a/internal/cli/workers.go +++ b/internal/cli/workers.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/rarimo/geo-points-svc/internal/config" + "github.com/rarimo/geo-points-svc/internal/data/evtypes" "github.com/rarimo/geo-points-svc/internal/service" "github.com/rarimo/geo-points-svc/internal/service/workers/expirywatch" "github.com/rarimo/geo-points-svc/internal/service/workers/nooneisforgotten" @@ -17,6 +18,7 @@ func runServices(ctx context.Context, cfg config.Config, wg *sync.WaitGroup) { var ( reopenerSig = make(chan struct{}) expiryWatchSig = make(chan struct{}) + evTypesSig = make(chan struct{}) noOneIsForgottenSig = make(chan struct{}) ) @@ -28,7 +30,11 @@ func runServices(ctx context.Context, cfg config.Config, wg *sync.WaitGroup) { }() } - // these services can safely run in parallel and don't have dependencies + // all services depend on event types + run(func() { evtypes.Init(ctx, cfg, evTypesSig) }) + <-evTypesSig + + // these services can safely run in parallel and depend only on event types run(func() { reopener.Run(ctx, cfg, reopenerSig) }) run(func() { expirywatch.Run(ctx, cfg, expiryWatchSig) }) diff --git a/internal/config/main.go b/internal/config/main.go index 53be374..9506b45 100644 --- a/internal/config/main.go +++ b/internal/config/main.go @@ -1,7 +1,8 @@ package config import ( - "github.com/rarimo/decentralized-auth-svc/pkg/auth" + "github.com/rarimo/geo-auth-svc/pkg/auth" + "github.com/rarimo/geo-auth-svc/pkg/hmacsig" "github.com/rarimo/geo-points-svc/internal/data/evtypes" "github.com/rarimo/saver-grpc-lib/broadcaster" zk "github.com/rarimo/zkverifier-kit" @@ -18,10 +19,10 @@ type Config interface { auth.Auther //nolint:misspell broadcaster.Broadcasterer evtypes.EventTypeser + hmacsig.SigCalculatorProvider Levels() Levels Verifier() *zk.Verifier - SigVerifier() []byte } type config struct { @@ -32,22 +33,23 @@ type config struct { broadcaster.Broadcasterer identity.VerifierProvider evtypes.EventTypeser + hmacsig.SigCalculatorProvider - levels comfig.Once - verifier comfig.Once - sigVerifier comfig.Once - getter kv.Getter + levels comfig.Once + verifier comfig.Once + getter kv.Getter } func New(getter kv.Getter) Config { return &config{ - getter: getter, - Databaser: pgdb.NewDatabaser(getter), - Listenerer: comfig.NewListenerer(getter), - Logger: comfig.NewLogger(getter, comfig.LoggerOpts{}), - Auther: auth.NewAuther(getter), //nolint:misspell - Broadcasterer: broadcaster.New(getter), - VerifierProvider: identity.NewVerifierProvider(getter), - EventTypeser: evtypes.NewConfig(getter), + getter: getter, + Databaser: pgdb.NewDatabaser(getter), + Listenerer: comfig.NewListenerer(getter), + Logger: comfig.NewLogger(getter, comfig.LoggerOpts{}), + Auther: auth.NewAuther(getter), //nolint:misspell + Broadcasterer: broadcaster.New(getter), + VerifierProvider: identity.NewVerifierProvider(getter), + EventTypeser: evtypes.NewConfig(getter), + SigCalculatorProvider: hmacsig.NewCalculatorProvider(getter), } } diff --git a/internal/config/sig_verifier.go b/internal/config/sig_verifier.go deleted file mode 100644 index 6680fca..0000000 --- a/internal/config/sig_verifier.go +++ /dev/null @@ -1,31 +0,0 @@ -package config - -import ( - "encoding/hex" - "fmt" - - "gitlab.com/distributed_lab/figure/v3" - "gitlab.com/distributed_lab/kit/kv" -) - -func (c *config) SigVerifier() []byte { - return c.sigVerifier.Do(func() interface{} { - var cfg struct { - VerificationKey string `fig:"verification_key,required"` - } - - err := figure.Out(&cfg). - From(kv.MustGetStringMap(c.getter, "sig_verifier")). - Please() - if err != nil { - panic(fmt.Errorf("failed to figure out sig_verifier: %w", err)) - } - - key, err := hex.DecodeString(cfg.VerificationKey) - if err != nil { - panic(fmt.Errorf("verification_key is not a hex: %w", err)) - } - - return key - }).([]byte) -} diff --git a/internal/data/event_types.go b/internal/data/event_types.go new file mode 100644 index 0000000..777baa0 --- /dev/null +++ b/internal/data/event_types.go @@ -0,0 +1,16 @@ +package data + +import ( + "github.com/rarimo/geo-points-svc/internal/data/evtypes/models" +) + +type EventTypesQ interface { + New() EventTypesQ + Insert(...models.EventType) error + Update(fields map[string]any) (*models.EventType, error) + Transaction(func() error) error + + Select() ([]models.EventType, error) + Get(name string) (*models.EventType, error) + FilterByNames(...string) EventTypesQ +} diff --git a/internal/data/evtypes/config.go b/internal/data/evtypes/config.go index 9f03cc4..26187bd 100644 --- a/internal/data/evtypes/config.go +++ b/internal/data/evtypes/config.go @@ -3,13 +3,14 @@ package evtypes import ( "fmt" + "github.com/rarimo/geo-points-svc/internal/data/evtypes/models" "gitlab.com/distributed_lab/figure/v3" "gitlab.com/distributed_lab/kit/comfig" "gitlab.com/distributed_lab/kit/kv" ) type EventTypeser interface { - EventTypes() Types + EventTypes() *Types } type config struct { @@ -21,10 +22,10 @@ func NewConfig(getter kv.Getter) EventTypeser { return &config{getter: getter} } -func (c *config) EventTypes() Types { +func (c *config) EventTypes() *Types { return c.once.Do(func() interface{} { var raw struct { - Types []EventConfig `fig:"types,required"` + Types []models.EventType `fig:"types,required"` } err := figure.Out(&raw). @@ -34,7 +35,7 @@ func (c *config) EventTypes() Types { panic(fmt.Errorf("failed to figure out event_types: %s", err)) } - m := make(map[string]EventConfig, len(raw.Types)) + m := make(map[string]models.EventType, len(raw.Types)) for _, t := range raw.Types { if !checkFreqValue(t.Frequency) { panic(fmt.Errorf("invalid frequency: %s", t.Frequency)) @@ -48,13 +49,13 @@ func (c *config) EventTypes() Types { m[t.Name] = t } - return Types{m, raw.Types} - }).(Types) + return &Types{m: m, list: raw.Types} + }).(*Types) } -func checkFreqValue(f Frequency) bool { +func checkFreqValue(f models.Frequency) bool { switch f { - case OneTime, Daily, Weekly, Unlimited: + case models.OneTime, models.Daily, models.Weekly, models.Unlimited: return true } return false diff --git a/internal/data/evtypes/filters.go b/internal/data/evtypes/filters.go index 41ae69b..c96d89f 100644 --- a/internal/data/evtypes/filters.go +++ b/internal/data/evtypes/filters.go @@ -2,6 +2,8 @@ package evtypes import ( "time" + + "github.com/rarimo/geo-points-svc/internal/data/evtypes/models" ) // Filter functions work in the following way: @@ -12,32 +14,32 @@ import ( // 2. For other Filter* functions, the configs matching the filter are excluded: // FilterExpired eliminates all expired events (instead of including only them) -type filter func(EventConfig) bool +type filter func(models.EventType) bool -func FilterExpired(ev EventConfig) bool { +func FilterExpired(ev models.EventType) bool { return ev.ExpiresAt != nil && ev.ExpiresAt.Before(time.Now().UTC()) } -func FilterNotStarted(ev EventConfig) bool { +func FilterNotStarted(ev models.EventType) bool { return ev.StartsAt != nil && ev.StartsAt.After(time.Now().UTC()) } -func FilterInactive(ev EventConfig) bool { +func FilterInactive(ev models.EventType) bool { return ev.Disabled || FilterExpired(ev) || FilterNotStarted(ev) } -func FilterNotOpenable(ev EventConfig) bool { +func FilterNotOpenable(ev models.EventType) bool { return FilterInactive(ev) || ev.NoAutoOpen } -func FilterByFrequency(f Frequency) func(EventConfig) bool { - return func(ev EventConfig) bool { +func FilterByFrequency(f models.Frequency) func(models.EventType) bool { + return func(ev models.EventType) bool { return ev.Frequency != f } } -func FilterByNames(names ...string) func(EventConfig) bool { - return func(ev EventConfig) bool { +func FilterByNames(names ...string) func(models.EventType) bool { + return func(ev models.EventType) bool { if len(names) == 0 { return false } @@ -50,8 +52,8 @@ func FilterByNames(names ...string) func(EventConfig) bool { } } -func FilterByFlags(flags ...string) func(EventConfig) bool { - return func(ev EventConfig) bool { +func FilterByFlags(flags ...string) func(models.EventType) bool { + return func(ev models.EventType) bool { if len(flags) == 0 { return false } @@ -64,7 +66,7 @@ func FilterByFlags(flags ...string) func(EventConfig) bool { } } -func isFiltered(ev EventConfig, filters ...filter) bool { +func isFiltered(ev models.EventType, filters ...filter) bool { for _, f := range filters { if f(ev) { return true diff --git a/internal/data/evtypes/init.go b/internal/data/evtypes/init.go new file mode 100644 index 0000000..1dec196 --- /dev/null +++ b/internal/data/evtypes/init.go @@ -0,0 +1,42 @@ +package evtypes + +import ( + "context" + "fmt" + + "github.com/rarimo/geo-points-svc/internal/data/pg" + "gitlab.com/distributed_lab/kit/comfig" + "gitlab.com/distributed_lab/kit/pgdb" +) + +type extConfig interface { + comfig.Logger + pgdb.Databaser + EventTypeser +} + +func Init(_ context.Context, cfg extConfig, sig chan struct{}) { + var ( + log = cfg.Log().WithField("who", "evtypes") + q = pg.NewEventTypes(cfg.DB().Clone()) + types = cfg.EventTypes() + ) + + dbTypes, err := q.New().Select() + if err != nil { + panic(fmt.Errorf("select all event types: %w", err)) + } + + defer func() { + types.dbSynced = true + sig <- struct{}{} + }() + + if len(dbTypes) == 0 { + log.Info("No event types in database") + return + } + + log.Debugf("Adding/overwriting event types from DB: %+v", dbTypes) + types.Push(dbTypes...) +} diff --git a/internal/data/evtypes/main.go b/internal/data/evtypes/main.go index 3acecf9..bc9b4ea 100644 --- a/internal/data/evtypes/main.go +++ b/internal/data/evtypes/main.go @@ -1,106 +1,17 @@ package evtypes import ( - "net/url" - "time" - "github.com/rarimo/geo-points-svc/internal/data" - "github.com/rarimo/geo-points-svc/resources" -) - -type Frequency string - -func (f Frequency) String() string { - return string(f) -} - -const ( - OneTime Frequency = "one-time" - Daily Frequency = "daily" - Weekly Frequency = "weekly" - Unlimited Frequency = "unlimited" -) - -const ( - TypeFreeWeekly = "free_weekly" - TypeBeReferred = "be_referred" - TypeReferralSpecific = "referral_specific" - TypePassportScan = "passport_scan" + "github.com/rarimo/geo-points-svc/internal/data/evtypes/models" ) -const ( - FlagActive = "active" - FlagNotStarted = "not_started" - FlagExpired = "expired" - FlagDisabled = "disabled" -) - -type EventConfig struct { - Name string `fig:"name,required"` - Description string `fig:"description,required"` - ShortDescription string `fig:"short_description,required"` - Reward int64 `fig:"reward,required"` - Title string `fig:"title,required"` - Frequency Frequency `fig:"frequency,required"` - StartsAt *time.Time `fig:"starts_at"` - ExpiresAt *time.Time `fig:"expires_at"` - NoAutoOpen bool `fig:"no_auto_open"` - AutoClaim bool `fig:"auto_claim"` - 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 { - switch { - case e.Disabled: - return FlagDisabled - case FilterNotStarted(e): - return FlagNotStarted - case FilterExpired(e): - return FlagExpired - default: - return FlagActive - } -} - -func (e EventConfig) Resource() resources.EventStaticMeta { - safeConv := func(u *url.URL) *string { - if u == nil { - return nil - } - s := u.String() - return &s - } - - var qrValue *string - if e.QRCodeValue != "" { - qrValue = &e.QRCodeValue - } - - return resources.EventStaticMeta{ - Name: e.Name, - Description: e.Description, - ShortDescription: e.ShortDescription, - Reward: e.Reward, - Title: e.Title, - Frequency: e.Frequency.String(), - StartsAt: e.StartsAt, - ExpiresAt: e.ExpiresAt, - ActionUrl: safeConv(e.ActionURL), - Logo: safeConv(e.Logo), - Flag: e.Flag(), - QrCodeValue: qrValue, - } -} - type Types struct { - m map[string]EventConfig - list []EventConfig + m map[string]models.EventType + list []models.EventType + dbSynced bool } -func (t Types) Get(name string, filters ...filter) *EventConfig { +func (t *Types) Get(name string, filters ...filter) *models.EventType { t.ensureInitialized() v, ok := t.m[name] if !ok || isFiltered(v, filters...) { @@ -110,9 +21,9 @@ func (t Types) Get(name string, filters ...filter) *EventConfig { return &v } -func (t Types) List(filters ...filter) []EventConfig { +func (t *Types) List(filters ...filter) []models.EventType { t.ensureInitialized() - res := make([]EventConfig, 0, len(t.list)) + res := make([]models.EventType, 0, len(t.list)) for _, v := range t.list { if isFiltered(v, filters...) { continue @@ -122,7 +33,7 @@ func (t Types) List(filters ...filter) []EventConfig { return res } -func (t Types) Names(filters ...filter) []string { +func (t *Types) Names(filters ...filter) []string { t.ensureInitialized() res := make([]string, 0, len(t.list)) for _, v := range t.list { @@ -134,7 +45,7 @@ func (t Types) Names(filters ...filter) []string { return res } -func (t Types) PrepareEvents(nullifier string, filters ...filter) []data.Event { +func (t *Types) PrepareEvents(nullifier string, filters ...filter) []data.Event { t.ensureInitialized() const extraCap = 1 // in case we append to the resulting slice outside the function events := make([]data.Event, 0, len(t.list)+extraCap) @@ -145,7 +56,7 @@ func (t Types) PrepareEvents(nullifier string, filters ...filter) []data.Event { } status := data.EventOpen - if et.Name == TypeFreeWeekly { + if et.Name == models.TypeFreeWeekly { status = data.EventFulfilled } @@ -159,8 +70,27 @@ func (t Types) PrepareEvents(nullifier string, filters ...filter) []data.Event { return events } -func (t Types) ensureInitialized() { - if t.m == nil || t.list == nil { +// Push adds new event type or overwrites existing one +func (t *Types) Push(types ...models.EventType) { + for _, et := range types { + _, ok := t.m[et.Name] + t.m[et.Name] = et + if !ok { + t.list = append(t.list, et) + continue + } + + for i := range t.list { + if t.list[i].Name == et.Name { + t.list[i] = et + break + } + } + } +} + +func (t *Types) ensureInitialized() { + if t.m == nil || t.list == nil || !t.dbSynced { panic("event types are not correctly initialized") } } diff --git a/internal/data/evtypes/models/event_type.go b/internal/data/evtypes/models/event_type.go new file mode 100644 index 0000000..f7de817 --- /dev/null +++ b/internal/data/evtypes/models/event_type.go @@ -0,0 +1,109 @@ +package models + +import ( + "net/url" + "time" + + "github.com/rarimo/geo-points-svc/resources" +) + +type EventType struct { + Name string `fig:"name,required" db:"name"` + Description string `fig:"description,required" db:"description"` + ShortDescription string `fig:"short_description,required" db:"short_description"` + Reward int64 `fig:"reward,required" db:"reward"` + Title string `fig:"title,required" db:"title"` + Frequency Frequency `fig:"frequency,required" db:"frequency"` + StartsAt *time.Time `fig:"starts_at" db:"starts_at"` + ExpiresAt *time.Time `fig:"expires_at" db:"expires_at"` + NoAutoOpen bool `fig:"no_auto_open" db:"no_auto_open"` + AutoClaim bool `fig:"auto_claim" db:"auto_claim"` + Disabled bool `fig:"disabled" db:"disabled"` + ActionURL *url.URL `fig:"action_url" db:"action_url"` + Logo *url.URL `fig:"logo" db:"logo"` + QRCodeValue *string `fig:"qr_code_value" db:"qr_code_value"` +} + +func ResourceToModel(r resources.EventStaticMeta) EventType { + uConv := func(s *string) *url.URL { + if s == nil { + return nil + } + u, _ := url.Parse(*s) + return u + } + + // intended that no_auto_open field is not accessible through API due to being + // related only to back-end + return EventType{ + Name: r.Name, + Description: r.Description, + ShortDescription: r.ShortDescription, + Reward: r.Reward, + Title: r.Title, + Frequency: Frequency(r.Frequency), + StartsAt: r.StartsAt, + ExpiresAt: r.ExpiresAt, + AutoClaim: r.AutoClaim, + Disabled: r.Disabled, + ActionURL: uConv(r.ActionUrl), + Logo: uConv(r.Logo), + QRCodeValue: r.QrCodeValue, + } +} + +func (e EventType) Flag() string { + switch { + case e.Disabled: + return FlagDisabled + case e.StartsAt != nil && e.StartsAt.After(time.Now().UTC()): + return FlagNotStarted + case e.ExpiresAt != nil && e.ExpiresAt.Before(time.Now().UTC()): + return FlagExpired + default: + return FlagActive + } +} + +func (e EventType) Resource() resources.EventStaticMeta { + safeConv := func(u *url.URL) *string { + if u == nil { + return nil + } + s := u.String() + return &s + } + + return resources.EventStaticMeta{ + Name: e.Name, + Description: e.Description, + ShortDescription: e.ShortDescription, + Reward: e.Reward, + Title: e.Title, + Frequency: e.Frequency.String(), + StartsAt: e.StartsAt, + ExpiresAt: e.ExpiresAt, + AutoClaim: e.AutoClaim, + ActionUrl: safeConv(e.ActionURL), + Logo: safeConv(e.Logo), + Flag: e.Flag(), + QrCodeValue: e.QRCodeValue, + } +} + +func (e EventType) ForUpdate() map[string]any { + return map[string]any{ + "description": e.Description, + "short_description": e.ShortDescription, + "reward": e.Reward, + "title": e.Title, + "frequency": e.Frequency, + "starts_at": e.StartsAt, + "expires_at": e.ExpiresAt, + "auto_claim": e.AutoClaim, + "disabled": e.Disabled, + "action_url": e.ActionURL, + "logo": e.Logo, + "qr_code_value": e.QRCodeValue, + } +} diff --git a/internal/data/evtypes/models/extra.go b/internal/data/evtypes/models/extra.go new file mode 100644 index 0000000..15b85ef --- /dev/null +++ b/internal/data/evtypes/models/extra.go @@ -0,0 +1,28 @@ +package models + +type Frequency string + +func (f Frequency) String() string { + return string(f) +} + +const ( + OneTime Frequency = "one-time" + Daily Frequency = "daily" + Weekly Frequency = "weekly" + Unlimited Frequency = "unlimited" +) + +const ( + TypeFreeWeekly = "free_weekly" + TypeBeReferred = "be_referred" + TypeReferralSpecific = "referral_specific" + TypePassportScan = "passport_scan" +) + +const ( + FlagActive = "active" + FlagNotStarted = "not_started" + FlagExpired = "expired" + FlagDisabled = "disabled" +) diff --git a/internal/data/pg/event_types.go b/internal/data/pg/event_types.go new file mode 100644 index 0000000..4dc434d --- /dev/null +++ b/internal/data/pg/event_types.go @@ -0,0 +1,123 @@ +package pg + +import ( + "database/sql" + "errors" + "fmt" + + "github.com/Masterminds/squirrel" + "github.com/rarimo/geo-points-svc/internal/data" + "github.com/rarimo/geo-points-svc/internal/data/evtypes/models" + "gitlab.com/distributed_lab/kit/pgdb" +) + +const eventTypesTable = "event_types" + +type eventTypes struct { + db *pgdb.DB + selector squirrel.SelectBuilder + updater squirrel.UpdateBuilder +} + +func NewEventTypes(db *pgdb.DB) data.EventTypesQ { + return &eventTypes{ + db: db, + selector: squirrel.Select("*").From(eventTypesTable), + updater: squirrel.Update(eventTypesTable), + } +} + +func (q *eventTypes) New() data.EventTypesQ { + return NewEventTypes(q.db) +} + +func (q *eventTypes) Insert(eventTypes ...models.EventType) error { + if len(eventTypes) == 0 { + return nil + } + + stmt := squirrel.Insert(eventTypesTable).Columns( + "name", + "description", + "short_description", + "reward", + "title", + "frequency", + "starts_at", + "expires_at", + "no_auto_open", + "auto_claim", + "disabled", + "action_url", + "logo", + "qr_code_value", + ) + for _, eventType := range eventTypes { + stmt = stmt.Values( + eventType.Name, + eventType.Description, + eventType.ShortDescription, + eventType.Reward, + eventType.Title, + eventType.Frequency, + eventType.StartsAt, + eventType.ExpiresAt, + eventType.NoAutoOpen, + eventType.AutoClaim, + eventType.Disabled, + eventType.ActionURL, + eventType.Logo, + eventType.QRCodeValue, + ) + } + + if err := q.db.Exec(stmt); err != nil { + return fmt.Errorf("insert event types [%+v]: %w", eventTypes, err) + } + + return nil +} + +func (q *eventTypes) Update(fields map[string]any) (res *models.EventType, err error) { + stmt := q.updater.SetMap(fields).Suffix("RETURNING *") + + if err = q.db.Get(&res, stmt); err != nil { + return nil, fmt.Errorf("update event type with map %+v: %w", fields, err) + } + + return res, nil +} + +func (q *eventTypes) Transaction(f func() error) error { + return q.db.Transaction(f) +} + +func (q *eventTypes) Select() ([]models.EventType, error) { + var res []models.EventType + + if err := q.db.Select(&res, q.selector); err != nil { + return nil, fmt.Errorf("select event types: %w", err) + } + + return res, nil +} + +func (q *eventTypes) Get(name string) (*models.EventType, error) { + stmt := q.selector.Where(squirrel.Eq{"name": name}) + + var res models.EventType + if err := q.db.Get(&res, stmt); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, fmt.Errorf("get event type: %w", err) + } + + return &res, nil +} + +func (q *eventTypes) FilterByNames(names ...string) data.EventTypesQ { + q.selector = q.selector.Where(squirrel.Eq{"name": names}) + q.updater = q.updater.Where(squirrel.Eq{"name": names}) + return q +} diff --git a/internal/service/handlers/claim_event.go b/internal/service/handlers/claim_event.go index db0831f..1ff7439 100644 --- a/internal/service/handlers/claim_event.go +++ b/internal/service/handlers/claim_event.go @@ -4,7 +4,7 @@ import ( "fmt" "net/http" - "github.com/rarimo/decentralized-auth-svc/pkg/auth" + "github.com/rarimo/geo-auth-svc/pkg/auth" "github.com/rarimo/geo-points-svc/internal/config" "github.com/rarimo/geo-points-svc/internal/data" "github.com/rarimo/geo-points-svc/internal/data/evtypes" diff --git a/internal/service/handlers/create_balance.go b/internal/service/handlers/create_balance.go index 7a9e671..1699d8d 100644 --- a/internal/service/handlers/create_balance.go +++ b/internal/service/handlers/create_balance.go @@ -4,9 +4,10 @@ import ( "fmt" "net/http" - "github.com/rarimo/decentralized-auth-svc/pkg/auth" + "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" "gitlab.com/distributed_lab/ape" "gitlab.com/distributed_lab/ape/problems" @@ -89,7 +90,7 @@ func prepareEventsWithRef(nullifier, refBy string, r *http.Request) []data.Event return events } - refType := EventTypes(r).Get(evtypes.TypeBeReferred, evtypes.FilterInactive) + refType := EventTypes(r).Get(models.TypeBeReferred, evtypes.FilterInactive) if refType == nil { Log(r).Debug("`Be referred` event is inactive, skipping it") return events @@ -100,7 +101,7 @@ func prepareEventsWithRef(nullifier, refBy string, r *http.Request) []data.Event return append(events, data.Event{ Nullifier: nullifier, - Type: evtypes.TypeBeReferred, + Type: models.TypeBeReferred, Status: data.EventFulfilled, }) } diff --git a/internal/service/handlers/create_event_type.go b/internal/service/handlers/create_event_type.go new file mode 100644 index 0000000..b8f9488 --- /dev/null +++ b/internal/service/handlers/create_event_type.go @@ -0,0 +1,48 @@ +package handlers + +import ( + "net/http" + + "github.com/rarimo/geo-auth-svc/pkg/auth" + "github.com/rarimo/geo-points-svc/internal/data/evtypes/models" + "github.com/rarimo/geo-points-svc/internal/service/requests" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func CreateEventType(w http.ResponseWriter, r *http.Request) { + req, err := requests.NewCreateEventType(r) + if err != nil { + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + if !auth.Authenticates(UserClaims(r), auth.AdminGrant) { + ape.RenderErr(w, problems.Unauthorized()) + return + } + + evType, err := EventTypesQ(r).Get(req.Data.Attributes.Name) + if err != nil { + Log(r).WithError(err).Error("Failed to get event type by name") + ape.RenderErr(w, problems.InternalError()) + return + } + + memEvType := EventTypes(r).Get(req.Data.Attributes.Name) + if evType != nil || memEvType != nil { + Log(r).Debugf("Event type %s already exists: inMem: %v, inDb: %v", req.Data.Attributes.Name, memEvType, evType) + ape.RenderErr(w, problems.Conflict()) + return + } + + typeModel := models.ResourceToModel(req.Data.Attributes) + if err = EventTypesQ(r).Insert(typeModel); err != nil { + Log(r).WithError(err).Error("Failed to insert event type") + ape.RenderErr(w, problems.InternalError()) + return + } + EventTypes(r).Push(typeModel) + + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/service/handlers/ctx.go b/internal/service/handlers/ctx.go index a182b98..457e68b 100644 --- a/internal/service/handlers/ctx.go +++ b/internal/service/handlers/ctx.go @@ -4,7 +4,8 @@ import ( "context" "net/http" - "github.com/rarimo/decentralized-auth-svc/resources" + "github.com/rarimo/geo-auth-svc/pkg/hmacsig" + "github.com/rarimo/geo-auth-svc/resources" "github.com/rarimo/geo-points-svc/internal/config" "github.com/rarimo/geo-points-svc/internal/data" "github.com/rarimo/geo-points-svc/internal/data/evtypes" @@ -23,7 +24,7 @@ const ( userClaimsCtxKey levelsCtxKey verifierCtxKey - sigVerifierCtxKey + sigCalculatorCtxKey ) func CtxLog(entry *logan.Entry) func(context.Context) context.Context { @@ -66,14 +67,24 @@ func ReferralsQ(r *http.Request) data.ReferralsQ { return r.Context().Value(referralsQCtxKey).(data.ReferralsQ).New() } -func CtxEventTypes(types evtypes.Types) func(context.Context) context.Context { +func CtxEventTypes(types *evtypes.Types) func(context.Context) context.Context { return func(ctx context.Context) context.Context { return context.WithValue(ctx, eventTypesCtxKey, types) } } -func EventTypes(r *http.Request) evtypes.Types { - return r.Context().Value(eventTypesCtxKey).(evtypes.Types) +func EventTypes(r *http.Request) *evtypes.Types { + return r.Context().Value(eventTypesCtxKey).(*evtypes.Types) +} + +func CtxEventTypesQ(q data.EventTypesQ) func(context.Context) context.Context { + return func(ctx context.Context) context.Context { + return context.WithValue(ctx, eventTypesCtxKey, q) + } +} + +func EventTypesQ(r *http.Request) data.EventTypesQ { + return r.Context().Value(eventTypesCtxKey).(data.EventTypesQ).New() } func CtxUserClaims(claim []resources.Claim) func(context.Context) context.Context { @@ -96,14 +107,14 @@ func Verifier(r *http.Request) *zk.Verifier { return r.Context().Value(verifierCtxKey).(*zk.Verifier) } -func CtxSigVerifier(sigVerifier []byte) func(context.Context) context.Context { +func CtxSigCalculator(calc hmacsig.Calculator) func(context.Context) context.Context { return func(ctx context.Context) context.Context { - return context.WithValue(ctx, sigVerifierCtxKey, sigVerifier) + return context.WithValue(ctx, sigCalculatorCtxKey, calc) } } -func SigVerifier(r *http.Request) []byte { - return r.Context().Value(sigVerifierCtxKey).([]byte) +func SigCalculator(r *http.Request) hmacsig.Calculator { + return r.Context().Value(sigCalculatorCtxKey).(hmacsig.Calculator) } func CtxLevels(levels config.Levels) func(context.Context) context.Context { diff --git a/internal/service/handlers/fulfill_qr_event.go b/internal/service/handlers/fulfill_qr_event.go index 0c87aef..3d17b96 100644 --- a/internal/service/handlers/fulfill_qr_event.go +++ b/internal/service/handlers/fulfill_qr_event.go @@ -4,10 +4,9 @@ import ( "net/http" "github.com/labstack/gommon/log" - "github.com/rarimo/decentralized-auth-svc/pkg/auth" + "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/service/hmacsig" "github.com/rarimo/geo-points-svc/internal/service/requests" "gitlab.com/distributed_lab/ape" "gitlab.com/distributed_lab/ape/problems" @@ -38,7 +37,7 @@ func FulfillQREvent(w http.ResponseWriter, r *http.Request) { } gotSig := r.Header.Get("Signature") - wantSig, err := hmacsig.CalculateQREventSignature(SigVerifier(r), event.Nullifier, event.ID, req.Data.Attributes.QrCode) + 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()) @@ -57,7 +56,7 @@ func FulfillQREvent(w http.ResponseWriter, r *http.Request) { ape.RenderErr(w, problems.Forbidden()) return } - if evType.QRCodeValue != req.Data.Attributes.QrCode { + if evType.QRCodeValue == nil || *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 diff --git a/internal/service/handlers/get_balance.go b/internal/service/handlers/get_balance.go index ea2065a..cb6dc0e 100644 --- a/internal/service/handlers/get_balance.go +++ b/internal/service/handlers/get_balance.go @@ -3,7 +3,7 @@ package handlers import ( "net/http" - "github.com/rarimo/decentralized-auth-svc/pkg/auth" + "github.com/rarimo/geo-auth-svc/pkg/auth" "github.com/rarimo/geo-points-svc/internal/data" "github.com/rarimo/geo-points-svc/internal/service/requests" "github.com/rarimo/geo-points-svc/resources" diff --git a/internal/service/handlers/get_event.go b/internal/service/handlers/get_event.go index 14c2a07..6c9b159 100644 --- a/internal/service/handlers/get_event.go +++ b/internal/service/handlers/get_event.go @@ -3,7 +3,7 @@ package handlers import ( "net/http" - "github.com/rarimo/decentralized-auth-svc/pkg/auth" + "github.com/rarimo/geo-auth-svc/pkg/auth" "github.com/rarimo/geo-points-svc/internal/data/evtypes" "github.com/rarimo/geo-points-svc/internal/service/requests" "github.com/rarimo/geo-points-svc/resources" diff --git a/internal/service/handlers/get_event_type.go b/internal/service/handlers/get_event_type.go new file mode 100644 index 0000000..c84fc5b --- /dev/null +++ b/internal/service/handlers/get_event_type.go @@ -0,0 +1,25 @@ +package handlers + +import ( + "net/http" + + "github.com/rarimo/geo-points-svc/internal/service/requests" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func GetEventType(w http.ResponseWriter, r *http.Request) { + name, err := requests.NewGetEventType(r) + if err != nil { + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + evType := EventTypes(r).Get(name) + if evType == nil { + ape.RenderErr(w, problems.NotFound()) + return + } + + ape.Render(w, newEventTypeResponse(*evType)) +} diff --git a/internal/service/handlers/list_event_types.go b/internal/service/handlers/list_event_types.go index b8ffdf5..7d2f160 100644 --- a/internal/service/handlers/list_event_types.go +++ b/internal/service/handlers/list_event_types.go @@ -4,6 +4,7 @@ import ( "net/http" "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" @@ -20,7 +21,7 @@ func ListEventTypes(w http.ResponseWriter, r *http.Request) { types := EventTypes(r).List( evtypes.FilterByNames(req.FilterName...), evtypes.FilterByFlags(req.FilterFlag...), - func(ev evtypes.EventConfig) bool { + func(ev models.EventType) bool { return len(req.FilterNotName) > 0 && !evtypes.FilterByNames(req.FilterNotName...)(ev) }, ) diff --git a/internal/service/handlers/list_events.go b/internal/service/handlers/list_events.go index 76c5a1f..da2213d 100644 --- a/internal/service/handlers/list_events.go +++ b/internal/service/handlers/list_events.go @@ -5,9 +5,10 @@ import ( "errors" "net/http" - "github.com/rarimo/decentralized-auth-svc/pkg/auth" + "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" @@ -27,10 +28,10 @@ func ListEvents(w http.ResponseWriter, r *http.Request) { } if req.FilterHasExpiration != nil { - filter := func(ev evtypes.EventConfig) bool { return ev.ExpiresAt != nil } + filter := func(ev models.EventType) bool { return ev.ExpiresAt != nil } // keep in mind that these filters eliminate values matching the condition, see evtypes/filters.go if *req.FilterHasExpiration { - filter = func(ev evtypes.EventConfig) bool { return ev.ExpiresAt == nil } + filter = func(ev models.EventType) bool { return ev.ExpiresAt == nil } } types := EventTypes(r).Names(filter) @@ -42,7 +43,7 @@ func ListEvents(w http.ResponseWriter, r *http.Request) { req.FilterType = append(req.FilterType, types...) } - inactiveTypes := EventTypes(r).Names(func(ev evtypes.EventConfig) bool { + inactiveTypes := EventTypes(r).Names(func(ev models.EventType) bool { return !evtypes.FilterInactive(ev) }) diff --git a/internal/service/handlers/middleware.go b/internal/service/handlers/middleware.go index 28e5acf..81d398b 100644 --- a/internal/service/handlers/middleware.go +++ b/internal/service/handlers/middleware.go @@ -4,7 +4,7 @@ import ( "context" "net/http" - "github.com/rarimo/decentralized-auth-svc/pkg/auth" + "github.com/rarimo/geo-auth-svc/pkg/auth" "github.com/rarimo/geo-points-svc/internal/data/pg" "gitlab.com/distributed_lab/ape" "gitlab.com/distributed_lab/ape/problems" @@ -48,6 +48,7 @@ func DBCloneMiddleware(db *pgdb.DB) func(http.Handler) http.Handler { CtxEventsQ(pg.NewEvents(clone)), CtxBalancesQ(pg.NewBalances(clone)), CtxReferralsQ(pg.NewReferrals(clone)), + CtxEventTypesQ(pg.NewEventTypes(clone)), } for _, extender := range extenders { diff --git a/internal/service/handlers/update_event_type.go b/internal/service/handlers/update_event_type.go new file mode 100644 index 0000000..b0d5280 --- /dev/null +++ b/internal/service/handlers/update_event_type.go @@ -0,0 +1,63 @@ +package handlers + +import ( + "net/http" + + "github.com/rarimo/geo-auth-svc/pkg/auth" + "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 UpdateEventType(w http.ResponseWriter, r *http.Request) { + req, err := requests.NewUpdateEventType(r) + if err != nil { + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + if !auth.Authenticates(UserClaims(r), auth.AdminGrant) { + ape.RenderErr(w, problems.Unauthorized()) + return + } + + evType := EventTypes(r).Get(req.Data.Attributes.Name) + if evType == nil { + evType, err = EventTypesQ(r).Get(req.Data.Attributes.Name) + if err != nil { + Log(r).WithError(err).Error("Failed to get event type by name") + ape.RenderErr(w, problems.InternalError()) + return + } + if evType == nil { + Log(r).Debugf("Event type %s not found", req.Data.Attributes.Name) + ape.RenderErr(w, problems.Conflict()) + return + } + } + + typeModel := models.ResourceToModel(req.Data.Attributes) + res, err := EventTypesQ(r).Update(typeModel.ForUpdate()) + if err != nil { + Log(r).WithError(err).Error("Failed to update event type") + ape.RenderErr(w, problems.InternalError()) + return + } + + EventTypes(r).Push(typeModel) + ape.Render(w, newEventTypeResponse(*res)) +} + +func newEventTypeResponse(evType models.EventType) resources.EventTypeResponse { + return resources.EventTypeResponse{ + Data: resources.EventType{ + Key: resources.Key{ + ID: evType.Name, + Type: resources.EVENT_TYPE, + }, + Attributes: evType.Resource(), + }, + } +} diff --git a/internal/service/handlers/verify_passport.go b/internal/service/handlers/verify_passport.go index 82c1c60..65123d3 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -11,10 +11,10 @@ import ( validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/google/jsonapi" zkptypes "github.com/iden3/go-rapidsnark/types" - "github.com/rarimo/decentralized-auth-svc/pkg/auth" + "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/service/hmacsig" + "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" zk "github.com/rarimo/zkverifier-kit" @@ -31,20 +31,18 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { return } - log := Log(r).WithFields(map[string]any{ - "balance.nullifier": req.Data.ID, - "balance.anonymous_id": req.Data.Attributes.AnonymousId, - "country": req.Data.Attributes.Country, - }) - var ( - country = req.Data.Attributes.Country anonymousID = req.Data.Attributes.AnonymousId proof = req.Data.Attributes.Proof + log = Log(r).WithFields(map[string]any{ + "balance.nullifier": req.Data.ID, + "balance.anonymous_id": anonymousID, + }) + + gotSig = r.Header.Get("Signature") ) - gotSig := r.Header.Get("Signature") - wantSig, err := hmacsig.CalculatePassportVerificationSignature(SigVerifier(r), req.Data.ID, country, anonymousID) + wantSig, err := SigCalculator(r).PassportVerificationSignature(req.Data.ID, anonymousID) 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()) @@ -96,15 +94,7 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { balAID = *balance.AnonymousID } - proofCountry, err := requests.ExtractCountry(*proof) - if err != nil { - log.WithError(err).Error("failed to extract country while proof was successfully verified") - ape.RenderErr(w, problems.InternalError()) - return - } - err = validation.Errors{ - "data/attributes/country": validation.Validate(country, validation.Required, validation.In(proofCountry)), "data/attributes/anonymous_id": validation.Validate(anonymousID, validation.Required, validation.In(balAID)), }.Filter() if err != nil { @@ -135,7 +125,7 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { } event, err := EventsQ(r).FilterByNullifier(balance.Nullifier). - FilterByType(evtypes.TypePassportScan). + FilterByType(models.TypePassportScan). FilterByStatus(data.EventClaimed). Get() if err != nil { @@ -147,8 +137,8 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { ape.Render(w, newEventClaimingStateResponse(req.Data.ID, event != nil)) } -func newEventClaimingStateResponse(id string, isClaimed bool) resources.PassportEventStateResponse { - var res resources.PassportEventStateResponse +func newEventClaimingStateResponse(id string, isClaimed bool) resources.EventClaimingStateResponse { + var res resources.EventClaimingStateResponse res.Data.ID = id res.Data.Type = resources.EVENT_CLAIMING_STATE res.Data.Attributes.Claimed = isClaimed @@ -226,7 +216,7 @@ func doPassportScanUpdates(r *http.Request, balance data.Balance, anonymousID st return fmt.Errorf("fulfill passport scan event: %w", err) } - evTypeRef := EventTypes(r).Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive) + evTypeRef := EventTypes(r).Get(models.TypeReferralSpecific, evtypes.FilterInactive) if evTypeRef == nil { Log(r).Debug("Referral specific event type is inactive") return nil @@ -271,14 +261,14 @@ func updateBalanceVerification(r *http.Request, balance data.Balance, anonymousI } func fulfillOrClaimPassportScanEvent(r *http.Request, balance data.Balance) error { - evTypePassport := EventTypes(r).Get(evtypes.TypePassportScan, evtypes.FilterInactive) + evTypePassport := EventTypes(r).Get(models.TypePassportScan, evtypes.FilterInactive) if evTypePassport == nil { Log(r).Debug("Passport scan event type is inactive") return nil } event, err := EventsQ(r).FilterByNullifier(balance.Nullifier). - FilterByType(evtypes.TypePassportScan). + FilterByType(models.TypePassportScan). FilterByStatus(data.EventOpen).Get() if err != nil { return fmt.Errorf("get open passport scan event: %w", err) @@ -318,7 +308,7 @@ func fulfillOrClaimPassportScanEvent(r *http.Request, balance data.Balance) erro } // evTypeRef must not be nil -func claimReferralSpecificEvents(r *http.Request, evTypeRef *evtypes.EventConfig, nullifier string) error { +func claimReferralSpecificEvents(r *http.Request, evTypeRef *models.EventType, nullifier string) error { if !evTypeRef.AutoClaim { Log(r).Debugf("auto claim for referral specific disabled") return nil @@ -332,7 +322,7 @@ func claimReferralSpecificEvents(r *http.Request, evTypeRef *evtypes.EventConfig events, err := EventsQ(r). FilterByNullifier(balance.Nullifier). - FilterByType(evtypes.TypeReferralSpecific). + FilterByType(models.TypeReferralSpecific). FilterByStatus(data.EventFulfilled). Select() if err != nil { @@ -362,7 +352,7 @@ func claimReferralSpecificEvents(r *http.Request, evTypeRef *evtypes.EventConfig return nil } -func addEventForReferrer(r *http.Request, evTypeRef *evtypes.EventConfig, balance data.Balance) error { +func addEventForReferrer(r *http.Request, evTypeRef *models.EventType, balance data.Balance) error { if evTypeRef == nil { return nil } diff --git a/internal/service/hmacsig/main.go b/internal/service/hmacsig/main.go deleted file mode 100644 index ffd125d..0000000 --- a/internal/service/hmacsig/main.go +++ /dev/null @@ -1,54 +0,0 @@ -package hmacsig - -import ( - "crypto/hmac" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "fmt" - - "github.com/google/uuid" -) - -func CalculatePassportVerificationSignature(key []byte, nullifier, country, anonymousID string) (string, error) { - bNull, err := hex.DecodeString(nullifier[2:]) - if err != nil { - return "", fmt.Errorf("nullifier is not hex: %w", err) - } - - bAID, err := hex.DecodeString(anonymousID) - if err != nil { - return "", fmt.Errorf("anonymousID is not hex: %w", err) - } - - h := hmac.New(sha256.New, key) - msg := append(bNull, []byte(country)...) - msg = append(msg, bAID...) - h.Write(msg) - - return hex.EncodeToString(h.Sum(nil)), nil -} - -func CalculateQREventSignature(key []byte, nullifier, eventID, qrCode string) (string, error) { - bNull, err := hex.DecodeString(nullifier[2:]) - if err != nil { - return "", fmt.Errorf("nullifier is not hex: %w", err) - } - - bID, err := uuid.Parse(eventID) - if err != nil { - return "", fmt.Errorf("eventID is not uuid: %w", err) - } - - bQR, err := base64.StdEncoding.DecodeString(qrCode) - if err != nil { - return "", fmt.Errorf("qrCode is not base64: %w", err) - } - - h := hmac.New(sha256.New, key) - msg := append(bNull, bID[:]...) - msg = append(msg, bQR...) - h.Write(msg) - - return hex.EncodeToString(h.Sum(nil)), nil -} diff --git a/internal/service/hmacsig/main_test.go b/internal/service/hmacsig/main_test.go deleted file mode 100644 index b18383c..0000000 --- a/internal/service/hmacsig/main_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package hmacsig - -import ( - "encoding/hex" - "testing" - - "github.com/stretchr/testify/require" -) - -// TestCalculateSignature use it to ensure matching signature on re-implementation -func TestCalculateSignature(t *testing.T) { - key := "ab6b3f7796728e0df9696c4a3eb600b49b51db9d230e94e9c67fef756d695b63" - nullifier := "0x973c253a93e8d2e6022721c6a8bd0205940b50cb478d485ca2cbc3354fae95ec" - country := "UKR" - anonymousID := "adeef82557bc0f95c8ffe38eca25e4d1d9da79ea14215ec52b4f21370dd60dbc" - - bKey, err := hex.DecodeString(key) - require.NoError(t, err) - sig, err := CalculatePassportVerificationSignature(bKey, nullifier, country, anonymousID) - require.NoError(t, err) - t.Log("Passport sig:", sig) - - eventID := "18593155-b6a3-4166-80f1-6bf4c5aeedf1" - qrCode := "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABaElEQVR4AWP4//8/AyUYw000" - sig, err = CalculateQREventSignature(bKey, nullifier, eventID, qrCode) - require.NoError(t, err) - t.Log("QR Event sig:", sig) -} diff --git a/internal/service/referralid/main_test.go b/internal/service/referralid/main_test.go index bb79be4..19e3f22 100644 --- a/internal/service/referralid/main_test.go +++ b/internal/service/referralid/main_test.go @@ -4,32 +4,32 @@ import "testing" func TestNew(t *testing.T) { tests := []struct { - name string - did string - index uint64 - want string + name string + nullifier string + index uint64 + want string }{ { - name: "Valid nullifier with index 0", - did: "2184ae1f990d26aa5bb84d54dc945ac3cce569cd828269802f0fa5c5c28f30a7", - want: "6xM70VgX4eh", + name: "Valid nullifier with index 0", + nullifier: "2184ae1f990d26aa5bb84d54dc945ac3cce569cd828269802f0fa5c5c28f30a7", + want: "6xM70VgX4eh", }, { - name: "Valid nullifier with index 1", - did: "2184ae1f990d26aa5bb84d54dc945ac3cce569cd828269802f0fa5c5c28f30a7", - index: 1, - want: "eLHv3hj5txB", + name: "Valid nullifier with index 1", + nullifier: "2184ae1f990d26aa5bb84d54dc945ac3cce569cd828269802f0fa5c5c28f30a7", + index: 1, + want: "eLHv3hj5txB", }, { - name: "Valid nullifier with index 258", - did: "2184ae1f990d26aa5bb84d54dc945ac3cce569cd828269802f0fa5c5c28f30a7", - index: 258, - want: "1hhJaHQB13G", + name: "Valid nullifier with index 258", + nullifier: "2184ae1f990d26aa5bb84d54dc945ac3cce569cd828269802f0fa5c5c28f30a7", + index: 258, + want: "1hhJaHQB13G", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := New(tt.did, tt.index); got != tt.want { + if got := New(tt.nullifier, tt.index); got != tt.want { t.Errorf("New() = %s, want %s", got, tt.want) } }) diff --git a/internal/service/requests/create_event_type.go b/internal/service/requests/create_event_type.go new file mode 100644 index 0000000..8ba56bf --- /dev/null +++ b/internal/service/requests/create_event_type.go @@ -0,0 +1,35 @@ +package requests + +import ( + "encoding/json" + "net/http" + + val "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/rarimo/geo-points-svc/internal/data/evtypes/models" + "github.com/rarimo/geo-points-svc/resources" +) + +func NewCreateEventType(r *http.Request) (req resources.EventTypeResponse, err error) { + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { + err = newDecodeError("body", err) + return + } + + attr := req.Data.Attributes + return req, val.Errors{ + // only QR code events can be currently created or updated + "data/id": val.Validate(req.Data.ID, val.Required), + "data/type": val.Validate(req.Data.Type, val.Required, val.In(resources.EVENT_TYPE)), + "data/attributes/action_url": val.Validate(attr.ActionUrl, is.URL), + "data/attributes/description": val.Validate(attr.Description, val.Required), + "data/attributes/frequency": val.Validate(attr.Frequency, val.Required, val.In(models.Unlimited)), + "data/attributes/logo": val.Validate(attr.Logo, is.URL), + "data/attributes/name": val.Validate(attr.Name, val.Required, val.In(req.Data.ID)), + "data/attributes/flag": val.Validate(attr.Flag, val.Empty), + "data/attributes/qr_code_value": val.Validate(attr.QrCodeValue, val.Required), + "data/attributes/reward": val.Validate(attr.Reward, val.Required, val.Min(1)), + "data/attributes/short_description": val.Validate(attr.ShortDescription, val.Required), + "data/attributes/title": val.Validate(attr.Title, val.Required), + }.Filter() +} diff --git a/internal/service/requests/get_event_type.go b/internal/service/requests/get_event_type.go new file mode 100644 index 0000000..e60dfcb --- /dev/null +++ b/internal/service/requests/get_event_type.go @@ -0,0 +1,14 @@ +package requests + +import ( + "net/http" + + "github.com/go-chi/chi" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +func NewGetEventType(r *http.Request) (name string, err error) { + name = chi.URLParam(r, "id") + return name, validation.Errors{"id": validation.Validate(name, validation.Required)}. + Filter() +} diff --git a/internal/service/requests/list_event_types.go b/internal/service/requests/list_event_types.go index 437cd8c..1d6ad18 100644 --- a/internal/service/requests/list_event_types.go +++ b/internal/service/requests/list_event_types.go @@ -4,7 +4,7 @@ import ( "net/http" val "github.com/go-ozzo/ozzo-validation/v4" - "github.com/rarimo/geo-points-svc/internal/data/evtypes" + "github.com/rarimo/geo-points-svc/internal/data/evtypes/models" "gitlab.com/distributed_lab/urlval/v4" ) @@ -22,10 +22,10 @@ func NewListEventTypes(r *http.Request) (req ListExpiredEvents, err error) { err = val.Errors{ "filter[flag]": val.Validate(req.FilterFlag, val.Each(val.In( - evtypes.FlagActive, - evtypes.FlagNotStarted, - evtypes.FlagExpired, - evtypes.FlagDisabled, + models.FlagActive, + models.FlagNotStarted, + models.FlagExpired, + models.FlagDisabled, ))), "filter[name][not]": val.Validate(req.FilterNotName, val.When(len(req.FilterName) > 0, val.Nil, val.Empty)), }.Filter() diff --git a/internal/service/requests/update_event_type.go b/internal/service/requests/update_event_type.go new file mode 100644 index 0000000..788998d --- /dev/null +++ b/internal/service/requests/update_event_type.go @@ -0,0 +1,32 @@ +package requests + +import ( + "encoding/json" + "net/http" + + "github.com/go-chi/chi" + val "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/rarimo/geo-points-svc/internal/data/evtypes/models" + "github.com/rarimo/geo-points-svc/resources" +) + +func NewUpdateEventType(r *http.Request) (req resources.EventTypeResponse, err error) { + name := chi.URLParam(r, "name") + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { + err = newDecodeError("body", err) + return + } + + attr := req.Data.Attributes + return req, val.Errors{ + // only QR code events can be currently created or updated + // only Unlimited frequency is intended to be used for them + "data/id": val.Validate(req.Data.ID, val.Required, val.In(name)), + "data/type": val.Validate(req.Data.Type, val.Required, val.In(resources.EVENT_TYPE)), + "data/attributes/action_url": val.Validate(attr.ActionUrl, is.URL), + "data/attributes/frequency": val.Validate(attr.Frequency, val.In(models.Unlimited)), + "data/attributes/logo": val.Validate(attr.Logo, is.URL), + "data/attributes/reward": val.Validate(attr.Reward, val.Min(1)), + }.Filter() +} diff --git a/internal/service/requests/verify_passport.go b/internal/service/requests/verify_passport.go index 55235c1..b45f3ed 100644 --- a/internal/service/requests/verify_passport.go +++ b/internal/service/requests/verify_passport.go @@ -2,17 +2,14 @@ package requests import ( "encoding/json" - "math/big" "net/http" "regexp" "strings" "github.com/go-chi/chi" val "github.com/go-ozzo/ozzo-validation/v4" - "github.com/go-ozzo/ozzo-validation/v4/is" zkptypes "github.com/iden3/go-rapidsnark/types" "github.com/rarimo/geo-points-svc/resources" - zk "github.com/rarimo/zkverifier-kit" ) var ( @@ -30,17 +27,12 @@ func NewVerifyPassport(r *http.Request) (req resources.VerifyPassportRequest, er req.Data.ID = strings.ToLower(req.Data.ID) var ( - attr = req.Data.Attributes - provingCountry = attr.Country // validate only when proof is provided - proof zkptypes.ZKProof // safe dereference + attr = req.Data.Attributes + proof zkptypes.ZKProof // safe dereference ) if attr.Proof != nil { proof = *attr.Proof - provingCountry, err = ExtractCountry(proof) - if err != nil { - return req, err - } } return req, val.Errors{ @@ -52,7 +44,6 @@ func NewVerifyPassport(r *http.Request) (req resources.VerifyPassportRequest, er val.Required, val.In(resources.VERIFY_PASSPORT)), "data/attributes/anonymous_id": val.Validate(attr.AnonymousId, val.Required, val.Match(hex32bRegexp)), - "data/attributes/country": val.Validate(attr.Country, val.Required, val.In(provingCountry), is.CountryCode3), "data/attributes/proof": val.Validate(attr.Proof, val.When(verifyPassportPathRegexp.MatchString(r.URL.Path), val.Required), val.When(joinProgramPathRegexp.MatchString(r.URL.Path), val.Nil)), @@ -60,20 +51,3 @@ func NewVerifyPassport(r *http.Request) (req resources.VerifyPassportRequest, er "data/attributes/proof/pub_signals": val.Validate(proof.PubSignals, val.When(attr.Proof != nil, val.Required, val.Length(22, 22))), }.Filter() } - -// ExtractCountry extracts country code from the proof, converting decimal UTF-8 -// code to ISO 3166-1 alpha-3 code. -func ExtractCountry(proof zkptypes.ZKProof) (string, error) { - if len(proof.PubSignals) <= int(zk.Citizenship) { - return "", val.Errors{"country_code": val.ErrLengthTooShort}.Filter() - } - - b, ok := new(big.Int).SetString(proof.PubSignals[zk.Citizenship], 10) - if !ok { - b = new(big.Int) - } - - code := string(b.Bytes()) - - return code, val.Errors{"country_code": val.Validate(code, val.Required, is.CountryCode3, val.In("GEO"))}.Filter() -} diff --git a/internal/service/router.go b/internal/service/router.go index 4bd1d22..65ce03e 100644 --- a/internal/service/router.go +++ b/internal/service/router.go @@ -22,14 +22,16 @@ func Run(ctx context.Context, cfg config.Config) { handlers.CtxEventTypes(cfg.EventTypes()), handlers.CtxLevels(cfg.Levels()), handlers.CtxVerifier(cfg.Verifier()), - handlers.CtxSigVerifier(cfg.SigVerifier()), + handlers.CtxSigCalculator(cfg.SigCalculator()), ), handlers.DBCloneMiddleware(cfg.DB()), ) + + authMW := handlers.AuthMiddleware(cfg.Auth(), cfg.Log()) r.Route("/integrations/geo-points-svc/v1", func(r chi.Router) { r.Route("/public", func(r chi.Router) { r.Route("/balances", func(r chi.Router) { - r.Use(handlers.AuthMiddleware(cfg.Auth(), cfg.Log())) + r.Use(authMW) r.Post("/", handlers.CreateBalance) r.Route("/{nullifier}", func(r chi.Router) { r.Get("/", handlers.GetBalance) @@ -38,14 +40,19 @@ func Run(ctx context.Context, cfg config.Config) { }) }) r.Route("/events", func(r chi.Router) { - r.Use(handlers.AuthMiddleware(cfg.Auth(), cfg.Log())) + r.Use(authMW) 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) - r.Get("/event_types", handlers.ListEventTypes) + r.Route("/event_types", func(r chi.Router) { + r.Get("/", handlers.ListEventTypes) + r.With(authMW).Post("/", handlers.CreateEventType) + r.Get("/{name}", handlers.GetEventType) + r.With(authMW).Patch("/{name}", handlers.UpdateEventType) + }) }) // must be accessible only within the cluster r.Route("/private", func(r chi.Router) { diff --git a/internal/service/workers/expirywatch/main.go b/internal/service/workers/expirywatch/main.go index ce17693..9ee95d2 100644 --- a/internal/service/workers/expirywatch/main.go +++ b/internal/service/workers/expirywatch/main.go @@ -8,6 +8,7 @@ import ( "github.com/go-co-op/gocron/v2" "github.com/rarimo/geo-points-svc/internal/config" "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/workers/cron" ) @@ -22,7 +23,7 @@ func Run(ctx context.Context, cfg config.Config, sig chan struct{}) { sig <- struct{}{} cron.Init(cfg.Log()) - expirable := w.types.List(func(ev evtypes.EventConfig) bool { + expirable := w.types.List(func(ev models.EventType) bool { return ev.Disabled || ev.ExpiresAt == nil || evtypes.FilterExpired(ev) }) diff --git a/internal/service/workers/expirywatch/watcher.go b/internal/service/workers/expirywatch/watcher.go index 322ebb4..218f6b2 100644 --- a/internal/service/workers/expirywatch/watcher.go +++ b/internal/service/workers/expirywatch/watcher.go @@ -7,6 +7,7 @@ import ( "github.com/rarimo/geo-points-svc/internal/config" "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/data/pg" "gitlab.com/distributed_lab/logan/v3" "gitlab.com/distributed_lab/running" @@ -14,7 +15,7 @@ import ( type watcher struct { q data.EventsQ - types evtypes.Types + types *evtypes.Types log *logan.Entry } @@ -27,7 +28,7 @@ func newWatcher(cfg config.Config) *watcher { } func (w *watcher) initialRun() error { - expired := w.types.Names(func(ev evtypes.EventConfig) bool { + expired := w.types.Names(func(ev models.EventType) bool { return !ev.Disabled && !evtypes.FilterExpired(ev) }) diff --git a/internal/service/workers/nooneisforgotten/main.go b/internal/service/workers/nooneisforgotten/main.go index 468e2cd..2d55fa8 100644 --- a/internal/service/workers/nooneisforgotten/main.go +++ b/internal/service/workers/nooneisforgotten/main.go @@ -7,6 +7,7 @@ import ( "github.com/rarimo/geo-points-svc/internal/config" "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/data/pg" "github.com/rarimo/geo-points-svc/internal/service/handlers" "gitlab.com/distributed_lab/kit/pgdb" @@ -42,8 +43,8 @@ func Run(cfg config.Config, sig chan struct{}) { // possible and to fulfill the rest of the events. // // Event will not be claimed if AutoClaim is disabled. -func updatePassportScanEvents(db *pgdb.DB, types evtypes.Types, levels config.Levels) error { - evType := types.Get(evtypes.TypePassportScan, evtypes.FilterInactive) +func updatePassportScanEvents(db *pgdb.DB, types *evtypes.Types, levels config.Levels) error { + evType := types.Get(models.TypePassportScan, evtypes.FilterInactive) if evType == nil { return nil } @@ -98,8 +99,8 @@ func updatePassportScanEvents(db *pgdb.DB, types evtypes.Types, levels config.Le // for friends who have scanned the passport, if they have not been added. // // Events are not added if the event type is inactive -func updateReferralUserEvents(db *pgdb.DB, types evtypes.Types) error { - evTypeRef := types.Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive) +func updateReferralUserEvents(db *pgdb.DB, types *evtypes.Types) error { + evTypeRef := types.Get(models.TypeReferralSpecific, evtypes.FilterInactive) if evTypeRef == nil { return nil } @@ -113,7 +114,7 @@ func updateReferralUserEvents(db *pgdb.DB, types evtypes.Types) error { for _, ref := range refPairs { toInsert = append(toInsert, data.Event{ Nullifier: ref.Referrer, - Type: evtypes.TypeReferralSpecific, + Type: models.TypeReferralSpecific, Status: data.EventFulfilled, Meta: data.Jsonb(fmt.Sprintf(`{"nullifier": "%s"}`, ref.Referred)), }) @@ -132,14 +133,14 @@ func updateReferralUserEvents(db *pgdb.DB, types evtypes.Types) error { // claimReferralSpecificEvents claim fulfilled events for invited // 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) +func claimReferralSpecificEvents(db *pgdb.DB, types *evtypes.Types, levels config.Levels) error { + evType := types.Get(models.TypeReferralSpecific, evtypes.FilterInactive) if evType == nil || !evType.AutoClaim { return nil } events, err := pg.NewEvents(db). - FilterByType(evtypes.TypeReferralSpecific). + FilterByType(models.TypeReferralSpecific). FilterByStatus(data.EventFulfilled). Select() if err != nil { diff --git a/internal/service/workers/reopener/init.go b/internal/service/workers/reopener/init.go index 8970d51..b9803b6 100644 --- a/internal/service/workers/reopener/init.go +++ b/internal/service/workers/reopener/init.go @@ -9,6 +9,7 @@ import ( "github.com/rarimo/geo-points-svc/internal/config" "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/data/pg" "github.com/rarimo/geo-points-svc/internal/service/workers/cron" "gitlab.com/distributed_lab/logan/v3" @@ -42,7 +43,7 @@ func initialRun(cfg config.Config) error { type initCollector struct { q data.EventsQ - types evtypes.Types + types *evtypes.Types log *logan.Entry } @@ -54,12 +55,12 @@ func (c *initCollector) collect() ([]data.ReopenableEvent, error) { weekStart = midnight.AddDate(0, 0, monOffset).Unix() ) - daily, err := c.selectReopenable(evtypes.Daily, midnight.Unix()) + daily, err := c.selectReopenable(models.Daily, midnight.Unix()) if err != nil { return nil, fmt.Errorf("select daily events: %w", err) } - weekly, err := c.selectReopenable(evtypes.Weekly, weekStart) + weekly, err := c.selectReopenable(models.Weekly, weekStart) if err != nil { return nil, fmt.Errorf("select weekly events: %w", err) } @@ -73,7 +74,7 @@ func (c *initCollector) collect() ([]data.ReopenableEvent, error) { return append(dw, absent...), nil } -func (c *initCollector) selectReopenable(freq evtypes.Frequency, before int64) ([]data.ReopenableEvent, error) { +func (c *initCollector) selectReopenable(freq models.Frequency, before int64) ([]data.ReopenableEvent, error) { types := c.types.Names(evtypes.FilterByFrequency(freq), evtypes.FilterInactive) if len(types) == 0 { @@ -127,7 +128,7 @@ func (c *initCollector) selectAbsent() ([]data.ReopenableEvent, error) { func runStartingWatchers(ctx context.Context, cfg config.Config) error { log := cfg.Log().WithField("who", "opener[initializer]") - notStartedEv := cfg.EventTypes().List(func(ev evtypes.EventConfig) bool { + notStartedEv := cfg.EventTypes().List(func(ev models.EventType) bool { return ev.Disabled || !evtypes.FilterNotStarted(ev) || evtypes.FilterExpired(ev) }) @@ -167,7 +168,7 @@ func startingWatcher(cfg config.Config, name string) func(context.Context) { events := make([]data.Event, len(balances)) status := data.EventOpen - if name == evtypes.TypeFreeWeekly { + if name == models.TypeFreeWeekly { status = data.EventFulfilled } diff --git a/internal/service/workers/reopener/main.go b/internal/service/workers/reopener/main.go index 09d5d9a..16f3038 100644 --- a/internal/service/workers/reopener/main.go +++ b/internal/service/workers/reopener/main.go @@ -8,7 +8,7 @@ import ( "github.com/go-co-op/gocron/v2" "github.com/rarimo/geo-points-svc/internal/config" "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/workers/cron" ) @@ -29,7 +29,7 @@ func Run(ctx context.Context, cfg config.Config, sig chan struct{}) { atDayStart := gocron.NewAtTimes(gocron.NewAtTime(0, 0, 0)) - daily := newWorker(cfg, evtypes.Daily) + daily := newWorker(cfg, models.Daily) _, err := cron.NewJob( gocron.DailyJob(1, atDayStart), gocron.NewTask(daily.job, ctx), @@ -39,7 +39,7 @@ func Run(ctx context.Context, cfg config.Config, sig chan struct{}) { panic(fmt.Errorf("reopener: failed to initialize daily job: %w", err)) } - weekly := newWorker(cfg, evtypes.Weekly) + weekly := newWorker(cfg, models.Weekly) _, err = cron.NewJob( gocron.WeeklyJob(1, gocron.NewWeekdays(time.Monday), atDayStart), gocron.NewTask(weekly.job, ctx), @@ -62,7 +62,7 @@ func prepareForReopening(events []data.ReopenableEvent) []data.Event { Status: data.EventOpen, } - if ev.Type == evtypes.TypeFreeWeekly { + if ev.Type == models.TypeFreeWeekly { res[i].Status = data.EventFulfilled } } diff --git a/internal/service/workers/reopener/worker.go b/internal/service/workers/reopener/worker.go index b5b967f..b9f3433 100644 --- a/internal/service/workers/reopener/worker.go +++ b/internal/service/workers/reopener/worker.go @@ -7,6 +7,7 @@ import ( "github.com/rarimo/geo-points-svc/internal/config" "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/data/pg" "gitlab.com/distributed_lab/logan/v3" "gitlab.com/distributed_lab/running" @@ -14,13 +15,13 @@ import ( type worker struct { name string - freq evtypes.Frequency + freq models.Frequency q data.EventsQ - types evtypes.Types + types *evtypes.Types log *logan.Entry } -func newWorker(cfg config.Config, freq evtypes.Frequency) *worker { +func newWorker(cfg config.Config, freq models.Frequency) *worker { name := fmt.Sprintf("reopener[%s]", freq.String()) return &worker{ name: name, diff --git a/resources/model_event_static_meta.go b/resources/model_event_static_meta.go index 9968934..cb8f77d 100644 --- a/resources/model_event_static_meta.go +++ b/resources/model_event_static_meta.go @@ -6,14 +6,18 @@ package resources import "time" -// Primary event metadata in plain JSON. This is a template to be filled by `dynamic` when it's present. +// Primary event metadata in plain JSON. This is a template to be filled by `dynamic` when it's present. This structure is also reused as request body to event type creation and update. type EventStaticMeta struct { // Page where you can fulfill the event - ActionUrl *string `json:"action_url,omitempty"` - Description string `json:"description"` + ActionUrl *string `json:"action_url,omitempty"` + // Whether the event is automatically claimed on fulfillment, or requires manual claim + AutoClaim bool `json:"auto_claim"` + Description string `json:"description"` + // Whether the event is disabled in the system. Disabled events can only be retrieved. + Disabled bool `json:"disabled"` // General event expiration date (UTC RFC3339) ExpiresAt *time.Time `json:"expires_at,omitempty"` - // Event configuration flag: - active: Events can be opened, fulfilled, claimed - not_started: Event are not available yet, see `starts_at` - expired: Event is not available, as it has already expired, see `expires_at` - disabled: Event is disabled in the system If event is disabled, it doesn't matter if it's expired or not started: it has `disabled` flag. + // Event configuration flag: - active: Events can be opened, fulfilled, claimed - not_started: Event are not available yet, see `starts_at` - expired: Event is not available, as it has already expired, see `expires_at` - disabled: Event is disabled in the system If event is disabled, it doesn't matter if it's expired or not started: it has `disabled` flag. Do not specify this field on creation: this structure is reused for request body too. Flag string `json:"flag"` // Event frequency, which means how often you can fulfill certain task and claim the reward. Frequency string `json:"frequency"` diff --git a/resources/model_join_program.go b/resources/model_join_program.go deleted file mode 100644 index 016ecb6..0000000 --- a/resources/model_join_program.go +++ /dev/null @@ -1,43 +0,0 @@ -/* - * GENERATED. Do not modify. Your changes might be overwritten! - */ - -package resources - -import "encoding/json" - -type JoinProgram struct { - Key - Attributes JoinProgramAttributes `json:"attributes"` -} -type JoinProgramRequest struct { - Data JoinProgram `json:"data"` - Included Included `json:"included"` -} - -type JoinProgramListRequest struct { - Data []JoinProgram `json:"data"` - Included Included `json:"included"` - Links *Links `json:"links"` - Meta json.RawMessage `json:"meta,omitempty"` -} - -func (r *JoinProgramListRequest) PutMeta(v interface{}) (err error) { - r.Meta, err = json.Marshal(v) - return err -} - -func (r *JoinProgramListRequest) GetMeta(out interface{}) error { - return json.Unmarshal(r.Meta, out) -} - -// MustJoinProgram - returns JoinProgram 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) MustJoinProgram(key Key) *JoinProgram { - var joinProgram JoinProgram - if c.tryFindEntry(key, &joinProgram) { - return &joinProgram - } - return nil -} diff --git a/resources/model_join_program_attributes.go b/resources/model_join_program_attributes.go deleted file mode 100644 index 12639f1..0000000 --- a/resources/model_join_program_attributes.go +++ /dev/null @@ -1,9 +0,0 @@ -/* - * GENERATED. Do not modify. Your changes might be overwritten! - */ - -package resources - -type JoinProgramAttributes struct { - Country string `json:"country"` -} diff --git a/resources/model_passport_event_state.go b/resources/model_passport_event_state.go deleted file mode 100644 index 79dd9f4..0000000 --- a/resources/model_passport_event_state.go +++ /dev/null @@ -1,43 +0,0 @@ -/* - * GENERATED. Do not modify. Your changes might be overwritten! - */ - -package resources - -import "encoding/json" - -type PassportEventState struct { - Key - Attributes PassportEventStateAttributes `json:"attributes"` -} -type PassportEventStateResponse struct { - Data PassportEventState `json:"data"` - Included Included `json:"included"` -} - -type PassportEventStateListResponse struct { - Data []PassportEventState `json:"data"` - Included Included `json:"included"` - Links *Links `json:"links"` - Meta json.RawMessage `json:"meta,omitempty"` -} - -func (r *PassportEventStateListResponse) PutMeta(v interface{}) (err error) { - r.Meta, err = json.Marshal(v) - return err -} - -func (r *PassportEventStateListResponse) GetMeta(out interface{}) error { - return json.Unmarshal(r.Meta, out) -} - -// MustPassportEventState - returns PassportEventState 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) MustPassportEventState(key Key) *PassportEventState { - var passportEventState PassportEventState - if c.tryFindEntry(key, &passportEventState) { - return &passportEventState - } - return nil -} diff --git a/resources/model_passport_event_state_attributes.go b/resources/model_passport_event_state_attributes.go deleted file mode 100644 index 1d3107c..0000000 --- a/resources/model_passport_event_state_attributes.go +++ /dev/null @@ -1,10 +0,0 @@ -/* - * GENERATED. Do not modify. Your changes might be overwritten! - */ - -package resources - -type PassportEventStateAttributes struct { - // If passport scan event was automatically claimed - Claimed bool `json:"claimed"` -} diff --git a/resources/model_resource_type.go b/resources/model_resource_type.go index 0a668a6..139576e 100644 --- a/resources/model_resource_type.go +++ b/resources/model_resource_type.go @@ -11,11 +11,9 @@ const ( BALANCE ResourceType = "balance" 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" VERIFY_PASSPORT ResourceType = "verify_passport" ) diff --git a/resources/model_verify_passport_attributes.go b/resources/model_verify_passport_attributes.go index 622f961..05bba01 100644 --- a/resources/model_verify_passport_attributes.go +++ b/resources/model_verify_passport_attributes.go @@ -9,8 +9,6 @@ import "github.com/iden3/go-rapidsnark/types" type VerifyPassportAttributes struct { // Unique identifier of the passport. AnonymousId string `json:"anonymous_id"` - // ISO 3166-1 alpha-3 country code, must match the one provided in `proof`. - Country string `json:"country"` // Query ZK passport verification proof. Required for endpoint `/v2/balances/{nullifier}/verifypassport`. Proof *types.ZKProof `json:"proof,omitempty"` }