diff --git a/apps/desktop/electron/mobile-host-ui.mjs b/apps/desktop/electron/mobile-host-ui.mjs index e973c75..b5cfa73 100644 --- a/apps/desktop/electron/mobile-host-ui.mjs +++ b/apps/desktop/electron/mobile-host-ui.mjs @@ -2,6 +2,9 @@ import { completeIce, rtcConfiguration, sendFrame } from './mobile/transport.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('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ı.') }) diff --git a/apps/desktop/electron/mobile-host.html b/apps/desktop/electron/mobile-host.html index 6eabdad..0668cbf 100644 --- a/apps/desktop/electron/mobile-host.html +++ b/apps/desktop/electron/mobile-host.html @@ -4,8 +4,12 @@
AITURK · BİLGİSAYAR
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.
Bağlantı kapalı.
+ +Çıkış, mobil bağlantıları kapatır ve bu penceredeki hesap/Google anahtarını Windows kasasından temizler; başlamış IDE işini durdurmaz.
+ +Telefondaki eşleştirme kimliğini ve 8 karakterli kodu buraya girin. Bu onay yalnız bu telefon ile bu bilgisayarı bağlar.
diff --git a/apps/desktop/electron/mobile-host.mjs b/apps/desktop/electron/mobile-host.mjs index e5b7966..dc86aa1 100644 --- a/apps/desktop/electron/mobile-host.mjs +++ b/apps/desktop/electron/mobile-host.mjs @@ -33,6 +33,31 @@ export function createMobileWindow({ directory, assets, resolveBackend, show = t await vault.save({ ...previous, accountToken: params.token }) 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') { 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, diff --git a/apps/mobile/README.md b/apps/mobile/README.md index 6319fe5..058f8b9 100644 --- a/apps/mobile/README.md +++ b/apps/mobile/README.md @@ -8,7 +8,9 @@ turkservis.online veya kurulu IDE bu kaynak değişiklikleriyle güncellenmedi. ## Çalışan ve test edilen parçalar - 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. - 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 @@ -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 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. -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. 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. @@ -89,7 +92,8 @@ Henüz tamamlanmayanlar: - 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. - 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 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. diff --git a/apps/mobile/android/app/src/main/java/online/turkservis/aiturk/mobile/MobileSecretsPlugin.java b/apps/mobile/android/app/src/main/java/online/turkservis/aiturk/mobile/MobileSecretsPlugin.java index b5eb133..4f00ca2 100644 --- a/apps/mobile/android/app/src/main/java/online/turkservis/aiturk/mobile/MobileSecretsPlugin.java +++ b/apps/mobile/android/app/src/main/java/online/turkservis/aiturk/mobile/MobileSecretsPlugin.java @@ -26,6 +26,16 @@ import java.util.concurrent.atomic.AtomicInteger; public class MobileSecretsPlugin extends Plugin { private static final String ALIAS = "aiturk.mobile.account.v1"; 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) { String path = call.getString("path", ""); String method = call.getString("method", "GET"); diff --git a/apps/mobile/scripts/browser-smoke.mjs b/apps/mobile/scripts/browser-smoke.mjs index 77ca195..1287306 100644 --- a/apps/mobile/scripts/browser-smoke.mjs +++ b/apps/mobile/scripts/browser-smoke.mjs @@ -23,6 +23,23 @@ try { await page.getByRole('heading', { name: 'Fikrini konuş. Birlikte geliştir.' }).waitFor() 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) + // 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 page.screenshot({ path: 'test-results/android-onboarding.png', fullPage: true }) 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.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() } diff --git a/apps/mobile/src/app.mjs b/apps/mobile/src/app.mjs index d334213..9b83dab 100644 --- a/apps/mobile/src/app.mjs +++ b/apps/mobile/src/app.mjs @@ -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 () => { - 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) 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 }) diff --git a/apps/mobile/src/index.html b/apps/mobile/src/index.html index 0883a00..5102947 100644 --- a/apps/mobile/src/index.html +++ b/apps/mobile/src/index.html @@ -4,8 +4,14 @@AITURK · ANDROID GELİŞTİRME SÜRÜMÜ
Bilgisayarına bağlanarak başla.
Bu ilk geliştirme sürümünde hesabınızın API anahtarıyla giriş yapılır. AITURK hesabınızı açın. Bu anahtar Google ses anahtarından ayrıdır.
+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.
+ +Giriş sayfasını aç + + +Kodu bilgisayardaki AITURK Mobil bağlantı penceresinde onaylayın. Eşleştirme kimliğini kopyalayabilirsiniz.