From 08ab312f924f7c7e12c454887da13350e51f6a23 Mon Sep 17 00:00:00 2001 From: Miguel de la Cruz Date: Thu, 16 Sep 2021 00:37:07 +0200 Subject: [PATCH] Add getGame and listGames endpoints, client methods and initial components --- server/api/game.go | 21 ++++++++++++++++++++ server/app/game.go | 14 ++++++++++++++ server/web/web.go | 1 + webapp/src/client.js | 41 ++++++++++++++++++++++++++++++++++++++-- webapp/src/index.js | 9 ++++++++- webapp/src/log.js | 8 ++++---- webapp/src/pages/game.js | 22 +++++++++++++++++++++ webapp/src/pages/home.js | 28 ++++++++++++++++++++++++++- 8 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 webapp/src/pages/game.js diff --git a/server/api/game.go b/server/api/game.go index 4a7ef02..26d5040 100644 --- a/server/api/game.go +++ b/server/api/game.go @@ -3,7 +3,9 @@ package api import ( "fmt" "net/http" + "strconv" + "github.com/gorilla/mux" "github.com/rs/zerolog/log" ) @@ -44,3 +46,22 @@ func (a *API) ListGames(w http.ResponseWriter, r *http.Request) { JSON(w, games, 200) } + +func (a *API) GetGame(w http.ResponseWriter, r *http.Request) { + gameID, _ := strconv.Atoi(mux.Vars(r)["id"]) + user, _ := UserFromRequest(r) + + game, err := a.App.GetGameForUser(gameID, user.ID) + if err != nil { + log.Error().Err(err).Msg("game couldn't be fetch") + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + if game == nil { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + + JSON(w, game, 200) +} diff --git a/server/app/game.go b/server/app/game.go index 42e1cbc..d11960a 100644 --- a/server/app/game.go +++ b/server/app/game.go @@ -14,6 +14,20 @@ func (a *App) AddMember(gameID, userID int, role string) (*model.GameMember, err return a.Store.Game().AddMember(gameID, userID, role) } +func (a *App) GetGameForUser(gameID, userID int) (*model.Game, error) { + game, err := a.Store.Game().GetByID(gameID) + if err != nil { + return nil, err + } + + // ToDo: create a helper like userIsMember or + // userHasPermissionToGame instead of checking ownership + if game.UserID != userID { + return nil, nil + } + return game, nil +} + func (a *App) ListGames() ([]*model.Game, error) { games, err := a.Store.Game().List() if err == sql.ErrNoRows { diff --git a/server/web/web.go b/server/web/web.go index 5bf7c6a..52f6d7f 100644 --- a/server/web/web.go +++ b/server/web/web.go @@ -41,6 +41,7 @@ func (w *WebServer) RegisterRoutes(api *api.API) { apiRouter.HandleFunc("/users", api.Secured(api.CreateUser)).Methods("POST") apiRouter.HandleFunc("/games", api.Secured(api.CreateGame)).Methods("POST") apiRouter.HandleFunc("/games", api.Secured(api.ListGames)).Methods("GET") + apiRouter.HandleFunc("/games/{id:[0-9]+}", api.Secured(api.GetGame)).Methods("GET") staticFSSub, _ := fs.Sub(staticFS, "static") staticFSHandler := StaticFSHandler{http.FileServer(http.FS(staticFSSub))} diff --git a/webapp/src/client.js b/webapp/src/client.js index 3320bb2..1945233 100644 --- a/webapp/src/client.js +++ b/webapp/src/client.js @@ -1,3 +1,14 @@ +import * as log from './log' + + +function tokenHeaders(headers) { + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${localStorage.getItem('token')}`, + ...headers + } +} + export class Client { login(username, password) { // ToDo: handle error @@ -9,13 +20,39 @@ export class Client { if (r.status === 200) { return r.text() } - console.error("INVALID") - throw new Error("invalid credentials") + log.error(`Got invalid response when trying to login. Code ${r.status}`) + throw new Error(`${r.status} invalid response`) }).then(token => { localStorage.setItem('token', token) return token }) } + + listGames() { + return fetch("/api/games", { + method: 'GET', + headers: tokenHeaders(), + }).then(r => { + if (r.status === 200) { + return r.json() + } + log.error(`Got invalid response when trying to list games. Code ${r.status}`) + throw new Error(`${r.status} invalid response`) + }) + } + + getGame(id) { + return fetch(`/api/games/${id}`, { + method: 'GET', + headers: tokenHeaders(), + }).then(r => { + if (r.status === 200) { + return r.json() + } + log.error(`Got invalid response when trying to get game ${id}. Code ${r.status}`) + throw new Error(`${r.status} invalid response`) + }) + } } const client = new Client() diff --git a/webapp/src/index.js b/webapp/src/index.js index a5fa1b9..7d31ab9 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -13,6 +13,7 @@ import { CssBaseline } from '@material-ui/core'; import { TokenContext } from './context' import Login from './pages/login' import Home from './pages/home' +import Game from './pages/game' const Secure = ({children}) => { @@ -36,7 +37,13 @@ const App = () => { - + + + + + + + diff --git a/webapp/src/log.js b/webapp/src/log.js index a1ca099..3297a76 100644 --- a/webapp/src/log.js +++ b/webapp/src/log.js @@ -13,25 +13,25 @@ function nowStr() { return `${now.getFullYear()}${now.getDate()}${now.getMonth()}.${now.getHours()}${now.getMinutes()}${now.getSeconds()}` } -export function Debug(msg) { +export function debug(msg) { if (level <= levelDebug) { console.log(`[DBG ${nowStr()}] ${msg}`) } } -export function Info(msg) { +export function info(msg) { if (level <= levelInfo) { console.log(`[INF ${nowStr()}] ${msg}`) } } -export function Warn(msg) { +export function warn(msg) { if (level <= levelWarn) { console.warn(`[WRN ${nowStr()}] ${msg}`) } } -export function Error(msg) { +export function error(msg) { if (level <= levelError) { console.error(`[ERR ${nowStr()}] ${msg}`) } diff --git a/webapp/src/pages/game.js b/webapp/src/pages/game.js new file mode 100644 index 0000000..c3c9c53 --- /dev/null +++ b/webapp/src/pages/game.js @@ -0,0 +1,22 @@ +import { useState, useEffect } from 'react' +import { useParams } from 'react-router-dom' + +import client from '../client' + + +const Game = () => { + const [game, setGame] = useState({}) + const { gameId } = useParams() + + useEffect(() => { + client.getGame(gameId).then(game => setGame(game)) + }, [gameId]) + + return ( +
+

{game.name}

+
+ ) +} + +export default Game diff --git a/webapp/src/pages/home.js b/webapp/src/pages/home.js index 043c4fa..5a80e74 100644 --- a/webapp/src/pages/home.js +++ b/webapp/src/pages/home.js @@ -1,12 +1,36 @@ -import { useContext } from 'react' +import { + useContext, + useEffect, + useState +} from 'react' +import { Link } from 'react-router-dom' import { Box, Button } from '@material-ui/core'; import { TokenContext } from '../context' +import client from '../client' +const GameList = () => { + const [games, setGames] = useState([]) + + useEffect(() => { + client.listGames().then(gameList => setGames(gameList)) + }, []) + + return ( +
+

Game List

+ +
    + {games.map(game => (
  • {game.name}
  • ))} +
+
+ ) +} + const Home = () => { const { token, setToken } = useContext(TokenContext) @@ -20,6 +44,8 @@ const Home = () => {

Home

+ +