Merge pull request #211 from GRA0007/refactor/backend

Refactor backend
This commit is contained in:
Benjamin Grant 2022-08-20 22:06:07 +10:00 committed by GitHub
commit 913f14d8b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 3181 additions and 711 deletions

View file

@ -19,6 +19,13 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 17
cache: yarn
cache-dependency-path: '**/yarn.lock'
- run: yarn install --immutable
- run: yarn build
- id: auth - id: auth
uses: google-github-actions/auth@v0 uses: google-github-actions/auth@v0
with: with:

View file

@ -19,22 +19,19 @@ If you speak a language other than English and you want to help translate Crab F
1. Clone the repo. 1. Clone the repo.
2. Run `yarn` in both backend and frontend folders. 2. Run `yarn` in both backend and frontend folders.
3. Run `node index.js` in the backend folder to start the API. **Note:** you will need a google cloud app set up with datastore enabled and set your `GOOGLE_APPLICATION_CREDENTIALS` environment variable to your service key path. 3. Run `yarn dev` in the backend folder to start the API. **Note:** you will need a google cloud app set up with datastore enabled and set your `GOOGLE_APPLICATION_CREDENTIALS` environment variable to your service key path.
4. Run `yarn start` in the frontend folder to start the front end. 4. Run `yarn dev` in the frontend folder to start the frontend.
### 🔌 Browser extension ### 🔌 Browser extension
The browser extension in `crabfit-browser-extension` can be tested by first running the frontend, and changing the iframe url in the extension's `popup.html` to match the local Crab Fit. Then it can be loaded as an unpacked extension in Chrome to test. The browser extension in `crabfit-browser-extension` can be tested by first running the frontend, and changing the iframe url in the extension's `popup.html` to match the local Crab Fit. Then it can be loaded as an unpacked extension in Chrome to test.
## Deploy ## Deploy
### 🦀 Frontend Deployments are managed with GitHub Workflows.
1. In the frontend folder `cd crabfit-frontend`
2. Run `./deploy.sh` to compile and deploy.
### ⚙️ Backend To deploy cron jobs (i.e. monthly cleanup of old events), run `gcloud app deploy cron.yaml`.
1. In the backend folder `cd crabfit-backend`
2. Deploy the backend `gcloud app deploy --project=crabfit --version=v1`
3. To deploy cron jobs (i.e. monthly cleanup of old events), run `gcloud app deploy cron.yaml`
### 🔌 Browser extension ### 🔌 Browser extension
Compress everything inside the `crabfit-browser-extension` folder and use that zip to deploy using Chrome web store and Mozilla Add-on store. Compress everything inside the `crabfit-browser-extension` folder and use that zip to deploy using Chrome web store and Mozilla Add-on store.

View file

@ -0,0 +1,50 @@
module.exports = {
'env': {
'es2021': true,
'node': true
},
'extends': 'eslint:recommended',
'overrides': [
],
'parserOptions': {
'ecmaVersion': 'latest',
'sourceType': 'module'
},
'rules': {
'indent': [
'error',
2
],
'linebreak-style': [
'error',
'unix'
],
'quotes': [
'error',
'single'
],
'semi': [
'error',
'never'
],
'eqeqeq': 2,
'no-return-await': 1,
'no-var': 2,
'prefer-const': 1,
'yoda': 2,
'no-trailing-spaces': 1,
'eol-last': [1, 'always'],
'no-unused-vars': [
1,
{
'args': 'all',
'argsIgnorePattern': '^_',
'ignoreRestSiblings': true
},
],
'arrow-parens': [
'error',
'as-needed'
],
}
}

View file

@ -6,3 +6,7 @@
.env .env
node_modules/ node_modules/
.parcel-cache
res
routes
swagger.yaml

View file

@ -1,11 +1,7 @@
/node_modules node_modules
dist
.DS_Store .parcel-cache
.env .env
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*

View file

@ -3,10 +3,6 @@ cron:
url: /tasks/cleanup url: /tasks/cleanup
schedule: every monday 09:00 schedule: every monday 09:00
target: api target: api
- description: "clean up old events without a visited date"
url: /tasks/legacyCleanup
schedule: every tuesday 09:00
target: api
- description: "remove people with an event id that no longer exists" - description: "remove people with an event id that no longer exists"
url: /tasks/removeOrphans url: /tasks/removeOrphans
schedule: 1st wednesday of month 09:00 schedule: 1st wednesday of month 09:00

