Skip to content

Commit

Permalink
v4.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
rzueger authored Jan 28, 2025
2 parents 2c2ecf3 + d016d3f commit 3118f7b
Show file tree
Hide file tree
Showing 56 changed files with 3,842 additions and 2,529 deletions.
55 changes: 55 additions & 0 deletions .github/workflows/firebase-hosting-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Deploy to Firebase Hosting on merge (dev)
'on':
push:
branches:
- develop
jobs:
build_and_deploy:
runs-on: ubuntu-latest
strategy:
matrix:
environment:
- lszt_test
- lszm_test
- lspv_test
environment: ${{ matrix.environment }}
steps:
- run: echo 'Running deplyoment for project ${{ vars.PROJECT }}'
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: 10
- run: npm ci
- run: npm run build --project=${{ vars.PROJECT }}
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: w9jds/setup-firebase@main
with:
project_id: ${{ vars.FIREBASE_PROJECT }}
tools-version: 13
firebase_token: ${{ secrets.FIREBASE_TOKEN }}
- run: firebase target:apply database main ${{ vars.FIREBASE_PROJECT }}
- run: firebase deploy --only hosting,database:main
build_and_deploy_functions:
runs-on: ubuntu-latest
strategy:
matrix:
environment:
- lszt_test
- lszm_test
- lspv_test
environment: ${{ matrix.environment }}
steps:
- run: echo 'Running functions deplyoment for project ${{ vars.PROJECT }}'
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: w9jds/setup-firebase@main
with:
project_id: ${{ vars.FIREBASE_PROJECT }}
tools-version: 13
firebase_token: ${{ secrets.FIREBASE_TOKEN }}
- run: cd functions && npm ci && cd ..
- run: firebase deploy --only functions
23 changes: 23 additions & 0 deletions .github/workflows/test-pull-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Build and test on PR
'on': pull_request
jobs:
build:
if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: 10
- run: npm ci
#- run: npm test TODO reenable tests when fixed
- run: npm run build
build_functions:
if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: cd functions && npm ci && cd ..
84 changes: 82 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

### Getting Started

#### Required Node Versions

Node Version for building the app: 10

Node Version for deploying to Firebase and for the cloud functions: 20

#### Start locally

```
$ npm install
$ npm start [--project={PROJECT_NAME}]
Expand Down Expand Up @@ -52,13 +60,46 @@ $ npm run build:prod [--project={PROJECT_NAME}]

#### Push to Firebase

Prerequisites: Firebase Tools must be installed (`npm install -g firebase-tools`).
Node version for this step: 20

Prerequisites: Firebase Tools must be installed (`npm install -g firebase-tools@13`).

**Caution:** Ensure that you have selected the right Firebase project (list all projects by typing `firebase list` and change it if necessary (with `firebase use`)).

##### Set up env

Set the realtime database name for the cloud functions:

```
$ firebase deploy
firebase functions:config:set rtdb.instance={RTDB NAME}
```

(e.g. `firebase functions:config:set rtdb.instance=lszt-test`)

##### Deploy app

Before executing this command, make sure the correct project was built using the Node version
mentioned at the beginning of this document.

```
$ firebase target:apply database main {RTDB NAME}
$ firebase deploy --only hosting,database:main
```

(e.g. `lszt-test` for `{RTDB NAME}`)

##### Deploy cloud functions

Use the following commands to deploy the cloud functions.

Before executing these commands, make sure you selected the correct Node version for the cloud
functions, which is mentioned at the beginning of this document.

```
$ cd functions && npm ci && cd ..
$ firebase deploy --only functions
```

## Cloud functions

### `auth`
Expand Down Expand Up @@ -142,3 +183,42 @@ Returns (example):
```

If no status is set, `{}` is returned.

#### Import users ####

##### Request #####

POST an array of users to this endpoint to sync the users list.

New users are added, existing ones are updated, and those which are saved in the database, but not present in the given
users array are removed from the database.

