Skip to content

Commit

Permalink
Creating lobby API config and making the UUID customizable (#396)
Browse files Browse the repository at this point in the history
* Creating lobby API config and making the UUID customizable

* Use shortid for game IDs

* Update test to use shortid

* fix heading sizes

* fix grammar

* fix capitalization

* Rename back to uuid
  • Loading branch information
vdfdev authored and nicolodavis committed May 24, 2019
1 parent b5eb687 commit 4964e3f
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 44 deletions.
2 changes: 1 addition & 1 deletion docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@
- [Game](api/Game.md)
- [Client](api/Client.md)
- [Server](api/Server.md)
- [API](api/API.md)
- [Lobby](api/Lobby.md)
- [Random](api/Random.md)
58 changes: 47 additions & 11 deletions docs/api/API.md → docs/api/Lobby.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
# API
# Lobby

The [Server](/api/Server) hosts a REST API that can be used to create and join games. It is particularly useful when you want to
### React components

You can use the lobby component with the code below:

```js
import { Lobby } from 'boardgame.io/react';

<Lobby
gameServer={`https://${window.location.hostname}:8000`}
lobbyServer={`https://${window.location.hostname}:8000`}
gameComponents={importedGames}
/>;
```

`importedGames` is an array of objects with these fields:

- `game`: The boardgame.io `Game` definition.
- `board`: The React component that will render the board.

### Server-side API

The [Server](/api/Server) hosts the Lobby REST API that can be used to create and join games. It is particularly useful when you want to
authenticate clients to prove that they have the right to send
actions on behalf of a player.

Expand All @@ -10,9 +31,24 @@ A game that is authenticated will not accept moves from a client on behalf of a

Use the `create` API call to create a game that requires credential tokens. When you call the `join` API, you can retrieve the credential token for a particular player.

### Creating a game
#### Configuration

You can pass `lobbyConfig` to configure the Lobby API
during server startup:

```js
server.run({ port: 8000, lobbyConfig });
```

Options are:

- `apiPort`: If specified, it runs the Lobby API in a separate Koa server on this port. Otherwise, it shares the same Koa server runnning on the default boardgame.io `port`.
- `apiCallback`: Called when the Koa server is ready. Only applicable if `apiPort` is specified.
- `shortid`: Function that returns an unique identifier, needed for creating new match codes and user's credentials in matches. If not specified, uses [shortid](https://www.npmjs.com/package/shortid).

#### Creating a game

#### POST `/games/{name}/create`
##### POST `/games/{name}/create`

Creates a new authenticated game for a game named `name`.

Expand All @@ -24,9 +60,9 @@ Accepts two parameters:

Returns `gameID`, which is the ID of the newly created game instance.

### Joining a game
#### Joining a game

#### POST `/games/{name}/{id}/join`
##### POST `/games/{name}/{id}/join`

Allows a player to join a particular game instance `id` of a game named `name`.

Expand All @@ -38,9 +74,9 @@ Accepts two parameters, all required:

Returns `playerCredentials` which is the token this player will require to authenticate their actions in the future.

### Leaving a game
#### Leaving a game

#### POST `/games/{name}/{id}/leave`
##### POST `/games/{name}/{id}/leave`

Leave the game instance `id` of a game named `name` previously joined by the player.

Expand All @@ -50,9 +86,9 @@ Accepts two parameters, all required:

`playerCredentials`: the authentication token of the player.

### Listing all instances of a given game
#### Listing all instances of a given game

#### GET `/games/{name}`
##### GET `/games/{name}`

Returns all instances of the game named `name`.

Expand All @@ -62,6 +98,6 @@ Returns an array of `rooms`. Each instance has fields:

`players`: the list of seats and players that have joined the game, if any.

### Client Authentication
#### Client Authentication

All actions for an authenticated game require an additional payload field `credentials`, which must be the given secret associated with the player.
6 changes: 0 additions & 6 deletions docs/api/Server.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,3 @@ server.run(8000);
```
server.run(8000, () => console.log("server running..."));
```

##### Running the API server on a separate port

```js
server.run({ port: 8000, apiPort: 8001 });
```
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
"react-cookies": "^0.1.0",
"react-dragtastic": "^2.4.3",
"redux": "^4.0.0",
"shortid": "^2.2.14",
"socket.io": "^2.1.1",
"uuid": "3.2.1"
},
Expand Down
37 changes: 27 additions & 10 deletions src/server/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@
const Koa = require('koa');
const Router = require('koa-router');
const koaBody = require('koa-body');
const uuid = require('uuid/v4');
const uuid = require('shortid').generate;
const cors = require('@koa/cors');

import { InitializeGame } from '../core/reducer';

const createCredentials = () => uuid();
const isGameMetadataKey = (key, gameName) =>
key.match(gameName + ':.*:metadata');
const getNamespacedGameID = (gameID, gameName) => `${gameName}:${gameID}`;
const getNewGameInstanceID = () => uuid();
const createGameMetadata = () => ({
players: {},
});
Expand All @@ -33,8 +31,15 @@ const GameMetadataKey = gameID => `${gameID}:metadata`;
* @param {number} numPlayers - The number of players.
* @param {object} setupData - User-defined object that's available
* during game setup.
* @param {object } lobbyConfig - Configuration options for the lobby.
*/
export const CreateGame = async (db, game, numPlayers, setupData) => {
export const CreateGame = async (
db,
game,
numPlayers,
setupData,
lobbyConfig
) => {
const gameMetadata = createGameMetadata();

const state = InitializeGame({
Expand All @@ -44,11 +49,11 @@ export const CreateGame = async (db, game, numPlayers, setupData) => {
});

for (let playerIndex = 0; playerIndex < numPlayers; playerIndex++) {
const credentials = createCredentials();
const credentials = lobbyConfig.uuid();
gameMetadata.players[playerIndex] = { id: playerIndex, credentials };
}

const gameID = getNewGameInstanceID();
const gameID = lobbyConfig.uuid();
const namespacedGameID = getNamespacedGameID(gameID, game.name);

await db.set(GameMetadataKey(namespacedGameID), gameMetadata);
Expand All @@ -57,12 +62,18 @@ export const CreateGame = async (db, game, numPlayers, setupData) => {
return gameID;
};

export const createApiServer = ({ db, games }) => {
export const createApiServer = ({ db, games, lobbyConfig }) => {
const app = new Koa();
return addApiToServer({ app, db, games });
return addApiToServer({ app, db, games, lobbyConfig });
};

export const addApiToServer = ({ app, db, games }) => {
export const addApiToServer = ({ app, db, games, lobbyConfig }) => {
if (!lobbyConfig) {
lobbyConfig = {};
}
if (!lobbyConfig.uuid) {
lobbyConfig = { ...lobbyConfig, uuid };
}
const router = new Router();

router.get('/games', async ctx => {
Expand All @@ -81,7 +92,13 @@ export const addApiToServer = ({ app, db, games }) => {
}

const game = games.find(g => g.name === gameName);
const gameID = await CreateGame(db, game, numPlayers, setupData);
const gameID = await CreateGame(
db,
game,
numPlayers,
setupData,
lobbyConfig
);

ctx.body = {
gameID,
Expand Down
7 changes: 6 additions & 1 deletion src/server/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -515,11 +515,16 @@ describe('.addApiToServer', () => {
setup: () => {},
}),
];
});

test('call .use method several times', async () => {
addApiToServer({ app: server, db, games });
expect(server.use.mock.calls.length).toBeGreaterThan(1);
});

test('call .use method several times', async () => {
test('call .use method several times with uuid', async () => {
const uuid = () => 'foo';
addApiToServer({ app: server, db, games, lobbyConfig: { uuid } });
expect(server.use.mock.calls.length).toBeGreaterThan(1);
});
});
Expand Down
16 changes: 8 additions & 8 deletions src/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ export const createServerRunConfig = (portOrConfig, callback) => {
if (portOrConfig && typeof portOrConfig === 'object') {
config.port = portOrConfig.port;
config.callback = portOrConfig.callback || callback;
config.apiPort = portOrConfig.apiPort;
config.apiCallback = portOrConfig.apiCallback;
config.lobbyConfig = portOrConfig.lobbyConfig;
} else {
config.port = portOrConfig;
config.callback = callback;
Expand Down Expand Up @@ -63,16 +62,17 @@ export function Server({ games, db, transport }) {
// DB
await db.connect();

// API
// Lobby API
const lobbyConfig = serverRunConfig.lobbyConfig;
let apiServer;
if (!serverRunConfig.apiPort) {
addApiToServer({ app, db, games });
if (!lobbyConfig || !lobbyConfig.apiPort) {
addApiToServer({ app, db, games, lobbyConfig });
} else {
// Run API in a separate Koa app.
const api = createApiServer({ db, games });
const api = createApiServer({ db, games, lobbyConfig });
apiServer = await api.listen(
serverRunConfig.apiPort,
serverRunConfig.apiCallback
lobbyConfig.apiPort,
lobbyConfig.apiCallback
);
logger.info(`API serving on ${apiServer.address().port}...`);
}
Expand Down
23 changes: 16 additions & 7 deletions src/server/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ describe('run', () => {

test('multiple servers running', async () => {
server = Server({ games: [game] });
runningServer = await server.run({ port: 57890, apiPort: 57891 });
runningServer = await server.run({
port: 57890,
lobbyConfig: { apiPort: 57891 },
});

expect(server).not.toBeUndefined();
expect(api.addApiToServer).not.toBeCalled();
Expand Down Expand Up @@ -178,23 +181,29 @@ describe('createServerRunConfig', () => {
callback: mockCallback,
});

expect(createServerRunConfig({ port: 1234, apiPort: 5467 })).toEqual({
expect(
createServerRunConfig({ port: 1234, lobbyConfig: { apiPort: 5467 } })
).toEqual({
port: 1234,
callback: undefined,
apiPort: 5467,
lobbyConfig: { apiPort: 5467 },
});
expect(
createServerRunConfig({
port: 1234,
callback: mockCallback,
apiPort: 5467,
apiCallback: mockApiCallback,
lobbyConfig: {
apiPort: 5467,
apiCallback: mockApiCallback,
},
})
).toEqual({
port: 1234,
callback: mockCallback,
apiPort: 5467,
apiCallback: mockApiCallback,
lobbyConfig: {
apiPort: 5467,
apiCallback: mockApiCallback,
},
});
});
});

0 comments on commit 4964e3f

Please sign in to comment.