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
.gitignore
.env
node_modules/

View file

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

View file

@ -1,6 +1,10 @@
runtime: nodejs10
entrypoint: node index.js
runtime: nodejs
service: api
env: flex
automatic_scaling:
min_num_instances: 1
max_num_instances: 4
endpoints_api_service:
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 package = require('./package.json');
const app = express();
const port = 8080;
const stats = require('./routes/stats');
const getEvent = require('./routes/getEvent');
const createEvent = require('./routes/createEvent');
const getPeople = require('./routes/getPeople');
const createPerson = require('./routes/createPerson');
const login = require('./routes/login');
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((req, res, next) => {
req.datastore = datastore;
next();
});
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.get('/event/:eventId/people', getPeople);
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, () => {
console.log(`Crabfit API listening at http://localhost:${port}`)

View file

@ -6,7 +6,17 @@
"author": "Ben Grant",
"license": "MIT",
"private": true,
"engines": {
"node": ">= 10.0.0"
},
"scripts": {
"start": "node index.js"
},
"dependencies": {
"@google-cloud/datastore": "^6.3.1",
"bcrypt": "^5.0.1",
"dayjs": "^1.10.4",
"dotenv": "^8.2.0",
"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;
if (event) {
console.log(event);
res.sendStatus(201);
} else {
try {
const eventId = generateId(event.name);
const currentTime = dayjs().unix();
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);
}
};

View file

@ -1,10 +1,36 @@
module.exports = (req, res) => {
const dayjs = require('dayjs');
const bcrypt = require('bcrypt');
module.exports = async (req, res) => {
const { eventId } = req.params;
const { person } = req.body;
if (eventId) {
try {
const event = (await req.datastore.get(req.datastore.key(['Event', eventId])))[0];
if (event) {
if (person) {
console.log(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);
@ -12,4 +38,8 @@ module.exports = (req, res) => {
} else {
res.sendStatus(404);
}
} catch (e) {
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;
if (eventId) {
try {
const event = (await req.datastore.get(req.datastore.key(['Event', eventId])))[0];
if (event) {
res.send({
id: 'event-name-4701240',
name: 'Event name',
eventCreated: 379642017932,
timezone: '247',
startTime: 0900,
endTime: 1700,
dates: [
'26022021',
'27022021',
'28022021',
],
id: eventId,
...event,
});
} else {
res.sendStatus(404);
}
} catch (e) {
console.error(e);
res.sendStatus(404);
}
};

View file

@ -1,27 +1,19 @@
module.exports = (req, res) => {
module.exports = async (req, res) => {
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({
people: [
{
name: 'Laura',
password: null,
eventId: 'event-name-4701240',
availability: [
[
'START',
'END',
],
[
'START',
'END',
],
],
},
],
people,
});
} else {
} catch (e) {
console.error(e);
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');
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({
eventCount: 0,
personCount: 0,
eventCount: eventCount || null,
personCount: personCount || null,
version: package.version,
});
};

View file

@ -1,18 +1,37 @@
module.exports = (req, res) => {
const { eventId, personId } = req.params;
const bcrypt = require('bcrypt');
module.exports = async (req, res) => {
const { eventId, personName } = req.params;
const { person } = req.body;
if (eventId) {
if (personId) {
if (person) {
res.send(person);
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 (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 {
res.sendStatus(400);
}
} else {
res.sendStatus(404);
}
} else {
res.sendStatus(404);
} catch (e) {
console.error(e);
res.sendStatus(400);
}
};

View file

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