This commit is contained in:
Ben Grant 2021-02-27 00:52:20 +11:00
parent 15d4e2f126
commit edcd4dcaa0
15 changed files with 1195 additions and 82 deletions

View file

@ -3,4 +3,6 @@
.git .git
.gitignore .gitignore
.env
node_modules/ node_modules/

View file

@ -1,6 +1,7 @@
/node_modules /node_modules
.DS_Store .DS_Store
.env
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local

View file

@ -1,6 +1,10 @@
runtime: nodejs10 runtime: nodejs
entrypoint: node index.js
service: api service: api
env: flex
automatic_scaling:
min_num_instances: 1
max_num_instances: 4
endpoints_api_service: endpoints_api_service:
name: api-dot-crabfit.appspot.com name: api-dot-crabfit.appspot.com

View file

@ -1,16 +1,30 @@
require('dotenv').config();
const { Datastore } = require('@google-cloud/datastore');
const express = require('express'); const express = require('express');
const package = require('./package.json'); const package = require('./package.json');
const app = express();
const port = 8080;
const stats = require('./routes/stats'); const stats = require('./routes/stats');
const getEvent = require('./routes/getEvent'); const getEvent = require('./routes/getEvent');
const createEvent = require('./routes/createEvent'); const createEvent = require('./routes/createEvent');
const getPeople = require('./routes/getPeople'); const getPeople = require('./routes/getPeople');
const createPerson = require('./routes/createPerson'); const createPerson = require('./routes/createPerson');
const login = require('./routes/login');
const updatePerson = require('./routes/updatePerson'); const updatePerson = require('./routes/updatePerson');
const app = express();
const port = 8080;
const datastore = new Datastore({
keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS,
});
app.use(express.json()); app.use(express.json());
app.use((req, res, next) => {
req.datastore = datastore;
next();
});
app.get('/', (req, res) => res.send(`Crabfit API v${package.version}`)); app.get('/', (req, res) => res.send(`Crabfit API v${package.version}`));
@ -19,7 +33,8 @@ app.get('/event/:eventId', getEvent);
app.post('/event', createEvent); app.post('/event', createEvent);
app.get('/event/:eventId/people', getPeople); app.get('/event/:eventId/people', getPeople);
app.post('/event/:eventId/people', createPerson); app.post('/event/:eventId/people', createPerson);
app.patch('/event/:eventId/people/:personId', updatePerson); app.get('/event/:eventId/people/:personName', login);
app.patch('/event/:eventId/people/:personName', updatePerson);
app.listen(port, () => { app.listen(port, () => {
console.log(`Crabfit API listening at http://localhost:${port}`) console.log(`Crabfit API listening at http://localhost:${port}`)

View file

@ -6,7 +6,17 @@
"author": "Ben Grant", "author": "Ben Grant",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"engines": {
"node": ">= 10.0.0"
},
"scripts": {
"start": "node index.js"
},
"dependencies": { "dependencies": {
"@google-cloud/datastore": "^6.3.1",
"bcrypt": "^5.0.1",
"dayjs": "^1.10.4",
"dotenv": "^8.2.0",
"express": "^4.17.1" "express": "^4.17.1"
} }
} }

View file

@ -1,10 +1,43 @@
module.exports = (req, res) => { const dayjs = require('dayjs');
const generateId = (name) => {
const id = name.trim().toLowerCase().replace(/[^A-Za-z0-9 ]/g, '').replace(/\s+/g, '-');
const number = Math.floor(100000 + Math.random() * 900000);
return `${id}-${number}`;
};
module.exports = async (req, res) => {
const { event } = req.body; const { event } = req.body;
if (event) { try {
console.log(event); const eventId = generateId(event.name);
res.sendStatus(201); const currentTime = dayjs().unix();
} else {
const entity = {
key: req.datastore.key(['Event', eventId]),
data: {
name: event.name.trim(),
created: currentTime,
timezone: event.timezone,
startTime: event.startTime,
endTime: event.endTime,
dates: event.dates,
},
};
await req.datastore.insert(entity);
res.status(201).send({
id: eventId,
name: event.name.trim(),
created: currentTime,
timezone: event.timezone,
startTime: event.startTime,
endTime: event.endTime,
dates: event.dates,
});
} catch (e) {
console.error(e);
res.sendStatus(400); res.sendStatus(400);
} }
}; };

View file

@ -1,15 +1,45 @@
module.exports = (req, res) => { const dayjs = require('dayjs');
const bcrypt = require('bcrypt');
module.exports = async (req, res) => {
const { eventId } = req.params; const { eventId } = req.params;
const { person } = req.body; const { person } = req.body;
if (eventId) { try {
if (person) { const event = (await req.datastore.get(req.datastore.key(['Event', eventId])))[0];
console.log(person);
res.sendStatus(201); if (event) {
if (person) {
const currentTime = dayjs().unix();
// If password
let hash = null;
if (person.password) {
hash = await bcrypt.hash(person.password, 10);
}
const entity = {
key: req.datastore.key('Person'),
data: {
name: person.name.trim(),
password: hash,
eventId: eventId,
created: currentTime,
availability: [],
},
};
await req.datastore.insert(entity);
res.sendStatus(201);
} else {
res.sendStatus(400);
}
} else { } else {
res.sendStatus(400); res.sendStatus(404);
} }
} else { } catch (e) {
res.sendStatus(404); console.error(e);
res.sendStatus(400);
} }
}; };

View file

@ -1,21 +1,19 @@
module.exports = (req, res) => { module.exports = async (req, res) => {
const { eventId } = req.params; const { eventId } = req.params;
if (eventId) { try {
res.send({ const event = (await req.datastore.get(req.datastore.key(['Event', eventId])))[0];
id: 'event-name-4701240',
name: 'Event name', if (event) {
eventCreated: 379642017932, res.send({
timezone: '247', id: eventId,
startTime: 0900, ...event,
endTime: 1700, });
dates: [ } else {
'26022021', res.sendStatus(404);
'27022021', }
'28022021', } catch (e) {
], console.error(e);
});
} else {
res.sendStatus(404); res.sendStatus(404);
} }
}; };

View file

@ -1,27 +1,19 @@
module.exports = (req, res) => { module.exports = async (req, res) => {
const { eventId } = req.params; const { eventId } = req.params;
if (eventId) { try {
const query = req.datastore.createQuery('Person').filter('eventId', eventId);
let people = (await req.datastore.runQuery(query))[0];
people = people.map(person => ({
name: person.name,
availability: person.availability,
}));
res.send({ res.send({
people: [ people,
{
name: 'Laura',
password: null,
eventId: 'event-name-4701240',
availability: [
[
'START',
'END',
],
[
'START',
'END',
],
],
},
],
}); });
} else { } catch (e) {
console.error(e);
res.sendStatus(404); res.sendStatus(404);
} }
}; };

View file

@ -0,0 +1,32 @@
const bcrypt = require('bcrypt');
module.exports = async (req, res) => {
const { eventId, personName } = req.params;
const { person } = req.body;
try {
const query = req.datastore.createQuery('Person')
.filter('eventId', eventId)
.filter('name', personName);
let personResult = (await req.datastore.runQuery(query))[0][0];
if (personResult) {
if (personResult.password) {
const passwordsMatch = person && person.password && await bcrypt.compare(person.password, personResult.password);
if (!passwordsMatch) {
return res.status(401).send('Incorrect password');
}
}
res.send({
name: personName,
availability: personResult.availability,
});
} else {
res.sendStatus(404);
}
} catch (e) {
console.error(e);
res.sendStatus(404);
}
};

View file

@ -1,9 +1,21 @@
const package = require('../package.json'); const package = require('../package.json');
module.exports = (req, res) => { module.exports = async (req, res) => {
let eventCount = null;
let personCount = null;
try {
const query = req.datastore.createQuery(['__Stat_Kind__']);
eventCount = (await req.datastore.runQuery(query.filter('kind_name', 'Event')))[0].count;
personCount = (await req.datastore.runQuery(query.filter('kind_name', 'Person')))[0].count;
} catch (e) {
console.error(e);
}
res.send({ res.send({
eventCount: 0, eventCount: eventCount || null,
personCount: 0, personCount: personCount || null,
version: package.version, version: package.version,
}); });
}; };

View file

@ -1,18 +1,37 @@
module.exports = (req, res) => { const bcrypt = require('bcrypt');
const { eventId, personId } = req.params;
module.exports = async (req, res) => {
const { eventId, personName } = req.params;
const { person } = req.body; const { person } = req.body;
if (eventId) { try {
if (personId) { const query = req.datastore.createQuery('Person')
if (person) { .filter('eventId', eventId)
res.send(person); .filter('name', personName);
let personResult = (await req.datastore.runQuery(query))[0][0];
if (personResult) {
if (person && person.availability) {
if (personResult.password) {
const passwordsMatch = person.password && await bcrypt.compare(person.password, personResult.password);
if (!passwordsMatch) {
return res.status(401).send('Incorrect password');
}
}
personResult.availability = person.availability;
await req.datastore.upsert(personResult);
res.sendStatus(200);
} else { } else {
res.sendStatus(400); res.sendStatus(400);
} }
} else { } else {
res.sendStatus(404); res.sendStatus(404);
} }
} else { } catch (e) {
res.sendStatus(404); console.error(e);
res.sendStatus(400);
} }
}; };

View file

@ -8,6 +8,35 @@ schemes:
- "https" - "https"
produces: produces:
- "application/json" - "application/json"
definitions:
Event:
type: "object"
properties:
id:
type: "string"
name:
type: "string"
created:
type: "integer"
timezone:
type: "string"
startTime:
type: "string"
endTime:
type: "string"
dates:
type: "array"
items:
type: "string"
Person:
type: "object"
properties:
name:
type: "string"
availability:
type: "array"
items:
type: "string"
paths: paths:
"/stats": "/stats":
get: get:
@ -16,6 +45,15 @@ paths:
responses: responses:
200: 200:
description: "OK" description: "OK"
schema:
type: "object"
properties:
eventCount:
type: "integer"
personCount:
type: "integer"
version:
type: "string"
"/event/{eventId}": "/event/{eventId}":
get: get:
summary: "Return an event details" summary: "Return an event details"
@ -24,11 +62,13 @@ paths:
- in: "path" - in: "path"
name: "eventId" name: "eventId"
required: true required: true
type: string type: "string"
description: "The ID of the event" description: "The ID of the event"
responses: responses:
200: 200:
description: "OK" description: "OK"
schema:
$ref: '#/definitions/Event'
404: 404:
description: "Not found" description: "Not found"
"/event": "/event":
@ -39,11 +79,27 @@ paths:
- in: "body" - in: "body"
name: "event" name: "event"
required: true required: true
type: object schema:
type: "object"
properties:
name:
type: "string"
timezone:
type: "string"
startTime:
type: "integer"
endTime:
type: "integer"
dates:
type: "array"
items:
type: "string"
description: "New event details" description: "New event details"
responses: responses:
201: 201:
description: "Created" description: "Created"
schema:
$ref: '#/definitions/Event'
400: 400:
description: "Invalid data" description: "Invalid data"
"/event/{eventId}/people": "/event/{eventId}/people":
@ -54,11 +110,18 @@ paths:
- in: "path" - in: "path"
name: "eventId" name: "eventId"
required: true required: true
type: string type: "string"
description: "The ID of the event" description: "The ID of the event"
responses: responses:
200: 200:
description: "OK" description: "OK"
schema:
type: "object"
properties:
people:
type: "array"
items:
$ref: "#/definitions/Person"
404: 404:
description: "Not found" description: "Not found"
post: post:
@ -68,12 +131,18 @@ paths:
- in: "path" - in: "path"
name: "eventId" name: "eventId"
required: true required: true
type: string type: "string"
description: "The ID of the event" description: "The ID of the event"
- in: "body" - in: "body"
name: "person" name: "person"
required: true required: true
type: object schema:
type: "object"
properties:
name:
type: "string"
password:
type: "string"
description: "New person details" description: "New person details"
responses: responses:
201: 201:
@ -82,7 +151,39 @@ paths:
description: "Not found" description: "Not found"
400: 400:
description: "Invalid data" description: "Invalid data"
"/event/{eventId}/people/{personId}": "/event/{eventId}/people/{personName}":
get:
summary: "Login as this person"
operationId: "getPerson"
parameters:
- in: "path"
name: "eventId"
required: true
type: "string"
description: "The ID of the event"
- in: "path"
name: "personName"
required: true
type: "string"
description: "The name of the person"
- in: "body"
name: "person"
required: false
schema:
type: "object"
properties:
password:
type: "string"
description: "Login details"
responses:
200:
description: "OK"
schema:
$ref: "#/definitions/Person"
401:
description: "Incorrect password"
404:
description: "Not found"
patch: patch:
summary: "Update this person's availabilities" summary: "Update this person's availabilities"
operationId: "patchPerson" operationId: "patchPerson"
@ -90,21 +191,31 @@ paths:
- in: "path" - in: "path"
name: "eventId" name: "eventId"
required: true required: true
type: string type: "string"
description: "The ID of the event" description: "The ID of the event"
- in: "path" - in: "path"
name: "personId" name: "personName"
required: true required: true
type: string type: "string"
description: "The ID of the person" description: "The name of the person"
- in: "body" - in: "body"
name: "person" name: "person"
required: true required: true
type: object schema:
type: "object"
properties:
password:
type: "string"
availability:
type: "array"
items:
type: "string"
description: "Updated person details" description: "Updated person details"
responses: responses:
200: 200:
description: "OK" description: "OK"
401:
description: "Incorrect password"
404: 404:
description: "Not found" description: "Not found"
400: 400:

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
#!/usr/bin/env bash
yarn build
cd build
cat > app.yaml << EOF
runtime: nodejs12
handlers:
- url: /(.*\..+)$
static_files: \1
upload: (.*\..+)$
- url: /.*
static_files: index.html
upload: index.html
EOF
gcloud app deploy --project=crabfit