-
Notifications
You must be signed in to change notification settings - Fork 2
Create event endpoint and service for core dashboard #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 52 commits
Commits
Show all changes
56 commits
Select commit
Hold shift + click to select a range
42dae43
feat: created event api repo, service, and handler
h1divp b05cd3d
docs: added /event route to openapi.yml
h1divp a63de79
feat: added more checks for CreateEvent
h1divp aba98c6
fix: corrected error message
h1divp 6f58093
removed extraneous code
h1divp af9aef3
feat: added GetEventById
h1divp f9e180e
feat: added update route. various fixes and refactored UpdateEventByI…
h1divp f8dc36b
feat: added generic badge component, event badge component, and icon …
hieunguyent12 bcb162d
fix: removed mounting empty node_modules in docker compose file (#41)
AlexanderWangY a1f7199
feat: added swagger ui renderer plugin
h1divp 21468ec
refactor: changed referance to link to other site that renders api docs
h1divp 91c1de4
TECH-107: Add mailing list (#40)
hugoliu-code b3ed0ff
Fix/discord pfp (#56)
AlexanderWangY 1e9139a
fix: allow go sum for build (#57)
AlexanderWangY 5432c3b
Stanley/basic bot structure (#59)
AlexanderWangY d975656
feat: added event card (#58)
hieunguyent12 c2dbd0f
feat: swamphacks infra files (#61)
AlexanderWangY ae76c93
fix: add more build images (#62)
AlexanderWangY 3782a4b
Fix/multi platform build (#63)
AlexanderWangY cc8337f
feat: new deployment for prod server (#64)
AlexanderWangY 8560457
hotfix: fix script to indented
AlexanderWangY be3c5cd
fix goose migraitons hotfix
AlexanderWangY 4f2f504
feat: dev deployment (#65)
AlexanderWangY 3f3e101
Feat/dev deployment api (#66)
AlexanderWangY 70d2dd9
hotfix: dev api
AlexanderWangY 2bb8706
Merge and fastforward timeline (#68)
AlexanderWangY a84c3b0
Admin dashboard links (#69)
AlexanderWangY b9777a7
Speedup git history and merge master to dev (#71)
AlexanderWangY 6886701
Create sql query to fetch event, user, and application information (…
h1divp 1836b10
feat: dev deployment (#65)
AlexanderWangY 0c7a96d
Feat/dev deployment api (#66)
AlexanderWangY 3b4f77e
Merge and fastforward timeline (#68)
AlexanderWangY b9dfe13
Refactor: made UpdateEvent sql easier to use, renamed query. Fix: rem…
h1divp 975dc0e
Revert "TECH-123: Create sql query to fetch event, user, and applicat…
h1divp c131f96
fastforward git history and changes (#74)
AlexanderWangY d8b1fbc
Merge branch 'dev' into phoenix/TECH-124
h1divp 424ddd3
fix: fixing rebase
h1divp 56925e9
feat: added delete route
h1divp 33e4b88
feat: added better validation for missing/empty parameters
h1divp ea10a6b
feat: progress in optional fields for CreateEvent
h1divp 26f4645
Fix: prevent SQL query default values being controlled by struct in h…
h1divp a76d72b
Fix: fixed default values being overridden by NULL in insert query
h1divp 79a8518
chore: updated comment
h1divp 5f11f00
Feat: error throws when no events are deleted after a delete request
h1divp b24ca84
Merge branch 'dev' into phoenix/TECH-124
h1divp 64aeb72
docs: updated openapi documentation
h1divp c991827
Fix: fixed error handling flow, events not being returned to client, …
h1divp 1d7cd07
Fix: protected routes
h1divp 510599b
Refactor: moved time validation functions
h1divp 635d2c8
Refactor: replaced required field function by using validator library
h1divp ed2f842
Fix: update query optional parameters now support null values
h1divp 36b4419
Feat: added middleware for checking user event role
h1divp 8b8a42d
Fix: RequireEventRole middleware now works
h1divp b0cd25c
Chore: go mod tidy
h1divp 71fc24c
Improvement: allow RequireRole middleware to take slices
h1divp 1e7ce1f
Improvement: allow superusers whenever when any role is required
h1divp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,217 @@ | ||
| package handlers | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "net/http" | ||
| "reflect" | ||
| "time" | ||
|
|
||
| "github.com/go-chi/chi/v5" | ||
| "github.com/go-playground/validator/v10" | ||
| "github.com/google/uuid" | ||
| "github.com/rs/zerolog" | ||
| res "github.com/swamphacks/core/apps/api/internal/api/response" | ||
| "github.com/swamphacks/core/apps/api/internal/config" | ||
| "github.com/swamphacks/core/apps/api/internal/db/sqlc" | ||
| "github.com/swamphacks/core/apps/api/internal/services" | ||
| ) | ||
|
|
||
| type EventHandler struct { | ||
| eventService *services.EventService | ||
| cfg *config.Config | ||
| logger zerolog.Logger | ||
| } | ||
|
|
||
| func NewEventHandler(eventService *services.EventService, cfg *config.Config, logger zerolog.Logger) *EventHandler { | ||
| return &EventHandler{ | ||
| eventService: eventService, | ||
| cfg: cfg, | ||
| logger: logger.With().Str("handler", "EventHandler").Str("component", "events").Logger(), | ||
| } | ||
| } | ||
|
|
||
| // Be very careful with the types in this struct. If a type is not a pointer (pointer types allow a null value), and the field is not present in the json body, its default value will be passed to the SQL query, and be a non-null value will be put into coalese(), which will then make a NULL value impossible and instead make the default value the type's zero value in Go. | ||
| type CreateEventFields struct { | ||
| Name string `json:"name" validate:"required,min=5,max=30"` | ||
| ApplicationOpen time.Time `json:"application_open" validate:"required"` | ||
| ApplicationClose time.Time `json:"application_close" validate:"required"` | ||
| StartTime time.Time `json:"start_time" validate:"required"` | ||
| EndTime time.Time `json:"end_time" validate:"required"` | ||
| Description *string `json:"description"` | ||
| Location *string `json:"location"` | ||
| LocationUrl *string `json:"location_url"` | ||
| MaxAttendees *int32 `json:"max_attendees"` | ||
| RsvpDeadline *time.Time `json:"rsvp_deadline"` | ||
| DecisionRelease *time.Time `json:"decision_release"` | ||
| WebsiteUrl *string `json:"website_url"` | ||
| IsPublished *bool `json:"is_published"` | ||
| } | ||
|
|
||
| func (st CreateEventFields) ValidateTimeFields() bool { | ||
| if st.ApplicationClose.Before(st.ApplicationOpen) || st.ApplicationClose.Equal(st.ApplicationOpen) { | ||
| return false | ||
| } | ||
| if st.EndTime.Before(st.StartTime) || st.EndTime.Equal(st.StartTime) { | ||
| return false | ||
| } | ||
| if st.ApplicationOpen.Before(time.Now()) || | ||
| st.ApplicationClose.Before(time.Now()) || | ||
| st.StartTime.Before(time.Now()) || | ||
| st.EndTime.Before(time.Now()) { | ||
| return false | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| func (h *EventHandler) CreateEvent(w http.ResponseWriter, r *http.Request) { | ||
|
|
||
| // Parse JSON body | ||
| var req CreateEventFields | ||
| decoder := json.NewDecoder(r.Body) | ||
| decoder.DisallowUnknownFields() // Prevents requests with extraneous fields | ||
| // This will also throw an error for empty values for fields which correspond to types that cannot convert an empty string to a zero value (e.g. time.Time) | ||
| if err := decoder.Decode(&req); err != nil { | ||
| res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Could not parse request body")) | ||
| return | ||
| } | ||
|
|
||
| validate := validator.New() | ||
| if err := validate.Struct(req); err != nil { | ||
| res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", err.Error())) | ||
| } | ||
|
|
||
| if !req.ValidateTimeFields() { | ||
| res.SendError(w, http.StatusBadRequest, res.NewError("invalid_time", "Time fields must be sequential and not in the past.")) | ||
| return | ||
| } | ||
|
|
||
| params := sqlc.CreateEventParams{ | ||
| Name: req.Name, | ||
| ApplicationOpen: req.ApplicationOpen, | ||
| ApplicationClose: req.ApplicationClose, | ||
| StartTime: req.StartTime, | ||
| EndTime: req.EndTime, | ||
| Description: req.Description, | ||
| Location: req.Location, | ||
| LocationUrl: req.LocationUrl, | ||
| MaxAttendees: req.MaxAttendees, | ||
| RsvpDeadline: req.RsvpDeadline, | ||
| DecisionRelease: req.DecisionRelease, | ||
| WebsiteUrl: req.WebsiteUrl, | ||
| IsPublished: req.IsPublished, | ||
| } | ||
|
|
||
| event, err := h.eventService.CreateEvent(r.Context(), params) | ||
| if err != nil { | ||
| if err == services.ErrFailedToCreateEvent { | ||
| res.SendError(w, http.StatusInternalServerError, res.NewError("creation_error", "Failed to create event")) | ||
| } else { | ||
| res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) | ||
| } | ||
| } | ||
|
|
||
| res.Send(w, http.StatusCreated, event) | ||
| } | ||
|
|
||
| func (h *EventHandler) GetEventByID(w http.ResponseWriter, r *http.Request) { | ||
| eventIdStr := chi.URLParam(r, "eventId") | ||
| if eventIdStr == "" { | ||
| res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) | ||
| return | ||
| } | ||
| eventId, err := uuid.Parse(eventIdStr) | ||
| if err != nil { | ||
| res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) | ||
| return | ||
| } | ||
|
|
||
| event, err := h.eventService.GetEventByID(r.Context(), eventId) | ||
| if err != nil { | ||
| switch err { | ||
| case services.ErrFailedToGetEvent: | ||
| res.SendError(w, http.StatusNotFound, res.NewError("no_event", "Event not found")) | ||
| default: | ||
| res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) | ||
| } | ||
| } | ||
|
|
||
| res.Send(w, http.StatusOK, event) | ||
| } | ||
|
|
||
| func (h *EventHandler) UpdateEventById(w http.ResponseWriter, r *http.Request) { | ||
| eventIdStr := chi.URLParam(r, "eventId") | ||
| if eventIdStr == "" { | ||
| res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) | ||
| return | ||
| } | ||
| eventId, err := uuid.Parse(eventIdStr) | ||
| if err != nil { | ||
| res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) | ||
| return | ||
| } | ||
|
|
||
| var req sqlc.UpdateEventByIdParams | ||
|
|
||
| decoder := json.NewDecoder(r.Body) | ||
| decoder.DisallowUnknownFields() // Prevents requests with extraneous fields | ||
| if err := decoder.Decode(&req); err != nil { | ||
| res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Invalid request body")) | ||
| return | ||
| } | ||
|
|
||
| // Refactorme: could be improved by unmarshalling values into a generic that can include nil information | ||
| // Todo: make sure that non nullable values can't be updated to null | ||
| // Todo: Time validation | ||
| req.NameDoUpdate = reflect.ValueOf(req.Name).IsValid() | ||
| req.DescriptionDoUpdate = reflect.ValueOf(req.Description).IsValid() | ||
| req.LocationDoUpdate = reflect.ValueOf(req.Location).IsValid() | ||
| req.LocationUrlDoUpdate = reflect.ValueOf(req.LocationUrl).IsValid() | ||
| req.MaxAttendeesDoUpdate = reflect.ValueOf(req.MaxAttendees).IsValid() | ||
| req.ApplicationOpenDoUpdate = reflect.ValueOf(req.ApplicationOpen).IsValid() | ||
| req.ApplicationCloseDoUpdate = reflect.ValueOf(req.ApplicationClose).IsValid() | ||
| req.RsvpDeadlineDoUpdate = reflect.ValueOf(req.RsvpDeadline).IsValid() | ||
| req.DecisionReleaseDoUpdate = reflect.ValueOf(req.DecisionRelease).IsValid() | ||
| req.StartTimeDoUpdate = reflect.ValueOf(req.StartTime).IsValid() | ||
| req.EndTimeDoUpdate = reflect.ValueOf(req.EndTime).IsValid() | ||
| req.WebsiteUrlDoUpdate = reflect.ValueOf(req.WebsiteUrl).IsValid() | ||
| req.IsPublishedDoUpdate = reflect.ValueOf(req.IsPublished).IsValid() | ||
| req.ID = eventId | ||
|
|
||
| event, err := h.eventService.UpdateEventById(r.Context(), req) | ||
|
|
||
| if err != nil { | ||
| switch err { | ||
| case services.ErrFailedToUpdateEvent: | ||
| res.SendError(w, http.StatusInternalServerError, res.NewError("patch_error", "Failed to update event")) | ||
| default: | ||
| res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) | ||
| } | ||
| } | ||
|
|
||
| res.Send(w, http.StatusOK, event) | ||
| } | ||
|
|
||
| func (h *EventHandler) DeleteEventById(w http.ResponseWriter, r *http.Request) { | ||
| eventIdStr := chi.URLParam(r, "eventId") | ||
| if eventIdStr == "" { | ||
| res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) | ||
| return | ||
| } | ||
| eventId, err := uuid.Parse(eventIdStr) | ||
| if err != nil { | ||
| res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) | ||
| return | ||
| } | ||
| err = h.eventService.DeleteEventById(r.Context(), eventId) | ||
|
|
||
| if err != nil { | ||
| switch err { | ||
| case services.ErrFailedToDeleteEvent: | ||
| res.SendError(w, http.StatusInternalServerError, res.NewError("delete_error", "Failed to delete event")) | ||
| default: | ||
| res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) | ||
| } | ||
| } | ||
|
|
||
| w.WriteHeader(http.StatusNoContent) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package middleware | ||
|
|
||
| import ( | ||
| "net/http" | ||
|
|
||
| "github.com/google/uuid" | ||
| "github.com/rs/zerolog" | ||
| "github.com/swamphacks/core/apps/api/internal/api/response" | ||
| "github.com/swamphacks/core/apps/api/internal/config" | ||
| "github.com/swamphacks/core/apps/api/internal/db" | ||
| "github.com/swamphacks/core/apps/api/internal/db/sqlc" | ||
| ) | ||
|
|
||
| const EventRoleContextKey ctxKey = "event_role" | ||
|
|
||
| type EventMiddleware struct { | ||
| db *db.DB | ||
| logger zerolog.Logger | ||
| cfg *config.Config | ||
| } | ||
|
|
||
| type UserRoleContext struct { | ||
| UserID uuid.UUID `json:"userId"` | ||
| EventID uuid.UUID `json:"eventId"` | ||
| Role sqlc.EventRoleType `json:"role"` | ||
| } | ||
|
|
||
| func NewEventMiddleware(db *db.DB, logger zerolog.Logger, cfg *config.Config) *EventMiddleware { | ||
| return &EventMiddleware{ | ||
| db: db, | ||
| logger: logger.With().Str("middleware", "EventMiddleware").Str("component", "api").Logger(), | ||
| cfg: cfg, | ||
| } | ||
| } | ||
|
|
||
| func (m *EventMiddleware) RequireEventRole(role sqlc.EventRoleType) func(http.Handler) http.Handler { | ||
| return func(next http.Handler) http.Handler { | ||
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| // get user from context | ||
| userCtx, ok := r.Context().Value(EventRoleContextKey).(*UserRoleContext) | ||
| if !ok { | ||
| m.logger.Warn().Msg("No user context found.") | ||
| response.SendError(w, http.StatusUnauthorized, response.NewError("no_auth", "You are not authorized.")) | ||
| return | ||
| } | ||
|
|
||
| // check if user role matches required role | ||
| if userCtx.Role != role { | ||
| m.logger.Warn().Msgf("User tried to access %s with insufficient permissions as role %s", r.URL.Path, string(userCtx.Role)) | ||
| response.SendError(w, http.StatusForbidden, response.NewError("forbidden", "You are forbidden from this resource.")) | ||
| return | ||
| } | ||
|
|
||
| next.ServeHTTP(w, r) | ||
| }) | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.