Skip to content

Commit

Permalink
Merge pull request #229 from eurofurence/issue-228-addinfo-pull
Browse files Browse the repository at this point in the history
read all add-info endpoint
  • Loading branch information
Jumpy-Squirrel authored Sep 8, 2024
2 parents dabd989 + 5f9b1f4 commit f928146
Show file tree
Hide file tree
Showing 11 changed files with 386 additions and 4 deletions.
71 changes: 71 additions & 0 deletions api/openapi-spec/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,60 @@ tags:
- name: info
description: Health and other public status information
paths:
/additional-info/{area}:
get:
tags:
- additional
summary: obtain all current additional info for an area
description: |
Returns the current additional info for an area (e.g. regdesk, sponsordesk, overdue, ...) for all attendees.
User will need to have permissions on any of their registrations as set in configuration to access it
(self-read permission is not sufficient).
Additional infos are returned as json objects encoded in strings.
operationId: getAllAdditionalInfo
parameters:
- name: area
in: path
description: Area (must match [a-z]+), note that 'overdue' is used internally. The configuration contains a list of allowed areas.
required: true
schema:
type: string
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/AdditionalInfoFullArea'
'400':
description: Invalid area supplied (area must match [a-z]+), or is not listed in the configuration and is thus also invalid. Note that area 'overdue' will also lead to this response, this area is used for internal purposes.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Authorization required
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: You do not have permission to see this attendee or area
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: An unexpected error occurred. This includes database errors. A best effort attempt is made to return details in the body.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
security:
- BearerAuth: []
- ApiKeyAuth: []
/attendees:
get:
tags:
Expand Down Expand Up @@ -1620,6 +1674,23 @@ paths:
- ApiKeyAuth: []
components:
schemas:
AdditionalInfoFullArea:
type: object
required:
- area
- values
properties:
area:
type: string
example: myarea
description: the area that was requested
values:
additionalProperties: true
description: a map of attendee ids to additional info values, encoded as strings. Note that contents of the values may need to be unquoted to extract json values. Also note there is no guarantee the string values will contain valid json, it fully depends on what the tool using this additional info area has written.
example:
'1': '{"hello":"you"}'
'2': '{"you":"too"}'
'3': 'not a json value'
Attendee:
type: object
required:
Expand Down
7 changes: 7 additions & 0 deletions internal/api/v1/addinfo/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package addinfo

