Add getGame and listGames endpoints, client methods and initial components

This commit is contained in:
Miguel de la Cruz 2021-09-16 00:37:07 +02:00
parent ff455414f8
commit 08ab312f92
8 changed files with 136 additions and 8 deletions

View file

@ -3,7 +3,9 @@ package api
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -44,3 +46,22 @@ func (a *API) ListGames(w http.ResponseWriter, r *http.Request) {
JSON(w, games, 200) 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)
}

View file

@ -14,6 +14,20 @@ func (a *App) AddMember(gameID, userID int, role string) (*model.GameMember, err
return a.Store.Game().AddMember(gameID, userID, role) 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) { func (a *App) ListGames() ([]*model.Game, error) {
games, err := a.Store.Game().List() games, err := a.Store.Game().List()
if err == sql.ErrNoRows { if err == sql.ErrNoRows {

View file

@ -41,6 +41,7 @@ func (w *WebServer) RegisterRoutes(api *api.API) {
apiRouter.HandleFunc("/users", api.Secured(api.CreateUser)).Methods("POST") apiRouter.HandleFunc("/users", api.Secured(api.CreateUser)).Methods("POST")
apiRouter.HandleFunc("/games", api.Secured(api.CreateGame)).Methods("POST") apiRouter.HandleFunc("/games", api.Secured(api.CreateGame)).Methods("POST")
apiRouter.HandleFunc("/games", api.Secured(api.ListGames)).Methods("GET") 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") staticFSSub, _ := fs.Sub(staticFS, "static")
staticFSHandler := StaticFSHandler{http.FileServer(http.FS(staticFSSub))} staticFSHandler := StaticFSHandler{http.FileServer(http.FS(staticFSSub))}

View file

@ -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 { export class Client {
login(username, password) { login(username, password) {
// ToDo: handle error // ToDo: handle error
@ -9,13 +20,39 @@ export class Client {
if (r.status === 200) { if (r.status === 200) {
return r.text() return r.text()
} }
console.error("INVALID") log.error(`Got invalid response when trying to login. Code ${r.status}`)
throw new Error("invalid credentials") throw new Error(`${r.status} invalid response`)
}).then(token => { }).then(token => {
localStorage.setItem('token', token) localStorage.setItem('token', token)
return 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() const client = new Client()

View file

@ -13,6 +13,7 @@ import { CssBaseline } from '@material-ui/core';
import { TokenContext } from './context' import { TokenContext } from './context'
import Login from './pages/login' import Login from './pages/login'
import Home from './pages/home' import Home from './pages/home'
import Game from './pages/game'
const Secure = ({children}) => { const Secure = ({children}) => {
@ -36,7 +37,13 @@ const App = () => {
<Login /> <Login />
</Route> </Route>
<Route path="/"> <Route path="/game/:gameId">
<Secure>
<Game />
</Secure>
</Route>
<Route exact path="/">
<Secure> <Secure>
<Home /> <Home />
</Secure> </Secure>

View file

@ -13,25 +13,25 @@ function nowStr() {
return `${now.getFullYear()}${now.getDate()}${now.getMonth()}.${now.getHours()}${now.getMinutes()}${now.getSeconds()}` return `${now.getFullYear()}${now.getDate()}${now.getMonth()}.${now.getHours()}${now.getMinutes()}${now.getSeconds()}`
} }
export function Debug(msg) { export function debug(msg) {
if (level <= levelDebug) { if (level <= levelDebug) {
console.log(`[DBG ${nowStr()}] ${msg}`) console.log(`[DBG ${nowStr()}] ${msg}`)
} }
} }
export function Info(msg) { export function info(msg) {
if (level <= levelInfo) { if (level <= levelInfo) {
console.log(`[INF ${nowStr()}] ${msg}`) console.log(`[INF ${nowStr()}] ${msg}`)
} }
} }
export function Warn(msg) { export function warn(msg) {
if (level <= levelWarn) { if (level <= levelWarn) {
console.warn(`[WRN ${nowStr()}] ${msg}`) console.warn(`[WRN ${nowStr()}] ${msg}`)
} }
} }
export function Error(msg) { export function error(msg) {
if (level <= levelError) { if (level <= levelError) {
console.error(`[ERR ${nowStr()}] ${msg}`) console.error(`[ERR ${nowStr()}] ${msg}`)
} }

22
webapp/src/pages/game.js Normal file
View file

@ -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 (
<div class="game">
<h1>{game.name}</h1>
</div>
)
}
export default Game

View file

@ -1,12 +1,36 @@
import { useContext } from 'react' import {
useContext,
useEffect,
useState
} from 'react'
import { Link } from 'react-router-dom'
import { import {
Box, Box,
Button Button
} from '@material-ui/core'; } from '@material-ui/core';
import { TokenContext } from '../context' import { TokenContext } from '../context'
import client from '../client'
const GameList = () => {
const [games, setGames] = useState([])
useEffect(() => {
client.listGames().then(gameList => setGames(gameList))
}, [])
return (
<div class="game-list">
<h3 class="game-list-title">Game List</h3>
<ul>
{games.map(game => (<li><Link to={`/game/${game.id}`}>{game.name}</Link></li>))}
</ul>
</div>
)
}
const Home = () => { const Home = () => {
const { token, setToken } = useContext(TokenContext) const { token, setToken } = useContext(TokenContext)
@ -20,6 +44,8 @@ const Home = () => {
<Box> <Box>
<h1>Home</h1> <h1>Home</h1>
<GameList />
<Button <Button
variant="contained" variant="contained"
color="secondary" color="secondary"