Spaces:
Paused
Paused
Upload main.rs
Browse files- src/main.rs +27 -9
src/main.rs
CHANGED
|
@@ -1353,21 +1353,23 @@ async fn pair_page(Host(host): Host, headers: HeaderMap, Query(q): Query<PairPag
|
|
| 1353 |
let safe_code = htmlesc(&code);
|
| 1354 |
let scheme = forwarded_proto(&headers);
|
| 1355 |
let app_id = deezer_app_id();
|
| 1356 |
-
let redirect_url = format!("{scheme}://{host}/pair/oauth_cb
|
| 1357 |
let oauth_url = format!(
|
| 1358 |
-
"https://connect.deezer.com/oauth/auth.php?app_id={}&redirect_uri={}&perms={}&response_type=token",
|
| 1359 |
urlenc(&app_id),
|
| 1360 |
urlenc(&redirect_url),
|
| 1361 |
-
urlenc("basic_access,email,manage_library,listening_history")
|
|
|
|
| 1362 |
);
|
| 1363 |
|
| 1364 |
let code_js = serde_json::to_string(&code).unwrap_or_else(|_| "\"\"".to_string());
|
| 1365 |
let oauth_url_js = serde_json::to_string(&oauth_url).unwrap_or_else(|_| "\"\"".to_string());
|
|
|
|
| 1366 |
|
| 1367 |
let mut html = String::with_capacity(6000);
|
| 1368 |
html.push_str(r#"<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">"#);
|
| 1369 |
html.push_str(r#"<title>Deezer Login</title>"#);
|
| 1370 |
-
html.push_str(r#"<style>body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;background:#0b0b0b;color:#fff;margin:0;padding:20px}.box{max-width:420px;margin:0 auto;background:#151515;padding:18px;border-radius:12px}button{width:100%;padding:12px 14px;border-radius:10px;border:0;background:#a238ff;color:#fff;font-weight:800;font-size:16px}.muted{color:#aaa;font-size:13px;line-height:1.35}.ok{color:#1db954}.err{color:#ff5a5a}a{color:#a238ff}</style>"#);
|
| 1371 |
html.push_str(r#"</head><body><div class="box">"#);
|
| 1372 |
html.push_str(r#"<h2 style="margin:0 0 6px">Deezer: вход для ТВ</h2>"#);
|
| 1373 |
html.push_str(r#"<div class="muted">Код пары: <b>"#);
|
|
@@ -1376,13 +1378,23 @@ async fn pair_page(Host(host): Host, headers: HeaderMap, Query(q): Query<PairPag
|
|
| 1376 |
html.push_str(r#"<div class="muted" style="margin:10px 0 14px">Нажми кнопку, войди в Deezer и подтверди доступ. После успеха вернись на телевизор.</div>"#);
|
| 1377 |
html.push_str(r#"<button id="dz-login" type="button">Войти через Deezer</button>"#);
|
| 1378 |
html.push_str(r#"<div id="msg" class="muted" style="margin:12px 0 0"></div>"#);
|
| 1379 |
-
html.push_str(r#"<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1380 |
html.push_str(r#"<script>(function(){"#);
|
| 1381 |
html.push_str("var code=");
|
| 1382 |
html.push_str(&code_js);
|
| 1383 |
html.push_str(r#";var oauthUrl="#);
|
| 1384 |
html.push_str(&oauth_url_js);
|
| 1385 |
-
html.push_str(r#";var msg=document.getElementById('msg');function setMsg(cls,text){msg.className=cls?(cls+' muted'):'muted';msg.textContent=text;}if(!code){setMsg('err','Нет кода пары. Открой ссылку заново с QR.');return;}document.getElementById('dz-login').addEventListener('click',function(){setMsg('','Открываю Deezer…');if(!oauthUrl){setMsg('err','Не удалось собрать Deezer OAuth URL');return;}window.location.href=oauthUrl;});})();</script>"#);
|
| 1386 |
html.push_str(r#"<p class="muted" style="margin:14px 0 0">Если Deezer пишет про неверный redirect/domain — нужно указать свой <b>DEEZER_APP_ID</b> для домена этого сервера.</p>"#);
|
| 1387 |
html.push_str(r#"</div></body></html>"#);
|
| 1388 |
Html(html)
|
|
@@ -1391,10 +1403,16 @@ async fn pair_page(Host(host): Host, headers: HeaderMap, Query(q): Query<PairPag
|
|
| 1391 |
#[derive(Deserialize)]
|
| 1392 |
struct PairOauthCbQuery {
|
| 1393 |
code: Option<String>,
|
|
|
|
| 1394 |
}
|
| 1395 |
|
| 1396 |
async fn pair_oauth_cb(Query(q): Query<PairOauthCbQuery>) -> impl IntoResponse {
|
| 1397 |
-
let code = q
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1398 |
let safe_code = htmlesc(&code);
|
| 1399 |
let code_js = serde_json::to_string(&code).unwrap_or_else(|_| "\"\"".to_string());
|
| 1400 |
|
|
@@ -1409,9 +1427,9 @@ async fn pair_oauth_cb(Query(q): Query<PairOauthCbQuery>) -> impl IntoResponse {
|
|
| 1409 |
html.push_str(r#"</b></div>"#);
|
| 1410 |
html.push_str(r#"<div id="msg" class="muted" style="margin:12px 0 0">Получаю токен…</div>"#);
|
| 1411 |
html.push_str(r#"<script>(function(){"#);
|
| 1412 |
-
html.push_str("var
|
| 1413 |
html.push_str(&code_js);
|
| 1414 |
-
html.push_str(r#";var msg=document.getElementById('msg');function setMsg(cls,text){msg.className=cls?(cls+' muted'):'muted';msg.textContent=text;}function parseHash(){var h=window.location.hash||'';if(h.charAt(0)==='#')h=h.slice(1);var out={};h.split('&').forEach(function(p){if(!p)return;var i=p.indexOf('=');var k=i>=0?p.slice(0,i):p;var v=i>=0?p.slice(i+1):'';try{k=decodeURIComponent(k);}catch(e){}try{v=decodeURIComponent(v);}catch(e){}out[k]=v;});return out;}if(!code){setMsg('err','Нет кода пары. Вернись на телевизор и обнови QR/ссылку.');return;}var
|
| 1415 |
html.push_str(r#"</div></body></html>"#);
|
| 1416 |
(StatusCode::OK, Html(html)).into_response()
|
| 1417 |
}
|
|
|
|
| 1353 |
let safe_code = htmlesc(&code);
|
| 1354 |
let scheme = forwarded_proto(&headers);
|
| 1355 |
let app_id = deezer_app_id();
|
| 1356 |
+
let redirect_url = format!("{scheme}://{host}/pair/oauth_cb");
|
| 1357 |
let oauth_url = format!(
|
| 1358 |
+
"https://connect.deezer.com/oauth/auth.php?app_id={}&redirect_uri={}&perms={}&response_type=token&state={}",
|
| 1359 |
urlenc(&app_id),
|
| 1360 |
urlenc(&redirect_url),
|
| 1361 |
+
urlenc("basic_access,email,manage_library,listening_history"),
|
| 1362 |
+
urlenc(&code)
|
| 1363 |
);
|
| 1364 |
|
| 1365 |
let code_js = serde_json::to_string(&code).unwrap_or_else(|_| "\"\"".to_string());
|
| 1366 |
let oauth_url_js = serde_json::to_string(&oauth_url).unwrap_or_else(|_| "\"\"".to_string());
|
| 1367 |
+
let redirect_url_txt = htmlesc(&redirect_url);
|
| 1368 |
|
| 1369 |
let mut html = String::with_capacity(6000);
|
| 1370 |
html.push_str(r#"<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">"#);
|
| 1371 |
html.push_str(r#"<title>Deezer Login</title>"#);
|
| 1372 |
+
html.push_str(r#"<style>body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;background:#0b0b0b;color:#fff;margin:0;padding:20px}.box{max-width:420px;margin:0 auto;background:#151515;padding:18px;border-radius:12px}button{width:100%;padding:12px 14px;border-radius:10px;border:0;background:#a238ff;color:#fff;font-weight:800;font-size:16px}.muted{color:#aaa;font-size:13px;line-height:1.35}.ok{color:#1db954}.err{color:#ff5a5a}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;background:#101010;border-radius:8px;padding:10px;word-break:break-all}a{color:#a238ff}input{width:100%;box-sizing:border-box;padding:12px 12px;border-radius:10px;border:1px solid #2b2b2b;background:#0f0f0f;color:#fff;outline:none}input:focus{border-color:#a238ff}.row{margin:10px 0 0}</style>"#);
|
| 1373 |
html.push_str(r#"</head><body><div class="box">"#);
|
| 1374 |
html.push_str(r#"<h2 style="margin:0 0 6px">Deezer: вход для ТВ</h2>"#);
|
| 1375 |
html.push_str(r#"<div class="muted">Код пары: <b>"#);
|
|
|
|
| 1378 |
html.push_str(r#"<div class="muted" style="margin:10px 0 14px">Нажми кнопку, войди в Deezer и подтверди доступ. После успеха вернись на телевизор.</div>"#);
|
| 1379 |
html.push_str(r#"<button id="dz-login" type="button">Войти через Deezer</button>"#);
|
| 1380 |
html.push_str(r#"<div id="msg" class="muted" style="margin:12px 0 0"></div>"#);
|
| 1381 |
+
html.push_str(r#"<div class="muted" style="margin:12px 0 8px">Если OAuth не работает (redirect_uri) — используй вход по логину/паролю ниже.</div>"#);
|
| 1382 |
+
html.push_str(r#"<form method="POST" action="/pair/complete" autocomplete="on" style="margin:10px 0 0">"#);
|
| 1383 |
+
html.push_str(r#"<input type="hidden" name="code" value="" id="pair-code-hidden">"#);
|
| 1384 |
+
html.push_str(r#"<div class="row"><input name="email" type="email" placeholder="Email" autocomplete="username" required></div>"#);
|
| 1385 |
+
html.push_str(r#"<div class="row"><input name="password" type="password" placeholder="Пароль" autocomplete="current-password" required></div>"#);
|
| 1386 |
+
html.push_str(r#"<div class="row"><button type="submit">Войти по логину и паролю</button></div>"#);
|
| 1387 |
+
html.push_str(r#"</form>"#);
|
| 1388 |
+
html.push_str(r#"<div class="muted" style="margin:14px 0 8px">Redirect URI для Deezer приложения:</div>"#);
|
| 1389 |
+
html.push_str(r#"<div class="mono">"#);
|
| 1390 |
+
html.push_str(&redirect_url_txt);
|
| 1391 |
+
html.push_str(r#"</div>"#);
|
| 1392 |
html.push_str(r#"<script>(function(){"#);
|
| 1393 |
html.push_str("var code=");
|
| 1394 |
html.push_str(&code_js);
|
| 1395 |
html.push_str(r#";var oauthUrl="#);
|
| 1396 |
html.push_str(&oauth_url_js);
|
| 1397 |
+
html.push_str(r#";var msg=document.getElementById('msg');function setMsg(cls,text){msg.className=cls?(cls+' muted'):'muted';msg.textContent=text;}if(!code){setMsg('err','Нет кода пары. Открой ссылку заново с QR.');return;}var hidden=document.getElementById('pair-code-hidden');if(hidden){hidden.value=code;}document.getElementById('dz-login').addEventListener('click',function(){setMsg('','Открываю Deezer…');if(!oauthUrl){setMsg('err','Не удалось собрать Deezer OAuth URL');return;}window.location.href=oauthUrl;});})();</script>"#);
|
| 1398 |
html.push_str(r#"<p class="muted" style="margin:14px 0 0">Если Deezer пишет про неверный redirect/domain — нужно указать свой <b>DEEZER_APP_ID</b> для домена этого сервера.</p>"#);
|
| 1399 |
html.push_str(r#"</div></body></html>"#);
|
| 1400 |
Html(html)
|
|
|
|
| 1403 |
#[derive(Deserialize)]
|
| 1404 |
struct PairOauthCbQuery {
|
| 1405 |
code: Option<String>,
|
| 1406 |
+
state: Option<String>,
|
| 1407 |
}
|
| 1408 |
|
| 1409 |
async fn pair_oauth_cb(Query(q): Query<PairOauthCbQuery>) -> impl IntoResponse {
|
| 1410 |
+
let code = q
|
| 1411 |
+
.code
|
| 1412 |
+
.or(q.state)
|
| 1413 |
+
.unwrap_or_default()
|
| 1414 |
+
.trim()
|
| 1415 |
+
.to_string();
|
| 1416 |
let safe_code = htmlesc(&code);
|
| 1417 |
let code_js = serde_json::to_string(&code).unwrap_or_else(|_| "\"\"".to_string());
|
| 1418 |
|
|
|
|
| 1427 |
html.push_str(r#"</b></div>"#);
|
| 1428 |
html.push_str(r#"<div id="msg" class="muted" style="margin:12px 0 0">Получаю токен…</div>"#);
|
| 1429 |
html.push_str(r#"<script>(function(){"#);
|
| 1430 |
+
html.push_str("var codeFromQuery=");
|
| 1431 |
html.push_str(&code_js);
|
| 1432 |
+
html.push_str(r#";var msg=document.getElementById('msg');function setMsg(cls,text){msg.className=cls?(cls+' muted'):'muted';msg.textContent=text;}function parseHash(){var h=window.location.hash||'';if(h.charAt(0)==='#')h=h.slice(1);var out={};h.split('&').forEach(function(p){if(!p)return;var i=p.indexOf('=');var k=i>=0?p.slice(0,i):p;var v=i>=0?p.slice(i+1):'';try{k=decodeURIComponent(k);}catch(e){}try{v=decodeURIComponent(v);}catch(e){}out[k]=v;});return out;}var h=parseHash();var code=(codeFromQuery||h.state||'');if(!code){setMsg('err','Нет кода пары. Вернись на телевизор и обнови QR/ссылку.');return;}var at=h.access_token||'';if(!at){setMsg('err','Deezer не вернул access_token. Попробуй ещё раз.');return;}try{history.replaceState(null,'',window.location.pathname+window.location.search);}catch(e){}fetch('/pair/oauth',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({code:code,access_token:at})}).then(function(r){return r.json().catch(function(){return {};}).then(function(j){return {ok:r.ok,json:j};});}).then(function(r){if(r.ok&&!r.json.error){setMsg('ok','Готово ✓ Вернись на телевизор.');}else{setMsg('err','Ошибка сохранения: '+(r.json.error||'error'));}}).catch(function(){setMsg('err','Ошибка сети при сохранении');});})();</script>"#);
|
| 1433 |
html.push_str(r#"</div></body></html>"#);
|
| 1434 |
(StatusCode::OK, Html(html)).into_response()
|
| 1435 |
}
|