Example payload:
```
POST /api/users/import
{
"users": [
{
"memberNr": "48434",
"firstname": "John",
"lastname": "Doe",
"phone": "+41791234567",
"email": "[email protected]"
},
{
"memberNr": "30443",
"firstname": "Jane",
"lastname": "Smith",
"phone": "+41791234568",
"email": "[email protected]"
},
...
]
}
```

##### Auth #####

This endpoint requires a Basic Auth header (username and password to use set in the function config:
`api.serviceuser.username` and `api.serviceuser.password`).
55 changes: 52 additions & 3 deletions firebase-rules.json → firebase-rules-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
],
"$departure_id": {
".write": "auth !== null && (!root.child('settings/lockDate').exists() || (!data.exists() && newData.exists() && newData.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24) || (data.exists() && !newData.exists() && data.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24) || (data.exists() && newData.exists() && data.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24 && newData.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24))",
".validate": "newData.hasChildren(['aircraftType', 'dateTime', 'departureRoute', 'duration', 'firstname', 'flightType', 'immatriculation', 'lastname', 'location', 'mtow', 'aircraftCategory', 'negativeTimestamp'])",
".validate": "newData.hasChildren(['aircraftType', 'dateTime', 'departureRoute', 'duration', 'email', 'firstname', 'flightType', 'immatriculation', 'lastname', 'location', 'mtow', 'aircraftCategory', 'negativeTimestamp'])",
"aircraftType": {
".validate": "newData.isString() && newData.val().length > 0"
},
Expand All @@ -25,6 +25,9 @@
"duration": {
".validate": "newData.isString() && newData.val().matches(/^\\d{2}:\\d{2}$/)"
},
"email": {
".validate": "newData.isString() && newData.val().matches(/^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$/)"
},
"firstname": {
".validate": "newData.isString() && newData.val().length > 0"
},
Expand Down Expand Up @@ -81,7 +84,7 @@
],
"$arrival_id": {
".write": "auth !== null && (!root.child('settings/lockDate').exists() || (!data.exists() && newData.exists() && newData.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24) || (data.exists() && !newData.exists() && data.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24) || (data.exists() && newData.exists() && data.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24 && newData.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24))",
".validate": "newData.hasChildren(['aircraftType', 'arrivalRoute', 'dateTime', 'firstname', 'flightType', 'immatriculation', 'landingCount', 'lastname', 'location', 'mtow', 'aircraftCategory', 'negativeTimestamp'])",
".validate": "newData.hasChildren(['aircraftType', 'arrivalRoute', 'dateTime', 'email', 'firstname', 'flightType', 'immatriculation', 'landingCount', 'lastname', 'location', 'mtow', 'aircraftCategory', 'negativeTimestamp'])",
"aircraftType": {
".validate": "newData.isString() && newData.val().length > 0"
},
Expand All @@ -91,6 +94,9 @@
"dateTime": {
".validate": "newData.isString() && newData.val().matches(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/)"
},
"email": {
".validate": "newData.isString() && newData.val().matches(/^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$/)"
},
"firstname": {
".validate": "newData.isString() && newData.val().length > 0"
},
Expand All @@ -103,6 +109,9 @@
"goAroundFeeSingle": {
".validate": "newData.isNumber() && newData.val() >= 0"
},
"goAroundFeeCode": {
".validate": "newData.isString() && newData.val().length > 0"
},
"goAroundFeeTotal": {
".validate": "newData.isNumber() && newData.val() >= 0"
},
Expand All @@ -115,6 +124,9 @@
"landingFeeSingle": {
".validate": "newData.isNumber() && newData.val() >= 0"
},
"landingFeeCode": {
".validate": "newData.isString() && newData.val().length > 0"
},
"landingFeeTotal": {
".validate": "newData.isNumber() && newData.val() >= 0"
},
Expand Down Expand Up @@ -153,6 +165,10 @@
}
}
},
"movementAssociations": {
".read": "auth !== null",
".write": false
},
"aircrafts": {
".read": "auth !== null",
".write": "auth !== null && root.child('admins/' + auth.uid).exists()",
Expand Down Expand Up @@ -289,7 +305,40 @@
".validate": "newData.isString() && newData.val().length > 0"
},
"status": {
".validate": "newData.val() === 'pending' || newData.val() === 'success' || newData.val() === 'failure' || newData.val() === 'cancelled'"
".validate": "newData.val() === 'pending' || newData.val() === 'success' || newData.val() === 'failure' || newData.val() === 'cancelled' || newData.val() === 'inprogress'"
},
"method": {
".validate": "newData.isString() && newData.val().length > 0"
},
"email": {
".validate": "newData.isString() && newData.val().length > 0"
},
"immatriculation": {
".validate": "newData.isString() && newData.val().length > 0"
},
"landings": {
".validate": "newData.isNumber() && newData.val() > 0"
},
"landingFeeSingle": {
".validate": "newData.isNumber() && newData.val() > 0"
},
"landingFeeCode": {
".validate": "newData.isString() && newData.val().length > 0"
},
"landingFeeTotal": {
".validate": "newData.isNumber() && newData.val() > 0"
},
"goArounds": {
".validate": "newData.isNumber() && newData.val() > 0"
},
"goAroundFeeSingle": {
".validate": "newData.isNumber() && newData.val() > 0"
},
"goAroundFeeCode": {
".validate": "newData.isString() && newData.val().length > 0"
},
"goAroundFeeTotal": {
".validate": "newData.isNumber() && newData.val() > 0"
},
"$other": {
".validate": false
Expand Down
11 changes: 6 additions & 5 deletions firebase.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
{
"database": {
"database": [{
"target": "main",
"rules": "build/firebase-rules.json"
},
}],
"hosting": {
"public": "build",
"ignore": [
"firebase-rules.json"
],
"rewrites": [{
"source": "/api/**",
"function": "api"
}]
},
"functions": {
"source": "functions"
}
}
29 changes: 29 additions & 0 deletions functions/api/basicAuth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const functions = require('firebase-functions')

const config = functions.config();

const basicAuth = (req, res, next) => {
if (!config.api || !config.api.serviceuser || !config.api.serviceuser.username || !config.api.serviceuser.password) {
console.info(
"Set configuration properties `api.serviceuser.username` and `api.serviceuser.password` for the API auth"
)
res.status(401).send('Unauthorized')
return
}

const authHeader = req.headers.authorization || ''
const [type, credentials] = authHeader.split(' ')

if (type === 'Basic' && credentials) {
const decoded = Buffer.from(credentials, 'base64').toString('utf-8')
const [username, password] = decoded.split(':')

if (username === config.api.serviceuser.username && password === config.api.serviceuser.password) {
return next()
}
}

res.status(401).send('Unauthorized')
}

module.exports = basicAuth
19 changes: 19 additions & 0 deletions functions/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const admin = require('firebase-admin')
const express = require('express')
const cors = require('cors')({origin: true, credentials: true})
const fetchAerodromeStatus = require('./fetchAerodromeStatus')
const basicAuth = require('./basicAuth')
const syncUsers = require('./syncUsers')

const api = express()

Expand All @@ -20,4 +22,21 @@ api.get('(/api)?/aerodrome/status', async (req, res) => {
res.send(status)
})

api.post('(/api)?/users/import', basicAuth, async (req, res) => {
try {
const users = req.body.users
if (!Array.isArray(users)) {
return res.status(400).send('Invalid users format')
}

const db = admin.database()
await syncUsers(db, users)

res.status(200).send({ message: 'Users imported successfully' })
} catch (e) {
console.error('Failed to import users', e)
res.status(500).send({ error: 'Failed to import users' })
}
})

module.exports = functions.https.onRequest(api)
Loading

0 comments on commit 3118f7b

Please sign in to comment.