Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 3 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ Service for integrating [PlaceOS](https://placeos.com/) with the workplace.
These environment variables are required for configuring an instance of Staff API

```console
SG_ENV=production # When set to production, the auth token in the request header will be used for auth, instead of static credentials from environment variables
SG_ENV=production

# Database config:
PG_DATABASE_URL=postgresql://user:password@hostname/placeos?max_pool_size=5&max_idle_pool_size=5

# Public key for decrypting and validating JWT tokens
JWT_PUBLIC=base64-public-key #same one used by PlaceOS rest-api

# Location of PlaceOS API
PLACE_URI=https://example.place.technology
# Redis, signals are published here for drivers and frontends to consume
REDIS_URL=redis://redis:6379
```

### Optional
Expand All @@ -29,19 +29,6 @@ PLACE_URI=https://example.place.technology
# Default Timezone
STAFF_TIME_ZONE=Australia/Sydney #default to UTC if not provided

# Host header to use when connecting to PlaceOS endpoints
PLACE_HOST_HEADER=example.place.technology

# Skip SSL verification when connecting to PlaceOS endpoints
SSL_VERIFY_NONE=true

# Sentry monitoring
SENTRY_DSN=<sentry dsn>

# Logstash log ingest
LOGSTASH_HOST=example.com
LOGSTASH_PORT=12345

# Sentry monitoring
SENTRY_DSN=<sentry dsn>

Expand Down
15 changes: 10 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ services:
ports:
- 5432:5432

redis:
image: eqalpha/keydb
healthcheck:
test: ["CMD", "keydb-cli", "ping"]
interval: 5s
timeout: 5s
retries: 10

migrator:
build:
context: ${PWD}/spec/migration
Expand All @@ -39,13 +47,10 @@ services:
depends_on:
- migrator
- db
- redis
security_opt:
- seccomp:unconfined
environment:
PG_DATABASE_URL: postgresql://postgres:password@db:5432/staff_api_test
PLACE_URI: http://toby.dev.place.tech
PLACE_EMAIL: toby@place.technology.com
PLACE_PASSWORD: password
PLACE_AUTH_CLIENT_ID: abc123
PLACE_AUTH_SECRET: abc123
REDIS_URL: redis://redis:6379
GITHUB_ACTION: ${GITHUB_ACTION-}
30 changes: 21 additions & 9 deletions shard.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ shards:

action-controller:
git: https://github.com/spider-gazelle/action-controller.git
version: 8.3.0
version: 8.15.0

active-model:
git: https://github.com/spider-gazelle/active-model.git
Expand Down Expand Up @@ -41,6 +41,10 @@ shards:
git: https://github.com/spider-gazelle/connect-proxy.git
version: 2.0.5

crc16:
git: https://github.com/maiha/crc16.cr.git
version: 0.1.0

cron_parser:
git: https://github.com/kostya/cron_parser.git
version: 0.4.0
Expand Down Expand Up @@ -99,15 +103,15 @@ shards:

json-schema:
git: https://github.com/spider-gazelle/json-schema.git
version: 1.3.3
version: 2.1.1

jwt:
git: https://github.com/crystal-community/jwt.git
version: 1.7.2

lucky_router:
git: https://github.com/luckyframework/lucky_router.git
version: 0.6.1
version: 0.6.2

neuroplastic:
git: https://github.com/spider-gazelle/neuroplastic.git
Expand Down Expand Up @@ -139,24 +143,32 @@ shards:

place_calendar:
git: https://github.com/placeos/calendar.git
version: 4.30.0

placeos:
git: https://github.com/placeos/crystal-client.git
version: 2.12.3
version: 4.30.1

placeos-log-backend:
git: https://github.com/place-labs/log-backend.git
version: 0.13.0

placeos-models:
git: https://github.com/placeos/models.git
version: 9.118.1
version: 9.119.0

pool:
git: https://github.com/ysbaddaden/pool.git
version: 0.2.4

promise:
git: https://github.com/spider-gazelle/promise.git
version: 3.2.1

redis:
git: https://github.com/place-labs/crystal-redis.git
version: 2.9.2+git.commit.52e111ee3bbbdc41fbd3509398c08cc0e8e40e59

redis-cluster:
git: https://github.com/place-labs/redis-cluster.cr.git
version: 0.8.13

sanitize:
git: https://github.com/straight-shoota/sanitize.git
version: 0.1.0+git.commit.c17d933fed22c6d3d615cca91a895166414389af
Expand Down
7 changes: 4 additions & 3 deletions shard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ dependencies:
placeos-models:
github: placeos/models

placeos:
github: placeos/crystal-client

placeos-log-backend:
github: place-labs/log-backend
version: ">= 0.8.13"
Expand All @@ -26,6 +23,10 @@ dependencies:
promise:
github: spider-gazelle/promise

# Redis client used to publish signals (supports clustered redis)
redis-cluster:
github: place-labs/redis-cluster.cr

secrets-env:
github: place-labs/secrets-env

Expand Down
28 changes: 0 additions & 28 deletions spec/controllers/booking_approval_permissions_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,6 @@ describe Bookings do
owner_headers = Mock::Headers.office365_normal_user(email: "owner@example.com")
other_headers = Mock::Headers.office365_normal_user(email: "other@example.com")

# saving a booking spawns signals to the placeos engine, stub the outbound calls
stub_engine = -> do
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/auth/oauth/token")
.to_return(body: File.read("./spec/fixtures/tokens/placeos_token.json"))
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/api/engine/v2/signal?channel=staff/booking/changed")
.to_return(body: "")
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/api/engine/v2/signal?channel=staff/booking/host_changed")
.to_return(body: "")
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/api/engine/v2/signal?channel=staff/guest/attending")
.to_return(body: "")
end

# an unapproved booking owned by owner@example.com, created by an admin
create_pending = -> do
status, body = BookingsHelper.http_create_booking(
Expand All @@ -61,7 +49,6 @@ describe Bookings do
describe "approval permissions" do
describe "#approve & #reject" do
it "forbids users without approval permissions" do
stub_engine.call
id = create_pending.call

client.post("#{BOOKINGS_BASE}/#{id}/approve", headers: owner_headers).status_code.should eq(403)
Expand All @@ -76,7 +63,6 @@ describe Bookings do
end

it "allows zone managers" do
stub_engine.call
id = create_pending.call

response = client.post("#{BOOKINGS_BASE}/#{id}/approve", headers: manager_headers)
Expand All @@ -97,7 +83,6 @@ describe Bookings do
end

it "allows admins" do
stub_engine.call
id = create_pending.call

response = client.post("#{BOOKINGS_BASE}/#{id}/approve", headers: admin_headers)
Expand All @@ -108,8 +93,6 @@ describe Bookings do

describe "#create" do
it "forbids creating an approved or rejected booking without permission" do
stub_engine.call

client.post(BOOKINGS_BASE, headers: owner_headers, body: approval_spec_booking_json(approved: true)).status_code.should eq(403)
client.post(BOOKINGS_BASE, headers: owner_headers, body: approval_spec_booking_json(rejected: true)).status_code.should eq(403)
client.post(BOOKINGS_BASE, headers: other_headers, body: approval_spec_booking_json(approved: true)).status_code.should eq(403)
Expand All @@ -118,8 +101,6 @@ describe Bookings do
end

it "allows users without permission to create pending bookings" do
stub_engine.call

response = client.post(BOOKINGS_BASE, headers: owner_headers, body: approval_spec_booking_json(approved: false, rejected: false))
response.status_code.should eq(201)
body = JSON.parse(response.body).as_h
Expand All @@ -129,8 +110,6 @@ describe Bookings do
end

it "allows zone managers to create approved or rejected bookings" do
stub_engine.call

response = client.post(BOOKINGS_BASE, headers: manager_headers, body: approval_spec_booking_json(approved: true))
response.status_code.should eq(201)
body = JSON.parse(response.body).as_h
Expand All @@ -149,8 +128,6 @@ describe Bookings do
end

it "allows admins to create approved bookings" do
stub_engine.call

response = client.post(BOOKINGS_BASE, headers: admin_headers, body: approval_spec_booking_json(approved: true))
response.status_code.should eq(201)
body = JSON.parse(response.body).as_h
Expand All @@ -161,7 +138,6 @@ describe Bookings do

describe "#update" do
it "forbids the booking owner changing the approval state" do
stub_engine.call
id = create_pending.call

client.patch("#{BOOKINGS_BASE}/#{id}", headers: owner_headers, body: {approved: true}.to_json).status_code.should eq(403)
Expand All @@ -177,7 +153,6 @@ describe Bookings do
end

it "allows the owner to echo back the current approval state" do
stub_engine.call
id = create_pending.call
client.post("#{BOOKINGS_BASE}/#{id}/approve", headers: manager_headers).status_code.should eq(200)

Expand All @@ -190,7 +165,6 @@ describe Bookings do
end

it "forbids the owner resetting an approved booking" do
stub_engine.call
id = create_pending.call
client.post("#{BOOKINGS_BASE}/#{id}/approve", headers: manager_headers).status_code.should eq(200)

Expand All @@ -199,7 +173,6 @@ describe Bookings do
end

it "allows zone managers to approve, reject and reset via update" do
stub_engine.call
id = create_pending.call

response = client.patch("#{BOOKINGS_BASE}/#{id}", headers: manager_headers, body: {approved: true}.to_json)
Expand Down Expand Up @@ -229,7 +202,6 @@ describe Bookings do
end

it "keeps an explicit approval when a manager also changes the booking time" do
stub_engine.call
id = create_pending.call

response = client.patch("#{BOOKINGS_BASE}/#{id}", headers: manager_headers, body: {
Expand Down
27 changes: 0 additions & 27 deletions spec/controllers/bookings_clash_e2e_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,6 @@ describe Bookings do
Guest.truncate
end

# the create action fans out signals to the engine -- stub them.
#
# NOTE:: these must NOT go in the `Spec.before_each` above. `Spec.before_each`
# is GLOBAL: it runs before every example in the whole suite, so registering
# stubs there shadows stubs other spec files register inside their own
# examples -- `WebMock::StubRegistry#find_stub` returns the FIRST match, and
# these would always be registered first. That silently broke the
# "signal payloads" specs in bookings_spec.cr, whose capturing stub for the
# same channel never matched.
stub_engine = -> do
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/auth/oauth/token")
.to_return(body: File.read("./spec/fixtures/tokens/placeos_token.json"))
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/api/engine/v2/signal?channel=staff/booking/changed").to_return(body: "")
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/api/engine/v2/signal?channel=staff/booking/host_changed").to_return(body: "")
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/api/engine/v2/signal?channel=staff/guest/attending").to_return(body: "")
end

# a fixed future anchor so the bookings are always valid (not in the past)
base = Time.utc(2026, 7, 6, 0, 0, 0)
rec_end = base.shift(days: 10).to_unix
Expand All @@ -39,7 +22,6 @@ describe Bookings do

describe "recurring vs a single-occurrence booking" do
it "rejects a single booking that clashes with one occurrence of a recurring booking" do
stub_engine.call
created = BookingsHelper.http_create_booking(
asset_id: "desk-r1", booking_start: at.call(0, 10, 0), booking_end: at.call(0, 11, 0),
recurrence_type: "DAILY", recurrence_end: rec_end, timezone: "UTC")
Expand All @@ -52,7 +34,6 @@ describe Bookings do
end

it "allows a single booking on the recurring asset at a non-overlapping time" do
stub_engine.call
created = BookingsHelper.http_create_booking(
asset_id: "desk-r2", booking_start: at.call(0, 10, 0), booking_end: at.call(0, 11, 0),
recurrence_type: "DAILY", recurrence_end: rec_end, timezone: "UTC")
Expand All @@ -65,7 +46,6 @@ describe Bookings do
end

it "allows a single booking on a different asset at the same time as an occurrence" do
stub_engine.call
created = BookingsHelper.http_create_booking(
asset_id: "desk-r3", booking_start: at.call(0, 10, 0), booking_end: at.call(0, 11, 0),
recurrence_type: "DAILY", recurrence_end: rec_end, timezone: "UTC")
Expand All @@ -79,7 +59,6 @@ describe Bookings do

describe "interleaved recurring bookings" do
it "rejects two interleaved recurring bookings on the same asset (partial time overlap)" do
stub_engine.call
first = BookingsHelper.http_create_booking(
asset_id: "desk-i1", booking_start: at.call(0, 10, 0), booking_end: at.call(0, 12, 0),
recurrence_type: "DAILY", recurrence_end: rec_end, timezone: "UTC")
Expand All @@ -93,7 +72,6 @@ describe Bookings do
end

it "allows two adjacent recurring bookings on the same asset (no overlap)" do
stub_engine.call
first = BookingsHelper.http_create_booking(
asset_id: "desk-i2", booking_start: at.call(0, 10, 0), booking_end: at.call(0, 11, 0),
recurrence_type: "DAILY", recurrence_end: rec_end, timezone: "UTC")
Expand All @@ -107,7 +85,6 @@ describe Bookings do
end

it "allows two recurring bookings on the same asset that fall on different weekdays" do
stub_engine.call
monday = base
until monday.day_of_week.monday?
monday = monday.shift(days: 1)
Expand Down Expand Up @@ -136,7 +113,6 @@ describe Bookings do
perth = Time::Location.load("Australia/Perth") # UTC+8

it "rejects a second overlapping all-day recurring booking that wraps UTC midnight" do
stub_engine.call
# Perth-local 00:00 -> 23:59 wraps UTC midnight (16:00 prev day -> 15:59);
# this is the row whose inverted time-of-day used to be dropped from the
# candidate set, letting a duplicate through.
Expand All @@ -159,7 +135,6 @@ describe Bookings do
end

it "allows the second all-day recurring booking on a different desk" do
stub_engine.call
b1_start = Time.local(2026, 7, 6, 0, 0, 0, location: perth)
b1_end = Time.local(2026, 7, 6, 23, 59, 0, location: perth)
perth_rec_end = b1_start.shift(days: 7).to_unix
Expand All @@ -180,7 +155,6 @@ describe Bookings do

describe "interleaved single bookings" do
it "rejects a partially overlapping single booking on the same asset" do
stub_engine.call
first = BookingsHelper.http_create_booking(
asset_id: "desk-s1", booking_start: at.call(0, 10, 0), booking_end: at.call(0, 12, 0))
status.call(first).should eq 201
Expand All @@ -191,7 +165,6 @@ describe Bookings do
end

it "allows an adjacent single booking on the same asset" do
stub_engine.call
first = BookingsHelper.http_create_booking(
asset_id: "desk-s2", booking_start: at.call(0, 10, 0), booking_end: at.call(0, 11, 0))
status.call(first).should eq 201
Expand Down
Loading
Loading