feat(account): VK ID web login to link a VK identity from a browser
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 1m4s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 1m4s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
A browser has no signed VK Mini App launch params, so linking VK on the web uses
VK ID's raw OAuth 2.1 flow (PKCE, no @vkid/sdk): the SPA redirects to VK's hosted
login and returns with an authorization code, which the gateway exchanges
server-side (confidential, under the VK "Web" app's protected key) for the trusted
vk user id — then the existing link/merge machinery attaches or merges it.
- fbs LinkVKRequest{code, device_id, code_verifier}; codec + TS bindings.
- backend link.Service ConfirmVK/MergeVK/attachVK (KindVK, mirror Telegram),
handleLinkVK[Merge], routes /user/link/vk[/merge], backendclient LinkVK[Merge].
- gateway internal/vkid confidential code exchange (id.vk.com/oauth2/auth);
transcode link.vk.confirm/merge (registered only when configured) + config
GATEWAY_VK_ID_{APP_ID,CLIENT_SECRET,REDIRECT_URL} + main wiring.
- UI lib/vkid (PKCE authorize redirect + callback), Profile "Link VK" control,
boot callback handling; a merge re-authorizes for a fresh code (VK codes are
single-use). Web-only (a redirect strands a Mini App webview).
- Deploy: VITE_VK_APP_ID + VITE_VK_ID_REDIRECT_URL build args + gateway env,
ci.yaml/prod-deploy TEST_/PROD_ vars, compose/Dockerfile/.env.example/README.
- Tests: vkid exchange unit (string/number user_id, id_token fallback, errors),
transcode link.vk, backend ConfirmVK/MergeVK inttest, codec encodeLinkVK.
- Docs: ARCHITECTURE §4, FUNCTIONAL(+ru), gateway README.
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
import { connection } from '../lib/connection.svelte';
|
||||
import { gateway } from '../lib/gateway';
|
||||
import { loginWidgetAvailable, requestTelegramLogin } from '../lib/telegram';
|
||||
import { startVKLink, vkWebLinkAvailable } from '../lib/vkid';
|
||||
import { t } from '../lib/i18n/index.svelte';
|
||||
import {
|
||||
awayDurationOk,
|
||||
@@ -45,7 +46,7 @@
|
||||
let emailSent = $state(false);
|
||||
// A pending irreversible merge surfaced after the code/widget was verified; the
|
||||
// dialog confirms it. tgData holds the Telegram widget payload for the merge step.
|
||||
let pendingMerge = $state<null | { kind: 'email' | 'telegram'; name: string; games: number; friends: number }>(null);
|
||||
let pendingMerge = $state<null | { kind: 'email' | 'telegram' | 'vk'; name: string; games: number; friends: number }>(null);
|
||||
let tgData = '';
|
||||
// The change-email sub-form is open (a new address is being entered/confirmed).
|
||||
let changingEmail = $state(false);
|
||||
@@ -56,6 +57,7 @@
|
||||
let deleteCode = $state('');
|
||||
let deletePhrase = $state('');
|
||||
const telegramLinkable = loginWidgetAvailable();
|
||||
const vkLinkable = vkWebLinkAvailable();
|
||||
|
||||
function defaultTz(): string {
|
||||
const b = browserOffset();
|
||||
@@ -82,7 +84,12 @@
|
||||
notificationsInAppOnly = p.notificationsInAppOnly;
|
||||
variantPrefs = [...p.variantPreferences];
|
||||
}
|
||||
onMount(populate);
|
||||
onMount(() => {
|
||||
populate();
|
||||
// A VK ID web-link callback captured on boot (app.vkLinkPending) is finished here, since
|
||||
// the redirect back from VK lost the in-app route and lands the app on a fresh mount.
|
||||
if (app.vkLinkPending) void processVKLink();
|
||||
});
|
||||
|
||||
const awayStart = $derived(`${startH}:${startM}`);
|
||||
const awayEnd = $derived(`${endH}:${endM}`);
|
||||
@@ -180,8 +187,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
// addVK starts VK ID web login for linking: it redirects the whole tab to VK's hosted login,
|
||||
// returning to the app with an auth code that boot hands back to processVKLink.
|
||||
function addVK() {
|
||||
void startVKLink('link');
|
||||
}
|
||||
|
||||
// processVKLink finishes a VK ID web-link callback captured on boot: it exchanges the code via
|
||||
// the gateway and either completes the link or opens the merge dialog. VK's authorization code
|
||||
// is single-use, so a required merge re-authorizes for a fresh code (see confirmMerge); that
|
||||
// returning 'merge' callback lands back here and completes.
|
||||
async function processVKLink() {
|
||||
const cb = app.vkLinkPending;
|
||||
app.vkLinkPending = null;
|
||||
if (!cb) return;
|
||||
try {
|
||||
if (cb.mode === 'merge') {
|
||||
await applyLinkResult(await gateway.linkVKMerge(cb.code, cb.deviceId, cb.verifier));
|
||||
populate();
|
||||
showToast(t('profile.merged'));
|
||||
return;
|
||||
}
|
||||
const r = await gateway.linkVK(cb.code, cb.deviceId, cb.verifier);
|
||||
if (r.status === 'merge_required') {
|
||||
pendingMerge = { kind: 'vk', name: r.secondaryDisplayName, games: r.secondaryGames, friends: r.secondaryFriends };
|
||||
return;
|
||||
}
|
||||
await applyLinkResult(r);
|
||||
populate();
|
||||
showToast(t('profile.linked'));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmMerge() {
|
||||
if (!pendingMerge) return;
|
||||
// A VK merge cannot reuse VK's single-use authorization code, so it re-authorizes for a fresh
|
||||
// one; the returning callback completes it (processVKLink). Email/Telegram re-send their proof.
|
||||
if (pendingMerge.kind === 'vk') {
|
||||
void startVKLink('merge');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const r =
|
||||
pendingMerge.kind === 'email'
|
||||
@@ -344,7 +391,7 @@
|
||||
|
||||
<!-- Sign-in methods: bind/change an email and link/unlink providers. A returning email
|
||||
(add) triggers the merge dialog below. On the web an account can add Telegram via the
|
||||
login widget; adding VK on the web is deferred (no VK OAuth) and inside a Mini App the
|
||||
login widget or VK via VK ID web login (a full-page redirect); inside a Mini App the
|
||||
host provider is already linked. Email is never unlinked — it is changed. Unlink is
|
||||
offered only when another identity remains (the backend also refuses the last one). -->
|
||||
<section class="accounts">
|
||||
@@ -413,6 +460,10 @@
|
||||
<button class="ghost danger" onclick={() => (confirmUnlink = { kind: 'vk', label: 'VK' })} disabled={!connection.online}>{t('profile.unlink')}</button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if vkLinkable}
|
||||
<button class="ghost tg" onclick={addVK} disabled={!connection.online}>
|
||||
<img src="vk-logo.svg" alt="" width="18" height="18" />{t('profile.linkVK')}
|
||||
</button>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user