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
10 changes: 8 additions & 2 deletions Sources/App/Controllers/AuthController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,16 @@ struct AuthController: RouteCollection {
case conflict
}

/// Redirects with `?link=<outcome>`, the query key/value contract `users-web`'s
/// `SettingsController`/`settings.leaf` actually reads (`req.query[String.self, at: "link"]`,
/// recognizing only "conflict", "error", and "success"). `.conflict` is the one reason with
/// its own banner copy; every other reason collapses to the generic "error" banner - `users-web`
/// never surfaces the specific denied/expired/unavailable distinction to the visitor.
private func linkErrorRedirect(_ req: Request, to returnTo: String, reason: LinkErrorReason)
-> Response
{
req.redirect(to: "\(returnTo)?link_error=\(reason.rawValue)")
let outcome = reason == .conflict ? "conflict" : "error"
return req.redirect(to: "\(returnTo)?link=\(outcome)")
}

/// Keyed by the link ticket itself (used as Auth0's `state`) rather than a separate locally
Expand Down Expand Up @@ -373,6 +379,6 @@ struct AuthController: RouteCollection {
return linkErrorRedirect(req, to: returnTo, reason: .unavailable)
}

return req.redirect(to: returnTo)
return req.redirect(to: "\(returnTo)?link=success")
}
}
20 changes: 20 additions & 0 deletions Tests/AppTests/AppTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,26 @@ struct LinkStartTests {
}
}

// MARK: - AuthController.linkCallback error redirects

@Test("linkCallback redirects with link=error, not the stale link_error= query key")
func linkCallbackErrorUsesLinkQueryKey() async throws {
try await withApp(configure: configure) { app in
// Auth0 itself returning an `error` param is the earliest branch in linkCallback - reached
// before any pending-link session lookup or network call, so this needs no fake Auth0/
// users-api server to exercise. Regression test for the contract users-web's
// SettingsController actually reads (`req.query[String.self, at: "link"]`) - a prior
// version redirected with `?link_error=<reason>` instead, which users-web silently ignored
// (no banner shown, confirmed on dev with a real conflict).
try await app.testing().test(.GET, "auth/link/callback?error=access_denied") { res in
#expect(res.status == .seeOther)
let location = res.headers.first(name: .location) ?? ""
#expect(location.contains("link=error"))
#expect(!location.contains("link_error="))
}
}
}

// MARK: - UsersAPIClient.linkComplete
//
// Exercised against a real, locally listening fake `users-api` rather than a mocked `Client`,
Expand Down
Loading