1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
require "pro_tacts/admin/device_setup"
require "pro_tacts/profile"
module ProTacts
class Web < Roda
# The configuration profile, served so the device reading this can
# provision itself, and the only renderer of it: rake
# profile:install downloads from here. The screen and the document
# are separate paths because a
# mobileconfig is not a page — the screen says what the download
# will do, and only the link fires the install flow.
#
# The hostname is the request's rather than PRO_TACTS_HOSTNAME:
# the address a device reached this app at is reachable from that
# device by construction, where an environment variable is a claim
# about some other machine's idea of where we live. It is the
# client's Host header, which the proxy in front passes along
# (ProxyAuth) — and past the identity gate in web.rb's trunk, the only
# devices asking are on the tailnet.
hash_branch("setup") do |r|
r.is do
r.get do
response["Content-Type"] = "text/html; charset=utf-8"
Admin::DeviceSetup.call(hostname: r.host, login: @login, name: ProTacts.config.instance_name)
end
end
# The media type is what starts the install: a device offers to
# install a profile because of what the response is, not what the
# path is called. The path carries .mobileconfig anyway, so a
# download that does land in a file system lands under the name
# the rake task writes. No Content-Disposition — an attachment
# disposition on this type is untested here, and the install flow
# is the point.
r.get "carddav.mobileconfig" do
response["Content-Type"] = "application/x-apple-aspen-config"
Profile.render(hostname: r.host, username: @login)
end
# Names the requester's own book, `rake book:name` for whoever is
# asking (docs/plans/2026-09-16-book-names.md); no name goes back
# to the login. Plain text, for curl rather than a screen.
r.post "book" do
response["Content-Type"] = "text/plain"
begin
store.name_book(@login, typecast_params.str("name"))
"#{@login} syncs #{Group::SYNC_PREFIX}#{store.book_name(@login)}\n"
rescue Sequel::UniqueConstraintViolation, Store::EveryonesBookName
# Another login's name, a group's already, or everyone's.
response.status = 409
"#{Group::SYNC_PREFIX}#{typecast_params.str('name', '').strip} is taken; #{@login}'s book is unchanged.\n"
end
end
end
end
end