feat: mod platform integration (showcase, cloud slots, cloud screen, server-side unlink)

Mod:
- %config slots/pull/publish/unpublish for the four cloud slots
- %showcase [new|popular] [page] | load | copy, with number references
- %cloud opens a CloudScreen (link/unlink, slots, showcase) on vanilla widgets
- %config load IDDQD works offline (everything off except ChinaHat)
- default backend URL is now the production gateway
- shared ConfigRemoteApplier and ClientThread replace per-class copies
- %link unlink revokes the link on the server, then clears the local token

Backend:
- POST /device/revoke (RFC 7009 style self-revoke, always 204), rate limited
  to 10/min per IP at the gateway

CloudScreen is compiled but has not been opened in a running client yet.
This commit is contained in:
loki5512344 2026-10-02 09:23:44 +02:00
parent e00ea8eb11
commit 44353e893c
Signed by: boba
GPG key ID: 253067914055423B
24 changed files with 894 additions and 52 deletions

View file

@ -99,3 +99,19 @@ pub async fn revoke_link(
Err(AppError::NotFound("no such device link".into()))
}
}
#[derive(Deserialize)]
pub struct RevokeRequest {
pub device_token: String,
}
/// Self-revoke for the mod's `%link unlink`: the gateway strips the bearer
/// header before forwarding, so the token travels in the body. Always 204 so
/// the endpoint is not an oracle for which tokens exist.
pub async fn revoke_current(
State(state): State<DeviceState>,
AppJson(req): AppJson<RevokeRequest>,
) -> Result<StatusCode, AppError> {
links::revoke_by_token(&state.pool, &req.device_token).await?;
Ok(StatusCode::NO_CONTENT)
}

View file

@ -52,3 +52,13 @@ pub async fn revoke(pool: &PgPool, account_id: Uuid, link_id: Uuid) -> Result<bo
.await?;
Ok(result.rows_affected() == 1)
}
/// Deletes the link a device token belongs to (RFC 7009-style self-revoke:
/// holding the token is the proof). `false` when the token is unknown.
pub async fn revoke_by_token(pool: &PgPool, token: &str) -> Result<bool, sqlx::Error> {
let result = sqlx::query("DELETE FROM device_links WHERE device_token_hash = $1")
.bind(hash_token(token))
.execute(pool)
.await?;
Ok(result.rows_affected() == 1)
}

View file

@ -49,6 +49,7 @@ pub fn build_app(pool: sqlx::PgPool, cfg: &Config) -> Router {
.route("/device/code", post(device::handlers::create_code))
.route("/device/confirm", post(device::handlers::confirm))
.route("/device/token", post(device::handlers::token))
.route("/device/revoke", post(device::handlers::revoke_current))
.route("/device/links", get(device::handlers::list_links))
.route(
"/device/links/{id}",