Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update required fields and validation error message #1

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 88 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,72 @@ Simple Serverless Keyed Event Log
=================================

## What problem does this solve?
You have an app and want to send data (e.g., events, logs, etc.) to an append-only database. You don't want to integrate any database connectors or other libraries into the app; you just want to make an HTTPS request and forget about it.

You have an app and want to send data (e.g., events, logs, etc.) to an
append-only database. You don't want to integrate any database connectors or
other libraries into the app; you just want to make an HTTPS request and forget
about it.

## What does this project provide?
This is the code for a [Firebase](https://firebase.google.com) project that provides an integrated solution to this problem. Specifically, it defines:

- an [HTTP-triggered function](https://firebase.google.com/docs/functions/http-events) providing endpoint you can query with your data
- the [database](https://firebase.google.com/docs/firestore/) (i.e., the configuration for it) where the data gets stored
- a [hosted](https://firebase.google.com/docs/hosting) webpage that allows quick access to the data
- a script that lets you download the data (and that you can use a starting point for more complicated data processing)
This is the code for a [Firebase](https://firebase.google.com) project that
provides an integrated solution to this problem. Specifically, it defines:

- an [HTTP-triggered
function](https://firebase.google.com/docs/functions/http-events) providing
endpoint you can query with your data
- the [database](https://firebase.google.com/docs/firestore/) (i.e., the
configuration for it) where the data gets stored
- a [hosted](https://firebase.google.com/docs/hosting) webpage that allows quick
access to the data
- a script that lets you download the data (and that you can use a starting
point for more complicated data processing)

With Firebase, you have the option of the function running under a custom domain name that you bring in.


## Usage
### Storing data
To store data, make a POST request to `https://your_project_url/save` with the following fields:

- `value`, the data you want to store
- `key`, the key you want to use to identify this data (it need not be unique). For example, this could be the type of the event you're logging.
- `uid`, an identifier for the user associated with this event
- `client`, an identifier for the client issuing this request (e.g., the app version)

These fields are required, but the data types are not enforced. Especially useful for `value`, they can be nested JSON objects.

#### No authentication
**Anyone can write to the database.** Requests are neither authenticated nor validated. It is assumed that you have an ex post facto way of identifying valid requests.


## Previewing data
To see your data on a webpage, go to `https://your_project_url/get`. You will need to authenticate with a whitelisted email (see Setup below).


## Downloading data
To store data, make a POST request to `https://your_project_url/save` with the
following **required** fields:

- `key` (string|any), the key you want to use to identify this data (it need not be unique).
For example, this could be the type of the event you're logging.
- `value` (any), the data you want to store. Can be any type, including nested
JSON objects.
- `uid` (string|any), an identifier for the user associated with this event
- `clientVersion` (string|any), an identifier for the client issuing this request (e.g., the
app version). Think of this as the client user-agent.
- `timestamp` (string|any), the timestamp of when the client generated the event. NOTE: This
should be when the event actually occurred on the client and NOT when it was
sent to the server.

Example event:
```javascript
{
key: "login_event",
value: {
something: "abc",
other: "xyz",
another: {
thing: "def"
}
},
uid: "123",
clientVersion: "v0.1.2",
timestamp: "2020-04-22T19:09:04.641Z",
customFieldA: "you can add as many custom fields as you want at the top level",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer custom fields to be down in value. That way, the top level has a finite and final list of fields, and we know that any additional or metadata fields we might add are out of the client's control.

customFieldB: "but, it likely makes more sense to add custom data to the value field"
}
```

### No authentication
**Anyone can write to the database.** Requests are neither authenticated nor
validated. It is assumed that you have an ex post facto way of identifying valid
requests.

### Previewing data
To see your data on a webpage, go to `https://your_project_url/get`. You will
need to authenticate with a whitelisted email (see [Setup](./Setup) below).

### Downloading data
A Python script for printing the contents of the database is provided in the `client` directory.

To run it, you will need to:
Expand All @@ -49,25 +81,40 @@ To run it, you will need to:
Here are the instructions for setting up your own instance of this application.

0. Clone this repo
1. Modify `firestore.rules` to whitelist the email address(es) you want to be able to access the data through a webpage. Authentication will be done (as with everything else) by [Firebase](https://firebase.google.com/docs/auth). You can also skip this step and only use the command-line client (see above).
1. Modify `firestore.rules` to whitelist the email address(es) you want to be
able to access the data through a webpage. Authentication will be done (as
with everything else) by [Firebase](https://firebase.google.com/docs/auth).
You can also skip this step and only use the command-line client (see above).
2. In the top level of the repo, run `npm install` to get `firebase-tools`
3. Run `npx firebase login` to log in
4. Go to the [Firebase console](https://console.firebase.google.com) and *Add project*
5. Once you've created the project, go to the *Authentication* product dashboard, click on *Sign-in method*, and enable the *Email/Password* option. (I also use the *Email link* option.)
4. Go to the [Firebase console](https://console.firebase.google.com) and *Add
project*
5. Once you've created the project, go to the *Authentication* product
dashboard, click on *Sign-in method*, and enable the *Email/Password* option.
(I also use the *Email link* option.)
6. Go to the *Database* dashboard and provision a database.
7. Run `npx firebase use --add` and select the project you just created. (`default` is a reasonable choice for the alias, when you're prompted for it.)
7. Run `npx firebase use --add` and select the project you just created.
(`default` is a reasonable choice for the alias, when you're prompted for
it.)
8. Go into the `functions` directory and run `npm install` there
9. Back in the root of the repo, run `npx firebase deploy`.

When the deploy is finished, the command will display the "Hosting URL." This is URL to use for storing and retrieving data (just remember to append `/save` or `/get`).
When the deploy is finished, the command will display the "Hosting URL." This is
URL to use for storing and retrieving data (just remember to append `/save` or
`/get`).

### Adding a custom domain
If you'd like to use your own domain for the URL (rather than the one provided by Firebase), you can follow these steps:

1. In the [Firebase console](https://console.firebase.google.com), select your project, and go to the *Hosting* dashboard.
2. Click the *Connect domain* button and follow the instructions. You'll need to update the DNS settings of your domain.
3. Go to the *Authentication* product dashboard, click on *Sign-in method*, and scroll down to *Authorized domains*. Add the (sub)domain you want to use.
4. After some time, you should be able to use your domain instead of the Firebase subdomain.

(These instructions were current at the time of writing, but the UI may have changed in the meantime.)

If you'd like to use your own domain for the URL (rather than the one provided
by Firebase), you can follow these steps:

1. In the [Firebase console](https://console.firebase.google.com), select your
project, and go to the *Hosting* dashboard.
2. Click the *Connect domain* button and follow the instructions. You'll need to
update the DNS settings of your domain.
3. Go to the *Authentication* product dashboard, click on *Sign-in method*, and
scroll down to *Authorized domains*. Add the (sub)domain you want to use.
4. After some time, you should be able to use your domain instead of the
Firebase subdomain.

(These instructions were current at the time of writing, but the UI may have
changed in the meantime.)
2 changes: 1 addition & 1 deletion firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ service cloud.firestore {
}

match /logs/{item} {
allow read: if request.auth.token.email_verified == true && (request.auth.token.email == 'YOUR_EMAIL_HERE' || request.auth.token.email == 'ANOTHER_EMAIL_HERE');
allow read: if request.auth.token.email_verified == true && request.auth.token.email in [EMAIL_ONE, EMAIL_TWO, EMAIL_THREE];
}
}
}
20 changes: 10 additions & 10 deletions functions/package-lock.json

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

61 changes: 44 additions & 17 deletions functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,59 @@ import * as functions from "firebase-functions";
import { store } from "./database";

export const save = functions.https.onRequest((request, response) => {
const event = {
// Client-provided values
uid: request.body.uid,
client: request.body.client,
key: request.body.key,
value: request.body.value,
const body = request.body;
console.log(`Body = ${JSON.stringify(body)}`);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is useful for debugging, but I'd remove this for production, since it will result in a lot of logs with unclear retention policies.


// Optional (TODO: make it required like the other fields)
sent: request.body.sent,
const event = Object.assign({}, body, {
receivedAt: new Date().toISOString()
});

// Additional values
received: new Date().toISOString()
};
const requiredFields: string[] = [
// A key used to identify the value in this event (it need not be unique).
// For example, this could be the type of the event.
'key',

// The data that the client wants to store in the database.
'value',

// An identifier for the user associated with this event.
'uid',

// An identifier for the client issuing this request (e.g., the app
// version). Think of this as the client user-agent.
'clientVersion',

// Timestamp for when the client generated the event. NOTE: This should be
// when the event actually occurred on the client and NOT when it was sent
// to the server.
'timestamp',
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be nice for the two timestamps to have more "symmetrical" names, e.g., clientTimestamp and serverTimestamp. (I'm also okay with the original field names, sent/received.)

(In general, I want to nudge us to use the server timestamp; the client timestamp should be used primarily for debugging, because it can be way off due to clock skew.)


// The timestamp for when the server received this event from the client.
'receivedAt'
];

// Validate data
if (!event.uid || !event.client || !event.key || !event.value) {
response.status(400).send("Bad Request");
return;
const missingFields = [];
for (const fieldName of requiredFields) {
// Unsafe casting to get around type error:
// https://stackoverflow.com/a/57192972.
if (event[fieldName] === undefined) {
missingFields.push(fieldName);
}
}

if (missingFields.length > 0) {
const errorMsg = `Missing required fields: [${missingFields.join(', ')}]`;
response.status(400).send(errorMsg);
}

// Store data
store(event)
.then(() => {
response.send("OK");
response.sendStatus(200);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the default behavior of the 200 response on Firebase, but can you make sure we still get some sort of text acknowledgment at the end of request? It's useful for manual debugging.

})
.catch(() => {
response.status(500).send("Internal Server Error");
.catch((e) => {
response.sendStatus(500);
console.log(`Error saving to the DB. Request: ${JSON.stringify(body)}. Error: ${e}.`);
});
});
Loading