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:
parent
e00ea8eb11
commit
44353e893c
24 changed files with 894 additions and 52 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -114,3 +114,36 @@ async fn unknown_device_token_does_not_authenticate() {
|
|||
.unwrap();
|
||||
assert_eq!(r, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn self_revoke_by_token_deletes_only_that_link() {
|
||||
let pool = common::test_pool().await;
|
||||
let server = common::test_server(accounts_service::build_app(
|
||||
pool.clone(),
|
||||
&common::test_config(),
|
||||
));
|
||||
let (account, _) = common::register_account(&server).await;
|
||||
let t1 = link_device(&server, account).await;
|
||||
let t2 = link_device(&server, account).await;
|
||||
|
||||
server
|
||||
.post("/device/revoke")
|
||||
.json(&serde_json::json!({ "device_token": t1 }))
|
||||
.await
|
||||
.assert_status(StatusCode::NO_CONTENT);
|
||||
// Unknown tokens answer the same way: no oracle.
|
||||
server
|
||||
.post("/device/revoke")
|
||||
.json(&serde_json::json!({ "device_token": "lvd_nope" }))
|
||||
.await
|
||||
.assert_status(StatusCode::NO_CONTENT);
|
||||
|
||||
let gone = accounts_service::device::links::authenticate(&pool, &t1)
|
||||
.await
|
||||
.unwrap();
|
||||
let kept = accounts_service::device::links::authenticate(&pool, &t2)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(gone, None);
|
||||
assert_eq!(kept, Some(account));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ pub fn rules() -> Vec<Rule> {
|
|||
rule(Method::POST, "/device/code", Quota::per_minute(n(10)), Ip),
|
||||
// The mod polls every 2–3 s for up to 10 min: 10/min would break linking.
|
||||
rule(Method::POST, "/device/token", Quota::per_minute(n(30)), Ip),
|
||||
rule(Method::POST, "/device/revoke", Quota::per_minute(n(10)), Ip),
|
||||
rule(Method::PUT, "/configs/*", Quota::per_minute(n(20)), Account),
|
||||
rule(
|
||||
Method::GET,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue