feat(mobile): add local QR scanning and desktop pairing invitations

This commit is contained in:
yilsem committed 2026-09-08 21:28:24 +03:00
1 parent c1929957a2
commit 4115f1b4c0
17 files changed
+178 -17

No files matched your search

+3
View File
@@ -1,10 +1,13 @@
import { completeIce, rtcConfiguration, sendFrame } from './mobile/transport.mjs'
import { drawQr } from './mobile/qr.mjs'
const host = window.mobileHost, $ = id => document.getElementById(id), peers = new Map()
const status = text => { $('status').textContent = text }
function action(id, work) { $(id).onclick = async () => { $(id).disabled = true; try { await work() } catch (error) { status(error.message) } finally { $(id).disabled = false } } }
action('login-start', async () => { const result = await host.call('account.login.start'); $('login-code').textContent = `Açılan hesap sayfasında yalnız bu kodu onaylayın: ${result.code}` })
action('login-check', async () => { const result = await host.call('account.login.check'); status(result.approved ? 'Hesaba giriş yapıldı. Bu bilgisayarı eşleştirebilirsiniz.' : 'Tarayıcıdaki kod onayı bekleniyor.') })
action('logout', async () => { await host.call('account.logout'); listening = false; $('login-code').textContent = ''; status('Mobil oturum kapatıldı; hesap ve Google anahtarı bu bilgisayar kasasından temizlendi.') })
action('invite', async () => { const invite = await host.call('invite.create'); drawQr($('invite-qr'), JSON.stringify(invite)); $('invite-qr').hidden = false; $('invite-status').textContent = `Telefonda aynı hesaba giriş yapıp bu QR kodunu tarayın. Son geçerlilik: ${invite.expiresAt}` })
action('invite-check', async () => { const result = await host.call('invite.check'); if (!result.pending) { status('QR ile gelen telefon henüz yok.'); return }; $('pairing').value = result.pending.pairingId; status(`${result.pending.name} bulundu. Telefondaki kısa kodu girip “Bu telefonu onayla” seçin.`) })
action('save', async () => { const token = $('account').value; $('account').value = ''; await host.call('account.save', { token }); status('Hesap anahtarı Windows kasasına kaydedildi.') })
action('pair', async () => { const pair = await host.call('desktop.pair'); $('code').textContent = `Açılan hesap sayfasında bu kodu onaylayın: ${pair.code}. Süre: ${pair.expiresAt}` })
action('approve', async () => { await host.call('phone.approve', { pairingId: $('pairing').value.trim(), code: $('phone-code').value.trim().toUpperCase() }); $('phone-code').value = ''; status('Telefon bu bilgisayarda onaylandı.') })
+2
View File
@@ -11,6 +11,8 @@
<label>AITURK hesap API anahtarı<input id="account" type="password" autocomplete="off"></label>
<button id="save">Hesap anahtarını kaydet</button></details><button id="pair">Bu bilgisayarı hesabımla eşleştir</button>
<p id="code"></p><hr>
<button id="invite">Telefon için QR oluştur</button><canvas id="invite-qr" hidden aria-label="Süreli telefon eşleştirme QR kodu"></canvas>
<p id="invite-status"></p><button id="invite-check">QR ile gelen telefonu getir</button>
<h2>Telefona izin ver</h2><p>Telefondaki eşleştirme kimliğini ve 8 karakterli kodu buraya girin. Bu onay yalnız bu telefon ile bu bilgisayarı bağlar.</p>
<label>Eşleştirme kimliği<input id="pairing"></label><label>Telefondaki kod<input id="phone-code" maxlength="8" autocapitalize="characters"></label>
<button id="approve">Bu telefonu onayla</button><button id="listen">Mobil bağlantıyı aç</button>
+3
View File
@@ -44,6 +44,7 @@ export function createMobileWindow({ directory, assets, resolveBackend, show = t
}
if (action === 'account.login.check') {
const previous = await vault.load()
if (previous.accountToken || clients.size) throw new Error('Mevcut hesap veya bağlantı sessizce değiştirilemez.')
if (!previous.pendingLogin) throw new Error('Önce giriş başlatın.')
const result = await new AccountClient({ accountToken: previous.pendingLogin.deviceCode }).call('/v1/aiturk/mobile-auth/poll', {})
if (result.state !== 'approved') return { approved: false }
@@ -70,6 +71,8 @@ export function createMobileWindow({ directory, assets, resolveBackend, show = t
return call(`/v1/aiturk/pairings/${params.pairingId}/approve`, { user_code: params.code })
}
if (action === 'devices') return call('/v1/aiturk/mobile/devices')
if (action === 'invite.create') return call('/v1/aiturk/mobile/invites', {})
if (action === 'invite.check') return call('/v1/aiturk/mobile/invites')
if (action === 'device.revoke') {
if (!/^[0-9a-f-]{36}$/.test(params.deviceId) || !Number.isInteger(params.version)) throw new Error('Geçersiz cihaz.')
const result = await call(`/v1/aiturk/devices/${params.deviceId}/revoke`, { expected_version: params.version })
@@ -67,6 +67,8 @@ for (const name of ['mobile-host.html', 'mobile-host-ui.mjs', 'mobile-preload.cj
cpSync(resolve(root, 'electron', name), resolve(distDir, name))
}
mkdirSync(resolve(distDir, 'mobile'), { recursive: true })
for (const name of ['transport.mjs', 'style.css']) {
for (const name of ['transport.mjs', 'style.css', 'qr.mjs']) {
cpSync(resolve(root, '../mobile/src', name), resolve(distDir, 'mobile', name))
}
mkdirSync(resolve(distDir, 'mobile/vendor'), { recursive: true })
cpSync(resolve(root, '../mobile/node_modules/qrcode-generator/dist/qrcode.mjs'), resolve(distDir, 'mobile/vendor/qrcode.mjs'))