diff --git a/config.yaml b/config.yaml index 168dba8..23fa68a 100644 --- a/config.yaml +++ b/config.yaml @@ -11,10 +11,20 @@ listener: event_types: types: - name: passport_scan - title: Points for passport scan reward: 5 + # there are default and localized configurations of texts + title: Points for passport scan description: Get points for scan passport and share data short_description: Short description + localized: + en-UK: + title: Points for passport scan + description: Get points for scan passport and share data + short_description: Short description + en-US: + title: Points for passport scan + description: Get points for scan passport and share data + short_description: Short description frequency: one-time action_url: https://... logo: https://... diff --git a/docs/spec/components/parameters/headerLang.yaml b/docs/spec/components/parameters/headerLang.yaml new file mode 100644 index 0000000..5623fbc --- /dev/null +++ b/docs/spec/components/parameters/headerLang.yaml @@ -0,0 +1,7 @@ +in: header +name: Accept-Language +description: Localization of event types. If locale is not configured for the type, the default is returned. +required: false +schema: + type: string + example: en-US 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 1dcc66e..b70b858 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 @@ -8,6 +8,7 @@ get: for each event type in the system. operationId: getEventTypes parameters: + - $ref: '#/components/parameters/headerLang' - in: query name: 'filter[name]' description: Filter by type name. Possible values should be hard-coded in the client. 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 index ab20afd..b17f4dc 100644 --- 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 @@ -4,6 +4,14 @@ get: summary: Get event type description: Returns public configuration of event type by its unique name operationId: getEventType + parameters: + - $ref: '#/components/parameters/headerLang' + - in: path + name: 'name' + required: true + schema: + type: string + example: "meetup_participation" responses: 200: description: Success @@ -23,7 +31,7 @@ get: 500: $ref: '#/components/responses/internalError' -patch: +put: tags: - Event types summary: Update event type @@ -34,6 +42,14 @@ patch: in Go, because differentiating between `{}` and `{"field": null}` requires custom unmarshalling implementation. operationId: updateEventType + parameters: + - $ref: '#/components/parameters/headerLang' + - in: path + name: 'name' + required: true + schema: + type: string + example: "meetup_participation" requestBody: required: true content: diff --git a/docs/spec/paths/integrations@geo-points-svc@v1@public@events.yaml b/docs/spec/paths/integrations@geo-points-svc@v1@public@events.yaml index 85ddae2..b08b59a 100644 --- a/docs/spec/paths/integrations@geo-points-svc@v1@public@events.yaml +++ b/docs/spec/paths/integrations@geo-points-svc@v1@public@events.yaml @@ -6,6 +6,7 @@ get: operationId: getEvents parameters: - $ref: '#/components/parameters/filterNullifier' + - $ref: '#/components/parameters/headerLang' - in: query name: 'filter[status]' description: | diff --git a/docs/spec/paths/integrations@geo-points-svc@v1@public@events@{id}.yaml b/docs/spec/paths/integrations@geo-points-svc@v1@public@events@{id}.yaml index 0756514..02d3815 100644 --- a/docs/spec/paths/integrations@geo-points-svc@v1@public@events@{id}.yaml +++ b/docs/spec/paths/integrations@geo-points-svc@v1@public@events@{id}.yaml @@ -4,6 +4,14 @@ get: summary: Get event description: Returns a single event by ID. operationId: getEvent + parameters: + - $ref: '#/components/parameters/headerLang' + - in: path + name: 'id' + required: true + schema: + type: string + example: "059c81dd-2a54-44a8-8142-c15ad8f88949" responses: 200: description: Success @@ -34,6 +42,7 @@ patch: User must be authorized, and event must be _fulfilled_ by him. operationId: claimEvent parameters: + - $ref: '#/components/parameters/headerLang' - in: path name: 'id' required: true diff --git a/internal/data/evtypes/models/event_type.go b/internal/data/evtypes/models/event_type.go index d707f90..3c52e84 100644 --- a/internal/data/evtypes/models/event_type.go +++ b/internal/data/evtypes/models/event_type.go @@ -1,26 +1,28 @@ package models import ( + "strings" "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 *string `fig:"action_url" db:"action_url"` - Logo *string `fig:"logo" db:"logo"` - QRCodeValue *string `fig:"qr_code_value" db:"qr_code_value"` + Name string `fig:"name,required" db:"name"` + Reward int64 `fig:"reward,required" db:"reward"` + Title string `fig:"title,required" db:"title"` + Description string `fig:"description,required" db:"description"` + ShortDescription string `fig:"short_description,required" db:"short_description"` + Localized LocalizationMap `fig:"localized" db:"localized"` + 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 *string `fig:"action_url" db:"action_url"` + Logo *string `fig:"logo" db:"logo"` + QRCodeValue *string `fig:"qr_code_value" db:"qr_code_value"` } func ResourceToModel(r resources.EventStaticMeta) EventType { @@ -56,13 +58,14 @@ func (e EventType) Flag() string { } } -func (e EventType) Resource() resources.EventStaticMeta { +func (e EventType) Resource(locale string) resources.EventStaticMeta { + l := e.GetLocalized(strings.ToLower(locale)) return resources.EventStaticMeta{ Name: e.Name, - Description: e.Description, - ShortDescription: e.ShortDescription, Reward: e.Reward, - Title: e.Title, + Description: l.Description, + ShortDescription: l.ShortDescription, + Title: l.Title, Frequency: e.Frequency.String(), StartsAt: e.StartsAt, ExpiresAt: e.ExpiresAt, @@ -90,3 +93,22 @@ func (e EventType) ForUpdate() map[string]any { "qr_code_value": e.QRCodeValue, } } + +func (e EventType) GetLocalized(locale string) Localized { + def := Localized{ + Title: e.Title, + Description: e.Description, + ShortDescription: e.ShortDescription, + } + + if len(e.Localized) == 0 { + return def + } + + v, ok := e.Localized[locale] + if !ok { + return def + } + + return v +} diff --git a/internal/data/evtypes/models/localized.go b/internal/data/evtypes/models/localized.go new file mode 100644 index 0000000..2f5cb71 --- /dev/null +++ b/internal/data/evtypes/models/localized.go @@ -0,0 +1,47 @@ +package models + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + + "gitlab.com/distributed_lab/kit/pgdb" +) + +// LocalizationMap maps 2-letter locales to Localized data and implements +// interfaces to work with DB +type LocalizationMap map[string]Localized + +type Localized struct { + Title string `fig:"title,required" json:"title"` + Description string `fig:"description,required" json:"description"` + ShortDescription string `fig:"short_description,required" json:"short_description"` +} + +func (l *LocalizationMap) Value() (driver.Value, error) { + if l == nil || len(*l) == 0 { + return nil, nil + } + return pgdb.JSONValue(l) +} + +func (l *LocalizationMap) Scan(src interface{}) error { + var data []byte + switch rawData := src.(type) { + case []byte: + data = rawData + case string: + data = []byte(rawData) + case nil: + return nil + default: + return fmt.Errorf("unexpected type for jsonb: %T", src) + } + + err := json.Unmarshal(data, l) + if err != nil { + return fmt.Errorf("failed to unmarshal: %w", err) + } + + return nil +} diff --git a/internal/service/handlers/claim_event.go b/internal/service/handlers/claim_event.go index 840c719..707aa0e 100644 --- a/internal/service/handlers/claim_event.go +++ b/internal/service/handlers/claim_event.go @@ -16,6 +16,8 @@ import ( "gitlab.com/distributed_lab/ape/problems" ) +const langHeader = "Accept-Language" + func ClaimEvent(w http.ResponseWriter, r *http.Request) { req, err := requests.NewClaimEvent(r) if err != nil { @@ -82,7 +84,7 @@ func ClaimEvent(w http.ResponseWriter, r *http.Request) { return } - ape.Render(w, newClaimEventResponse(*event, evType.Resource(), *balance)) + ape.Render(w, newClaimEventResponse(*event, evType.Resource(r.Header.Get(langHeader)), *balance)) } // claimEvent requires event to exist diff --git a/internal/service/handlers/get_event.go b/internal/service/handlers/get_event.go index 6c9b159..384f591 100644 --- a/internal/service/handlers/get_event.go +++ b/internal/service/handlers/get_event.go @@ -42,5 +42,5 @@ func GetEvent(w http.ResponseWriter, r *http.Request) { return } - ape.Render(w, resources.EventResponse{Data: newEventModel(*event, evType.Resource())}) + ape.Render(w, resources.EventResponse{Data: newEventModel(*event, evType.Resource(r.Header.Get(langHeader)))}) } diff --git a/internal/service/handlers/get_event_type.go b/internal/service/handlers/get_event_type.go index c84fc5b..bf849d5 100644 --- a/internal/service/handlers/get_event_type.go +++ b/internal/service/handlers/get_event_type.go @@ -21,5 +21,5 @@ func GetEventType(w http.ResponseWriter, r *http.Request) { return } - ape.Render(w, newEventTypeResponse(*evType)) + ape.Render(w, newEventTypeResponse(*evType, r.Header.Get(langHeader))) } diff --git a/internal/service/handlers/list_event_types.go b/internal/service/handlers/list_event_types.go index 7d2f160..3b67148 100644 --- a/internal/service/handlers/list_event_types.go +++ b/internal/service/handlers/list_event_types.go @@ -33,7 +33,7 @@ func ListEventTypes(w http.ResponseWriter, r *http.Request) { ID: t.Name, Type: resources.EVENT_TYPE, }, - Attributes: t.Resource(), + Attributes: t.Resource(r.Header.Get(langHeader)), } } diff --git a/internal/service/handlers/list_events.go b/internal/service/handlers/list_events.go index da2213d..6dd5bdc 100644 --- a/internal/service/handlers/list_events.go +++ b/internal/service/handlers/list_events.go @@ -104,7 +104,7 @@ func getOrderedEventsMeta(events []data.Event, r *http.Request) ([]resources.Eve if evType == nil { return nil, errors.New("wrong event type is stored in DB: might be bad event config") } - res[i] = evType.Resource() + res[i] = evType.Resource(r.Header.Get(langHeader)) } return res, nil diff --git a/internal/service/handlers/update_event_type.go b/internal/service/handlers/update_event_type.go index 242709c..b3c3645 100644 --- a/internal/service/handlers/update_event_type.go +++ b/internal/service/handlers/update_event_type.go @@ -53,19 +53,19 @@ func UpdateEventType(w http.ResponseWriter, r *http.Request) { } EventTypes(r).Push(typeModel) - resp := newEventTypeResponse(res[0]) + resp := newEventTypeResponse(res[0], r.Header.Get(langHeader)) resp.Data.Attributes.QrCodeValue = typeModel.QRCodeValue ape.Render(w, resp) } -func newEventTypeResponse(evType models.EventType) resources.EventTypeResponse { +func newEventTypeResponse(evType models.EventType, locale string) resources.EventTypeResponse { return resources.EventTypeResponse{ Data: resources.EventType{ Key: resources.Key{ ID: evType.Name, Type: resources.EVENT_TYPE, }, - Attributes: evType.Resource(), + Attributes: evType.Resource(locale), }, } } diff --git a/internal/service/handlers/verify_passport.go b/internal/service/handlers/verify_passport.go index 36bbd15..839e1c2 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -188,7 +188,8 @@ func getAndVerifyBalanceEligibility( } // never panics because of request validation - proof.PubSignals[zk.Nullifier] = mustHexToInt(nullifier) + ni := zk.Indexes(zk.GeorgianPassport)[zk.Nullifier] + proof.PubSignals[ni] = mustHexToInt(nullifier) err = Verifier(r).VerifyProof(*proof) if err != nil { if errors.Is(err, identity.ErrContractCall) { diff --git a/internal/service/router.go b/internal/service/router.go index d41dbf9..5615f2d 100644 --- a/internal/service/router.go +++ b/internal/service/router.go @@ -52,7 +52,7 @@ func Run(ctx context.Context, cfg config.Config) { r.Get("/", handlers.ListEventTypes) r.With(authMW).Post("/", handlers.CreateEventType) r.Get("/{name}", handlers.GetEventType) - r.With(authMW).Patch("/{name}", handlers.UpdateEventType) + r.With(authMW).Put("/{name}", handlers.UpdateEventType) }) }) // must be accessible only within the cluster