aboutsummaryrefslogtreecommitdiffstats
path: root/api
diff options
context:
space:
mode:
Diffstat (limited to 'api')
-rw-r--r--api/dto/auth.go4
-rw-r--r--api/dto/conference.go36
-rw-r--r--api/dto/favourites.go10
-rw-r--r--api/dto/schedule.go10
-rw-r--r--api/dto/util.go6
-rw-r--r--api/handlers/auth.go11
-rw-r--r--api/handlers/conference.go103
-rw-r--r--api/handlers/favourites.go19
-rw-r--r--api/handlers/schedule.go26
-rw-r--r--api/middleware/admin.go27
-rw-r--r--api/middleware/auth.go38
-rw-r--r--api/router.go14
12 files changed, 234 insertions, 70 deletions
diff --git a/api/dto/auth.go b/api/dto/auth.go
index 0379f21..734bb31 100644
--- a/api/dto/auth.go
+++ b/api/dto/auth.go
@@ -11,12 +11,14 @@ type LoginOAuthCallbackRequest struct {
}
type LoginOAuthOutboundResponse struct {
- URL string `json:"url" validate:"required"`
+ URL string `json:"url"`
}
type LoginResponse struct {
ID int32 `json:"id"`
+ Token string `json:"token"`
Username string `json:"username"`
+ Admin bool `json:"admin"`
}
type LoginOptionsResponse struct {
diff --git a/api/dto/conference.go b/api/dto/conference.go
new file mode 100644
index 0000000..99e6c08
--- /dev/null
+++ b/api/dto/conference.go
@@ -0,0 +1,36 @@
+package dto
+
+import (
+ "time"
+
+ "github.com/LMBishop/confplanner/pkg/database/sqlc"
+)
+
+type ConferenceResponse struct {
+ ID int32 `json:"id"`
+ Title string `json:"title"`
+ URL string `json:"url"`
+ Venue string `json:"venue"`
+ City string `json:"city"`
+}
+
+func (dst *ConferenceResponse) Scan(src sqlc.Conference) {
+ dst.ID = src.ID
+ dst.Title = src.Title.String
+ dst.URL = src.Url
+ dst.Venue = src.Venue.String
+ dst.City = src.City.String
+}
+
+type GetScheduleResponse struct {
+ Schedule interface{} `json:"schedule"`
+ LastUpdated time.Time `json:"lastUpdated"`
+}
+
+type CreateConferenceRequest struct {
+ URL string `json:"url" validate:"required"`
+}
+
+type DeleteConferenceRequest struct {
+ ID int32 `json:"id"`
+}
diff --git a/api/dto/favourites.go b/api/dto/favourites.go
index 0f8021f..44c68a2 100644
--- a/api/dto/favourites.go
+++ b/api/dto/favourites.go
@@ -3,8 +3,9 @@ package dto
import "github.com/LMBishop/confplanner/pkg/database/sqlc"
type CreateFavouritesRequest struct {
- GUID *string `json:"eventGuid"`
- ID *int32 `json:"eventId"`
+ ConferenceID int32 `json:"conferenceID" validate:"required"`
+ GUID *string `json:"eventGuid"`
+ EventID *int32 `json:"eventId"`
}
type CreateFavouritesResponse struct {
@@ -29,6 +30,7 @@ func (dst *GetFavouritesResponse) Scan(src sqlc.Favourite) {
}
type DeleteFavouritesRequest struct {
- GUID *string `json:"eventGuid"`
- ID *int32 `json:"eventId"`
+ ConferenceID int32 `json:"conferenceID" validate:"required"`
+ GUID *string `json:"eventGuid"`
+ EventID *int32 `json:"eventId"`
}
diff --git a/api/dto/schedule.go b/api/dto/schedule.go
deleted file mode 100644
index 0d652aa..0000000
--- a/api/dto/schedule.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package dto
-
-import (
- "time"
-)
-
-type GetScheduleResponse struct {
- Schedule interface{} `json:"schedule"`
- LastUpdated time.Time `json:"lastUpdated"`
-}
diff --git a/api/dto/util.go b/api/dto/util.go
index cb3ea52..651f026 100644
--- a/api/dto/util.go
+++ b/api/dto/util.go
@@ -36,8 +36,8 @@ func WrapResponseFunc(dtoFunc func(http.ResponseWriter, *http.Request) error) ht
}
}
-func WriteDto(w http.ResponseWriter, r *http.Request, o error) {
- if o, ok := o.(Response); ok {
+func WriteDto(w http.ResponseWriter, r *http.Request, err error) {
+ if o, ok := err.(Response); ok {
data, err := json.Marshal(o)
if err != nil {
w.WriteHeader(500)
@@ -49,6 +49,6 @@ func WriteDto(w http.ResponseWriter, r *http.Request, o error) {
w.Write(data)
} else {
w.WriteHeader(500)
- slog.Error("internal server error handling request", "error", o)
+ slog.Error("internal server error handling request", "error", err)
}
}
diff --git a/api/handlers/auth.go b/api/handlers/auth.go
index c19fc3a..7b0a4c8 100644
--- a/api/handlers/auth.go
+++ b/api/handlers/auth.go
@@ -34,23 +34,18 @@ func Login(authService auth.Service, store session.Service) http.HandlerFunc {
}
// TODO X-Forwarded-For
- session, err := store.Create(user.ID, user.Username, r.RemoteAddr, r.UserAgent())
+ session, err := store.Create(user.ID, user.Username, r.RemoteAddr, r.UserAgent(), user.Admin)
if err != nil {
return err
}
- cookie := &http.Cookie{
- Name: "confplanner_session",
- Value: session.Token,
- Path: "/api",
- }
- http.SetCookie(w, cookie)
-
return &dto.OkResponse{
Code: http.StatusOK,
Data: &dto.LoginResponse{
ID: user.ID,
+ Token: session.Token,
Username: user.Username,
+ Admin: session.Admin,
},
}
})
diff --git a/api/handlers/conference.go b/api/handlers/conference.go
new file mode 100644
index 0000000..42cbfc1
--- /dev/null
+++ b/api/handlers/conference.go
@@ -0,0 +1,103 @@
+package handlers
+
+import (
+ "errors"
+ "net/http"
+ "strconv"
+
+ "github.com/LMBishop/confplanner/api/dto"
+ "github.com/LMBishop/confplanner/pkg/conference"
+ "github.com/golang-cz/nilslice"
+)
+
+func GetSchedule(service conference.Service) http.HandlerFunc {
+ return dto.WrapResponseFunc(func(w http.ResponseWriter, r *http.Request) error {
+ conferenceID, err := strconv.Atoi(r.PathValue("id"))
+ if err != nil {
+ return &dto.ErrorResponse{
+ Code: http.StatusBadRequest,
+ Message: "Bad conference ID",
+ }
+ }
+
+ schedule, lastUpdated, err := service.GetSchedule(int32(conferenceID))
+ if err != nil {
+ return err
+ }
+
+ return &dto.OkResponse{
+ Code: http.StatusOK,
+ Data: &dto.GetScheduleResponse{
+ Schedule: nilslice.Initialize(*schedule),
+ LastUpdated: lastUpdated,
+ },
+ }
+ })
+}
+
+func GetConferences(service conference.Service) http.HandlerFunc {
+ return dto.WrapResponseFunc(func(w http.ResponseWriter, r *http.Request) error {
+ conferences, err := service.GetConferences()
+ if err != nil {
+ return err
+ }
+
+ var conferencesResponse []*dto.ConferenceResponse
+ for _, c := range conferences {
+ conference := &dto.ConferenceResponse{}
+ conference.Scan(c)
+ conferencesResponse = append(conferencesResponse, conference)
+ }
+
+ return &dto.OkResponse{
+ Code: http.StatusOK,
+ Data: conferencesResponse,
+ }
+ })
+}
+
+func CreateConference(service conference.Service) http.HandlerFunc {
+ return dto.WrapResponseFunc(func(w http.ResponseWriter, r *http.Request) error {
+ var request dto.CreateConferenceRequest
+ if err := dto.ReadDto(r, &request); err != nil {
+ return err
+ }
+
+ createdConference, err := service.CreateConference(request.URL)
+ if err != nil {
+ if errors.Is(err, conference.ErrScheduleFetch) {
+ return &dto.ErrorResponse{
+ Code: http.StatusBadRequest,
+ Message: "Could not fetch schedule from URL (is it a valid pentabarf XML file?)",
+ }
+ }
+ return err
+ }
+
+ var response dto.ConferenceResponse
+ response.Scan(*createdConference)
+ return &dto.OkResponse{
+ Code: http.StatusCreated,
+ Data: response,
+ }
+ })
+}
+
+func DeleteConference(service conference.Service) http.HandlerFunc {
+ return dto.WrapResponseFunc(func(w http.ResponseWriter, r *http.Request) error {
+ var request dto.DeleteConferenceRequest
+ if err := dto.ReadDto(r, &request); err != nil {
+ return err
+ }
+
+ err := service.DeleteConference(request.ID)
+ if err != nil {
+ return err
+ }
+
+ return &dto.OkResponse{
+ Code: http.StatusOK,
+ Data: nil,
+ }
+ })
+}
diff --git a/api/handlers/favourites.go b/api/handlers/favourites.go
index 502cb43..d5fe8c3 100644
--- a/api/handlers/favourites.go
+++ b/api/handlers/favourites.go
@@ -2,6 +2,7 @@ package handlers
import (
"net/http"
+ "strconv"
"github.com/LMBishop/confplanner/api/dto"
"github.com/LMBishop/confplanner/pkg/favourites"
@@ -16,7 +17,7 @@ func CreateFavourite(service favourites.Service) http.HandlerFunc {
return err
}
- if request.GUID == nil && request.ID == nil {
+ if request.GUID == nil && request.EventID == nil {
return &dto.ErrorResponse{
Code: http.StatusBadRequest,
Message: "One of event GUID or event ID must be specified",
@@ -34,7 +35,7 @@ func CreateFavourite(service favourites.Service) http.HandlerFunc {
}
}
- createdFavourite, err := service.CreateFavouriteForUser(session.UserID, uuid, request.ID)
+ createdFavourite, err := service.CreateFavouriteForUser(session.UserID, uuid, request.EventID, request.ConferenceID)
if err != nil {
return err
}
@@ -50,9 +51,17 @@ func CreateFavourite(service favourites.Service) http.HandlerFunc {
func GetFavourites(service favourites.Service) http.HandlerFunc {
return dto.WrapResponseFunc(func(w http.ResponseWriter, r *http.Request) error {
+ conferenceID, err := strconv.Atoi(r.PathValue("id"))
+ if err != nil {
+ return &dto.ErrorResponse{
+ Code: http.StatusBadRequest,
+ Message: "Bad conference ID",
+ }
+ }
+
session := r.Context().Value("session").(*session.UserSession)
- favourites, err := service.GetFavouritesForUser(session.UserID)
+ favourites, err := service.GetFavouritesForUserConference(session.UserID, int32(conferenceID))
if err != nil {
return err
}
@@ -79,7 +88,7 @@ func DeleteFavourite(service favourites.Service) http.HandlerFunc {
return err
}
- if request.GUID == nil && request.ID == nil {
+ if request.GUID == nil && request.EventID == nil {
return &dto.ErrorResponse{
Code: http.StatusBadRequest,
Message: "One of event GUID or event ID must be specified",
@@ -96,7 +105,7 @@ func DeleteFavourite(service favourites.Service) http.HandlerFunc {
}
}
- err = service.DeleteFavouriteForUserByEventDetails(session.UserID, uuid, request.ID)
+ err = service.DeleteFavouriteForUserByEventDetails(session.UserID, uuid, request.EventID, request.ConferenceID)
if err != nil {
if err == favourites.ErrNotFound {
return &dto.ErrorResponse{
diff --git a/api/handlers/schedule.go b/api/handlers/schedule.go
deleted file mode 100644
index 061e6f9..0000000
--- a/api/handlers/schedule.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package handlers
-
-import (
- "net/http"
-
- "github.com/LMBishop/confplanner/api/dto"
- "github.com/LMBishop/confplanner/pkg/schedule"
- "github.com/golang-cz/nilslice"
-)
-
-func GetSchedule(service schedule.Service) http.HandlerFunc {
- return dto.WrapResponseFunc(func(w http.ResponseWriter, r *http.Request) error {
- schedule, lastUpdated, err := service.GetSchedule()
- if err != nil {
- return err
- }
-
- return &dto.OkResponse{
- Code: http.StatusOK,
- Data: &dto.GetScheduleResponse{
- Schedule: nilslice.Initialize(*schedule),
- LastUpdated: lastUpdated,
- },
- }
- })
-}
diff --git a/api/middleware/admin.go b/api/middleware/admin.go
new file mode 100644
index 0000000..fd43cd6
--- /dev/null
+++ b/api/middleware/admin.go
@@ -0,0 +1,27 @@
+package middleware
+
+import (
+ "net/http"
+
+ "github.com/LMBishop/confplanner/api/dto"
+ "github.com/LMBishop/confplanner/pkg/session"
+ "github.com/LMBishop/confplanner/pkg/user"
+)
+
+func MustAuthoriseAdmin(service user.Service, store session.Service) func(http.HandlerFunc) http.HandlerFunc {
+ return func(next http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ session := r.Context().Value("session").(*session.UserSession)
+
+ if !session.Admin {
+ dto.WriteDto(w, r, &dto.ErrorResponse{
+ Code: http.StatusForbidden,
+ Message: "Forbidden",
+ })
+ return
+ }
+
+ next(w, r)
+ }
+ }
+}
diff --git a/api/middleware/auth.go b/api/middleware/auth.go
index eb362b0..438a8a1 100644
--- a/api/middleware/auth.go
+++ b/api/middleware/auth.go
@@ -3,7 +3,9 @@ package middleware
import (
"context"
"errors"
+ "fmt"
"net/http"
+ "strings"
"github.com/LMBishop/confplanner/api/dto"
"github.com/LMBishop/confplanner/pkg/session"
@@ -13,15 +15,17 @@ import (
func MustAuthenticate(service user.Service, store session.Service) func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
- var sessionToken string
- for _, cookie := range r.Cookies() {
- if cookie.Name == "confplanner_session" {
- sessionToken = cookie.Value
- break
- }
+ authHeader := r.Header.Get("Authorization")
+ token, err := extractBearerToken(authHeader)
+ if err != nil {
+ dto.WriteDto(w, r, &dto.ErrorResponse{
+ Code: http.StatusUnauthorized,
+ Message: "Unauthorized",
+ })
+ return
}
- s := store.GetByToken(sessionToken)
+ s := store.GetByToken(token)
if s == nil {
dto.WriteDto(w, r, &dto.ErrorResponse{
Code: http.StatusUnauthorized,
@@ -30,7 +34,7 @@ func MustAuthenticate(service user.Service, store session.Service) func(http.Han
return
}
- _, err := service.GetUserByID(s.UserID)
+ u, err := service.GetUserByID(s.UserID)
if err != nil {
if errors.Is(err, user.ErrUserNotFound) {
store.Destroy(s.SessionID)
@@ -44,9 +48,27 @@ func MustAuthenticate(service user.Service, store session.Service) func(http.Han
return
}
+ s.Username = u.Username
+ s.Admin = u.Admin
+
ctx := context.WithValue(r.Context(), "session", s)
next(w, r.WithContext(ctx))
}
}
}
+
+func extractBearerToken(header string) (string, error) {
+ const prefix = "Bearer "
+ if header == "" {
+ return "", fmt.Errorf("authorization header missing")
+ }
+ if !strings.HasPrefix(header, prefix) {
+ return "", fmt.Errorf("invalid authorization scheme")
+ }
+ token := strings.TrimSpace(header[len(prefix):])
+ if token == "" {
+ return "", fmt.Errorf("token is empty")
+ }
+ return token, nil
+}
diff --git a/api/router.go b/api/router.go
index 38e67e6..af52785 100644
--- a/api/router.go
+++ b/api/router.go
@@ -7,9 +7,9 @@ import (
"github.com/LMBishop/confplanner/api/middleware"
"github.com/LMBishop/confplanner/pkg/auth"
"github.com/LMBishop/confplanner/pkg/calendar"
+ "github.com/LMBishop/confplanner/pkg/conference"
"github.com/LMBishop/confplanner/pkg/favourites"
"github.com/LMBishop/confplanner/pkg/ical"
- "github.com/LMBishop/confplanner/pkg/schedule"
"github.com/LMBishop/confplanner/pkg/session"
"github.com/LMBishop/confplanner/pkg/user"
)
@@ -17,7 +17,7 @@ import (
type ApiServices struct {
UserService user.Service
FavouritesService favourites.Service
- ScheduleService schedule.Service
+ ConferenceService conference.Service
CalendarService calendar.Service
IcalService ical.Service
SessionService session.Service
@@ -26,6 +26,7 @@ type ApiServices struct {
func NewServer(apiServices ApiServices, baseURL string) *http.ServeMux {
mustAuthenticate := middleware.MustAuthenticate(apiServices.UserService, apiServices.SessionService)
+ admin := middleware.MustAuthoriseAdmin(apiServices.UserService, apiServices.SessionService)
mux := http.NewServeMux()
@@ -34,12 +35,15 @@ func NewServer(apiServices ApiServices, baseURL string) *http.ServeMux {
mux.HandleFunc("POST /login/{provider}", handlers.Login(apiServices.AuthService, apiServices.SessionService))
mux.HandleFunc("POST /logout", mustAuthenticate(handlers.Logout(apiServices.SessionService)))
- mux.HandleFunc("GET /favourites", mustAuthenticate(handlers.GetFavourites(apiServices.FavouritesService)))
+ mux.HandleFunc("GET /conference", mustAuthenticate(handlers.GetConferences(apiServices.ConferenceService)))
+ mux.HandleFunc("GET /conference/{id}", mustAuthenticate(handlers.GetSchedule(apiServices.ConferenceService)))
+ mux.HandleFunc("POST /conference", mustAuthenticate(admin(handlers.CreateConference(apiServices.ConferenceService))))
+ mux.HandleFunc("DELETE /conference", mustAuthenticate(admin(handlers.DeleteConference(apiServices.ConferenceService))))
+
+ mux.HandleFunc("GET /favourites/{id}", mustAuthenticate(handlers.GetFavourites(apiServices.FavouritesService)))
mux.HandleFunc("POST /favourites", mustAuthenticate(handlers.CreateFavourite(apiServices.FavouritesService)))
mux.HandleFunc("DELETE /favourites", mustAuthenticate(handlers.DeleteFavourite(apiServices.FavouritesService)))
- mux.HandleFunc("GET /schedule", mustAuthenticate(handlers.GetSchedule(apiServices.ScheduleService)))
-
mux.HandleFunc("GET /calendar", mustAuthenticate(handlers.GetCalendar(apiServices.CalendarService, baseURL)))
mux.HandleFunc("POST /calendar", mustAuthenticate(handlers.CreateCalendar(apiServices.CalendarService, baseURL)))
mux.HandleFunc("DELETE /calendar", mustAuthenticate(handlers.DeleteCalendar(apiServices.CalendarService)))