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 Name) (Guest, error) Get() ([]Guest, error) Add(guest Guest) error Update(guest Guest) error Delete(id 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, request *http.Request) { if err := g.deleteGuest(request); 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()) } expirationTime := g.setExpirationTime() claims := g.createClaims(name, expirationTime) key, err := g.readGuestKey() if err != nil { return nil, errors.NewAppError(http.StatusInternalServerError, err.Error()) } token, err := g.createToken(claims, 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) createClaims(name Name, time time.Time) *Claims { return &Claims{ Name: name, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time), }, } } 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) createToken(claims *Claims, key []byte) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(key) } func (g *GuestHandler) marshalResponse(guest Guest, token string) ([]byte, error) { loginResponse := g.createLoginResponse(guest, token) return json.Marshal(loginResponse) } func (g *GuestHandler) createLoginResponse(guest Guest, token string) *Login { return &Login{ Guest: guest, Token: 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 { authorizationHeader := g.getToken(r) claims := g.newClaims() token, err := g.parseWithClaims(authorizationHeader, claims, 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 { matches := guestIdRegex.FindStringSubmatch(r.URL.Path) return len(matches) < 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") } guestId := getId(r) err = g.store.Delete(guestId) if err != nil { return errors.NewAppError(http.StatusInternalServerError, err.Error()) } return nil } func getId(r *http.Request) string { return guestIdRegex.FindStringSubmatch(r.URL.Path)[1] }