View file

@ -1,61 +1,61 @@
require('dotenv').config(); import { config } from 'dotenv'
import { Datastore } from '@google-cloud/datastore'
import express from 'express'
import cors from 'cors'
const { Datastore } = require('@google-cloud/datastore'); import packageJson from './package.json'
const express = require('express');
const cors = require('cors');
const package = require('./package.json'); import {
stats,
getEvent,
createEvent,
getPeople,
createPerson,
login,
updatePerson,
taskCleanup,
taskRemoveOrphans,
} from './routes'
const stats = require('./routes/stats'); config()
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 taskCleanup = require('./routes/taskCleanup'); const app = express()
const taskLegacyCleanup = require('./routes/taskLegacyCleanup'); const port = 8080
const taskRemoveOrphans = require('./routes/taskRemoveOrphans');
const app = express();
const port = 8080;
const corsOptions = { const corsOptions = {
origin: process.env.NODE_ENV === 'production' ? 'https://crab.fit' : 'http://localhost:5173', origin: process.env.NODE_ENV === 'production' ? 'https://crab.fit' : 'http://localhost:5173',
}; }
const datastore = new Datastore({ const datastore = new Datastore({
keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS, keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS,
}); })
app.use(express.json()); app.use(express.json())
app.use((req, res, next) => { app.use((req, _res, next) => {
req.datastore = datastore; req.datastore = datastore
req.types = { req.types = {
event: process.env.NODE_ENV === 'production' ? 'Event' : 'DevEvent', event: process.env.NODE_ENV === 'production' ? 'Event' : 'DevEvent',
person: process.env.NODE_ENV === 'production' ? 'Person' : 'DevPerson', person: process.env.NODE_ENV === 'production' ? 'Person' : 'DevPerson',
stats: process.env.NODE_ENV === 'production' ? 'Stats' : 'DevStats', stats: process.env.NODE_ENV === 'production' ? 'Stats' : 'DevStats',
}; }
next(); next()
}); })
app.options('*', cors(corsOptions)); app.options('*', cors(corsOptions))
app.use(cors(corsOptions)); app.use(cors(corsOptions))
app.get('/', (req, res) => res.send(`Crabfit API v${package.version}`)); app.get('/', (_req, res) => res.send(`Crabfit API v${packageJson.version}`))
app.get('/stats', stats); app.get('/stats', stats)
app.get('/event/:eventId', getEvent); 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.post('/event/:eventId/people/:personName', login); app.post('/event/:eventId/people/:personName', login)
app.patch('/event/:eventId/people/:personName', updatePerson); app.patch('/event/:eventId/people/:personName', updatePerson)
// Tasks // Tasks
app.get('/tasks/cleanup', taskCleanup); app.get('/tasks/cleanup', taskCleanup)
app.get('/tasks/legacyCleanup', taskLegacyCleanup); app.get('/tasks/removeOrphans', taskRemoveOrphans)
app.get('/tasks/removeOrphans', taskRemoveOrphans);
app.listen(port, () => { app.listen(port, () => {
console.log(`Crabfit API listening at http://localhost:${port} in ${process.env.NODE_ENV === 'production' ? 'prod' : 'dev'} mode`) console.log(`Crabfit API listening at http://localhost:${port} in ${process.env.NODE_ENV === 'production' ? 'prod' : 'dev'} mode`)
}); })

View file

@ -1,24 +1,34 @@
{ {
"name": "crabfit-backend", "name": "crabfit-backend",
"version": "1.1.0", "version": "2.0.0",
"description": "API for Crabfit", "description": "API for Crabfit",
"main": "index.js",
"author": "Ben Grant", "author": "Ben Grant",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"private": true, "private": true,
"source": "index.js",
"main": "dist/index.js",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=12.0.0"
}, },
"scripts": { "scripts": {
"start": "node index.js" "build:dev": "NODE_ENV=development parcel build --no-cache",
"dev": "rm -rf .parcel-cache dist && NODE_ENV=development nodemon --exec \"yarn build:dev && yarn start\" --watch routes --watch res --watch index.js",
"build": "parcel build",
"start": "node ./dist/index.js",
"lint": "eslint index.js ./routes"
}, },
"dependencies": { "dependencies": {
"@google-cloud/datastore": "^6.3.1", "@google-cloud/datastore": "^7.0.0",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"dayjs": "^1.10.4", "dayjs": "^1.11.5",
"dotenv": "^8.2.0", "dotenv": "^16.0.1",
"express": "^4.17.1", "express": "^4.18.1",
"punycode": "^2.1.1" "punycode": "^2.1.1"
},
"devDependencies": {
"eslint": "^8.22.0",
"nodemon": "^2.0.19",
"parcel": "^2.7.0"
} }
} }

