Platform-neutral successor to Discord Authenticator, forked from its v0.1.2
state with fresh history. Same vault, same UI, same live-message mechanism —
now behind one `Sink` that speaks three platforms.
Why each platform needs what it needs (this drove the whole settings model):
- Discord: an incoming-webhook URL. Webhooks can edit their own messages.
- Slack: a bot token (xoxb-, scope chat:write) + channel ID. Slack incoming
webhooks can post but never edit, and editing IS the live mechanism, so
chat.postMessage / chat.update via a bot is the only path.
- Telegram: a bot token + chat id. No webhook concept exists. The app detects
the chat id itself from the bot's pending updates ("send /start, then
Detect"), since users rarely know it.
- core: Provider enum; settings hold all three configs so switching back and
forth never re-asks for tokens; per-platform shape validation that names the
common mistakes (xoxp- user token, #name instead of channel ID); a
destination key that clears message ids on platform/channel change but not
on token rotation
- delivery: shared DeliveryError + pacing Governor; Discord client refactored
onto them; new Slack client (classifies Slack's ok:false-on-HTTP-200
errors) and Telegram client ("message is not modified" is success; chat
detection via getUpdates); provider-agnostic message builders — code in a
copyable span everywhere, rollover time as live as each platform allows
- http-api: setup accepts an optional provider; settings PUT/GET carry the
full model; /api/slack/whoami, /api/telegram/whoami, /api/telegram/chats
- webui: three-way optional picker on setup and in Settings → Delivery with
guided per-platform fields; "Check token" (Slack) and "Detect chat"
(Telegram); inline white mark via currentColor
- brand: white mark on the dark plate; blue stays with the Discord sibling
- docs/TEAMS.md: exploration for v0.2 — Teams connectors were disabled May
2026 and Workflows webhooks can't edit, so live tiles need a Bot Framework
bot; options for the inbound endpoint on a homelab and a recommended scope
Verified against mock Discord, Slack and Telegram servers speaking each
platform's real protocol shape: edits target the same message rather than
re-posting, deleted tiles are recovered, dead credentials are surfaced with a
next step, renames refresh without a rollover, and switching platform reposts
on the new one and leaves the old one alone. 138 tests.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
54 lines
1.8 KiB
Rust
54 lines
1.8 KiB
Rust
//! HTTP surface: the JSON API, the embedded UI, and the auth gate.
|
|
#![forbid(unsafe_code)]
|
|
|
|
pub mod app;
|
|
pub mod auth;
|
|
pub mod qr;
|
|
pub mod state;
|
|
|
|
pub use app::router;
|
|
pub use state::AppState;
|
|
|
|
use axum::{
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
Json,
|
|
};
|
|
|
|
/// Map a domain error onto an HTTP response.
|
|
///
|
|
/// The message is written to be shown verbatim in a toast, so this keeps
|
|
/// whatever `openmfa-core` said rather than substituting a generic one —
|
|
/// with the exception of crypto failures, whose detail goes to the log
|
|
/// instead of the browser.
|
|
pub fn error_response(e: &openmfa_core::Error) -> Response {
|
|
use openmfa_core::Error;
|
|
let (status, message) = match e {
|
|
Error::Invalid(m) => (StatusCode::BAD_REQUEST, m.clone()),
|
|
Error::NotFound(_) => (StatusCode::NOT_FOUND, e.to_string()),
|
|
Error::Conflict(m) => (StatusCode::CONFLICT, m.clone()),
|
|
Error::Crypto(detail) => {
|
|
tracing::error!(target: "openmfa::http", "cryptography error: {detail}");
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"the vault could not be read — check the server log".to_string(),
|
|
)
|
|
}
|
|
Error::Io(detail) => {
|
|
tracing::error!(target: "openmfa::http", "i/o error: {detail}");
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"could not write to disk — check the server log".to_string(),
|
|
)
|
|
}
|
|
other => {
|
|
tracing::error!(target: "openmfa::http", "unexpected error: {other}");
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"something went wrong — check the server log".to_string(),
|
|
)
|
|
}
|
|
};
|
|
(status, Json(serde_json::json!({ "error": message }))).into_response()
|
|
}
|