feat(mobile): connect Android and IDE to browser account login

This commit is contained in:
yilsem committed 2026-09-08 21:10:49 +03:00
1 parent b66daed07a
commit c1929957a2
8 files changed
+109 -8

No files matched your search

+3
View File
@@ -2,6 +2,9 @@ import { completeIce, rtcConfiguration, sendFrame } from './mobile/transport.mjs
const host = window.mobileHost, $ = id => document.getElementById(id), peers = new Map() const host = window.mobileHost, $ = id => document.getElementById(id), peers = new Map()
const status = text => { $('status').textContent = text } 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 } } } 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('save', async () => { const token = $('account').value; $('account').value = ''; await host.call('account.save', { token }); status('Hesap anahtarı Windows kasasına kaydedildi.') }) 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('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ı.') }) 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ı.') })
+5 -1
View File
@@ -4,8 +4,12 @@
<p class="eyebrow">AITURK · BİLGİSAYAR</p><h1>Telefonunu bağla.</h1> <p class="eyebrow">AITURK · BİLGİSAYAR</p><h1>Telefonunu bağla.</h1>
<p>Bu pencere açıkken eşleştirdiğiniz Android telefon seçtiğiniz IDE konuşmasına erişebilir. Google anahtarı Windows güvenli kasasında tutulur.</p> <p>Bu pencere açıkken eşleştirdiğiniz Android telefon seçtiğiniz IDE konuşmasına erişebilir. Google anahtarı Windows güvenli kasasında tutulur.</p>
<p id="status" role="status" aria-live="polite">Bağlantı kapalı.</p> <p id="status" role="status" aria-live="polite">Bağlantı kapalı.</p>
<button id="login-start">AITURK hesabımla giriş yap</button><p id="login-code"></p><button id="login-check">Girişi kontrol et</button>
<p>Çıkış, mobil bağlantıları kapatır ve bu penceredeki hesap/Google anahtarını Windows kasasından temizler; başlamış IDE işini durdurmaz.</p>
<button id="logout">Mobil oturumdan çık ve kasayı temizle</button>
<details><summary>Gelişmiş: hesap API anahtarı</summary>
<label>AITURK hesap API anahtarı<input id="account" type="password" autocomplete="off"></label> <label>AITURK hesap API anahtarı<input id="account" type="password" autocomplete="off"></label>
<button id="save">Hesap anahtarını kaydet</button><button id="pair">Bu bilgisayarı hesabımla eşleştir</button> <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> <p id="code"></p><hr>
<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> <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> <label>Eşleştirme kimliği<input id="pairing"></label><label>Telefondaki kod<input id="phone-code" maxlength="8" autocapitalize="characters"></label>
+25
View File
@@ -33,6 +33,31 @@ export function createMobileWindow({ directory, assets, resolveBackend, show = t
await vault.save({ ...previous, accountToken: params.token }) await vault.save({ ...previous, accountToken: params.token })
return { saved: true } return { saved: true }
} }
if (action === 'account.login.start') {
const previous = await vault.load()
if (previous.accountToken || clients.size) throw new Error('Kayıtlı hesap var. Önce mevcut cihazı iptal edin.')
const login = await new AccountClient({}).call('/v1/aiturk/mobile-auth/start', {})
if (!/^https:\/\/turkservis\.online\/account\/aiturk\/mobile-login\/[0-9a-f-]{36}$/.test(login.verificationUri)) throw new Error('Geçersiz giriş adresi.')
await vault.save({ ...previous, pendingLogin: login })
await shell.openExternal(login.verificationUri)
return { code: login.userCode }
}
if (action === 'account.login.check') {
const previous = await vault.load()
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 }
delete previous.pendingLogin
await vault.save({ ...previous, accountToken: result.accountToken })
return { approved: true }
}
if (action === 'account.logout') {
const previous = await vault.load(), token = previous.accountToken || previous.pendingLogin?.deviceCode
if (token?.startsWith('tsml_')) await new AccountClient({ accountToken: token }).call('/v1/aiturk/mobile-auth/logout', {})
for (const [id, entry] of clients) { entry.client.close(); send({ channelId: id, close: true }) }
clients.clear(); await vault.save({})
return { cleared: true }
}
if (action === 'desktop.pair') { if (action === 'desktop.pair') {
const result = await call('/v1/aiturk/pairings', { display_name: hostname(), platform: 'windows-x64' }) const result = await call('/v1/aiturk/pairings', { display_name: hostname(), platform: 'windows-x64' })
await vault.save({ ...await vault.load(), deviceId: result.device_id, deviceToken: result.device_token, await vault.save({ ...await vault.load(), deviceId: result.device_id, deviceToken: result.device_token,
+8 -4
View File
@@ -8,7 +8,9 @@ turkservis.online veya kurulu IDE bu kaynak değişiklikleriyle güncellenmedi.
## Çalışan ve test edilen parçalar ## Çalışan ve test edilen parçalar
- Capacitor 8.5.1 Android projesi; Türkçe erişilebilir kurulum ve yazılı kullanım. - Capacitor 8.5.1 Android projesi; Türkçe erişilebilir kurulum ve yazılı kullanım.
- Mevcut AITURK hesap API anahtarı ve cihaz sırrıyla hesap/cihaz doğrulaması. - Tarayıcıda mevcut AITURK hesabıyla kısa kod onayı; uygulamaya parola girilmez.
Dar kapsamlı mobil oturum Windows/Android kasasında tutulur. Mevcut hesap API
anahtarı gelişmiş seçenek olarak korunur. Bu yeni giriş henüz canlıya dağıtılmadı.
Telefonun eşleşmesini hedef bilgisayar kendi cihaz tokenıyla onaylar. Telefonun eşleşmesini hedef bilgisayar kendi cihaz tokenıyla onaylar.
- WebRTC DTLS/SCTP veri kanalı; büyük Unicode mesajları sınırlı parçalara bölme. - WebRTC DTLS/SCTP veri kanalı; büyük Unicode mesajları sınırlı parçalara bölme.
- Mevcut IDE `/api/sessions` ve JSON-RPC adaptörü. Konuşma ve seçili model - Mevcut IDE `/api/sessions` ve JSON-RPC adaptörü. Konuşma ve seçili model
@@ -50,9 +52,10 @@ Derlenmiş IDE'yi `--aiturk-mobile` parametresiyle açın. IDE zaten açıksa pa
mobil bağlantı penceresini açar; çalışmakta olan konuşma kapatılmaz. mobil bağlantı penceresini açar; çalışmakta olan konuşma kapatılmaz.
Mobil pencere açılmadan uzaktan erişim etkinleşmez. Mobil pencere açılmadan uzaktan erişim etkinleşmez.
1. Bilgisayardaki mobil pencerede AITURK hesap API anahtarını kaydedin. 1. Bilgisayardaki mobil pencerede “AITURK hesabımla giriş yap” seçin; tarayıcıda
hesabınıza girip uygulamanın kodunu onaylayın. “Girişi kontrol et” ile tamamlayın.
2. Bilgisayarı hesapla eşleştirin; açılan mevcut portal sayfasında kodu onaylayın. 2. Bilgisayarı hesapla eşleştirin; açılan mevcut portal sayfasında kodu onaylayın.
3. Android'de aynı hesap anahtarıyla eşleştirme kodu oluşturun. 3. Android'de aynı AITURK hesabıyla tarayıcı kod girişini tamamlayıp eşleştirme kodu oluşturun.
4. Telefondaki kimlik/kodu bilgisayardaki mobil pencerede açıkça onaylayın. 4. Telefondaki kimlik/kodu bilgisayardaki mobil pencerede açıkça onaylayın.
5. Bilgisayarda “Mobil bağlantıyı aç”, telefonda “Bilgisayarları bul” ve “Bağlan”. 5. Bilgisayarda “Mobil bağlantıyı aç”, telefonda “Bilgisayarları bul” ve “Bağlan”.
6. Gerçek IDE konuşmasını seçin. Yazılı görevi görünür önizlemeden gönderin. 6. Gerçek IDE konuşmasını seçin. Yazılı görevi görünür önizlemeden gönderin.
@@ -89,7 +92,8 @@ Henüz tamamlanmayanlar:
- Kullanıcıya ait test anahtarıyla Google Live doğrulaması ve Türkçe konuşma - Kullanıcıya ait test anahtarıyla Google Live doğrulaması ve Türkçe konuşma
senaryoları. Sıfır gerçek Google çağrısı yapıldı; ücret oluştuğu iddia edilmiyor. senaryoları. Sıfır gerçek Google çağrısı yapıldı; ücret oluştuğu iddia edilmiyor.
- Canlı gateway/TURN kurulumu, ayrı mobil ve bilgisayar ağlarıyla tam akış. - Canlı gateway/TURN kurulumu, ayrı mobil ve bilgisayar ağlarıyla tam akış.
- QR tarama ve tek dokunuşla hesap girişi; ilk sürüm kod/hesap anahtarı kullanır. - QR tarama. Tarayıcı kısa kod girişi kodlandı ve HTTP/DB ile sahte yanıtlı
tarayıcı arayüz testleri geçti; gerçek Android giriş kabulü henüz yapılmadı.
- Açık talimatla prepare_task/submit_task devri ve görev kimlikli terminal - Açık talimatla prepare_task/submit_task devri ve görev kimlikli terminal
olayları kodlandı. 25 Node testi ve üç görevli gerçek yerel IDE bağlantı olayları kodlandı. 25 Node testi ve üç görevli gerçek yerel IDE bağlantı
testi geçti. Canlı Google transkript sırası ve Türkçe kalite kabulü henüz yok. testi geçti. Canlı Google transkript sırası ve Türkçe kalite kabulü henüz yok.
@@ -26,6 +26,16 @@ import java.util.concurrent.atomic.AtomicInteger;
public class MobileSecretsPlugin extends Plugin { public class MobileSecretsPlugin extends Plugin {
private static final String ALIAS = "aiturk.mobile.account.v1"; private static final String ALIAS = "aiturk.mobile.account.v1";
private final AtomicInteger requests = new AtomicInteger(); private final AtomicInteger requests = new AtomicInteger();
@PluginMethod public void openLogin(PluginCall call) {
String url = call.getString("url", "");
if (!url.matches("https://turkservis\\.online/account/aiturk/mobile-login/[0-9a-f-]{36}")) {
call.reject("Geçersiz giriş adresi."); return;
}
try {
getActivity().startActivity(new android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(url)));
call.resolve();
} catch (Exception error) { call.reject("Tarayıcı açılamadı."); }
}
@PluginMethod public void request(PluginCall call) { @PluginMethod public void request(PluginCall call) {
String path = call.getString("path", ""); String path = call.getString("path", "");
String method = call.getString("method", "GET"); String method = call.getString("method", "GET");
+18 -1
View File
@@ -23,6 +23,23 @@ try {
await page.getByRole('heading', { name: 'Fikrini konuş. Birlikte geliştir.' }).waitFor() await page.getByRole('heading', { name: 'Fikrini konuş. Birlikte geliştir.' }).waitFor()
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false) assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false)
assert.ok((await page.getByRole('button', { name: 'Eşleştirme kodu oluştur' }).boundingBox()).height >= 48) assert.ok((await page.getByRole('button', { name: 'Eşleştirme kodu oluştur' }).boundingBox()).height >= 48)
// UI wiring only: real account authentication is covered by gateway HTTP/DB
// tests. Never submit a credential to the live portal from this smoke test.
const loginCalls = []
await page.route('https://turkservis.online/v1/aiturk/mobile-auth/**', async route => {
const action = new URL(route.request().url()).pathname.split('/').at(-1); loginCalls.push(action)
const result = action === 'start' ? { deviceCode: 'tsml_fixture', userCode: 'ABCDEFGH',
verificationUri: 'https://turkservis.online/account/aiturk/mobile-login/11111111-1111-4111-8111-111111111111' }
: action === 'poll' ? { state: 'approved', accountToken: 'tsml_fixture' } : { revoked: true }
await route.fulfill({ status: 200, contentType: 'application/json', headers: { 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify(result) })
})
await page.getByRole('button', { name: 'AITURK hesabımla giriş yap' }).click()
await page.getByText('Yalnız kendi hesabınızda bu kodu onaylayın: ABCDEFGH').waitFor()
await page.getByRole('button', { name: 'Girişi kontrol et' }).click()
await page.getByText('Hesaba giriş yapıldı.', { exact: true }).waitFor()
await page.getByRole('button', { name: 'Bu uygulamadaki hesaptan çık' }).click()
await page.getByText('Bu uygulamanın oturumu kapatıldı.', { exact: false }).waitFor()
assert.deepEqual(loginCalls, ['start', 'poll', 'logout'])
await mkdir('test-results', { recursive: true }) await mkdir('test-results', { recursive: true })
await page.screenshot({ path: 'test-results/android-onboarding.png', fullPage: true }) await page.screenshot({ path: 'test-results/android-onboarding.png', fullPage: true })
const result = await page.evaluate(async () => { const result = await page.evaluate(async () => {
@@ -50,5 +67,5 @@ try {
}) })
assert.equal(result.length, result.expected); assert.equal(result.dtls, 'connected'); assert.equal(result.action, 'session.history') assert.equal(result.length, result.expected); assert.equal(result.dtls, 'connected'); assert.equal(result.action, 'session.history')
assert.deepEqual(errors, []) assert.deepEqual(errors, [])
console.log(JSON.stringify({ onboarding: 'passed', realWebRTC: result, errors })) console.log(JSON.stringify({ onboarding: 'passed', mockedLoginUi: 'passed', realWebRTC: result, errors }))
} finally { await browser.close(); server.close() } } finally { await browser.close(); server.close() }
+33 -1
View File
@@ -30,8 +30,40 @@ function event(frame) {
} }
} }
} }
action('login-start', async () => {
if (credentials.accountToken || connection) throw new Error('Kayıtlı hesabınız var; önce mevcut cihaz erişimini iptal edin.')
const result = await new AccountClient({}).call('/v1/aiturk/mobile-auth/start', {})
if (!/^https:\/\/turkservis\.online\/account\/aiturk\/mobile-login\/[0-9a-f-]{36}$/.test(result.verificationUri)) throw new Error('Geçersiz giriş sayfası.')
credentials.pendingLogin = result; await saveCredentials()
$('login-code').textContent = `Yalnız kendi hesabınızda bu kodu onaylayın: ${result.userCode}`
$('login-link').href = result.verificationUri; $('login-link').hidden = false
if (native()) await native().openLogin({ url: result.verificationUri })
status('Tarayıcıda giriş yaptıktan sonra buraya dönüp girişi kontrol edin.')
})
action('login-check', async () => {
if (!credentials.pendingLogin) throw new Error('Önce giriş başlatın.')
const result = await new AccountClient({ accountToken: credentials.pendingLogin.deviceCode }).call('/v1/aiturk/mobile-auth/poll', {})
if (result.state !== 'approved') { status('Tarayıcıdaki kod onayı bekleniyor.'); return }
credentials.accountToken = result.accountToken; delete credentials.pendingLogin; await saveCredentials()
account = new AccountClient(credentials); $('login-code').textContent = 'Hesaba giriş yapıldı.'; $('login-link').hidden = true
status('Hesap bağlı. Şimdi telefonun eşleştirme kodunu oluşturun.')
})
action('logout', async () => {
const token = credentials.accountToken || credentials.pendingLogin?.deviceCode
// Revoke the app-specific credential before dropping the local copy. A
// network failure leaves it available for an explicit retry.
if (token?.startsWith('tsml_')) await new AccountClient({ accountToken: token }).call('/v1/aiturk/mobile-auth/logout', {})
connectionEpoch++; stopVoice(); connection?.close(); connection = null; selected = null; account = null
credentials = {}; pendingTask = null; await saveCredentials()
$('work').hidden = true; $('login-link').hidden = true; $('login-code').textContent = ''; $('pair-code').textContent = ''
status('Bu uygulamanın oturumu kapatıldı. IDE’de başlamış iş kendi çalışma kurallarıyla devam edebilir.')
})
action('pair', async () => { action('pair', async () => {
credentials = { accountToken: $('account-token').value.trim() }; $('account-token').value = '' if (credentials.deviceId) throw new Error('Bu telefon zaten eşleşme başlatmış. Bilgisayarları bulun veya mevcut erişimi iptal edin.')
const manual = $('account-token').value.trim(); $('account-token').value = ''
if (manual && credentials.accountToken && manual !== credentials.accountToken) throw new Error('Kayıtlı hesap sessizce değiştirilemez.')
if (manual) credentials.accountToken = manual
if (!credentials.accountToken) throw new Error('Önce AITURK hesabınıza giriş yapın.')
account = new AccountClient(credentials) account = new AccountClient(credentials)
const result = await account.call('/v1/aiturk/pairings', { display_name: $('device-name').value.trim(), platform: 'android' }) const result = await account.call('/v1/aiturk/pairings', { display_name: $('device-name').value.trim(), platform: 'android' })
Object.assign(credentials, { deviceId: result.device_id, deviceToken: result.device_token, pairingId: result.pairing_id }) Object.assign(credentials, { deviceId: result.device_id, deviceToken: result.device_token, pairingId: result.pairing_id })
+7 -1
View File
@@ -4,8 +4,14 @@
<p class="eyebrow">AITURK · ANDROID GELİŞTİRME SÜRÜMÜ</p><h1>Fikrini konuş.<br>Birlikte geliştir.</h1> <p class="eyebrow">AITURK · ANDROID GELİŞTİRME SÜRÜMÜ</p><h1>Fikrini konuş.<br>Birlikte geliştir.</h1>
<p id="status" class="status" role="status" aria-live="polite">Bilgisayarına bağlanarak başla.</p> <p id="status" class="status" role="status" aria-live="polite">Bilgisayarına bağlanarak başla.</p>
<section id="setup"><h2>1. Aynı AITURK hesabı</h2> <section id="setup"><h2>1. Aynı AITURK hesabı</h2>
<p>Bu ilk geliştirme sürümünde hesabınızın API anahtarıyla giriş yapılır. <a href="https://turkservis.online/account" target="_blank" rel="noreferrer">AITURK hesabınızı açın</a>. Bu anahtar Google ses anahtarından ayrıdır.</p> <p>Tarayıcıda AITURK hesabınıza giriş yapın ve burada gösterilen kısa kodu onaylayın. Google ses anahtarı ayrı kalır.</p>
<button id="login-start">AITURK hesabımla giriş yap</button><pre id="login-code" aria-live="polite"></pre>
<a id="login-link" hidden target="_blank" rel="noreferrer">Giriş sayfasını aç</a>
<button id="login-check" class="secondary">Girişi kontrol et</button>
<button id="logout" class="secondary">Bu uygulamadaki hesaptan çık</button>
<details><summary>Gelişmiş: mevcut hesap API anahtarını kullan</summary>
<label>AITURK hesap API anahtarı<input id="account-token" type="password" autocomplete="off" autocapitalize="none" spellcheck="false"></label> <label>AITURK hesap API anahtarı<input id="account-token" type="password" autocomplete="off" autocapitalize="none" spellcheck="false"></label>
</details>
<label>Telefon adı<input id="device-name" maxlength="80" value="Android telefonum"></label><button id="pair">Eşleştirme kodu oluştur</button> <label>Telefon adı<input id="device-name" maxlength="80" value="Android telefonum"></label><button id="pair">Eşleştirme kodu oluştur</button>
<pre id="pair-code" aria-live="polite"></pre><p>Kodu bilgisayardaki AITURK Mobil bağlantı penceresinde onaylayın. Eşleştirme kimliğini kopyalayabilirsiniz.</p> <pre id="pair-code" aria-live="polite"></pre><p>Kodu bilgisayardaki AITURK Mobil bağlantı penceresinde onaylayın. Eşleştirme kimliğini kopyalayabilirsiniz.</p>
<button id="devices" class="secondary">Onaydan sonra bilgisayarları bul</button> <button id="devices" class="secondary">Onaydan sonra bilgisayarları bul</button>