M12e: contract pass (pair invite kind, admin name in invite links, sync failed flag, rejection actor, device version name, template id, update check result, untouched accounts, D-027 re-stamp, PushEvent in :core:model, snackbar hosts)
Server: E-3 invite_kind; E-12 link ?from=<admin name>, E-4 passes it into kvitt://invite; X-Kvitt-App-Version-Name stored in devices.app_version_name (migration 0003, schema 3); OpenAPI and examples follow. Phone: :core:model, :core:fakes, :data, features and :app updated together; goldens re-recorded for S-20 (version name) and new S-1/S-22 states. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
//! 426 `protocol_unsupported`. It runs before the token check, so an outdated app always learns it must update.
|
||||
//! * [`AuthDevice`] is the extractor for authenticated handlers: `Authorization: Bearer <token>`, SHA-256 hex lookup
|
||||
//! of a device; missing, malformed or unknown is 401 `unauthorized`, a revoked device 401 `device_revoked`.
|
||||
//! `last_seen_at` and `app_version_code` are refreshed at most once per hour per device.
|
||||
//! `last_seen_at`, `app_version_code` and `app_version_name` are refreshed at most once per hour per device.
|
||||
//! * [`AdminDevice`] additionally requires the `admin` role (403 `forbidden`, SPEC §8.4).
|
||||
|
||||
use std::fmt::Write as _;
|
||||
@@ -30,12 +30,16 @@ pub const PROTOCOL_HEADER: &str = "x-kvitt-protocol";
|
||||
pub const PROTOCOL_VERSION: &str = "1";
|
||||
/// Header carrying the app's `versionCode`.
|
||||
pub const APP_VERSION_HEADER: &str = "x-kvitt-app-version";
|
||||
/// Header carrying the app's `versionName` (schema 3, shown on S-20).
|
||||
pub const APP_VERSION_NAME_HEADER: &str = "x-kvitt-app-version-name";
|
||||
/// Longest `versionName` stored (the `devices.app_version_name` CHECK of migration 0003).
|
||||
pub const APP_VERSION_NAME_MAX: usize = 32;
|
||||
/// Prefix of every device token (SPEC §8.2).
|
||||
pub const TOKEN_PREFIX: &str = "kvt_";
|
||||
/// Length of a token: `kvt_` plus 43 base64url characters (32 bytes, no padding).
|
||||
pub const TOKEN_LEN: usize = 47;
|
||||
|
||||
/// How often `last_seen_at` / `app_version_code` are written at most (SPEC §8.2).
|
||||
/// How often `last_seen_at` / `app_version_code` / `app_version_name` are written at most (SPEC §8.2).
|
||||
const TOUCH_INTERVAL: Duration = Duration::hours(1);
|
||||
|
||||
/// Lowercase hex SHA-256 of `text` (token and invite code hashes, SPEC §8.2, §8.3).
|
||||
@@ -103,6 +107,15 @@ fn app_version_code(headers: &HeaderMap) -> Option<i64> {
|
||||
.filter(|v| *v >= 0)
|
||||
}
|
||||
|
||||
/// The `X-Kvitt-App-Version-Name` value: trimmed, 1 to [`APP_VERSION_NAME_MAX`] characters; anything else is
|
||||
/// ignored (the stored value is kept), as an unparsable `X-Kvitt-App-Version` is.
|
||||
pub fn app_version_name(headers: &HeaderMap) -> Option<String> {
|
||||
let value = headers.get(APP_VERSION_NAME_HEADER)?.to_str().ok()?.trim();
|
||||
(1..=APP_VERSION_NAME_MAX)
|
||||
.contains(&value.chars().count())
|
||||
.then(|| value.to_owned())
|
||||
}
|
||||
|
||||
impl AuthDevice {
|
||||
/// Authenticates the request headers against the database.
|
||||
pub async fn authenticate(headers: &HeaderMap, state: &AppState) -> Result<Self, AppError> {
|
||||
@@ -141,11 +154,12 @@ impl AuthDevice {
|
||||
if due {
|
||||
let mut tx = crate::db::begin_immediate(&state.db).await?;
|
||||
sqlx::query(
|
||||
"UPDATE devices SET last_seen_at = ?, app_version_code = COALESCE(?, app_version_code) \
|
||||
WHERE id = ?",
|
||||
"UPDATE devices SET last_seen_at = ?, app_version_code = COALESCE(?, app_version_code), \
|
||||
app_version_name = COALESCE(?, app_version_name) WHERE id = ?",
|
||||
)
|
||||
.bind(format_ts(now))
|
||||
.bind(app_version_code(headers))
|
||||
.bind(app_version_name(headers))
|
||||
.bind(device_id.to_string())
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
Reference in New Issue
Block a user