⚠️ This is a snapshot repository for peer review, it does not have the complete history of the project.The actual project is managed in a private GitLab repository.
Koru is an API to manage long-term shared expenses. The frontend mobile application can be found at koru_app
The project requires a rust development environment, which can easily be installed using rustup
It also uses git hooks to enforce proper formatting (cargo fmt) and usage of conventional commits
These can be installed by running:
./scripts/git-hooks.shThe CI/CD pipeline will fail on incorrect formatting, so it is highly recommended to install the hooks.
The application supports optional backends that can be enabled through feature flags.
postgres: use PostgreSQL as data storeredis-bus: use Redis as event busredis-session: use Redis for session managementredis: enables bothredis-busandredis-sessionpushy: use Pushy for notification (does not do anything by itself, it needsnotification)notification: enables notification sending (currently directly enablespushysince it is the only implementation)openapi: enables Swagger UI endpoint & OpenApi docs (available at/swagger-ui/)development: enablespostgres,redis,notificationandopenapiproduction: enablespostgres,redisandnotification
By default, all features are disabled and the app will run using an in memory data store, event bus and session store.
Even though they work, it is not recommended to use the in memory implementations, the production feature should be preferred for production use.
When running commands locally with the production feature enabled, Postgres & Redis will be required, therefore you will need to ensure additional tools are available on your system:
You can then run local Postgres & Redis containers in the background in order to be able to compile, test and run the application.
# Start Postgres & Redis containers, apply SQLx migrations
./scripts/init-db.shNote: a cleanup script is also available ./scripts/stop-db.sh
The app tests can be executed using:
cargo testIt will run the unit & integration tests using the in memory configurations.
To run the integration tests with production feature, relying on Postgres & Redis, run:
KORU_ENV=integration cargo test --features production --no-default-featuresNote: to enable app logs during tests, add
TEST_LOG=1environment variable. Logs are in bunyan format, so you may prefer to pipe them throughbunyan.
Note: It is also possible to run the tests using nextest
# Install with cargo install --locked cargo-nextest # Replace `cargo test` with cargo nextest run
The app can be executed locally using:
# buyan piping is optional, but since the logs use bunyan format, it will help making them more readable.
cargo run | bunyanIt will run the app using the in memory configurations.
To run the integration tests with production feature, relying on Postgres & Redis, run:
# buyan piping is optional, but since the logs use bunyan format, it will help making them more readable.
KORU_ENV=integration cargo run --features production --no-default-features | buyanAn executable version of the application can be built with:
cargo build --release --bin koru --features production --no-default-featuresThis will build a fully featured application with support for in memory, Postgres and Redis backends.
Features can be adjusted to support only the ones you need. But it should be noted that if some features are not included, trying to set them up in configuration will lead to a crash on startup. If no features are provided, only in memory is supported.
A docker image can be built with:
docker build -t koru:latest .It will build an image with all features, and prod configuration (see Configuration). For other configurations, edit Dockerfile.
Note that the build requires SQLx offline data, which can be created with:
cargo sqlx prepare --merged -- --all-targets --features production --no-default-featuresNote: if it fails with a warning no queries found, try updating sqlx-cli cargo install sqlx-cli
To simplify the different commands, it is possible to use cargo-make, Makefile.toml defines the different tasks.
Also not mandatory, it is highly recommended.
Note: Provided
Makefile.tomlrelies on nextest for testing.# Install it by running cargo install --locked cargo-nextest
# Install cargo make
cargo install --force cargo-make
# install git-hooks, setup redis & postgres if necessary, run fmt, clippy, unit test & integration test
cargo make
# setup redis & postgres if necessary, run fmt, clippy, unit test & integration test
cargo make all-test
# run fmt, clippy, unit test
cargo make unit-test
# setup redis & postgres if necessary, run fmt, clippy & integration test
cargo make integration-test
# setup redis & postgres if necessary
cargo make setup-infra
# generate SQLx offline data
cargo make sqlx-offline
# install git-hooks
cargo make git-hooks
# generate OpenApi file (in openapi folder)
cargo make openapi
# See Makefile.toml for more.Here are some curl commands showing the use of the API
# Register
curl -i -H 'Content-Type: application/json' -d '{"name":"rbiland","password":"123","email":"r@r1.com"}' "http://localhost:8000/register"
# Login
curl -i -H 'Content-Type: application/json' -d '{"password":"123","email":"r@r1.com"}' -c cookie "http://localhost:8000/login"
# Create group
curl -i -H 'Content-Type: application/json' -d '{"name":"my group","color":{"red":0,"green":255,"blue":0}}' -b cookie "http://localhost:8000/groups"
# Get groups
curl -i -b cookie "http://localhost:8000/groups"
# Delete group (REPLACE GROUP_ID)
curl -i -b cookie -X DELETE "http://localhost:8000/groups/GROUP_ID"
# Create expense (REPLACE GROUP_ID)
curl -i -H 'Content-Type: application/json' -d '{"description":"my expense", "amount": 12}' -b cookie "http://localhost:8000/groups/GROUP_ID/expenses"
# Update expense (REPLACE GROUP_ID & EXPENSE_ID)
curl -i -H 'Content-Type: application/json' -d '{"description":"my expense 2", "amount": 20}' -b cookie -X PUT "http://localhost:8000/groups/GROUP_ID/expenses/EXPENSE_ID"
# Delete expense (REPLACE GROUP_ID & EXPENSE_ID)
curl -i -b cookie -X DELETE "http://localhost:8000/groups/GROUP_ID/expenses/EXPENSE_ID"
# Get un-settled expenses (REPLACE GROUP_ID)
curl -i -b cookie "http://localhost:8000/groups/GROUP_ID/expenses"
# Generate group token (REPLACE GROUP_ID)
curl -i -b cookie "http://localhost:8000/groups/GROUP_ID/token"
# Join group (needs a second user) (REPLACE GROUP_ID & TOKEN)
curl -i -H 'Content-Type: application/json' -d '{"token":"TOKEN","color":{"red":0,"green":255,"blue":0}}' -b cookie2 "http://localhost:8000/groups/GROUP_ID/members"
# Settle (REPLACE GROUP_ID)
curl -i -b cookie -X POST "http://localhost:8000/groups/GROUP_ID/settlements"
# Get past settlements (REPLACE GROUP_ID)
curl -i -b cookie "http://localhost:8000/groups/GROUP_ID/settlements"
# Get expenses of settlement (REPLACE GROUP_ID & STL_ID)
curl -i -b cookie "http://localhost:8000/groups/GROUP_ID/expenses?settlement_id=STL_ID"
# Change color (REPLACE GROUP_ID)
curl -i -H 'Content-Type: application/json' -d '{"color":{"red":255,"green":255,"blue":255}}' -b cookie -X PATCH "http://localhost:8000/groups/GROUP_ID/members"
# Register device (REPLACE MY_DEVICE_ID)
curl -i -H 'Content-Type: application/json' -d '{"device":"MY_DEVICE_ID"}' -b cookie "http://localhost:8000/devices"
# Remove device
curl -i -b cookie -X DELETE "http://localhost:8000/devices"Optionally, a Swagger UI endpoint is available with the openapi feature (included in development and default).
It can be accessed at http://localhost:8000/swagger-ui/.
To run an executable that includes the endpoint you can run:
# Default config includes OpenApi
cargo run
# development config is the same as production but with openapi, so basically a dev build
cargo run --features developmentTo build a docker image that includes OpenApi you can run:
docker build --build-arg KORU_FEATURES=development -t koru:development .To generate the OpenAPI file (in openapi folder) run:
cargo run --bin gen-openapi
# Or
cargo make openapiThe application is configured using stacked configurations, from yaml files and environment variables.
config/default.yml: base configuration fileconfig/${KORU_ENV}.yml: environment specific configuration file, based onKORU_ENVenvironment variable. Possible values arelocal(default),integration, andprod.- Environment variables starting with
KORUand using__as separators between the different levels. See examples bellow.
The following shows all available configurations, it should be noted that it cannot be used as-is, where ## CHOOSE ONE is mentioned, only one option should be provided.
Configurations requiring specific feature flags are commented accordingly.
api:
host: localhost
port: 8000
session:
hmac: Very_Long-SecRet!00##123456789-Very_Long-SecRet!00##123456789-Very_Long-SecRet!00##123456789
duration: 20
store: ### CHOOSE ONE
redis: ### --features redis-session
host: localhost
port: 6379
memory:
application:
auth: ### CHOOSE ONE
none:
internal:
token:
jwt:
secret: dEmOSecreT!
notification:
pushy:
url: localhost
token: YOUR_TOKEN
database: ### CHOOSE ONE
postgres: ### --features postgres
host: localhost
port: 5432
username: postgres
password: password
name: koru
memory:
event_bus: ### CHOOSE ONE
redis: ### --features redis-bus
event_channel: koru_events
host: localhost
port: 6379
memory:Configurations can be overridden using environment variables, for example:
- to change the JWT secret, you would use
KORU__APPLICATION__TOKEN__JWT__SECRET=whatever - to change the Redis event bus channel, you would use
KORU__EVENT_BUS__REDIS__EVENT_CHANNEL=channel
| Configuration | Explanation |
|---|---|
api.host |
Application API host |
api.port |
Application API port |
application.token |
Generator to use for generating join group tokens (Currently only JWT is available) |
application.token.jwt.secret |
Secret for the JWT token generator |
api.session |
Session management configuration |
api.session.hmac |
HMAC for signing cookies |
api.session.duration |
Session duration in Days |
api.session.store |
Session store to use redis (requires --features redis-session) or memory |
api.session.store.redis.host |
Host for Redis instance |
api.session.store.redis.port |
Port for Redis instance |
api.session.store.memory |
Use the the in memory session store |
database |
Database to use for data storage postgres (requires --features postgres) or memory |
database.postgres.host |
Host for Postgres instance |
database.postgres.port |
Port for Postgres instance |
database.postgres.username |
Username for Postgres instance |
database.postgres.password |
Password for Postgres instance |
database.postgres.name |
Postgres database name |
database.memory |
Use the in memory data store |
event_bus |
Event bus to use for sending and processing events asynchronously redis (requires --features redis-bus) or memory |
event_bus.redis.host |
Host for Redis instance |
event_bus.redis.port |
Port for Redis instance |
event_bus.redis.event_channel |
PubSub channel name for sending generated events |
event_bus.memory |
Use the in memory event bus |
application.notification |
Notification service to use for sending user notifications based on events (Currently only Pushy is available) |
application.notification.pushy.url |
URL of the Pushy service |
application.notification.pushy.token |
API key for Pushy |
application.auth |
Application auth mechanism to use none or internal |
application.auth.none |
Disable auth in the application, no password will be needed for registration & login |
application.auth.internal |
Use the internal store for authentication |
| Configuration | Default | Local | Integration | Prod |
|---|---|---|---|---|
api.host |
localhost | localhost | 0.0.0.0 | |
api.port |
8000 | |||
application.token |
||||
application.token.jwt.secret |
fake value | ENV_VAR | ||
api.session |
||||
api.session.hmac |
fake value | ENV_VAR | ||
api.session.duration |
20 | |||
api.session.store |
memory | redis | redis | |
api.session.store.redis.host |
localhost | redis | ||
api.session.store.redis.port |
6379 | 6379 | ||
api.session.store.memory |
||||
database |
memory | postgres | postgres | |
database.postgres.host |
localhost | postgres | ||
database.postgres.port |
5432 | 5432 | ||
database.postgres.username |
postgres | ENV_VAR | ||
database.postgres.password |
password | ENV_VAR | ||
database.postgres.name |
koru | koru | ||
database.memory |
||||
event_bus |
memory | redis | redis | |
event_bus.redis.host |
localhost | redis | ||
event_bus.redis.port |
6379 | 6379 | ||
event_bus.redis.event_channel |
koru_events | koru_events | ||
event_bus.memory |
||||
application.notification |
||||
application.notification.pushy.url |
localhost | https://api.pushy.me | ||
application.notification.pushy.token |
fake value | ENV_VAR | ||
application.auth |
internal | internal | internal | |
application.auth.none |
||||
application.auth.internal |