iOS: the player goes to ipxapi.sdf1.net with one API token; unread badge (#419, #421)

The Access service token plus API token pair was three values to enter
and Access in front of both. The player, the badge and CarPlay now send
the ipx API token alone to https://ipxapi.sdf1.net: a route of the
rays-unraid tunnel to the same ipx-web, with no Access application, held
to the paths the app uses by a custom firewall rule on the sdf1.net zone
(403 for anything without a bearer header or off the list: the page, the
login form, /api/login, ?token= sign-in, the admin API). docs/sso.md
says so, since it also says not to bypass Access for /api/*; this is
that warning's narrow version, and the rule is on Cloudflare, not here.

The unread badge (#421) shows All Subscriptions' count, as the page's
badge does: set when the app comes forward, after an item is read, and
by a background refresh iOS runs now and then. The test target had no
Info.plist and could never have run; it does now.

Build 4. Not yet run on a phone with the token set.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Ray Slakinski
2026-11-08 14:11:03 -05:00
parent 7d4456bb2f
commit 3626a30b52
14 changed files with 234 additions and 64 deletions

View File

@@ -63,7 +63,7 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg
f.clearButtonMode = .whileEditing
}
if Server.url != nil { alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) }
alert.addAction(UIAlertAction(title: "Player Tokens…", style: .default) { [weak self] _ in
alert.addAction(UIAlertAction(title: "Player Token…", style: .default) { [weak self] _ in
self?.askForCredentials()
})
alert.addAction(UIAlertAction(title: "Open", style: .default) { [weak self, weak alert] _ in
@@ -81,36 +81,41 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg
present(alert, animated: true)
}
/// The tokens the player and CarPlay use in place of the page's cookie (#419). Left as they
/// are when a field is empty; all three emptied clears them.
private func askForCredentials() {
/// The token the player and CarPlay use in place of the page's cookie, and the address they
/// send it to (#419). An empty token field keeps the one there is; Clear removes it.
private func askForCredentials(_ problem: String? = nil) {
guard presentedViewController == nil else { return }
let alert = UIAlertController(
title: "Player tokens",
message: "An ipx API token (Settings, API tokens) and a Cloudflare Access service token, "
+ "so playback and CarPlay keep working when the page's sign-in ends. "
+ (Credentials.isSet ? "Set now; leave a field empty to keep it." : "Not set."),
title: "Player token",
message: problem ?? "An ipx API token (Settings, API tokens) so playback and CarPlay keep "
+ "working when the page's sign-in ends. "
+ (Credentials.isSet ? "Set now; leave the token empty to keep it." : "Not set."),
preferredStyle: .alert)
let fields: [(String, Credentials.Field)] = [
("ipx API token", .apiToken), ("Access client ID", .accessId),
("Access client secret", .accessSecret),
]
for (placeholder, _) in fields {
alert.addTextField { f in
f.placeholder = placeholder
f.isSecureTextEntry = true
f.autocapitalizationType = .none
f.autocorrectionType = .no
}
alert.addTextField { f in
f.placeholder = "ipx API token"
f.isSecureTextEntry = true
f.autocapitalizationType = .none
f.autocorrectionType = .no
}
alert.addTextField { f in
f.text = Server.apiURL?.absoluteString ?? Server.suggestedAPI
f.keyboardType = .URL
f.autocapitalizationType = .none
f.autocorrectionType = .no
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Clear", style: .destructive) { _ in
Credentials.Field.allCases.forEach { Credentials[$0] = nil }
Credentials.apiToken = nil
})
alert.addAction(UIAlertAction(title: "Save", style: .default) { [weak alert] _ in
for (i, (_, field)) in fields.enumerated() {
if let v = alert?.textFields?[i].text, !v.isEmpty { Credentials[field] = v }
alert.addAction(UIAlertAction(title: "Save", style: .default) { [weak self, weak alert] _ in
let fields = alert?.textFields ?? []
guard fields.count == 2, let url = Server.normalise(fields[1].text ?? ""),
url.scheme == "https" else {
self?.askForCredentials("The player address has to be https, such as \(Server.suggestedAPI).")
return
}
Server.apiURL = url
if let token = fields[0].text, !token.isEmpty { Credentials.apiToken = token }
})
present(alert, animated: true)
}