type AdditionalInfoFullArea struct {
Area string `json:"area"` // the area that was requested

Values map[string]string `json:"values"` // values by attendee id
}
1 change: 1 addition & 0 deletions internal/repository/database/dbrepo/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type Repository interface {
UpdateBan(ctx context.Context, b *entity.Ban) error
DeleteBan(ctx context.Context, b *entity.Ban) error

GetAllAdditionalInfoForArea(ctx context.Context, area string) ([]*entity.AdditionalInfo, error)
GetAdditionalInfoFor(ctx context.Context, attendeeId uint, area string) (*entity.AdditionalInfo, error)
WriteAdditionalInfo(ctx context.Context, ad *entity.AdditionalInfo) error

Expand Down
4 changes: 4 additions & 0 deletions internal/repository/database/historizeddb/implementation.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ func (r *HistorizingRepository) DeleteBan(ctx context.Context, b *entity.Ban) er

// --- additional info ---

func (r *HistorizingRepository) GetAllAdditionalInfoForArea(ctx context.Context, area string) ([]*entity.AdditionalInfo, error) {
return r.wrappedRepository.GetAllAdditionalInfoForArea(ctx, area)
}

func (r *HistorizingRepository) GetAdditionalInfoFor(ctx context.Context, attendeeId uint, area string) (*entity.AdditionalInfo, error) {
return r.wrappedRepository.GetAdditionalInfoFor(ctx, attendeeId, area)
}
Expand Down
16 changes: 16 additions & 0 deletions internal/repository/database/inmemorydb/implementation.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,22 @@ func (r *InMemoryRepository) GetAllAdditionalInfoOrEmptyMap(ctx context.Context,
return byAttendeeId
}

func (r *InMemoryRepository) GetAllAdditionalInfoForArea(ctx context.Context, area string) ([]*entity.AdditionalInfo, error) {
result := make([]*entity.AdditionalInfo, 0)
for attendeeId, areaMap := range r.addInfo {
areaValue, ok := areaMap[area]
if ok {
entry := &entity.AdditionalInfo{
AttendeeId: attendeeId,
Area: area,
JsonValue: areaValue.JsonValue,
}
result = append(result, entry)
}
}
return result, nil
}

func (r *InMemoryRepository) GetAdditionalInfoFor(ctx context.Context, attendeeId uint, area string) (*entity.AdditionalInfo, error) {
byAttendeeId := r.GetAllAdditionalInfoOrEmptyMap(ctx, attendeeId)
ai, ok := byAttendeeId[area]
Expand Down
30 changes: 30 additions & 0 deletions internal/repository/database/mysqldb/implementation.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,36 @@ func (r *MysqlRepository) DeleteBan(ctx context.Context, b *entity.Ban) error {

// --- additional info ---

func (r *MysqlRepository) GetAllAdditionalInfoForArea(ctx context.Context, area string) ([]*entity.AdditionalInfo, error) {
result := make([]*entity.AdditionalInfo, 0)
addInfoBuffer := entity.AdditionalInfo{}
queryBuffer := entity.AdditionalInfo{Area: area}

rows, err := r.db.Model(&entity.AdditionalInfo{}).Where(&queryBuffer).Order("attendee_id").Rows()
if err != nil {
aulogging.Logger.Ctx(ctx).Error().WithErr(err).Printf("error reading additional infos for area %s: %s", area, err.Error())
return result, err
}
defer func() {
err2 := rows.Close()
if err2 != nil {
aulogging.Logger.Ctx(ctx).Warn().WithErr(err2).Printf("secondary error closing recordset during additional info read: %s", err2.Error())
}
}()

for rows.Next() {
err = r.db.ScanRows(rows, &addInfoBuffer)
if err != nil {
aulogging.Logger.Ctx(ctx).Error().WithErr(err).Printf("error reading additional info during find for area %s: %s", area, err.Error())
return result, err
}
copiedAddInfo := addInfoBuffer
result = append(result, &copiedAddInfo)
}

return result, nil
}

func (r *MysqlRepository) GetAdditionalInfoFor(ctx context.Context, attendeeId uint, area string) (*entity.AdditionalInfo, error) {
var ai entity.AdditionalInfo
err := r.db.Model(&entity.AdditionalInfo{}).Where(&entity.AdditionalInfo{AttendeeId: attendeeId, Area: area}).Last(&ai).Error
Expand Down
16 changes: 16 additions & 0 deletions internal/service/attendeesrv/addinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,27 @@ package attendeesrv

import (
"context"
"fmt"
"github.com/eurofurence/reg-attendee-service/internal/repository/config"
"github.com/eurofurence/reg-attendee-service/internal/repository/database"
"github.com/eurofurence/reg-attendee-service/internal/web/util/ctxvalues"
)

func (s *AttendeeServiceImplData) GetFullAdditionalInfoArea(ctx context.Context, area string) (map[string]string, error) {
entries, err := database.GetRepository().GetAllAdditionalInfoForArea(ctx, area)
if err != nil {
return make(map[string]string), err
}
result := make(map[string]string)
for _, entry := range entries {
if entry != nil {
key := fmt.Sprintf("%d", entry.AttendeeId)
result[key] = entry.JsonValue
}
}
return result, nil
}

func (s *AttendeeServiceImplData) GetAdditionalInfo(ctx context.Context, attendeeId uint, area string) (string, error) {
existing, err := database.GetRepository().GetAdditionalInfoFor(ctx, attendeeId, area)
return existing.JsonValue, err
Expand Down
5 changes: 5 additions & 0 deletions internal/service/attendeesrv/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ type AttendeeService interface {
GetBan(ctx context.Context, id uint) (*entity.Ban, error)
GetAllBans(ctx context.Context) ([]*entity.Ban, error)

// GetFullAdditionalInfoArea obtains all additional info values for an area.
//
// May return an empty map if no entries found. This is not an error.
GetFullAdditionalInfoArea(ctx context.Context, area string) (map[string]string, error)

// GetAdditionalInfo obtains additional info for a given attendeeId and area.
//
// If this returns an empty string, then no value existed.
Expand Down
58 changes: 54 additions & 4 deletions internal/web/controller/addinfoctl/addinfoctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
aulogging "github.com/StephanHCB/go-autumn-logging"
"github.com/eurofurence/reg-attendee-service/internal/api/v1/addinfo"
"github.com/eurofurence/reg-attendee-service/internal/repository/config"
"github.com/eurofurence/reg-attendee-service/internal/service/attendeesrv"
"github.com/eurofurence/reg-attendee-service/internal/web/filter"
Expand Down Expand Up @@ -33,6 +34,8 @@ func Create(server chi.Router, attendeeSrv attendeesrv.AttendeeService) {
server.Post("/api/rest/v1/attendees/{id}/additional-info/{area}", filter.LoggedInOrApiToken(filter.WithTimeout(3*time.Second, writeAdditionalInfoHandler)))
server.Delete("/api/rest/v1/attendees/{id}/additional-info/{area}", filter.LoggedInOrApiToken(filter.WithTimeout(3*time.Second, deleteAdditionalInfoHandler)))

server.Get("/api/rest/v1/additional-info/{area}", filter.LoggedInOrApiToken(filter.WithTimeout(60*time.Second, getAllAdditionalInfoHandler)))

areaRegexp = regexp.MustCompile("^[a-z]+$")
}

Expand Down Expand Up @@ -110,6 +113,26 @@ func deleteAdditionalInfoHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}

func getAllAdditionalInfoHandler(w http.ResponseWriter, r *http.Request) {
ctx, area, err := ctxFullAreaReadAllowedAndExists_MustReturn(w, r)
if err != nil {
return
}

values, err := attendeeService.GetFullAdditionalInfoArea(ctx, area)
if err != nil {
ctlutil.ErrorHandler(ctx, w, r, "addinfo.read.error", http.StatusInternalServerError, url.Values{})
return
}

result := addinfo.AdditionalInfoFullArea{
Area: area,
Values: values,
}
w.Header().Add(headers.ContentType, media.ContentTypeApplicationJson)
ctlutil.WriteJson(ctx, w, &result)
}

func ctxIdAreaAllowedAndExists_MustReturn(w http.ResponseWriter, r *http.Request, wantWriteAccess bool) (context.Context, uint, string, error) {
ctx := r.Context()

Expand Down Expand Up @@ -145,29 +168,56 @@ func ctxIdAreaAllowedAndExists_MustReturn(w http.ResponseWriter, r *http.Request
return ctx, id, area, nil
}

func ctxFullAreaReadAllowedAndExists_MustReturn(w http.ResponseWriter, r *http.Request) (context.Context, string, error) {
ctx := r.Context()

area, err := areaFromVarsValidated_MustReturn(ctx, w, r)
if err != nil {
return ctx, area, err
}

allowed, err := attendeeService.CanAccessAdditionalInfoArea(ctx, area)
if err != nil {
ctlutil.ErrorHandler(ctx, w, r, "addinfo.read.error", http.StatusInternalServerError, url.Values{})
return ctx, area, err
}
if !allowed {
culprit := ctxvalues.Subject(ctx)
ctlutil.UnauthorizedError(ctx, w, r, "you are not authorized for this additional info area - the attempt has been logged", fmt.Sprintf("unauthorized access attempt for add info area %s by %s", area, culprit))
return ctx, area, errors.New("forbidden")
}

return ctx, area, nil
}

func idAndAreaFromVarsValidated_MustReturn(ctx context.Context, w http.ResponseWriter, r *http.Request) (uint, string, error) {
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
ctlutil.InvalidAttendeeIdErrorHandler(ctx, w, r, idStr)
return uint(id), "", err
}
area, err := areaFromVarsValidated_MustReturn(ctx, w, r)
return uint(id), area, err
}

func areaFromVarsValidated_MustReturn(ctx context.Context, w http.ResponseWriter, r *http.Request) (string, error) {
area := chi.URLParam(r, "area")
if !areaRegexp.MatchString(area) {
aulogging.Logger.Ctx(ctx).Warn().Printf("received invalid additional info area '%s'", area)
ctlutil.ErrorHandler(ctx, w, r, "addinfo.area.invalid", http.StatusBadRequest, url.Values{"area": []string{"must match [a-z]+"}})
return uint(id), area, errors.New("invalid additional info area")
return area, errors.New("invalid additional info area")
}
if area == "overdue" {
aulogging.Logger.Ctx(ctx).Warn().Printf("received invalid additional info area '%s'", area)
ctlutil.ErrorHandler(ctx, w, r, "addinfo.area.invalid", http.StatusBadRequest, url.Values{"area": []string{"the special value 'overdue' is used internally and is forbidden here"}})
return uint(id), area, errors.New("invalid additional info area")
return area, errors.New("invalid additional info area")
}
if validation.NotInAllowedValues(config.AdditionalInfoFieldNames(), area) {
aulogging.Logger.Ctx(ctx).Warn().Printf("received additional info area '%s' not listed in configuration", area)
ctlutil.ErrorHandler(ctx, w, r, "addinfo.area.unlisted", http.StatusBadRequest, url.Values{"area": []string{"areas must be enabled in configuration"}})
return uint(id), area, errors.New("unlisted additional info area")
return area, errors.New("unlisted additional info area")
}

return uint(id), area, nil
return area, nil
}
4 changes: 4 additions & 0 deletions internal/web/controller/attendeectl/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ func (s *MockAttendeeService) GetAdditionalInfo(ctx context.Context, attendeeId
return "", nil
}

func (s *MockAttendeeService) GetFullAdditionalInfoArea(ctx context.Context, area string) (map[string]string, error) {
return map[string]string{}, nil
}

func (s *MockAttendeeService) WriteAdditionalInfo(ctx context.Context, attendeeId uint, area string, value string) error {
return nil
}
Expand Down
Loading

0 comments on commit f928146

Please sign in to comment.