View file

@ -1,81 +1,84 @@
const dayjs = require('dayjs'); import dayjs from 'dayjs'
const punycode = require('punycode/'); import punycode from 'punycode/'
const adjectives = require('../res/adjectives.json'); import adjectives from '../res/adjectives.json'
const crabs = require('../res/crabs.json'); import crabs from '../res/crabs.json'
const capitalize = string => string.charAt(0).toUpperCase() + string.slice(1); const capitalize = string => string.charAt(0).toUpperCase() + string.slice(1)
// Generate a random name based on an adjective and a crab species // Generate a random name based on an adjective and a crab species
const generateName = () => { const generateName = () =>
return `${capitalize(adjectives[Math.floor(Math.random() * adjectives.length)])} ${crabs[Math.floor(Math.random() * crabs.length)]} Crab`; `${capitalize(adjectives[Math.floor(Math.random() * adjectives.length)])} ${crabs[Math.floor(Math.random() * crabs.length)]} Crab`
};
// Generate a slug for the crab fit // Generate a slug for the crab fit
const generateId = name => { const generateId = name => {
let id = punycode.encode(name.trim().toLowerCase()).trim().replace(/[^A-Za-z0-9 ]/g, '').replace(/\s+/g, '-'); let id = punycode.encode(name.trim().toLowerCase()).trim().replace(/[^A-Za-z0-9 ]/g, '').replace(/\s+/g, '-')
if (id.replace(/-/g, '') === '') { if (id.replace(/-/g, '') === '') {
id = generateName().trim().toLowerCase().replace(/[^A-Za-z0-9 ]/g, '').replace(/\s+/g, '-'); id = generateName().trim().toLowerCase().replace(/[^A-Za-z0-9 ]/g, '').replace(/\s+/g, '-')
} }
const number = Math.floor(100000 + Math.random() * 900000); const number = Math.floor(100000 + Math.random() * 900000)
return `${id}-${number}`; return `${id}-${number}`
}; }
module.exports = async (req, res) => { const createEvent = async (req, res) => {
const { event } = req.body; const { event } = req.body
try { try {
const name = event.name.trim() === '' ? generateName() : event.name.trim(); const name = event.name.trim() === '' ? generateName() : event.name.trim()
let eventId = generateId(name); let eventId = generateId(name)
const currentTime = dayjs().unix(); const currentTime = dayjs().unix()
// Check if the event ID already exists, and if so generate a new one // Check if the event ID already exists, and if so generate a new one
let eventResult; let eventResult
do { do {
const query = req.datastore.createQuery(req.types.event) const query = req.datastore.createQuery(req.types.event)
.select('__key__') .select('__key__')
.filter('__key__', req.datastore.key([req.types.event, eventId])); .filter('__key__', req.datastore.key([req.types.event, eventId]))
eventResult = (await req.datastore.runQuery(query))[0][0]; eventResult = (await req.datastore.runQuery(query))[0][0]
if (eventResult !== undefined) { if (eventResult !== undefined) {
eventId = generateId(name); eventId = generateId(name)
} }
} while (eventResult !== undefined); } while (eventResult !== undefined)
const entity = { const entity = {
key: req.datastore.key([req.types.event, eventId]), key: req.datastore.key([req.types.event, eventId]),
data: { data: {
name: name, name: name,
created: currentTime, created: currentTime,
times: event.times, times: event.times,
timezone: event.timezone, timezone: event.timezone,
}, },
}; }
await req.datastore.insert(entity); await req.datastore.insert(entity)
res.status(201).send({ res.status(201).send({
id: eventId, id: eventId,
name: name, name: name,
created: currentTime, created: currentTime,
times: event.times, times: event.times,
timezone: event.timezone, timezone: event.timezone,
}); })
// Update stats // Update stats
let eventCountResult = (await req.datastore.get(req.datastore.key([req.types.stats, 'eventCount'])))[0] || null; const eventCountResult = (await req.datastore.get(req.datastore.key([req.types.stats, 'eventCount'])))[0] || null
if (eventCountResult) { if (eventCountResult) {
eventCountResult.value++; await req.datastore.upsert({
await req.datastore.upsert(eventCountResult); ...eventCountResult,
value: eventCountResult.value + 1,
})
} else { } else {
await req.datastore.insert({ await req.datastore.insert({
key: req.datastore.key([req.types.stats, 'eventCount']), key: req.datastore.key([req.types.stats, 'eventCount']),
data: { value: 1 }, data: { value: 1 },
}); })
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e)
res.sendStatus(400); res.status(400).send({ error: 'An error occurred while creating the event' })
} }
}; }
export default createEvent

View file

@ -1,61 +1,65 @@
const dayjs = require('dayjs'); import dayjs from 'dayjs'
const bcrypt = require('bcrypt'); import bcrypt from 'bcrypt'
module.exports = async (req, res) => { const createPerson = async (req, res) => {
const { eventId } = req.params; const { eventId } = req.params
const { person } = req.body; const { person } = req.body
try { try {
const event = (await req.datastore.get(req.datastore.key([req.types.event, eventId])))[0]; const event = (await req.datastore.get(req.datastore.key([req.types.event, eventId])))[0]
const query = req.datastore.createQuery(req.types.person) const query = req.datastore.createQuery(req.types.person)
.filter('eventId', eventId) .filter('eventId', eventId)
.filter('name', person.name); .filter('name', person.name)
let personResult = (await req.datastore.runQuery(query))[0][0]; const personResult = (await req.datastore.runQuery(query))[0][0]
if (event) { if (event) {
if (person && personResult === undefined) { if (person && personResult === undefined) {
const currentTime = dayjs().unix(); const currentTime = dayjs().unix()
// If password // If password
let hash = null; let hash = null
if (person.password) { if (person.password) {
hash = await bcrypt.hash(person.password, 10); hash = await bcrypt.hash(person.password, 10)
} }
const entity = { const entity = {
key: req.datastore.key(req.types.person), key: req.datastore.key(req.types.person),
data: { data: {
name: person.name.trim(), name: person.name.trim(),
password: hash, password: hash,
eventId: eventId, eventId: eventId,
created: currentTime, created: currentTime,
availability: [], availability: [],
}, },
}; }
await req.datastore.insert(entity); await req.datastore.insert(entity)
res.sendStatus(201); res.status(201).send({ success: 'Created' })
// Update stats // Update stats
let personCountResult = (await req.datastore.get(req.datastore.key([req.types.stats, 'personCount'])))[0] || null; const personCountResult = (await req.datastore.get(req.datastore.key([req.types.stats, 'personCount'])))[0] || null
if (personCountResult) { if (personCountResult) {
personCountResult.value++; await req.datastore.upsert({
await req.datastore.upsert(personCountResult); ...personCountResult,
value: personCountResult.value + 1,
})
} else { } else {
await req.datastore.insert({ await req.datastore.insert({
key: req.datastore.key([req.types.stats, 'personCount']), key: req.datastore.key([req.types.stats, 'personCount']),
data: { value: 1 }, data: { value: 1 },
}); })
} }
} else { } else {
res.sendStatus(400); res.status(400).send({ error: 'Unable to create person' })
} }
} else { } else {
res.sendStatus(404); res.status(404).send({ error: 'Event does not exist' })
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e)
res.sendStatus(400); res.status(400).send({ error: 'An error occurred while creating the person' })
} }
}; }
export default createPerson

View file

@ -1,25 +1,29 @@
const dayjs = require('dayjs'); import dayjs from 'dayjs'
module.exports = async (req, res) => { const getEvent = async (req, res) => {
const { eventId } = req.params; const { eventId } = req.params
try { try {
let event = (await req.datastore.get(req.datastore.key([req.types.event, eventId])))[0]; const event = (await req.datastore.get(req.datastore.key([req.types.event, eventId])))[0]
if (event) { if (event) {
res.send({ res.send({
id: eventId, id: eventId,
...event, ...event,
}); })
// Update last visited time // Update last visited time
event.visited = dayjs().unix(); await req.datastore.upsert({
await req.datastore.upsert(event); ...event,
} else { visited: dayjs().unix()
res.sendStatus(404); })
} } else {
} catch (e) { res.status(404).send({ error: 'Event not found' })
console.error(e); }
res.sendStatus(404); } catch (e) {
} console.error(e)
}; res.status(404).send({ error: 'Event not found' })
}
}
export default getEvent

View file

@ -1,20 +1,20 @@
module.exports = async (req, res) => { const getPeople = async (req, res) => {
const { eventId } = req.params; const { eventId } = req.params
try { try {
const query = req.datastore.createQuery(req.types.person).filter('eventId', eventId); const query = req.datastore.createQuery(req.types.person).filter('eventId', eventId)
let people = (await req.datastore.runQuery(query))[0]; let people = (await req.datastore.runQuery(query))[0]
people = people.map(person => ({ people = people.map(person => ({
name: person.name, name: person.name,
availability: person.availability, availability: person.availability,
created: person.created, created: person.created,
})); }))
res.send({ res.send({ people })
people, } catch (e) {
}); console.error(e)
} catch (e) { res.status(404).send({ error: 'Person not found' })
console.error(e); }
res.sendStatus(404); }
}
}; export default getPeople

View file

@ -0,0 +1,10 @@
export { default as stats } from './stats'
export { default as getEvent } from './getEvent'
export { default as createEvent } from './createEvent'
export { default as getPeople } from './getPeople'
export { default as createPerson } from './createPerson'
export { default as login } from './login'
export { default as updatePerson } from './updatePerson'
export { default as taskCleanup } from './taskCleanup'
export { default as taskRemoveOrphans } from './taskRemoveOrphans'

View file

@ -1,33 +1,35 @@
const bcrypt = require('bcrypt'); import bcrypt from 'bcrypt'
module.exports = async (req, res) => { const login = async (req, res) => {
const { eventId, personName } = req.params; const { eventId, personName } = req.params
const { person } = req.body; const { person } = req.body
try { try {
const query = req.datastore.createQuery(req.types.person) const query = req.datastore.createQuery(req.types.person)
.filter('eventId', eventId) .filter('eventId', eventId)
.filter('name', personName); .filter('name', personName)
let personResult = (await req.datastore.runQuery(query))[0][0]; const personResult = (await req.datastore.runQuery(query))[0][0]
if (personResult) { if (personResult) {
if (personResult.password) { if (personResult.password) {
const passwordsMatch = person && person.password && await bcrypt.compare(person.password, personResult.password); const passwordsMatch = person && person.password && await bcrypt.compare(person.password, personResult.password)
if (!passwordsMatch) { if (!passwordsMatch) {
return res.status(401).send('Incorrect password'); return res.status(401).send({ error: 'Incorrect password' })
} }
} }
res.send({ res.send({
name: personName, name: personName,
availability: personResult.availability, availability: personResult.availability,
created: personResult.created, created: personResult.created,
}); })
} else { } else {
res.sendStatus(404); res.status(404).send({ error: 'Person does not exist' })
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e)
res.sendStatus(404); res.status(400).send({ error: 'An error occurred' })
} }
}; }
export default login

View file

@ -1,27 +1,29 @@
const package = require('../package.json'); import packageJson from '../package.json'
module.exports = async (req, res) => { const stats = async (req, res) => {
let eventCount = null; let eventCount = null
let personCount = null; let personCount = null
try { try {
const eventResult = (await req.datastore.get(req.datastore.key([req.types.stats, 'eventCount'])))[0] || null; const eventResult = (await req.datastore.get(req.datastore.key([req.types.stats, 'eventCount'])))[0] || null
const personResult = (await req.datastore.get(req.datastore.key([req.types.stats, 'personCount'])))[0] || null; const personResult = (await req.datastore.get(req.datastore.key([req.types.stats, 'personCount'])))[0] || null
if (eventResult) { if (eventResult) {
eventCount = eventResult.value; eventCount = eventResult.value
} }
if (personResult) { if (personResult) {
personCount = personResult.value; personCount = personResult.value
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e)
} }
res.send({ res.send({
eventCount, eventCount,
personCount, personCount,
version: package.version, version: packageJson.version,
}); })
}; }
export default stats

View file

@ -1,46 +1,48 @@
const dayjs = require('dayjs'); import dayjs from 'dayjs'
module.exports = async (req, res) => { const taskCleanup = async (req, res) => {
if (req.header('X-Appengine-Cron') === undefined) { if (req.header('X-Appengine-Cron') === undefined) {
return res.status(400).send('This task can only be run from a cron job'); return res.status(400).send({ error: 'This task can only be run from a cron job' })
} }
const threeMonthsAgo = dayjs().subtract(3, 'month').unix();
console.log(`Running cleanup task at ${dayjs().format('h:mma D MMM YYYY')}`); const threeMonthsAgo = dayjs().subtract(3, 'month').unix()
try { console.log(`Running cleanup task at ${dayjs().format('h:mma D MMM YYYY')}`)
try {
// Fetch events that haven't been visited in over 3 months // Fetch events that haven't been visited in over 3 months
const eventQuery = req.datastore.createQuery(req.types.event).filter('visited', '<', threeMonthsAgo); const eventQuery = req.datastore.createQuery(req.types.event).filter('visited', '<', threeMonthsAgo)
let oldEvents = (await req.datastore.runQuery(eventQuery))[0]; const oldEvents = (await req.datastore.runQuery(eventQuery))[0]
if (oldEvents && oldEvents.length > 0) { if (oldEvents && oldEvents.length > 0) {
let oldEventIds = oldEvents.map(e => e[req.datastore.KEY].name); const oldEventIds = oldEvents.map(e => e[req.datastore.KEY].name)
console.log(`Found ${oldEventIds.length} events to remove`); console.log(`Found ${oldEventIds.length} events to remove`)
// Fetch availabilities linked to the events discovered // Fetch availabilities linked to the events discovered
let peopleDiscovered = 0; let peopleDiscovered = 0
await Promise.all(oldEventIds.map(async (eventId) => { await Promise.all(oldEventIds.map(async eventId => {
const peopleQuery = req.datastore.createQuery(req.types.person).filter('eventId', eventId); const peopleQuery = req.datastore.createQuery(req.types.person).filter('eventId', eventId)
let oldPeople = (await req.datastore.runQuery(peopleQuery))[0]; const oldPeople = (await req.datastore.runQuery(peopleQuery))[0]
if (oldPeople && oldPeople.length > 0) { if (oldPeople && oldPeople.length > 0) {
peopleDiscovered += oldPeople.length; peopleDiscovered += oldPeople.length
await req.datastore.delete(oldPeople.map(person => person[req.datastore.KEY])); await req.datastore.delete(oldPeople.map(person => person[req.datastore.KEY]))
} }
})); }))
await req.datastore.delete(oldEvents.map(event => event[req.datastore.KEY])); await req.datastore.delete(oldEvents.map(event => event[req.datastore.KEY]))
console.log(`Cleanup successful: ${oldEventIds.length} events and ${peopleDiscovered} people removed`); console.log(`Cleanup successful: ${oldEventIds.length} events and ${peopleDiscovered} people removed`)
res.sendStatus(200); res.sendStatus(200)
} else { } else {
console.log(`Found 0 events to remove, ending cleanup`); console.log('Found 0 events to remove, ending cleanup')
res.sendStatus(404); res.sendStatus(404)
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e)
res.sendStatus(404); res.sendStatus(404)
} }
}; }
export default taskCleanup

View file

@ -1,68 +0,0 @@
const dayjs = require('dayjs');
module.exports = async (req, res) => {
if (req.header('X-Appengine-Cron') === undefined) {
return res.status(400).send('This task can only be run from a cron job');
}
const threeMonthsAgo = dayjs().subtract(3, 'month').unix();
console.log(`Running LEGACY cleanup task at ${dayjs().format('h:mma D MMM YYYY')}`);
try {
// Fetch events that haven't been visited in over 3 months
const eventQuery = req.datastore.createQuery(req.types.event).order('created');
let oldEvents = (await req.datastore.runQuery(eventQuery))[0];
oldEvents = oldEvents.filter(event => !event.hasOwnProperty('visited'));
if (oldEvents && oldEvents.length > 0) {
console.log(`Found ${oldEvents.length} events that were missing a visited date`);
// Filter events that are older than 3 months and missing a visited date
oldEvents = oldEvents.filter(event => event.created < threeMonthsAgo);
if (oldEvents && oldEvents.length > 0) {
let oldEventIds = oldEvents.map(e => e[req.datastore.KEY].name);
// Fetch availabilities linked to the events discovered
let eventsRemoved = 0;
let peopleRemoved = 0;
await Promise.all(oldEventIds.map(async (eventId) => {
const peopleQuery = req.datastore.createQuery(req.types.person).filter('eventId', eventId);
let oldPeople = (await req.datastore.runQuery(peopleQuery))[0];
let deleteEvent = true;
if (oldPeople && oldPeople.length > 0) {
oldPeople.forEach(person => {
if (person.created >= threeMonthsAgo) {
deleteEvent = false;
}
});
}
if (deleteEvent) {
if (oldPeople && oldPeople.length > 0) {
peopleRemoved += oldPeople.length;
await req.datastore.delete(oldPeople.map(person => person[req.datastore.KEY]));
}
eventsRemoved++;
await req.datastore.delete(req.datastore.key([req.types.event, eventId]));
}
}));
console.log(`Legacy cleanup successful: ${eventsRemoved} events and ${peopleRemoved} people removed`);
res.sendStatus(200);
} else {
console.log('Found 0 events that are older than 3 months and missing a visited date, ending LEGACY cleanup');
res.sendStatus(404);
}
} else {
console.error('Found no events that are missing a visited date, ending LEGACY cleanup [DISABLE ME!]');
res.sendStatus(404);
}
} catch (e) {
console.error(e);
res.sendStatus(404);
}
};

View file

@ -1,46 +1,48 @@
const dayjs = require('dayjs'); import dayjs from 'dayjs'
module.exports = async (req, res) => { const taskRemoveOrphans = async (req, res) => {
if (req.header('X-Appengine-Cron') === undefined) { if (req.header('X-Appengine-Cron') === undefined) {
return res.status(400).send('This task can only be run from a cron job'); return res.status(400).send({ error: 'This task can only be run from a cron job' })
} }
const threeMonthsAgo = dayjs().subtract(3, 'month').unix(); const threeMonthsAgo = dayjs().subtract(3, 'month').unix()
console.log(`Running orphan removal task at ${dayjs().format('h:mma D MMM YYYY')}`); console.log(`Running orphan removal task at ${dayjs().format('h:mma D MMM YYYY')}`)
try { try {
// Fetch people that are older than 3 months // Fetch people that are older than 3 months
const peopleQuery = req.datastore.createQuery(req.types.person).filter('created', '<', threeMonthsAgo); const peopleQuery = req.datastore.createQuery(req.types.person).filter('created', '<', threeMonthsAgo)
let oldPeople = (await req.datastore.runQuery(peopleQuery))[0]; const oldPeople = (await req.datastore.runQuery(peopleQuery))[0]
if (oldPeople && oldPeople.length > 0) { if (oldPeople && oldPeople.length > 0) {
console.log(`Found ${oldPeople.length} people older than 3 months, checking for events`); console.log(`Found ${oldPeople.length} people older than 3 months, checking for events`)
// Fetch events linked to the people discovered // Fetch events linked to the people discovered
let peopleWithoutEvents = 0; let peopleWithoutEvents = 0
await Promise.all(oldPeople.map(async (person) => { await Promise.all(oldPeople.map(async person => {
let event = (await req.datastore.get(req.datastore.key([req.types.event, person.eventId])))[0]; const event = (await req.datastore.get(req.datastore.key([req.types.event, person.eventId])))[0]
if (!event) { if (!event) {
peopleWithoutEvents++; peopleWithoutEvents++
await req.datastore.delete(person[req.datastore.KEY]); await req.datastore.delete(person[req.datastore.KEY])
} }
})); }))
if (peopleWithoutEvents > 0) { if (peopleWithoutEvents > 0) {
console.log(`Orphan removal successful: ${oldEventIds.length} events and ${peopleDiscovered} people removed`); console.log(`Orphan removal successful: ${peopleWithoutEvents} people removed`)
res.sendStatus(200); res.sendStatus(200)
} else { } else {
console.log(`Found 0 people without events, ending orphan removal`); console.log('Found 0 people without events, ending orphan removal')
res.sendStatus(404); res.sendStatus(404)
} }
} else { } else {
console.log(`Found 0 people older than 3 months, ending orphan removal`); console.log('Found 0 people older than 3 months, ending orphan removal')
res.sendStatus(404); res.sendStatus(404)
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e)
res.sendStatus(404); res.sendStatus(404)
} }
}; }
export default taskRemoveOrphans

View file

@ -1,37 +1,40 @@
const bcrypt = require('bcrypt'); import bcrypt from 'bcrypt'
module.exports = async (req, res) => { const updatePerson = async (req, res) => {
const { eventId, personName } = req.params; const { eventId, personName } = req.params
const { person } = req.body; const { person } = req.body
try { try {
const query = req.datastore.createQuery(req.types.person) const query = req.datastore.createQuery(req.types.person)
.filter('eventId', eventId) .filter('eventId', eventId)
.filter('name', personName); .filter('name', personName)
let personResult = (await req.datastore.runQuery(query))[0][0]; const personResult = (await req.datastore.runQuery(query))[0][0]
if (personResult) { if (personResult) {
if (person && person.availability) { if (person && person.availability) {
if (personResult.password) { if (personResult.password) {
const passwordsMatch = person.password && await bcrypt.compare(person.password, personResult.password); const passwordsMatch = person.password && await bcrypt.compare(person.password, personResult.password)
if (!passwordsMatch) { if (!passwordsMatch) {
return res.status(401).send('Incorrect password'); return res.status(401).send({ error: 'Incorrect password' })
} }
} }
personResult.availability = person.availability; await req.datastore.upsert({
...personResult,
availability: person.availability,
})
await req.datastore.upsert(personResult); res.status(200).send({ success: 'Updated' })
} else {
res.status(400).send({ error: 'Availability must be set' })
}
} else {
res.status(404).send({ error: 'Person not found' })
}
} catch (e) {
console.error(e)
res.status(400).send('An error occurred')
}
}
res.sendStatus(200); export default updatePerson
} else {
res.sendStatus(400);
}
} else {
res.sendStatus(404);
}
} catch (e) {
console.error(e);
res.sendStatus(400);
}
};

View file

@ -230,19 +230,6 @@ paths:
description: "Not found" description: "Not found"
400: 400:
description: "Not called from a cron job" description: "Not called from a cron job"
"/tasks/legacyCleanup":
get:
summary: "Delete events inactive for more than 3 months that don't have a visited date"
operationId: "taskLegacyCleanup"
tags:
- tasks
responses:
200:
description: "OK"
404:
description: "Not found"
400:
description: "Not called from a cron job"
"/tasks/removeOrphans": "/tasks/removeOrphans":
get: get:
summary: "Deletes people if the event they were created under no longer exists" summary: "Deletes people if the event they were created under no longer exists"

File diff suppressed because it is too large Load diff

View file

@ -8,11 +8,12 @@ import { StaleWhileRevalidate, NetworkFirst } from 'workbox-strategies'
skipWaiting() skipWaiting()
clientsClaim() clientsClaim()
cleanupOutdatedCaches()
// Injection point // Injection point
precacheAndRoute(self.__WB_MANIFEST) precacheAndRoute(self.__WB_MANIFEST)
cleanupOutdatedCaches()
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$') const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$')
registerRoute( registerRoute(
// Return false to exempt requests from being fulfilled by index.html. // Return false to exempt requests from being fulfilled by index.html.

View file

@ -33,14 +33,8 @@ const api = {
if (!response.ok) { if (!response.ok) {
throw response throw response
} }
const json = await response.json()
//TODO: hack until api update return Promise.resolve(json)
try {
const json = await response.json()
return Promise.resolve(json)
} catch (e) {
return Promise.resolve(response)
}
} catch (error) { } catch (error) {
return handleError(error) return handleError(error)
} }
@ -58,14 +52,8 @@ const api = {
if (!response.ok) { if (!response.ok) {
throw response throw response
} }
const json = await response.json()
//TODO: hack until api update return Promise.resolve(json)
try {
const json = await response.json()
return Promise.resolve(json)
} catch (e) {
return Promise.resolve(response)
}
} catch (error) { } catch (error) {
return handleError(error) return handleError(error)
} }