Реализация Account Linking (привязка соцсетей к аккаунту) на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация Account Linking (привязка соцсетей к аккаунту) на сайте
Средняя
от 1 рабочего дня до 3 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Привязка аккаунтов социальных сетей

Привязка позволяет пользователю с email/password аккаунтом добавить Google или GitHub как способ входа — и наоборот. Главная сложность: безопасно идентифицировать владельца перед привязкой.

Архитектура: схема данных

model User {
  id           String        @id @default(cuid())
  email        String        @unique
  passwordHash String?       // null если только соцсети
  accounts     LinkedAccount[]
}

model LinkedAccount {
  id                String   @id @default(cuid())
  userId            String
  provider          String   // 'google' | 'github' | 'apple'
  providerAccountId String   // ID в системе провайдера
  providerEmail     String?  // email от провайдера
  linkedAt          DateTime @default(now())

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
  @@index([userId])
}

Server Actions: привязка провайдера

// app/settings/security/actions.ts
'use server';

import { auth } from '@/auth';
import { redirect } from 'next/navigation';
import { generateState } from '@/lib/oauth';

export async function initiateLinkProvider(provider: string) {
  const session = await auth();
  if (!session) redirect('/login');

  // Генерируем state с userId (защита от CSRF)
  const state = await generateState({
    userId: session.user.id,
    action: 'link',
    provider,
  });

  // Перенаправляем на OAuth flow с параметром link=true
  redirect(`/api/auth/link/${provider}?state=${state}`);
}

export async function unlinkProvider(accountId: string) {
  const session = await auth();
  if (!session) redirect('/login');

  // Проверяем, что это аккаунт текущего пользователя
  const account = await db.linkedAccount.findFirst({
    where: { id: accountId, userId: session.user.id }
  });

  if (!account) {
    throw new Error('Account not found');
  }

  // Нельзя отвязать единственный способ входа
  const accountsCount = await db.linkedAccount.count({
    where: { userId: session.user.id }
  });
  const hasPassword = await db.user.findUnique({
    where: { id: session.user.id },
    select: { passwordHash: true }
  });

  if (accountsCount <= 1 && !hasPassword?.passwordHash) {
    throw new Error('Cannot unlink the only login method');
  }

  await db.linkedAccount.delete({ where: { id: accountId } });
}

OAuth Callback: обработка привязки

// app/api/auth/callback/[provider]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { provider: string } }
) {
  const { searchParams } = new URL(request.url);
  const code = searchParams.get('code');
  const state = searchParams.get('state');

  // Расшифровываем state
  const stateData = await verifyState(state);
  if (!stateData || stateData.action !== 'link') {
    redirect('/settings/security?error=invalid_state');
  }

  // Обмениваем code на access token
  const tokens = await exchangeCode(params.provider, code);
  const providerUser = await fetchProviderUser(params.provider, tokens.accessToken);

  // Проверяем: не привязан ли уже этот аккаунт к другому пользователю
  const existingLink = await db.linkedAccount.findUnique({
    where: {
      provider_providerAccountId: {
        provider: params.provider,
        providerAccountId: providerUser.id,
      }
    }
  });

  if (existingLink && existingLink.userId !== stateData.userId) {
    redirect('/settings/security?error=account_already_linked');
  }

  if (!existingLink) {
    await db.linkedAccount.create({
      data: {
        userId: stateData.userId,
        provider: params.provider,
        providerAccountId: providerUser.id,
        providerEmail: providerUser.email,
      }
    });
  }

  redirect('/settings/security?success=linked');
}

UI компонент управления

// components/LinkedAccountsManager.tsx
'use client';

import { useState, useTransition } from 'react';
import { initiateLinkProvider, unlinkProvider } from './actions';

const PROVIDERS = [
  { id: 'google', name: 'Google', icon: <GoogleIcon /> },
  { id: 'github', name: 'GitHub', icon: <GitHubIcon /> },
  { id: 'apple', name: 'Apple', icon: <AppleIcon /> },
];

export function LinkedAccountsManager({
  linkedAccounts,
  hasPassword,
}: {
  linkedAccounts: Array<{ id: string; provider: string; providerEmail: string | null }>;
  hasPassword: boolean;
}) {
  const [isPending, startTransition] = useTransition();
  const linkedProviders = new Set(linkedAccounts.map(a => a.provider));

  const canUnlink = (provider: string) => {
    // Нельзя отвязать если это единственный способ входа
    const otherAccounts = linkedAccounts.filter(a => a.provider !== provider);
    return hasPassword || otherAccounts.length > 0;
  };

  return (
    <div className="space-y-4">
      {PROVIDERS.map((provider) => {
        const linked = linkedAccounts.find(a => a.provider === provider.id);

        return (
          <div key={provider.id} className="flex items-center justify-between p-4 border rounded-lg">
            <div className="flex items-center gap-3">
              {provider.icon}
              <div>
                <p className="font-medium">{provider.name}</p>
                {linked && (
                  <p className="text-sm text-gray-500">{linked.providerEmail}</p>
                )}
              </div>
            </div>

            {linked ? (
              <button
                onClick={() => startTransition(() => unlinkProvider(linked.id))}
                disabled={!canUnlink(provider.id) || isPending}
                className="text-red-600 disabled:opacity-50"
              >
                Отвязать
              </button>
            ) : (
              <button
                onClick={() => startTransition(() => initiateLinkProvider(provider.id))}
                disabled={isPending}
                className="text-blue-600"
              >
                Привязать
              </button>
            )}
          </div>
        );
      })}
    </div>
  );
}

Реализация привязки/отвязки OAuth провайдеров с защитой от блокировки аккаунта — 2–3 рабочих дня.