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 namedescription
access_eventsWe store access events here. This currently isn't used by anything other than reports (e.g. how many swipes per day).
*communicationThe communication table is basically an enum of types of messages that we can send out
*communication_loga log of messages that we have sent out
member_countsEveryday, we update how many members we have for each membership level. This allows us to track how our membership has changed each month
member_creditdeprecated - can be removed
member_resourcestores the relationship between members and what resources they have access to
member_tiersan enum of member levels
membersmembership information
paymentsdeprecated - can be removed - will be removed on next migrate up
resourcesresource information - name, address, how to communicate with the resource
usersusers 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.

idnameemailrfidmember_tier_idsubscription_id
<some_id>nullnullnullinactive<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.

idnameemailrfidmember_tier_idsubscription_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.

  1. Get the new member's subscription_id. If you don't know how to get this, you may have to contact the treasurer.
  2. verify that we don't already have a member with that subscription_id (there's a constraint that prevents multiple members having the same subscription_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 Range13.56 MHz ISM Band
Host InterfaceSPI / I2C / UART
Operating Supply Voltage2.5 V to 3.3 V
Max. Operating Current13-26mA
Min. Current(Power down)10µA
Logic Inputs5V Tolerant
Read Range5 cm

more info

esprfid

esprfid github

buy

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 CommandTopicHostPortMessage
subfrontdoor/synclocalhost1883
subfrontdoor/sendlocalhost1883
subfrontdoorlocalhost1883
pubfrontdoorlocalhost1883{"doorip": "192.168.1.211", "cmd": "listusr"}
pubfrontdoor/synclocalhost1883{"type":"heartbeat","time":1616731044,"ip":"192.168.1.211","door":"esp-rfid"}
pubfrontdoor/sendlocalhost1883{"cmd":"log","type":"access","time":'$(date +%s)',"isKnown":"true","access":"Always","username":"Fake User","uid":"not an rfid tag","door":"frontdoor"}
pubfrontdoorlocalhost1883{"doorip": "192.168.1.211", "cmd": "deletusers"}
pubfrontdoorlocalhost1883{"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.

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:

  1. Install Docker, VS Code, and the VS Code Remote-Containers extension.
  2. Clone the repo to a folder on WSL if you are using Windows for better performance.
  3. 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.

Open from container

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.

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.

usernamepassword
test@test.comtest