diff --git a/back/store.go b/back/store.go index 5758372..e717028 100644 --- a/back/store.go +++ b/back/store.go @@ -3,6 +3,7 @@ package back import ( "context" "errors" + "fmt" "log" "os" @@ -13,6 +14,15 @@ import ( var ErrNotFound = errors.New("not found") +type UnauthorizedError struct { + EventID string + AdminCode string +} + +func (u UnauthorizedError) Error() string { + return fmt.Sprintf("unauthorized: EventID = %s, AdminCode = %s", u.EventID, u.AdminCode) +} + type GenString func(length int) (string, error) type Store struct { @@ -164,6 +174,37 @@ func (s *Store) CreateEvent(ctx context.Context, cmd CreateEventCommand) (result return } +type CheckEventAdminCodeQuery struct { + EventID string + AdminCode string +} + +func (s *Store) AuthorizeEventAdmin(ctx context.Context, query CheckEventAdminCodeQuery) error { + conn := s.pool.Get(ctx) + defer s.pool.Put(conn) + + const dbQuery = ` + SELECT 1 + FROM event + WHERE id = ? AND admin_code = ?;` + var doesMatch bool + err := sqlitex.Exec(conn, dbQuery, + func(stmt *sqlite.Stmt) error { + doesMatch = true + return nil + }, query.EventID, query.AdminCode) + if err != nil { + return err + } + if !doesMatch { + return UnauthorizedError{ + EventID: query.EventID, + AdminCode: query.AdminCode, + } + } + return nil +} + type GetEventMetadataQuery struct { EventID string } diff --git a/back/store_test.go b/back/store_test.go index e390cf4..172911f 100644 --- a/back/store_test.go +++ b/back/store_test.go @@ -175,6 +175,7 @@ func TestGetEventResponseSummary(t *testing.T) { func TestCreateEventResponse(t *testing.T) { store, err := back.NewMemoryStore(back.SecureGenString) is.New(t).NoErr(err) + defer store.Close() createEvent := func(is *is.I) (eventID string) { event, err := store.CreateEvent(context.Background(), back.CreateEventCommand{ @@ -238,3 +239,64 @@ func TestCreateEventResponse(t *testing.T) { is.Equal(dateHours, response.DateHours) }) } + +func TestAuthorizeEventAdmin(t *testing.T) { + store, err := back.NewMemoryStore(back.SecureGenString) + is.New(t).NoErr(err) + defer store.Close() + + t.Run("returns ErrUnauthorized if admin code is wrong", func(t *testing.T) { + is := is.New(t) + + event, err := store.CreateEvent(context.Background(), back.CreateEventCommand{ + Name: "my event", + Description: "stuff happening", + Earliest: date.Today(), + Latest: date.Today(), + }) + is.NoErr(err) + + badAdminCode := event.AdminCode + "x" + err = store.AuthorizeEventAdmin(context.Background(), back.CheckEventAdminCodeQuery{ + EventID: event.EventID, + AdminCode: badAdminCode, + }) + is.Equal(err, back.UnauthorizedError{ + EventID: event.EventID, + AdminCode: badAdminCode, + }) + }) + + t.Run("return ErrUnauthorized if event does not exist", func(t *testing.T) { + is := is.New(t) + + randString, err := back.SecureGenString(10) + is.NoErr(err) + err = store.AuthorizeEventAdmin(context.Background(), back.CheckEventAdminCodeQuery{ + EventID: randString, + AdminCode: randString, + }) + is.Equal(err, back.UnauthorizedError{ + EventID: randString, + AdminCode: randString, + }) + }) + + t.Run("returns nil if admin code is correct", func(t *testing.T) { + is := is.New(t) + + event, err := store.CreateEvent(context.Background(), back.CreateEventCommand{ + Name: "my event", + Description: "stuff happening", + Earliest: date.Today(), + Latest: date.Today(), + }) + is.NoErr(err) + + err = store.AuthorizeEventAdmin(context.Background(), back.CheckEventAdminCodeQuery{ + EventID: event.EventID, + AdminCode: event.AdminCode, + }) + is.NoErr(err) + }) +} diff --git a/front/server.go b/front/server.go index 633c610..15fb77b 100644 --- a/front/server.go +++ b/front/server.go @@ -2,6 +2,7 @@ package front import ( "context" + "errors" "fmt" "io" "log" @@ -300,7 +301,28 @@ func (h *handler) handleDoCreate(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, pathCreateSuccess+"?"+successQuery.Encode(), http.StatusSeeOther) } +func (h *handler) blockUnauthorizedAdmin(w http.ResponseWriter, r *http.Request) bool { + eventID := r.URL.Query().Get(fieldNameEventID) + adminCode := r.URL.Query().Get(fieldNameAdminCode) + + err := h.store.AuthorizeEventAdmin(context.Background(), back.CheckEventAdminCodeQuery{ + EventID: eventID, + AdminCode: adminCode, + }) + var authError back.UnauthorizedError + if errors.As(err, &authError) { + http.Error(w, "Event not found", http.StatusNotFound) + logError(authError) + return true + } + return internalServerError(w, err) +} + func (h *handler) handleCreateSuccess(w http.ResponseWriter, r *http.Request) { + if h.blockUnauthorizedAdmin(w, r) { + return + } + eventID := r.URL.Query().Get(fieldNameEventID) event, err := h.store.GetEventMetadata(r.Context(), back.GetEventMetadataQuery{ EventID: eventID, @@ -348,8 +370,13 @@ func (h *handler) handleCreateSuccess(w http.ResponseWriter, r *http.Request) { } func (h *handler) handleAdmin(w http.ResponseWriter, r *http.Request) { + if h.blockUnauthorizedAdmin(w, r) { + return + } + query := r.URL.Query() eventID := query.Get(fieldNameEventID) + metadata, err := h.store.GetEventMetadata(context.Background(), back.GetEventMetadataQuery{ EventID: eventID, }) diff --git a/todo.txt b/todo.txt index f9d259a..ed95b4b 100644 --- a/todo.txt +++ b/todo.txt @@ -1,11 +1,12 @@ Essential: ------------ Show response after submission -Make earliest and latest dates required for creation +Require earliest and latest dates for creation +Ensure latest date is at least earliest date +Ensure date span is within a maximum +Add timeout to request context Prevent blank event names and guest names Consider some front-end, regex-based field validation -Authenticate admin page with admin code - It might be sufficient to require that when getting the response summary. Consider redirecting from /create/do to admin Cleanup: @@ -24,4 +25,5 @@ Make the sqlite pool size configurable More features: --------------- +Show validation errors in a header, rather than on their own page Allow updating metadata on admin page