Simple Go HTTP service to track office attendance using RFID/UID scans (designed for ESP32 or similar devices). It records sign-in and sign-out events, keeps an in-memory list of current attendees, and persists visits to a local SQLite database.
Can be used by the IEEE Office Scanner ESP32 device and the IEEE Office Discord Bot to provide office presence tracking.
- Scan endpoint: Accepts POSTed UID payloads from an RFID reader to toggle sign-in / sign-out.
- Current attendees: Returns who is currently in the room and when they signed in.
- Visits management: Retrieve, filter, and delete completed visits (signin + signout) stored in SQLite via API; export visits as CSV.
- Scan history: Keeps the last 10 scans in memory (uid + timestamp) and exposes them via an API.
- Members management: Create/list registered members (UID, name, discord_id) via API; import/export members with JSON file.
- Discord sign-in/out: Sign in or out by providing a member's
discord_id. - Nightly cleanup: Force sign-out of all active attendees at 4:00 AM local time.
- Persistent store: Uses
data/attendance.db(SQLite) and persists active attendees todata/current_attendees.json. - CORS support: Configurable cross-origin resource sharing for web-based frontends.
main.go— application source with HTTP handlers for/scan,/current,/visits, and/members.Dockerfile— multi-stage build for producing a small runtime container.docker-compose.yml— convenient compose file to run the service locally.data/— folder for runtime files:members.json,current_attendees.json,attendance.db.
- Go toolchain (tested with Go 1.25). The project uses modules —
go.modis present. - Docker (optional) to build and run the containerized image.
- Run directly (development):
go run .- Build a local binary and run:
go build -o attendance .
./attendanceThe server listens on :8080 by default. Set the environmental variables as needed (see Configuration below).
The server can be configured using environment variables:
ALLOWED_ORIGINS- CORS allowed origins (default:*for all origins)- Set to specific origins for production:
ALLOWED_ORIGINS=https://yourdomain.com - Use comma-separated list for multiple origins:
ALLOWED_ORIGINS=http://localhost:3000,https://yourdomain.com
- Set to specific origins for production:
SCANNER_API_KEY- API key for ESP32 scanner (optional, enables authentication)DISCORD_BOT_API_KEY- API key for Discord bot (optional, enables authentication)API_KEYS- Comma-separated list of additional API keys (optional)
You can set them using a .env file and a tool like direnv or dotenv, or export them in your shell before running the server (e.g., export SCANNER_API_KEY=yourkey). The Docker Compose setup automatically loads from .env.
Security Note: If any API key is configured, all endpoints (except /health) require the X-API-Key header. See SECURITY.md for detailed setup instructions.
Copy .env.example to .env and customize as needed.
- Build the image locally:
docker build -t ieee-office-backend:latest .- Run container (mount
./dataso records persist on the host):
docker run --rm -p 8080:8080 -v "$(pwd)/data:/data" ieee-office-backend:latest- Start service with compose (the included
docker-compose.ymlmounts./datainto the container at/data):
docker compose up --builddata/members.json— used by the export/import endpoints. Expected format: a JSON array of members, each withname,uid, anddiscord_id. Example:
[
{ "name": "Alice", "uid": "UID_ABC_123", "discord_id": "111111111" },
{ "name": "Bob", "uid": "UID_XYZ_456", "discord_id": "222222222" }
]data/current_attendees.json— automatically written/loaded by the server to track currently signed-in UIDs.data/attendance.db— SQLite DB file created by the app to store members and visits.
If API keys are configured (see Configuration and Security sections), all endpoints except /health require the X-API-Key header for authentication.
Without API key (when no keys are configured):
curl http://localhost:8080/currentWith API key (when authentication is enabled):
curl http://localhost:8080/current -H 'X-API-Key: your-api-key-here'All examples below show commands without API keys for brevity. Add -H 'X-API-Key: your-api-key-here' to any request when authentication is enabled.
POST /scan— body:{ "uid": "<UID string>" }. The server will: - Returnstatus: "in"on successful sign-in. - Returnstatus: "out"on sign-out and persist a visit to the DB. - Unknown UID returns HTTP403 Forbidden.
Example:
curl -X POST http://localhost:8080/scan -H 'Content-Type: application/json' \
-d '{"uid":"UID_ABC_123"}'
# With API key:
curl -X POST http://localhost:8080/scan -H 'Content-Type: application/json' \
-H 'X-API-Key: your-api-key-here' -d '{"uid":"UID_ABC_123"}'GET /scan-history— returns the last 10 scans (newest first). Each item hasuidandtime(RFC3339).
curl http://localhost:8080/scan-historyGET /current— returns JSON array of currently signed-in users (name + signin_time).
curl http://localhost:8080/currentGET /visits— returns visits (name, signin_time, signout_time). Supports optional query parameters for filtering:from- RFC3339 formatted start date (inclusive) to filter visits from this date onwardsto- RFC3339 formatted end date (inclusive) to filter visits up to this datemember_id- filter visits by specific member IDlimit- maximum number of records to return (newest first)format- output format:json(default) orcsvfor CSV file download
# Get all history as JSON (default)
curl http://localhost:8080/visits
# Get history from a specific date
curl "http://localhost:8080/visits?from=2024-01-01T00:00:00Z"
# Get history within a date range
curl "http://localhost:8080/visits?from=2024-01-01T00:00:00Z&to=2024-12-31T23:59:59Z"
# Get the 10 most recent visits
curl "http://localhost:8080/visits?limit=10"
# Get all visits for a specific member
curl "http://localhost:8080/visits?member_id=5"
# Combine filters: get 50 most recent visits from member 5 in January 2024
curl "http://localhost:8080/visits?member_id=5&from=2024-01-01T00:00:00Z&to=2024-01-31T23:59:59Z&limit=50"
# Export all visits as CSV file
curl "http://localhost:8080/visits?format=csv" -o visits.csv
# Export filtered visits as CSV (all filters work with CSV format)
curl "http://localhost:8080/visits?format=csv&from=2024-01-01T00:00:00Z&to=2024-12-31T23:59:59Z" -o visits.csvThe CSV export includes columns: Name, Sign In Time, Sign Out Time, and Duration.
DELETE /visits— delete visits based on filters. Requires at least one filter (from,to, ormember_id) to prevent accidental deletion of all visits.from- RFC3339 formatted start date to delete visits from this date onwardsto- RFC3339 formatted end date to delete visits up to this datemember_id- delete visits for a specific member ID
# Delete all visits from January 2024 onwards
curl -X DELETE "http://localhost:8080/visits?from=2024-01-01T00:00:00Z"
# Delete all visits up to December 2023
curl -X DELETE "http://localhost:8080/visits?to=2023-12-31T23:59:59Z"
# Delete visits within a specific date range
curl -X DELETE "http://localhost:8080/visits?from=2024-01-01T00:00:00Z&to=2024-01-31T23:59:59Z"
# Delete all visits for a specific member
curl -X DELETE "http://localhost:8080/visits?member_id=5"
# Delete visits for a specific member within a date range
curl -X DELETE "http://localhost:8080/visits?member_id=5&from=2024-01-01T00:00:00Z&to=2024-01-31T23:59:59Z"Returns the number of visits deleted. Returns 400 if no filters are provided.
GET /members— returns registered members stored in the DB.
curl http://localhost:8080/membersPOST /members— create a new member. Body:{ "name": "Charlie", "uid": "UID_123", "discord_id": "333333333" }.
curl -X POST http://localhost:8080/members -H 'Content-Type: application/json' \
-d '{"name":"Charlie","uid":"UID_123","discord_id":"333333333"}'PUT /members/{id}— update an existing member by ID. Body:{ "name": "Charlie Updated", "uid": "UID_123", "discord_id": "333333333" }.
curl -X PUT http://localhost:8080/members/1 -H 'Content-Type: application/json' \
-d '{"name":"Charlie Updated","uid":"UID_123","discord_id":"333333333"}'Returns the updated member on success, 404 if member not found, or 409 if the UID conflicts with another member.
DELETE /members/{id}— delete an existing member by ID.
curl -X DELETE http://localhost:8080/members/1Returns a success message on deletion, 404 if member not found, or 409 if the member is currently signed in. Note: Deleting a member will cascade delete all associated visit history.
GET /count— returns the count of currently signed-in attendees.
curl http://localhost:8080/countResponse: {"count": 3}
GET /health— health check endpoint that returns200 OKwith "OK" text response.
curl http://localhost:8080/healthPOST /sign-out-all— signs out all currently signed-in attendees. Returns a message with the count of people signed out.
curl -X POST http://localhost:8080/sign-out-allResponse: {"message": "Signed out all attendees (3 total)."}
POST /sign-in-discord— sign in a member by Discord ID. Body:{ "discord_id": "111111111" }.
curl -X POST http://localhost:8080/sign-in-discord -H 'Content-Type: application/json' \
-d '{"discord_id":"111111111"}'POST /sign-out-discord— sign out a member by Discord ID. Body:{ "discord_id": "111111111" }.
curl -X POST http://localhost:8080/sign-out-discord -H 'Content-Type: application/json' \
-d '{"discord_id":"111111111"}'GET /export-members— export all members todata/members.json.
curl http://localhost:8080/export-membersPOST /import-members— import members fromdata/members.jsoninto the database (existing UIDs are ignored).
curl -X POST http://localhost:8080/import-members- Unit tests are included, run them with:
# Run all tests
go test ./...
# Run with verbose output
go test -v ./...
# Run with race detector (recommended)
go test -race ./...
# Run specific test category
go test -v -run TestAPIKey # API key authentication tests
go test -v -run TestHandle # Handler tests- Handler tests: All HTTP endpoints (scan, members, history, etc.)
- API key authentication: Middleware validation, multiple keys, environment loading
- Integration tests: End-to-end workflows with authentication
- Edge cases: Invalid input, missing data, error conditions
All tests use in-memory SQLite databases for speed and isolation.
- Keep the
data/folder mounted on persistent storage when running in containers so the SQLite DB and current attendees are not lost between restarts. - Security: Configure API keys for production deployment (see SECURITY.md)
- Concurrency: shared in-memory maps are protected by an
RWMutex. File I/O and DB operations are performed outside of locks where possible to avoid blocking. - Nightly cleanup at 4:00 AM clears active attendees. Sign out times are set to 4:00 AM for those visits.