package guest import ( "encoding/json" "net/http" "os" "regexp" "time" "git.huntm.net/wedding/server/errors" "github.com/golang-jwt/jwt/v5" ) var ( guestRegex = regexp.MustCompile(`^/api/guests/*$`) guestIdRegex = regexp.MustCompile(`^/api/guests/([a-z0-9-]+)$`) ) type GuestHandler struct { store GuestStore } type GuestStore interface { Find(Name) (Guest, error) Get() ([]Guest, error) Add(Guest) error Update(Guest) error Delete(string) error } func NewGuestHandler(s GuestStore) *GuestHandler { return &GuestHandler{ s, } } func (g *GuestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch { case r.Method == http.MethodOptions: w.WriteHeader(http.StatusOK) case r.Method == http.MethodPost && r.URL.Path == "/api/guests/login": g.handleLogIn(w, r) case r.Method == http.MethodPut && guestIdRegex.MatchString(r.URL.Path): g.handlePut(w, r) case r.Method == http.MethodGet && guestRegex.MatchString(r.URL.Path): g.handleGet(w, r) case r.Method == http.MethodPost && guestRegex.MatchString(r.URL.Path): g.handlePost(w, r) case r.Method == http.MethodDelete && guestIdRegex.MatchString(r.URL.Path): g.handleDelete(w, r) default: w.WriteHeader(http.StatusNotFound) } } func (g *GuestHandler) handleLogIn(w http.ResponseWriter, r *http.Request) { token, err := g.logIn(r) if err != nil { http.Error(w, string(err.Message), err.Status) } else { w.Write(token) } } func (g *GuestHandler) handlePut(w http.ResponseWriter, r *http.Request) { if err := g.putGuest(r); err != nil { http.Error(w, string(err.Message), err.Status) } else { w.WriteHeader(http.StatusOK) } } func (g *GuestHandler) handleGet(w http.ResponseWriter, r *http.Request) { guests, err := g.getGuests(r) if err != nil { http.Error(w, string(err.Message), err.Status) } else { w.Write(guests) } } func (g *GuestHandler) handlePost(w http.ResponseWriter, r *http.Request) { if err := g.postGuest(r); err != nil { http.Error(w, string(err.Message), err.Status) } else { w.WriteHeader(http.StatusOK) } } func (g *GuestHandler) handleDelete(w http.ResponseWriter, r *http.Request) { if err := g.deleteGuest(r); err != nil { http.Error(w, string(err.Message), err.Status) } else { w.WriteHeader(http.StatusOK) } } func (g *GuestHandler) logIn(r *http.Request) ([]byte, *errors.AppError) { name, err := g.decodeName(r) if err != nil { return nil, errors.NewAppError(http.StatusBadRequest, err.Error()) } guest, err := g.store.Find(name) if err != nil { return nil, errors.NewAppError(http.StatusUnauthorized, err.Error()) } key, err := g.readGuestKey() if err != nil { return nil, errors.NewAppError(http.StatusInternalServerError, err.Error()) } token, err := g.newToken(NewClaims(name, g.setExpirationTime()), key) if err != nil { return nil, errors.NewAppError(http.StatusInternalServerError, err.Error()) } jsonBytes, err := g.marshalResponse(guest, token) if err != nil { return nil, errors.NewAppError(http.StatusInternalServerError, err.Error()) } return jsonBytes, nil } func (g *GuestHandler) decodeName(r *http.Request) (Name, error) { var name Name err := json.NewDecoder(r.Body).Decode(&name) r.Body.Close() return name, err } func (g *GuestHandler) setExpirationTime() time.Time { return time.Now().Add(15 * time.Minute) } func (g *GuestHandler) readGuestKey() ([]byte, error) { return os.ReadFile(os.Getenv("GUEST_KEY")) } func (g *GuestHandler) readAdminKey() ([]byte, error) { return os.ReadFile(os.Getenv("ADMIN_KEY")) } func (g *GuestHandler) newToken(claims *Claims, key []byte) (string, error) { return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(key) } func (g *GuestHandler) marshalResponse(guest Guest, token string) ([]byte, error) { return json.Marshal(NewLogin(guest, token)) } func (g *GuestHandler) putGuest(r *http.Request) *errors.AppError { guestKey, err := g.readGuestKey() if err != nil { return errors.NewAppError(http.StatusInternalServerError, err.Error()) } if err := g.validateToken(r, guestKey); err != nil { return err } if g.findId(r) { return errors.NewAppError(http.StatusNotFound, "cannot update guest that does not exist") } guest, err := g.decodeGuest(r) if err != nil { return errors.NewAppError(http.StatusNotFound, err.Error()) } if err := g.store.Update(guest); err != nil { return errors.NewAppError(http.StatusInternalServerError, err.Error()) } return nil } func (g *GuestHandler) validateToken(r *http.Request, key []byte) *errors.AppError { token, err := g.parseWithClaims(g.getToken(r), g.newClaims(), key) if err != nil { if err == jwt.ErrSignatureInvalid { return errors.NewAppError(http.StatusUnauthorized, err.Error()) } return errors.NewAppError(http.StatusBadRequest, err.Error()) } if !token.Valid { return errors.NewAppError(http.StatusUnauthorized, "invalid token") } return nil } func (g *GuestHandler) getToken(r *http.Request) string { return r.Header.Get("Authorization") } func (g *GuestHandler) newClaims() *Claims { return &Claims{} } func (g *GuestHandler) parseWithClaims(token string, claims *Claims, key []byte) (*jwt.Token, error) { return jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (any, error) { return key, nil }) } func (g *GuestHandler) findId(r *http.Request) bool { return len(guestIdRegex.FindStringSubmatch(r.URL.Path)) < 2 } func (g *GuestHandler) decodeGuest(r *http.Request) (Guest, error) { var guest Guest err := json.NewDecoder(r.Body).Decode(&guest) defer r.Body.Close() return guest, err } func (g *GuestHandler) getGuests(r *http.Request) ([]byte, *errors.AppError) { adminKey, err := g.readAdminKey() if err != nil { return nil, errors.NewAppError(http.StatusInternalServerError, err.Error()) } if err := g.validateToken(r, adminKey); err != nil { return nil, err } guests, err := g.store.Get() if err != nil { return nil, errors.NewAppError(http.StatusInternalServerError, err.Error()) } jsonBytes, err := json.Marshal(guests) if err != nil { return nil, errors.NewAppError(http.StatusInternalServerError, err.Error()) } return jsonBytes, nil } func (g *GuestHandler) postGuest(r *http.Request) *errors.AppError { adminKey, err := g.readAdminKey() if err != nil { return errors.NewAppError(http.StatusInternalServerError, err.Error()) } if err := g.validateToken(r, adminKey); err != nil { return err } guest, err := g.decodeGuest(r) if err != nil { return errors.NewAppError(http.StatusBadRequest, err.Error()) } guests, err := g.store.Get() if err != nil { return errors.NewAppError(http.StatusInternalServerError, err.Error()) } if g.isExistingGuest(guests, guest) { return errors.NewAppError(http.StatusConflict, "guest already exists") } if err := g.store.Add(guest); err != nil { return errors.NewAppError(http.StatusInternalServerError, err.Error()) } return nil } func (g *GuestHandler) isExistingGuest(guests []Guest, newGuest Guest) bool { for _, guest := range guests { if guest.Id == newGuest.Id { return true } } return false } func (g *GuestHandler) deleteGuest(r *http.Request) *errors.AppError { adminKey, err := g.readAdminKey() if err != nil { return errors.NewAppError(http.StatusInternalServerError, err.Error()) } if err := g.validateToken(r, adminKey); err != nil { return err } if g.findId(r) { return errors.NewAppError(http.StatusNotFound, "cannot delete guest that does not exist") } if err := g.store.Delete(getId(r)); err != nil { return errors.NewAppError(http.StatusInternalServerError, err.Error()) } return nil } func getId(r *http.Request) string { return guestIdRegex.FindStringSubmatch(r.URL.Path)[1] }