package guest import ( "encoding/json" "errors" "net/http" "os" "regexp" "strconv" "time" "github.com/golang-jwt/jwt/v5" ) var ( guestRegex = regexp.MustCompile(`^/guests/*$`) guestIDRegex = regexp.MustCompile(`^/guests/([0-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 int) error } type appError struct { Error error Message string Code int } func NewGuestHandler(guestStore GuestStore) *GuestHandler { return &GuestHandler{ guestStore, } } func (handler *GuestHandler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) { switch { case request.Method == http.MethodOptions: responseWriter.WriteHeader(http.StatusOK) case request.Method == http.MethodPost && request.URL.Path == "/guests/login": handler.handleLogIn(responseWriter, request) case request.Method == http.MethodPut && guestIDRegex.MatchString(request.URL.Path): handler.handlePut(responseWriter, request) case request.Method == http.MethodGet && guestRegex.MatchString(request.URL.Path): handler.handleGet(responseWriter, request) case request.Method == http.MethodPost && guestRegex.MatchString(request.URL.Path): handler.handlePost(responseWriter, request) case request.Method == http.MethodDelete && guestIDRegex.MatchString(request.URL.Path): handler.handleDelete(responseWriter, request) default: responseWriter.WriteHeader(http.StatusNotFound) } } func (handler *GuestHandler) handleLogIn(responseWriter http.ResponseWriter, request *http.Request) { token, err := handler.logIn(request) if err != nil { http.Error(responseWriter, err.Message, err.Code) } else { responseWriter.Write(token) } } func (handler *GuestHandler) handlePut(responseWriter http.ResponseWriter, request *http.Request) { if err := handler.putGuest(request); err != nil { http.Error(responseWriter, err.Message, err.Code) } else { responseWriter.WriteHeader(http.StatusOK) } } func (handler *GuestHandler) handleGet(responseWriter http.ResponseWriter, request *http.Request) { guests, err := handler.getGuests(request) if err != nil { http.Error(responseWriter, err.Message, err.Code) } else { responseWriter.Write(guests) } } func (handler *GuestHandler) handlePost(responseWriter http.ResponseWriter, request *http.Request) { if err := handler.postGuest(request); err != nil { http.Error(responseWriter, err.Message, err.Code) } else { responseWriter.WriteHeader(http.StatusOK) } } func (handler *GuestHandler) handleDelete(responseWriter http.ResponseWriter, request *http.Request) { if err := handler.deleteGuest(request); err != nil { http.Error(responseWriter, err.Message, err.Code) } else { responseWriter.WriteHeader(http.StatusOK) } } func (handler *GuestHandler) logIn(request *http.Request) ([]byte, *appError) { name, err := handler.decodeName(request) if err != nil { return []byte{}, &appError{err, "{ \" message\": \"Failed to unmarshal name\" }", http.StatusBadRequest} } guest, err := handler.store.Find(name) if err != nil { return []byte{}, &appError{err, "{ \"message\": \"Guest not found\" }", http.StatusUnauthorized} } expirationTime := handler.setExpirationTime() claims := handler.createClaims(name, expirationTime) key, err := handler.readGuestKey() if err != nil { return []byte{}, &appError{err, "{ \"message\": \"Failed to read secret key\" }", http.StatusInternalServerError} } token, err := handler.createToken(claims, key) if err != nil { return []byte{}, &appError{err, "{ \"message\": \"Failed to create token\" }", http.StatusInternalServerError} } jsonBytes, err := handler.marshalResponse(guest, token) if err != nil { return []byte{}, &appError{err, "{ \"message\": \"Failed to marshal response\" }", http.StatusInternalServerError} } return jsonBytes, nil } func (handler *GuestHandler) decodeName(request *http.Request) (Name, error) { var name Name err := json.NewDecoder(request.Body).Decode(&name) defer request.Body.Close() return name, err } func (handler *GuestHandler) setExpirationTime() time.Time { return time.Now().Add(15 * time.Minute) } func (handler *GuestHandler) createClaims(name Name, expirationTime time.Time) *Claims { return &Claims{ Name: name, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expirationTime), }, } } func (handler *GuestHandler) readGuestKey() ([]byte, error) { // TODO: use properties file return os.ReadFile("C:\\Users\\mhunt\\guest.pem") } func (handler *GuestHandler) readAdminKey() ([]byte, error) { return os.ReadFile("C:\\Users\\mhunt\\admin.pem") } func (handler *GuestHandler) createToken(claims *Claims, key []byte) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(key) } func (handler *GuestHandler) marshalResponse(guest Guest, token string) ([]byte, error) { loginResponse := handler.createLoginResponse(guest, token) return json.Marshal(loginResponse) } func (handler *GuestHandler) createLoginResponse(weddingGuest Guest, token string) *Login { return &Login{ Guest: weddingGuest, Token: token, } } func (handler *GuestHandler) putGuest(request *http.Request) *appError { guestKey, err := handler.readGuestKey() if err != nil { return &appError{err, "{ \"message\": \"Failed to read secret key\" }", http.StatusInternalServerError} } if err := handler.validateToken(request, guestKey); err != nil { return err } if handler.findID(request) { return &appError{errors.New("ID not found"), "{ \"message\": \"ID not found\" }", http.StatusNotFound} } guest, err := handler.decodeGuest(request) if err != nil { return &appError{err, "{ \"message\": \"Invalid guest\" }", http.StatusBadRequest} } if err := handler.store.Update(guest); err != nil { return &appError{err, "{ \"message\": \"Failed to update guest\" }", http.StatusInternalServerError} } return nil } func (handler *GuestHandler) validateToken(request *http.Request, key []byte) *appError { authorizationHeader := handler.getToken(request) claims := handler.newClaims() token, err := handler.parseWithClaims(authorizationHeader, claims, key) if err != nil { if err == jwt.ErrSignatureInvalid { return &appError{err, "{ \"message\": \"Invalid signature\" }", http.StatusUnauthorized} } return &appError{err, "{ \"message\": \"Failed to parse claims\" }", http.StatusBadRequest} } if !token.Valid { return &appError{err, "{ \"message\": \"Invalid token\" }", http.StatusUnauthorized} } return nil } func (handler *GuestHandler) getToken(request *http.Request) string { return request.Header.Get("Authorization") } func (handler *GuestHandler) newClaims() *Claims { return &Claims{} } func (handler *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 (handler *GuestHandler) findID(request *http.Request) bool { matches := guestIDRegex.FindStringSubmatch(request.URL.Path) return len(matches) < 2 } func (handler *GuestHandler) decodeGuest(request *http.Request) (Guest, error) { var guest Guest err := json.NewDecoder(request.Body).Decode(&guest) defer request.Body.Close() return guest, err } func (handler *GuestHandler) getGuests(request *http.Request) ([]byte, *appError) { adminKey, err := handler.readAdminKey() if err != nil { return []byte{}, &appError{err, "{ \"message\": \"Failed to read secret key\" }", http.StatusInternalServerError} } if err := handler.validateToken(request, adminKey); err != nil { return []byte{}, err } guests, err := handler.store.Get() if err != nil { return []byte{}, &appError{err, "{ \"message\": \"Failed to get guests\" }", http.StatusInternalServerError} } jsonBytes, err := json.Marshal(guests) if err != nil { return []byte{}, &appError{err, "{ \"message\": \"Failed to marshal guests\" }", http.StatusInternalServerError} } return jsonBytes, nil } func (handler *GuestHandler) postGuest(request *http.Request) *appError { adminKey, err := handler.readAdminKey() if err != nil { return &appError{err, "{ \"message\": \"Failed to read secret key\" }", http.StatusInternalServerError} } if err := handler.validateToken(request, adminKey); err != nil { return err } guest, err := handler.decodeGuest(request) if err != nil { return &appError{err, "{ \"message\": \"Invalid guest\" }", http.StatusBadRequest} } guests, err := handler.store.Get() if err != nil { return &appError{err, "{ \"message\": \"Failed to get guests\" }", http.StatusInternalServerError} } if err := handler.checkExistingGuests(guests, guest); err != nil { return &appError{err, "{ \"message\": \"ID already exists\" }", http.StatusConflict} } if err := handler.store.Add(guest); err != nil { return &appError{err, "{ \"message\": \"Failed to add guest\" }", http.StatusInternalServerError} } return nil } func (handler *GuestHandler) checkExistingGuests(guests []Guest, newGuest Guest) error { for _, guest := range guests { if guest.ID == newGuest.ID { return errors.New("ID already exists") } } return nil } func (handler *GuestHandler) deleteGuest(request *http.Request) *appError { adminKey, err := handler.readAdminKey() if err != nil { return &appError{err, "{ \"message\": \"Failed to read secret key\" }", http.StatusInternalServerError} } if err := handler.validateToken(request, adminKey); err != nil { return err } if handler.findID(request) { return &appError{errors.New("ID not found"), "ID not found", http.StatusNotFound} } guestID, err := getID(request) if err != nil { return &appError{err, "{ \"message\": \"Failed to parse ID\" }", http.StatusInternalServerError} } err = handler.store.Delete(int(guestID)) if err != nil { return &appError{err, "{ \"message\": \"Failed to get guests\" }", http.StatusInternalServerError} } return nil } func getID(request *http.Request) (int64, error) { return strconv.ParseInt(guestIDRegex.FindStringSubmatch(request.URL.Path)[1], 10, 32) }