From f548b4422297b6df0c789c25cd912b7cdac06ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20St=C4=99pie=C5=84?= Date: Thu, 2 Jan 2025 15:53:30 +0100 Subject: [PATCH 1/3] Implement api and auth primitives --- cmd/feelbeatserver/main.go | 3 ++ cmd/feelbeatserver/setupapi.go | 22 +++++++++++ internal/infra/api/handlers/creategame.go | 36 ++++++++++++++++++ internal/infra/api/logapicall.go | 16 ++++++++ internal/infra/api/parsebody.go | 15 ++++++++ internal/infra/api/readbody.go | 16 ++++++++ .../infra/auth/authorizethroughspotify.go | 33 +++++++++++++++++ internal/infra/auth/authwrapper.go | 5 +++ internal/infra/ws/servewebsockets.go | 2 +- internal/infra/ws/{hub.go => wshub.go} | 16 ++++---- internal/lib/component/main.go | 2 + internal/thirdparty/spotify/apicall.go | 14 +++++++ internal/thirdparty/spotify/apiurl.go | 3 ++ internal/thirdparty/spotify/getuserprofile.go | 37 +++++++++++++++++++ 14 files changed, 211 insertions(+), 9 deletions(-) create mode 100644 cmd/feelbeatserver/setupapi.go create mode 100644 internal/infra/api/handlers/creategame.go create mode 100644 internal/infra/api/logapicall.go create mode 100644 internal/infra/api/parsebody.go create mode 100644 internal/infra/api/readbody.go create mode 100644 internal/infra/auth/authorizethroughspotify.go create mode 100644 internal/infra/auth/authwrapper.go rename internal/infra/ws/{hub.go => wshub.go} (80%) create mode 100644 internal/thirdparty/spotify/apicall.go create mode 100644 internal/thirdparty/spotify/apiurl.go create mode 100644 internal/thirdparty/spotify/getuserprofile.go diff --git a/cmd/feelbeatserver/main.go b/cmd/feelbeatserver/main.go index 125c74f..105900d 100644 --- a/cmd/feelbeatserver/main.go +++ b/cmd/feelbeatserver/main.go @@ -6,6 +6,7 @@ import ( "net/http" "os" + "github.com/feelbeatapp/feelbeatserver/internal/infra/auth" "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" "github.com/feelbeatapp/feelbeatserver/internal/infra/ws" "github.com/feelbeatapp/feelbeatserver/internal/lib/component" @@ -39,6 +40,8 @@ func main() { ws.ServeWebsockets(hub, w, r) }) + setupAPI(auth.AuthorizeThroughSpotify) + fblog.Info(component.FeelBeatServer, "Server started", "port", port) log.Fatal(http.ListenAndServe(fmt.Sprintf("localhost:%d", port), nil)) } diff --git a/cmd/feelbeatserver/setupapi.go b/cmd/feelbeatserver/setupapi.go new file mode 100644 index 0000000..4634632 --- /dev/null +++ b/cmd/feelbeatserver/setupapi.go @@ -0,0 +1,22 @@ +package main + +import ( + "github.com/feelbeatapp/feelbeatserver/internal/infra/api/handlers" + "github.com/feelbeatapp/feelbeatserver/internal/infra/auth" + "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" + "github.com/feelbeatapp/feelbeatserver/internal/lib/component" +) + +type Authorizer interface { + authorize() +} + +func setupAPI(authWrapper auth.AuthWrapper) { + handlers := []func(auth.AuthWrapper){handlers.ServeCreateGame} + + fblog.Info(component.Api, "Setting up REST API", "handlers count", len(handlers)) + + for _, f := range handlers { + f(authWrapper) + } +} diff --git a/internal/infra/api/handlers/creategame.go b/internal/infra/api/handlers/creategame.go new file mode 100644 index 0000000..d20962c --- /dev/null +++ b/internal/infra/api/handlers/creategame.go @@ -0,0 +1,36 @@ +package handlers + +import ( + "net/http" + + "github.com/feelbeatapp/feelbeatserver/internal/infra/api" + "github.com/feelbeatapp/feelbeatserver/internal/infra/auth" + "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" + "github.com/feelbeatapp/feelbeatserver/internal/lib/component" +) + +type createGamePayload struct { + Test string `json:"test"` +} + +func createGameHandler(userId string, res http.ResponseWriter, req *http.Request) { + var payload createGamePayload + err := api.ParseBody(req.Body, &payload) + if err != nil { + http.Error(res, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + fblog.Error(component.Api, "Error: ", "err", err) + return + } + + _, err = res.Write([]byte("Endpoint hit!, Auth success, You are: " + userId)) + if err != nil { + api.LogApiError("Couldn't write response", err, userId, req) + return + } + + api.LogApiCall(userId, req) +} + +func ServeCreateGame(authWrapper auth.AuthWrapper) { + http.HandleFunc("/api/v1/create", authWrapper(createGameHandler)) +} diff --git a/internal/infra/api/logapicall.go b/internal/infra/api/logapicall.go new file mode 100644 index 0000000..27676ba --- /dev/null +++ b/internal/infra/api/logapicall.go @@ -0,0 +1,16 @@ +package api + +import ( + "net/http" + + "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" + "github.com/feelbeatapp/feelbeatserver/internal/lib/component" +) + +func LogApiCall(userId string, req *http.Request) { + fblog.Info(component.Api, req.Method+" "+req.URL.String(), "user", userId, "ip", req.RemoteAddr) +} + +func LogApiError(message string, err error, userId string, req *http.Request) { + fblog.Error(component.Api, req.Method+" "+req.URL.String()+": "+message, "err", err, "user", userId, "ip", req.RemoteAddr) +} diff --git a/internal/infra/api/parsebody.go b/internal/infra/api/parsebody.go new file mode 100644 index 0000000..92c53b5 --- /dev/null +++ b/internal/infra/api/parsebody.go @@ -0,0 +1,15 @@ +package api + +import ( + "encoding/json" + "io" +) + +func ParseBody(body io.ReadCloser, out any) error { + bytes, err := ReadBody(body) + if err != nil { + return err + } + + return json.Unmarshal(bytes, out) +} diff --git a/internal/infra/api/readbody.go b/internal/infra/api/readbody.go new file mode 100644 index 0000000..d252e3b --- /dev/null +++ b/internal/infra/api/readbody.go @@ -0,0 +1,16 @@ +package api + +import ( + "io" +) + +func ReadBody(body io.ReadCloser) ([]byte, error) { + defer body.Close() + + bytes, err := io.ReadAll(body) + if err != nil { + return []byte{}, err + } + + return bytes, nil +} diff --git a/internal/infra/auth/authorizethroughspotify.go b/internal/infra/auth/authorizethroughspotify.go new file mode 100644 index 0000000..e1e0efe --- /dev/null +++ b/internal/infra/auth/authorizethroughspotify.go @@ -0,0 +1,33 @@ +package auth + +import ( + "net/http" + "strings" + + "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" + "github.com/feelbeatapp/feelbeatserver/internal/lib/component" + "github.com/feelbeatapp/feelbeatserver/internal/thirdparty/spotify" +) + +func AuthorizeThroughSpotify(handler func(string, http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { + return func(res http.ResponseWriter, req *http.Request) { + authHeader := req.Header.Get("Authorization") + + splits := strings.Split(authHeader, "Bearer ") + if len(splits) != 2 { + http.Error(res, "Incorrect authorization format", http.StatusBadRequest) + fblog.Error(component.Auth, "Incorrect authorization format", "url", req.URL, "addr", req.RemoteAddr) + return + } + token := splits[1] + + userId, err := spotify.GetUserId(token) + if err != nil { + http.Error(res, http.StatusText(http.StatusForbidden), http.StatusForbidden) + fblog.Error(component.Auth, "Access denied", "reason", err) + return + } + + handler(userId, res, req) + } +} diff --git a/internal/infra/auth/authwrapper.go b/internal/infra/auth/authwrapper.go new file mode 100644 index 0000000..5f98e66 --- /dev/null +++ b/internal/infra/auth/authwrapper.go @@ -0,0 +1,5 @@ +package auth + +import "net/http" + +type AuthWrapper func(func(string, http.ResponseWriter, *http.Request)) func (http.ResponseWriter, *http.Request) diff --git a/internal/infra/ws/servewebsockets.go b/internal/infra/ws/servewebsockets.go index 06c7dd1..ad8b7bd 100644 --- a/internal/infra/ws/servewebsockets.go +++ b/internal/infra/ws/servewebsockets.go @@ -15,7 +15,7 @@ var upgrader = websocket.Upgrader{ }, } -func ServeWebsockets(hub *BasicHub, w http.ResponseWriter, r *http.Request) { +func ServeWebsockets(hub *WSHub, w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) fblog.Info(component.WebSocket, "received new connection", "ip", r.RemoteAddr) diff --git a/internal/infra/ws/hub.go b/internal/infra/ws/wshub.go similarity index 80% rename from internal/infra/ws/hub.go rename to internal/infra/ws/wshub.go index 2ebe45a..5af5f62 100644 --- a/internal/infra/ws/hub.go +++ b/internal/infra/ws/wshub.go @@ -5,7 +5,7 @@ import ( "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" ) -type BasicHub struct { +type WSHub struct { clients map[HubClient]bool broadcast chan ClientMessage register chan HubClient @@ -13,8 +13,8 @@ type BasicHub struct { exit chan bool } -func NewHub() *BasicHub { - return &BasicHub{ +func NewHub() *WSHub { + return &WSHub{ clients: make(map[HubClient]bool), broadcast: make(chan ClientMessage), register: make(chan HubClient), @@ -23,7 +23,7 @@ func NewHub() *BasicHub { } } -func (h *BasicHub) Run() { +func (h *WSHub) Run() { defer func() { for c := range h.clients { c.Close() @@ -55,18 +55,18 @@ func (h *BasicHub) Run() { } } -func (h *BasicHub) RegisterClient(client HubClient) { +func (h *WSHub) RegisterClient(client HubClient) { h.register <- client } -func (h *BasicHub) Broadcast(message ClientMessage) { +func (h *WSHub) Broadcast(message ClientMessage) { h.broadcast <- message } -func (h *BasicHub) UnregisterClient(client HubClient) { +func (h *WSHub) UnregisterClient(client HubClient) { h.unregister <- client } -func (h *BasicHub) Stop() { +func (h *WSHub) Stop() { h.exit <- true } diff --git a/internal/lib/component/main.go b/internal/lib/component/main.go index 8bf7246..9e1947f 100644 --- a/internal/lib/component/main.go +++ b/internal/lib/component/main.go @@ -6,6 +6,8 @@ const ( WebSocket = "websocket" Client = "client" Hub = "hub" + Api = "api" + Auth = "auth" AudioDownloadTask = "audiodownloadtask" ) diff --git a/internal/thirdparty/spotify/apicall.go b/internal/thirdparty/spotify/apicall.go new file mode 100644 index 0000000..e31cc3e --- /dev/null +++ b/internal/thirdparty/spotify/apicall.go @@ -0,0 +1,14 @@ +package spotify + +import "net/http" + +func newGetApiCall(path string, token string) (*http.Request, error) { + req, err := http.NewRequest("GET", apiUrl+path, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+token) + + return req, nil +} diff --git a/internal/thirdparty/spotify/apiurl.go b/internal/thirdparty/spotify/apiurl.go new file mode 100644 index 0000000..71de8aa --- /dev/null +++ b/internal/thirdparty/spotify/apiurl.go @@ -0,0 +1,3 @@ +package spotify + +const apiUrl = "https://api.spotify.com/v1" diff --git a/internal/thirdparty/spotify/getuserprofile.go b/internal/thirdparty/spotify/getuserprofile.go new file mode 100644 index 0000000..0be0d9f --- /dev/null +++ b/internal/thirdparty/spotify/getuserprofile.go @@ -0,0 +1,37 @@ +package spotify + +import ( + "fmt" + "net/http" + + "github.com/buger/jsonparser" + "github.com/feelbeatapp/feelbeatserver/internal/infra/api" +) + +func GetUserId(token string) (string, error) { + req, err := newGetApiCall("/me", token) + if err != nil { + return "", fmt.Errorf("Failed to create user id request: %w", err) + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("User id request failed: %w", err) + } + + if res.StatusCode != http.StatusOK { + return "", fmt.Errorf("Auth request failed: %s", res.Status) + } + + bytes, err := api.ReadBody(res.Body) + if err != nil { + return "", fmt.Errorf("Couldn't read user id request body: %w", err) + } + + userid, err := jsonparser.GetString(bytes, "id") + if err != nil { + return "", fmt.Errorf("Failed to parse user profile: %w", err) + } + + return userid, nil +} From 759127a751e23047d7df01e6f42d86c87efd7fb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20St=C4=99pie=C5=84?= Date: Fri, 3 Jan 2025 19:20:27 +0100 Subject: [PATCH 2/3] Prepare basic API-client integration --- cmd/feelbeatserver/setupapi.go | 6 ++++-- internal/infra/api/handlers/creategame.go | 21 ++++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/cmd/feelbeatserver/setupapi.go b/cmd/feelbeatserver/setupapi.go index 4634632..e9ceb43 100644 --- a/cmd/feelbeatserver/setupapi.go +++ b/cmd/feelbeatserver/setupapi.go @@ -11,12 +11,14 @@ type Authorizer interface { authorize() } +const baseUrl = "/api/v1" + func setupAPI(authWrapper auth.AuthWrapper) { - handlers := []func(auth.AuthWrapper){handlers.ServeCreateGame} + handlers := []func(string, auth.AuthWrapper){handlers.ServeCreateGame} fblog.Info(component.Api, "Setting up REST API", "handlers count", len(handlers)) for _, f := range handlers { - f(authWrapper) + f(baseUrl, authWrapper) } } diff --git a/internal/infra/api/handlers/creategame.go b/internal/infra/api/handlers/creategame.go index d20962c..637341b 100644 --- a/internal/infra/api/handlers/creategame.go +++ b/internal/infra/api/handlers/creategame.go @@ -1,6 +1,8 @@ package handlers import ( + "encoding/json" + "fmt" "net/http" "github.com/feelbeatapp/feelbeatserver/internal/infra/api" @@ -13,6 +15,10 @@ type createGamePayload struct { Test string `json:"test"` } +type createGameResponse struct { + RoomId string `json:"roomId"` +} + func createGameHandler(userId string, res http.ResponseWriter, req *http.Request) { var payload createGamePayload err := api.ParseBody(req.Body, &payload) @@ -22,7 +28,16 @@ func createGameHandler(userId string, res http.ResponseWriter, req *http.Request return } - _, err = res.Write([]byte("Endpoint hit!, Auth success, You are: " + userId)) + resJson, err := json.Marshal(createGameResponse{ + RoomId: "haha it's room id", + }) + if err != nil { + api.LogApiError("Couldn't encode response", err, userId, req) + return + } + + res.Header().Set("Content-Type", "application/json") + _, err = res.Write(resJson) if err != nil { api.LogApiError("Couldn't write response", err, userId, req) return @@ -31,6 +46,6 @@ func createGameHandler(userId string, res http.ResponseWriter, req *http.Request api.LogApiCall(userId, req) } -func ServeCreateGame(authWrapper auth.AuthWrapper) { - http.HandleFunc("/api/v1/create", authWrapper(createGameHandler)) +func ServeCreateGame(baseUrl string, authWrapper auth.AuthWrapper) { + http.HandleFunc(fmt.Sprintf("%s/create", baseUrl), authWrapper(createGameHandler)) } From f4a45e1d2e8405fed1db24e2235609eeab9f4502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20St=C4=99pie=C5=84?= Date: Sat, 4 Jan 2025 00:22:38 +0100 Subject: [PATCH 3/3] Prototype room creation api --- cmd/feelbeatserver/main.go | 3 +- cmd/feelbeatserver/setupapi.go | 15 ++++---- go.mod | 2 +- go.sum | 4 +-- .../api/{handlers => roomapi}/creategame.go | 32 +++++++++++------ internal/infra/api/roomapi/roomapi.go | 15 ++++++++ internal/lib/component/main.go | 12 +++---- internal/lib/feelbeaterror/errorcode.go | 16 +++++++++ internal/lib/feelbeaterror/feelbeaterror.go | 12 +++++++ internal/lib/room/room.go | 19 +++++++++++ internal/lib/room/roomsettings.go | 10 ++++++ .../roomrepository/inmemoryroomrepository.go | 34 +++++++++++++++++++ internal/lib/roomrepository/roomrepository.go | 7 ++++ internal/lib/validation/roomsettings.go | 8 +++++ internal/thirdparty/spotify/getuserprofile.go | 2 ++ 15 files changed, 164 insertions(+), 27 deletions(-) rename internal/infra/api/{handlers => roomapi}/creategame.go (54%) create mode 100644 internal/infra/api/roomapi/roomapi.go create mode 100644 internal/lib/feelbeaterror/errorcode.go create mode 100644 internal/lib/feelbeaterror/feelbeaterror.go create mode 100644 internal/lib/room/room.go create mode 100644 internal/lib/room/roomsettings.go create mode 100644 internal/lib/roomrepository/inmemoryroomrepository.go create mode 100644 internal/lib/roomrepository/roomrepository.go create mode 100644 internal/lib/validation/roomsettings.go diff --git a/cmd/feelbeatserver/main.go b/cmd/feelbeatserver/main.go index 105900d..0a6dc81 100644 --- a/cmd/feelbeatserver/main.go +++ b/cmd/feelbeatserver/main.go @@ -10,6 +10,7 @@ import ( "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" "github.com/feelbeatapp/feelbeatserver/internal/infra/ws" "github.com/feelbeatapp/feelbeatserver/internal/lib/component" + "github.com/feelbeatapp/feelbeatserver/internal/lib/roomrepository" "github.com/knadh/koanf/v2" ) @@ -40,7 +41,7 @@ func main() { ws.ServeWebsockets(hub, w, r) }) - setupAPI(auth.AuthorizeThroughSpotify) + setupAPI(auth.AuthorizeThroughSpotify, roomrepository.NewInMemoryRoomRepository()) fblog.Info(component.FeelBeatServer, "Server started", "port", port) log.Fatal(http.ListenAndServe(fmt.Sprintf("localhost:%d", port), nil)) diff --git a/cmd/feelbeatserver/setupapi.go b/cmd/feelbeatserver/setupapi.go index e9ceb43..4df1993 100644 --- a/cmd/feelbeatserver/setupapi.go +++ b/cmd/feelbeatserver/setupapi.go @@ -1,20 +1,21 @@ package main import ( - "github.com/feelbeatapp/feelbeatserver/internal/infra/api/handlers" + "github.com/feelbeatapp/feelbeatserver/internal/infra/api/roomapi" "github.com/feelbeatapp/feelbeatserver/internal/infra/auth" "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" "github.com/feelbeatapp/feelbeatserver/internal/lib/component" + "github.com/feelbeatapp/feelbeatserver/internal/lib/roomrepository" ) -type Authorizer interface { - authorize() -} - const baseUrl = "/api/v1" -func setupAPI(authWrapper auth.AuthWrapper) { - handlers := []func(string, auth.AuthWrapper){handlers.ServeCreateGame} +// TODO: Add contexts to api handler for graceful shutdown + +func setupAPI(authWrapper auth.AuthWrapper, roomRepo roomrepository.RoomRepository) { + roomApi := roomapi.NewRoomApi(roomRepo) + + handlers := []func(string, auth.AuthWrapper){roomApi.ServeCreateGame} fblog.Info(component.Api, "Setting up REST API", "handlers count", len(handlers)) diff --git a/go.mod b/go.mod index 3935de7..281a448 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require github.com/gorilla/websocket v1.5.3 require ( github.com/buger/jsonparser v1.1.1 + github.com/google/uuid v1.6.0 github.com/knadh/koanf/parsers/toml v0.1.0 github.com/knadh/koanf/providers/env v1.0.0 github.com/knadh/koanf/providers/file v1.1.2 @@ -26,7 +27,6 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/objx v0.5.2 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/sys v0.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 5f7779b..99004fd 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= @@ -40,8 +42,6 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/internal/infra/api/handlers/creategame.go b/internal/infra/api/roomapi/creategame.go similarity index 54% rename from internal/infra/api/handlers/creategame.go rename to internal/infra/api/roomapi/creategame.go index 637341b..ca97c86 100644 --- a/internal/infra/api/handlers/creategame.go +++ b/internal/infra/api/roomapi/creategame.go @@ -1,7 +1,8 @@ -package handlers +package roomapi import ( "encoding/json" + "errors" "fmt" "net/http" @@ -9,18 +10,16 @@ import ( "github.com/feelbeatapp/feelbeatserver/internal/infra/auth" "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" "github.com/feelbeatapp/feelbeatserver/internal/lib/component" + "github.com/feelbeatapp/feelbeatserver/internal/lib/feelbeaterror" + "github.com/feelbeatapp/feelbeatserver/internal/lib/room" ) -type createGamePayload struct { - Test string `json:"test"` -} - type createGameResponse struct { RoomId string `json:"roomId"` } -func createGameHandler(userId string, res http.ResponseWriter, req *http.Request) { - var payload createGamePayload +func (r RoomApi) createGameHandler(userId string, res http.ResponseWriter, req *http.Request) { + var payload room.RoomSettings err := api.ParseBody(req.Body, &payload) if err != nil { http.Error(res, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) @@ -28,8 +27,21 @@ func createGameHandler(userId string, res http.ResponseWriter, req *http.Request return } + roomId, err := r.roomRepo.CreateRoom(userId, payload) + if err != nil { + var fbError *feelbeaterror.FeelBeatError + if errors.As(err, &fbError) { + http.Error(res, string(fbError.UserMessage), feelbeaterror.StatusCode(fbError.UserMessage)) + } else { + http.Error(res, feelbeaterror.Default, feelbeaterror.StatusCode(feelbeaterror.Default)) + } + + api.LogApiError("Create room failed", err, userId, req) + return + } + resJson, err := json.Marshal(createGameResponse{ - RoomId: "haha it's room id", + RoomId: roomId, }) if err != nil { api.LogApiError("Couldn't encode response", err, userId, req) @@ -46,6 +58,6 @@ func createGameHandler(userId string, res http.ResponseWriter, req *http.Request api.LogApiCall(userId, req) } -func ServeCreateGame(baseUrl string, authWrapper auth.AuthWrapper) { - http.HandleFunc(fmt.Sprintf("%s/create", baseUrl), authWrapper(createGameHandler)) +func (r RoomApi) ServeCreateGame(baseUrl string, authWrapper auth.AuthWrapper) { + http.HandleFunc(fmt.Sprintf("%s/create", baseUrl), authWrapper(r.createGameHandler)) } diff --git a/internal/infra/api/roomapi/roomapi.go b/internal/infra/api/roomapi/roomapi.go new file mode 100644 index 0000000..dad01e7 --- /dev/null +++ b/internal/infra/api/roomapi/roomapi.go @@ -0,0 +1,15 @@ +package roomapi + +import ( + "github.com/feelbeatapp/feelbeatserver/internal/lib/roomrepository" +) + +type RoomApi struct { + roomRepo roomrepository.RoomRepository +} + +func NewRoomApi(roomRepo roomrepository.RoomRepository) RoomApi { + return RoomApi{ + roomRepo: roomRepo, + } +} diff --git a/internal/lib/component/main.go b/internal/lib/component/main.go index 9e1947f..bb515e3 100644 --- a/internal/lib/component/main.go +++ b/internal/lib/component/main.go @@ -2,12 +2,12 @@ package component const ( FeelBeatServer = "FeelBeatServer" - Config = "config" - WebSocket = "websocket" - Client = "client" - Hub = "hub" - Api = "api" - Auth = "auth" + Config = "config" + WebSocket = "websocket" + Client = "client" + Hub = "hub" + Api = "api" + Auth = "auth" AudioDownloadTask = "audiodownloadtask" ) diff --git a/internal/lib/feelbeaterror/errorcode.go b/internal/lib/feelbeaterror/errorcode.go new file mode 100644 index 0000000..fc470fb --- /dev/null +++ b/internal/lib/feelbeaterror/errorcode.go @@ -0,0 +1,16 @@ +package feelbeaterror + +import "net/http" + +type ErrorCode string + +const ( + Default = "unexpected_error" +) + +func StatusCode(code ErrorCode) int { + switch code { + default: + return http.StatusInternalServerError + } +} diff --git a/internal/lib/feelbeaterror/feelbeaterror.go b/internal/lib/feelbeaterror/feelbeaterror.go new file mode 100644 index 0000000..51a60ba --- /dev/null +++ b/internal/lib/feelbeaterror/feelbeaterror.go @@ -0,0 +1,12 @@ +package feelbeaterror + +import "fmt" + +type FeelBeatError struct { + DebugMessage string + UserMessage ErrorCode +} + +func (e *FeelBeatError) Error() string { + return fmt.Sprintf("%s: %s", e.UserMessage, e.DebugMessage) +} diff --git a/internal/lib/room/room.go b/internal/lib/room/room.go new file mode 100644 index 0000000..b5b3f51 --- /dev/null +++ b/internal/lib/room/room.go @@ -0,0 +1,19 @@ +package room + +type Room struct { + id string + ownerId string + settings RoomSettings +} + +func NewRoom(id string, ownerId string, settings RoomSettings) Room { + return Room{ + id: id, + settings: settings, + ownerId: ownerId, + } +} + +func (r Room) Id() string { + return r.id +} diff --git a/internal/lib/room/roomsettings.go b/internal/lib/room/roomsettings.go new file mode 100644 index 0000000..217e12c --- /dev/null +++ b/internal/lib/room/roomsettings.go @@ -0,0 +1,10 @@ +package room + +type RoomSettings struct { + MaxPlayers int `json:"maxPlayers"` + TurnCount int `json:"turnCount"` + TimePenaltyPerSecond int `json:"timePenaltyPerSecond"` + BasePoints int `json:"basePoints"` + IncorrectGuessPenalty int `json:"incorrectGuessPenalty"` + PlaylistId string `json:"playlistId"` +} diff --git a/internal/lib/roomrepository/inmemoryroomrepository.go b/internal/lib/roomrepository/inmemoryroomrepository.go new file mode 100644 index 0000000..30ed7e3 --- /dev/null +++ b/internal/lib/roomrepository/inmemoryroomrepository.go @@ -0,0 +1,34 @@ +package roomrepository + +import ( + "fmt" + + "github.com/feelbeatapp/feelbeatserver/internal/lib/room" + "github.com/feelbeatapp/feelbeatserver/internal/lib/validation" + "github.com/google/uuid" +) + +type InMemoryRoomRepository struct { + rooms map[string]room.Room +} + +func NewInMemoryRoomRepository() InMemoryRoomRepository { + return InMemoryRoomRepository{ + rooms: make(map[string]room.Room), + } +} + +// TODO: Implement fetching playlist details from spotify +func (r InMemoryRoomRepository) CreateRoom(ownderId string, settings room.RoomSettings) (string, error) { + err := validation.ValidateRoomSettings(settings) + if err != nil { + return "", err + } + + newRoom := room.NewRoom(uuid.NewString(), ownderId, settings) + r.rooms[newRoom.Id()] = newRoom + + fmt.Println(r.rooms) + + return newRoom.Id(), nil +} diff --git a/internal/lib/roomrepository/roomrepository.go b/internal/lib/roomrepository/roomrepository.go new file mode 100644 index 0000000..23bd8a9 --- /dev/null +++ b/internal/lib/roomrepository/roomrepository.go @@ -0,0 +1,7 @@ +package roomrepository + +import "github.com/feelbeatapp/feelbeatserver/internal/lib/room" + +type RoomRepository interface { + CreateRoom(string, room.RoomSettings) (string, error) +} diff --git a/internal/lib/validation/roomsettings.go b/internal/lib/validation/roomsettings.go new file mode 100644 index 0000000..3bbcede --- /dev/null +++ b/internal/lib/validation/roomsettings.go @@ -0,0 +1,8 @@ +package validation + +import "github.com/feelbeatapp/feelbeatserver/internal/lib/room" + +// TODO: Implement validation of room settings +func ValidateRoomSettings(settings room.RoomSettings) error { + return nil +} diff --git a/internal/thirdparty/spotify/getuserprofile.go b/internal/thirdparty/spotify/getuserprofile.go index 0be0d9f..853968f 100644 --- a/internal/thirdparty/spotify/getuserprofile.go +++ b/internal/thirdparty/spotify/getuserprofile.go @@ -8,6 +8,8 @@ import ( "github.com/feelbeatapp/feelbeatserver/internal/infra/api" ) +// TODO: Rethink error handling in GetUserId + func GetUserId(token string) (string, error) { req, err := newGetApiCall("/me", token) if err != nil {