Admin Access
Admin access is the ability to assign rfid fobs to members and change what they have access to.
Granting access
To give someone access, you assign them to the admin
resource.
The admin
resource is a special resource that doesn't broadcast MQTT messages.
If a member has the admin
resource, they will see reports
, members
, and resources
tabs in the UI.
API
SwaggerUI
SwaggerUI is the static site for serving up our api documentation.
The static html/js files can be found in the SwaggerUI release (in the dist folder) https://github.com/swagger-api/swagger-ui
Generate the documentation
This project is documented with go-swagger
Download the binary from here
You will probably have to allow the binary to be executable
chmod +x swagger_linux_amd64
Then copy/move the binary somewhere you can access it
cp swagger_linux_amd64 /usr/local/bin/swagger
Once you have your swagger binary, you can run the gendocs.sh
script in the root of the repo.
This will create the swagger.json
file and place it in the ./docs/swaggerui/
directory.
You will then be able to access the swagger documentation at /swaggerui/
in your web browser.
Database
The database runs Postgres.
I recommend using pgadmin to interact with the database. Ideally direct manipulation of the DB should be avoided (there are some edge cases where this is necessary). The database schema is managed with migrations. see the main readme of this repo for more info.
Tables
table name | description |
---|---|
access_events | We store access events here. This currently isn't used by anything other than reports (e.g. how many swipes per day). |
*communication | The communication table is basically an enum of types of messages that we can send out |
*communication_log | a log of messages that we have sent out |
member_counts | Everyday, we update how many members we have for each membership level. This allows us to track how our membership has changed each month |
member_credit | deprecated - can be removed |
member_resource | stores the relationship between members and what resources they have access to |
member_tiers | an enum of member levels |
members | membership information |
payments | deprecated - can be removed - will be removed on next migrate up |
resources | resource information - name, address, how to communicate with the resource |
users | users are tied to members. The distinction is that users use the dashboard. We don't support non-members making user accounts |
*Emails are currently disabled
Disaster Recovery
If this system goes down, How do we recover?
We take periodic snapshots and backups of the entire DB. It's probably preferable to restore the entire DB.
However, if that's not an option, read on.
Most of the things in the database can be calculated or retrieved.
The bare minimum you need to restore the system is returned from the following query.
SELECT name, email, rfid, subscription_id
FROM membership.members;
Member Status
Member Status is evaluated daily. The subscription_id
is used to lookup the member's subscription in the payment provider.
Occasionally, we have members that have Credited
memberships. This can happen if someone wants to pay for only a month of membership or if someone gifts a few months of membership to someone.
With the bare minimum restore, Credited
members would not persisted their Credited
status. This should be tracked out of band from this application (i.e. the treasurer keeps track of who has this and when they need to be removed from Credited
status).
RFID fobs
In a pinch, you could get a list of membership details (name, email, subscription_id) from the treasurer and then get a list of the active RFIDs off of the the rfid device (it store them on local flash).
Worst case scenario
If you've lost everything, you'll just have to get people to register their fobs again.
The dashboard does have an option for self service. A member can create an account and assign themselves a new fob. Normal members don't have access to change other members fobs and they don't have access to assign new access to themselves.
Edge Cases
note: Be extra careful when making direct changes to the DB. This is typically not recommended. We humans make errors.
An existing member starts a subscription for someone else.
We received the subscriptionID from paypal, but we already have that paying member's email in the DB. The DB has a constraint that prevents it from using the same email address for two different members. So, the system created a new "member" with only the subscriptionID populated. (i.e. no name, no email) i.e.
id | name | rfid | member_tier_id | subscription_id | |
---|---|---|---|---|---|
<some_id> | null | null | null | inactive | <some_subscription_id> |
I created a new member for them in the dashboard. This allowed me to input their name and email address and assign them a fob that worked immediately.
id | name | rfid | member_tier_id | subscription_id | |
---|---|---|---|---|---|
<some_id> | <some_name> | <some_email> | <some_rfid> | <some_membership_level> | null |
On the following day, the member would lose access because they don't have a subscription_id
This can't be fixed in the UI because the UI doesn't allow you to delete members and you aren't allowed to give two members the same subscription_id
.
The only way to fix this is to run a few DB queries.
- Get the new member's
subscription_id
. If you don't know how to get this, you may have to contact the treasurer. - verify that we don't already have a member with that
subscription_id
(there's a constraint that prevents multiple members having the samesubscription_id
)
SELECT id, name, email, rfid, member_tier_id, subscription_id
FROM membership.members
WHERE subscription_id = '<new subscription id>';
In this case, I found that the new subscription_id
overwrote the original member's existing subscription_id
.
So, I will have to go find their existing subscription_id
and correct this.
UPDATE membership.members
SET subscription_id='<original subscription id>'
WHERE subscription_id = '<new subscription id>'
corrected.
Now that we are sure that no other member has that subscription_id
, we can update their subscription_id
via the UI.
Frontdoor
Note that this device stores access information locally in flash. If for some reason, it does not have network connection, the device will still operate. However, it will not receive updates.
It's important to occasionally check that the device is connected. One easy way to do this is in the member dashboard ui on the resources tab.
The device tells us that it's online at some interval. If it loses connection (or if the server just started), the dashboard will know that it's offline.
Infrastructure
flowchart LR; database[(Database)]<-. sql connection over ssl .->firewall <-.->proxy([http reverse proxy])<-.->server(member dashboard/server) <-. mqtt messages .->mqttbroker([mosquitto/mqtt broker]) mqttbroker<-. mqtt message .->frontdoor([front door rfid]) mqttbroker<-. mqtt message .->cnc([CNC rfid]) mqttbroker<-. mqtt message .->laser([laser rfid]) mqttbroker<-. mqtt message .->resources([rfid device])
- The db runs on a hosting provider in the cloud.
- The server is internal. The server could also run in the cloud, but that would require us to expose the MQTT broker through the firewall.
- The web ui (which runs on the server) is exposed through a proxy.
- The server communicates to the rfid endpoints (aka resources) through the MQTT protocol.
Database
The database runs postgres.
Server
Responsibilities:
- handle backend requests for the "Member Dashboard" (aka the web ui)
- serve up the web ui
- publish/subscribe MQTT messages
- run tasks at scheduled intervals (e.g. reach out to payment providers to verify that members are up to date)
Web UI
The web ui is a dashboard that makes managing the space more convenient. It controls who has access to what and shows some membership stats.
MQTT Broker
The MQTT Broker allows the server to communicate to resources on the network (e.g. the frontdoor rfid device).
Resources
Resources is a term used for RFID devices. However, they don't have to be an rfid device. Essentially, a resource is just something that we can communicate with via MQTT.
For example, you could potentially build out an LED light controller that publishes and subscribes to MQTT events and then build some module on this member dashboard to control it.
Integrations
This app uses a few integrations. See below to find more information on how to request access through those vendors
Slack webhook setup
https://hackrva.slack.com/apps/A0F7XDUAZ-incoming-webhooks
MailGun api
https://app.mailgun.com/app/account/security/api_keys
Payment Provider
Paypal
New Member
When a member creates a subscription, Paypal sends us a message via a webhook. There is no guaranty that this will be immediate. The message might not include all the expected information.
Hopefully we receive their subscription_id
and their email address.
If the information isn't correct, we can modify it in the UI.
Evaluating Membership
Memberships are evaluated when the server starts up and every day at the same time.
RFID
Reader
We are using the RC522 RFID reader/writer module.
Frequency Range | 13.56 MHz ISM Band |
Host Interface | SPI / I2C / UART |
Operating Supply Voltage | 2.5 V to 3.3 V |
Max. Operating Current | 13-26mA |
Min. Current(Power down) | 10µA |
Logic Inputs | 5V Tolerant |
Read Range | 5 cm |
esprfid
The "esprfid" blue board works well for our purposes for the most part.
Some issues:
- after several months of operation, we've experienced memory corruption (this could have to do with how frequently we update it).
- we currently don't have a verification step between the rfid reader and the member server
- in the event of a power outage, we seem to lose connection (or at least we don't receive mqtt messages from the rfid device). This could be related to the rfid device coming online before the mqtt broker is available. Usually power cycling the rfid device gets restored to a state that can send/receive mqtt messages.
mqtt examples
Mqtt Command | Topic | Host | Port | Message |
---|---|---|---|---|
sub | frontdoor/sync | localhost | 1883 | |
sub | frontdoor/send | localhost | 1883 | |
sub | frontdoor | localhost | 1883 | |
pub | frontdoor | localhost | 1883 | {"doorip": "192.168.1.211", "cmd": "listusr"} |
pub | frontdoor/sync | localhost | 1883 | {"type":"heartbeat","time":1616731044,"ip":"192.168.1.211","door":"esp-rfid"} |
pub | frontdoor/send | localhost | 1883 | {"cmd":"log","type":"access","time":'$(date +%s)',"isKnown":"true","access":"Always","username":"Fake User","uid":"not an rfid tag","door":"frontdoor"} |
pub | frontdoor | localhost | 1883 | {"doorip": "192.168.1.211", "cmd": "deletusers"} |
pub | frontdoor | localhost | 1883 | {"doorip": "192.168.1.211", "cmd": "adduser", "user": "dustin", "uid": "4755ca35", "acctype":1,"validuntil":-86400} |
Getting Started
HighLevel
For Dev purposes, we use a fake "InMemory" db.
We can seed fake content with generators (see ./test/generators
) that we have built.
The frontend gets embeded into the binary. So, we don't need docker to host any database.
- Install golang
- Install Node
- build and run with
make
- Install dependencies (see: Install Dependencies)
e.g.
make build-ui
make run
Hopefully that makes the app fairly easy to stand up and get to hacking on.
Install Dependencies
Download go modules
note: go modules might download automatically the first time you run the app
go get ./...
Download node_modules
cd web
npm ci
Dev Setup
Note: The Docker and Remote-Containers setup will soon be deprecated for this project. Please refer to Getting Started for more updated dev setup instructions.
The Easy Way (using Remote-Containers)
To get started quickly, follow these steps:
- Install Docker, VS Code, and the VS Code Remote-Containers extension.
- Clone the repo to a folder on WSL if you are using Windows for better performance.
- Open the folder in VS Code and choose "Reopen in Container" or run the "Remote-Containers: Open Folder in Container..." command and select the local folder.
You don't need to install any other dependencies.
To start the backend server, debug it in VS Code. Alternatively, you can use sh buildandrun.sh
to start the server without debugging.
Please refer to the UI README for instructions on starting the web app.
# navigate to ui folder
cd ui
# install node modules
$ npm ci
# run local env
$ npm run start
If you feel cramped in the VS Code terminal pane, you can still connect to dev container shell from your favorite terminal using
docker exec -it -u vscode memberdashboard_dev_1 bash
Now, go write code and implement features!
Installing flutter
Follow instructions for installing flutter manually: https://docs.flutter.dev/get-started/install/linux#install-flutter-manually
It's also recommended to install the flutter dev tools.
CONFIG file
This app requires a config file.
The path for the config file is set using the MEMBER_SERVER_CONFIG_FILE
environment variable.
the sample.config.json
can be used as a template for this file.
export MEMBER_SERVER_CONFIG_FILE="/etc/hackrva/config.json"
Development Tasks
Database Migrations
Database migrations are managed using golang-migrate/migrate. The CLI is already available when using Remote-Containers, otherwise it will need to be installed.
How to add a migration
A migration can be added by creating an up and down migration file in the migrations folder. The migration file names should be named according to {sequentialNumber}{description}.up.sql and {sequentialNumber}{description}.down.sql. Migrations can be created with the following command.
make migration name=<NAME>
Populate the up and down scripts. Up scripts should be idempotent. Down scripts should revert all changes made by up script
How to run a migration
Migrations can be applied using the CLI
make migrate-up
Generating Swagger Docs
When using Remote-Containers, swagger documentation can be updated using.
make swagger
Otherwise, follow instructions in ./docs/README.md to install swagger first.
Querying Postgres
The following options are available if using Remote-Containers.
- Use the PostgreSQL extension in VS Code
- Use pgAdmin on localhost:8080 with info@hackrva.org/test
- Use make run-sql or make run-sql-command
Troubleshooting devContainer
Sometimes i'm getting an error when tryign to build the containers.
Command failed: docker inspect --type image memberdashboard_dev
This happened after I ran a docker system prune -a
, but I think I've seen it before.
To fix this, build the containers with the following command then reopen vscode in Container
docker-compose -f docker-compose.yml -f .devcontainer/docker-compose.yml build
How to install golang-migrate
go install github.com/golang-migrate/migrate/v4/cmd/migrate
Seed the DB with test data
note: before seeding the db, it has to be up and running with the current schema run the db migrations
make migrate-up
seed the db
make seed
This will create some random members and a test user.
username | password |
---|---|
test@test.com | test |