diff --git a/.github/workflows/deploy_api.yml b/.github/workflows/deploy_api.yml
new file mode 100644
index 0000000..79c336e
--- /dev/null
+++ b/.github/workflows/deploy_api.yml
@@ -0,0 +1,58 @@
+name: Deploy API
+
+on:
+ push:
+ branches: ['main']
+ paths: ['api/**']
+
+env:
+ REGISTRY: ghcr.io
+
+jobs:
+ build-and-push:
+ name: Build Docker image and push to GitHub Container Registry
+ runs-on: ubuntu-latest
+
+ defaults:
+ run:
+ working-directory: api
+
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: docker/login-action@v2
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - uses: docker/metadata-action@v4
+ id: meta
+ with:
+ images: ${{ env.REGISTRY }}/${{ github.repository }}/api
+ - uses: docker/build-push-action@v4
+ with:
+ context: .
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+
+ deploy:
+ needs: [build-and-push]
+ name: Deploy to EC2
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: appleboy/ssh-action@master
+ with:
+ host: ${{ secrets.EC2_HOST }}
+ username: ${{ secrets.EC2_USERNAME }}
+ key: ${{ secrets.EC2_SSH_KEY }}
+ script: |
+ docker login ${{ env.REGISTRY }} -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
+ docker pull ${{ env.REGISTRY }}/${{ github.repository }}/api:latest
+ docker stop crabfit-api
+ docker rm crabfit-api
+ docker run -d -p 3000:3000 --name crabfit-api --env-file ./.env ${{ env.REGISTRY }}/${{ github.repository }}/api:latest
diff --git a/.github/workflows/deploy_backend.yml b/.github/workflows/deploy_backend.yml
deleted file mode 100644
index 951c2e2..0000000
--- a/.github/workflows/deploy_backend.yml
+++ /dev/null
@@ -1,37 +0,0 @@
-name: Deploy Backend
-
-on:
- push:
- branches: ['main']
- paths: ['crabfit-backend/**']
-
-jobs:
- deploy:
- runs-on: ubuntu-latest
-
- defaults:
- run:
- working-directory: crabfit-backend
-
- permissions:
- contents: read
- id-token: write
-
- steps:
- - 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
- uses: google-github-actions/auth@v0
- with:
- credentials_json: '${{ secrets.GCP_SA_KEY }}'
- - id: deploy
- uses: google-github-actions/deploy-appengine@v0
- with:
- working_directory: crabfit-backend
- version: v1
diff --git a/.github/workflows/deploy_frontend.yml b/.github/workflows/deploy_frontend.yml
index 6ba43c7..e495b49 100644
--- a/.github/workflows/deploy_frontend.yml
+++ b/.github/workflows/deploy_frontend.yml
@@ -3,7 +3,7 @@ name: Deploy Frontend
on:
push:
branches: ['main']
- paths: ['crabfit-frontend/**']
+ paths: ['frontend/**']
jobs:
deploy:
@@ -11,7 +11,7 @@ jobs:
defaults:
run:
- working-directory: crabfit-frontend
+ working-directory: frontend
permissions:
contents: read
@@ -33,5 +33,5 @@ jobs:
- id: deploy
uses: google-github-actions/deploy-appengine@v0
with:
- working_directory: crabfit-frontend
+ working_directory: frontend
version: v1
diff --git a/.gitignore b/.gitignore
index 6e56161..cfc9619 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,2 @@
/graphics
.DS_Store
-/crabfit-browser-extension/*.zip
diff --git a/README.md b/README.md
index 1b44745..8ba0b35 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,6 @@
Align your schedules to find the perfect time that works for everyone.
Licensed under the GNU GPLv3.
-
-
## Contributing
### ⭐️ Bugs or feature requests
@@ -15,16 +13,19 @@ If you find any bugs or have a feature request, please create an issue by **Note**
+> `memory-adaptor` is the default if no features are specified. Ensure you specify a different adaptor when deploying.
+
+### Adding an adaptor
+
+See [adding an adaptor](adaptors/README.md#adding-an-adaptor) in the adaptors readme.
+
+## Environment
+
+### CORS
+
+In release mode, a `FRONTEND_URL` environment variable is required to correctly restrict cross-origin requests to the frontend.
+
+### Cleanup task
+
+By default, anyone can run the cleanup task at `/tasks/cleanup`. This is usually not an issue, as it's based on when the events were last visited, and not when it's run, but if you'd prefer to restrict runs of the cleanup task (as it can be intensive), set a `CRON_KEY` environment variable in `.env`. This will require sending an `X-Cron-Key` header to the route with a value that matches `CRON_KEY`, or the route will return a 401 Unauthorized error.
diff --git a/api/adaptors/README.md b/api/adaptors/README.md
new file mode 100644
index 0000000..f8e7f1b
--- /dev/null
+++ b/api/adaptors/README.md
@@ -0,0 +1,22 @@
+# Crab Fit Storage Adaptors
+
+This directory contains sub-crates that connect Crab Fit to a database of some sort. For a list of available adaptors, see the [api readme](../README.md).
+
+## Adding an adaptor
+
+The suggested flow is copying an existing adaptor, such as `memory`, and altering the code to work with your chosen database.
+
+Note, you will need to have the following crates as dependencies in your adaptor:
+
+- `common`
Includes a trait for implementing your adaptor, as well as structs your adaptor needs to return.
+- `async-trait`
Required because the trait from `common` uses async functions, make sure you include `#[async_trait]` above your trait implementation.
+- `chrono`
Required to deal with dates in the common structs and trait function signatures.
+
+Once you've created the adaptor, you'll need to make sure it's included as a dependency in the root [`Cargo.toml`](../Cargo.toml), and add a feature flag with the same name. Make sure you also document the new adaptor in the [api readme](../README.md).
+
+Finally, add a new version of the `create_adaptor` function in the [`adaptors.rs`](../src/adaptors.rs) file that will only compile if the specific feature flag you added is set. Don't forget to add a `not` version of the feature to the default memory adaptor function at the bottom of the file.
+
+## FAQ
+
+Why is it spelt "adaptor" and not "adapter"?
+> The maintainer lives in Australia, where it's usually spelt "adaptor" 😎
diff --git a/api/adaptors/datastore/Cargo.toml b/api/adaptors/datastore/Cargo.toml
new file mode 100644
index 0000000..da37e8c
--- /dev/null
+++ b/api/adaptors/datastore/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "datastore-adaptor"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+async-trait = "0.1.68"
+chrono = "0.4.24"
+common = { path = "../../common" }
+# Uses custom version of google-cloud that has support for NULL values
+google-cloud = { git = "https://github.com/GRA0007/google-cloud-rs.git", features = ["datastore", "derive"] }
+serde = "1.0.163"
+serde_json = "1.0.96"
+tokio = { version = "1.28.1", features = ["rt-multi-thread"] }
diff --git a/api/adaptors/datastore/README.md b/api/adaptors/datastore/README.md
new file mode 100644
index 0000000..ca3a3d6
--- /dev/null
+++ b/api/adaptors/datastore/README.md
@@ -0,0 +1,13 @@
+# Google Datastore Adaptor
+
+This adaptor works with [Google Cloud Datastore](https://cloud.google.com/datastore). Please note that it's compatible with Firestore in Datastore mode, but not with Firestore.
+
+## Environment
+
+To use this adaptor, make sure you have the `GCP_CREDENTIALS` environment variable set to your service account credentials in JSON format. See [this page](https://developers.google.com/workspace/guides/create-credentials#service-account) for info on setting up a service account and generating credentials.
+
+Example:
+
+```env
+GCP_CREDENTIALS='{"type":"service_account","project_id":"my-project"}'
+```
diff --git a/api/adaptors/datastore/src/lib.rs b/api/adaptors/datastore/src/lib.rs
new file mode 100644
index 0000000..dc38e1b
--- /dev/null
+++ b/api/adaptors/datastore/src/lib.rs
@@ -0,0 +1,328 @@
+use std::{env, error::Error, fmt::Display};
+
+use async_trait::async_trait;
+use chrono::{DateTime, NaiveDateTime, Utc};
+use common::{Adaptor, Event, Person, Stats};
+use google_cloud::{
+ authorize::ApplicationCredentials,
+ datastore::{Client, Filter, FromValue, IntoValue, Key, KeyID, Query},
+};
+use tokio::sync::Mutex;
+
+pub struct DatastoreAdaptor {
+ client: Mutex,
+}
+
+// Keys
+const STATS_KIND: &str = "Stats";
+const EVENT_KIND: &str = "Event";
+const PERSON_KIND: &str = "Person";
+const STATS_EVENTS_ID: &str = "eventCount";
+const STATS_PEOPLE_ID: &str = "personCount";
+
+#[async_trait]
+impl Adaptor for DatastoreAdaptor {
+ type Error = DatastoreAdaptorError;
+
+ async fn get_stats(&self) -> Result {
+ let mut client = self.client.lock().await;
+
+ let event_key = Key::new(STATS_KIND).id(STATS_EVENTS_ID);
+ let event_stats: DatastoreStats = client.get(event_key).await?.unwrap_or_default();
+
+ let person_key = Key::new(STATS_KIND).id(STATS_PEOPLE_ID);
+ let person_stats: DatastoreStats = client.get(person_key).await?.unwrap_or_default();
+
+ Ok(Stats {
+ event_count: event_stats.value,
+ person_count: person_stats.value,
+ })
+ }
+
+ async fn increment_stat_event_count(&self) -> Result {
+ let mut client = self.client.lock().await;
+
+ let key = Key::new(STATS_KIND).id(STATS_EVENTS_ID);
+ let mut event_stats: DatastoreStats = client.get(key.clone()).await?.unwrap_or_default();
+
+ event_stats.value += 1;
+ client.put((key, event_stats.clone())).await?;
+ Ok(event_stats.value)
+ }
+
+ async fn increment_stat_person_count(&self) -> Result {
+ let mut client = self.client.lock().await;
+
+ let key = Key::new(STATS_KIND).id(STATS_PEOPLE_ID);
+ let mut person_stats: DatastoreStats = client.get(key.clone()).await?.unwrap_or_default();
+
+ person_stats.value += 1;
+ client.put((key, person_stats.clone())).await?;
+ Ok(person_stats.value)
+ }
+
+ async fn get_people(&self, event_id: String) -> Result