| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- 'use client'
-
- import React, { useEffect, useMemo, useRef } from 'react'
- import { useTranslation } from 'react-i18next'
- import { useRouter, useSearchParams } from 'next/navigation'
- import Button from '@/app/components/base/button'
- import Avatar from '@/app/components/base/avatar'
- import Loading from '@/app/components/base/loading'
- import Toast from '@/app/components/base/toast'
- import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
- import { useAppContext } from '@/context/app-context'
- import { useAuthorizeOAuthApp, useOAuthAppInfo } from '@/service/use-oauth'
- import {
- RiAccountCircleLine,
- RiGlobalLine,
- RiInfoCardLine,
- RiMailLine,
- RiTranslate2,
- } from '@remixicon/react'
- import dayjs from 'dayjs'
-
- export const OAUTH_AUTHORIZE_PENDING_KEY = 'oauth_authorize_pending'
- export const REDIRECT_URL_KEY = 'oauth_redirect_url'
-
- const OAUTH_AUTHORIZE_PENDING_TTL = 60 * 3
-
- function setItemWithExpiry(key: string, value: string, ttl: number) {
- const item = {
- value,
- expiry: dayjs().add(ttl, 'seconds').unix(),
- }
- localStorage.setItem(key, JSON.stringify(item))
- }
-
- function buildReturnUrl(pathname: string, search: string) {
- try {
- const base = `${globalThis.location.origin}${pathname}${search}`
- return base
- }
- catch {
- return pathname + search
- }
- }
-
- export default function OAuthAuthorize() {
- const { t } = useTranslation()
-
- const SCOPE_INFO_MAP: Record<string, { icon: React.ComponentType<{ className?: string }>, label: string }> = {
- 'read:name': {
- icon: RiInfoCardLine,
- label: t('oauth.scopes.name'),
- },
- 'read:email': {
- icon: RiMailLine,
- label: t('oauth.scopes.email'),
- },
- 'read:avatar': {
- icon: RiAccountCircleLine,
- label: t('oauth.scopes.avatar'),
- },
- 'read:interface_language': {
- icon: RiTranslate2,
- label: t('oauth.scopes.languagePreference'),
- },
- 'read:timezone': {
- icon: RiGlobalLine,
- label: t('oauth.scopes.timezone'),
- },
- }
-
- const router = useRouter()
- const language = useLanguage()
- const searchParams = useSearchParams()
- const client_id = decodeURIComponent(searchParams.get('client_id') || '')
- const redirect_uri = decodeURIComponent(searchParams.get('redirect_uri') || '')
- const { userProfile } = useAppContext()
- const { data: authAppInfo, isLoading, isError } = useOAuthAppInfo(client_id, redirect_uri)
- const { mutateAsync: authorize, isPending: authorizing } = useAuthorizeOAuthApp()
- const hasNotifiedRef = useRef(false)
-
- const isLoggedIn = useMemo(() => {
- try {
- return Boolean(localStorage.getItem('console_token') && localStorage.getItem('refresh_token'))
- }
- catch { return false }
- }, [])
-
- const onLoginSwitchClick = () => {
- try {
- const returnUrl = buildReturnUrl('/account/oauth/authorize', `?client_id=${encodeURIComponent(client_id)}&redirect_uri=${encodeURIComponent(redirect_uri)}`)
- setItemWithExpiry(OAUTH_AUTHORIZE_PENDING_KEY, returnUrl, OAUTH_AUTHORIZE_PENDING_TTL)
- router.push(`/signin?${REDIRECT_URL_KEY}=${encodeURIComponent(returnUrl)}`)
- }
- catch {
- router.push('/signin')
- }
- }
-
- const onAuthorize = async () => {
- if (!client_id || !redirect_uri)
- return
- try {
- const { code } = await authorize({ client_id })
- const url = new URL(redirect_uri)
- url.searchParams.set('code', code)
- globalThis.location.href = url.toString()
- }
- catch (err: any) {
- Toast.notify({
- type: 'error',
- message: `${t('oauth.error.authorizeFailed')}: ${err.message}`,
- })
- }
- }
-
- useEffect(() => {
- const invalidParams = !client_id || !redirect_uri
- if ((invalidParams || isError) && !hasNotifiedRef.current) {
- hasNotifiedRef.current = true
- Toast.notify({
- type: 'error',
- message: invalidParams ? t('oauth.error.invalidParams') : t('oauth.error.authAppInfoFetchFailed'),
- duration: 0,
- })
- }
- }, [client_id, redirect_uri, isError])
-
- if (isLoading) {
- return (
- <div className='bg-background-default-subtle'>
- <Loading type='app' />
- </div>
- )
- }
-
- return (
- <div className='bg-background-default-subtle'>
- {authAppInfo?.app_icon && (
- <div className='w-max rounded-2xl border-[0.5px] border-components-panel-border bg-text-primary-on-surface p-3 shadow-lg'>
- <img src={authAppInfo.app_icon} alt='app icon' className='h-10 w-10 rounded' />
- </div>
- )}
-
- <div className={`mb-4 mt-5 flex flex-col gap-2 ${isLoggedIn ? 'pb-2' : ''}`}>
- <div className='title-4xl-semi-bold'>
- {isLoggedIn && <div className='text-text-primary'>{t('oauth.connect')}</div>}
- <div className='text-[var(--color-saas-dify-blue-inverted)]'>{authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('oauth.unknownApp')}</div>
- {!isLoggedIn && <div className='text-text-primary'>{t('oauth.tips.notLoggedIn')}</div>}
- </div>
- <div className='body-md-regular text-text-secondary'>{isLoggedIn ? `${authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('oauth.unknownApp')} ${t('oauth.tips.loggedIn')}` : t('oauth.tips.needLogin')}</div>
- </div>
-
- {isLoggedIn && userProfile && (
- <div className='flex items-center justify-between rounded-xl bg-background-section-burn-inverted p-3'>
- <div className='flex items-center gap-2.5'>
- <Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} />
- <div>
- <div className='system-md-semi-bold text-text-secondary'>{userProfile.name}</div>
- <div className='system-xs-regular text-text-tertiary'>{userProfile.email}</div>
- </div>
- </div>
- <Button variant='tertiary' size='small' onClick={onLoginSwitchClick}>{t('oauth.switchAccount')}</Button>
- </div>
- )}
-
- {isLoggedIn && Boolean(authAppInfo?.scope) && (
- <div className='mt-2 flex flex-col gap-2.5 rounded-xl bg-background-section-burn-inverted px-[22px] py-5 text-text-secondary'>
- {authAppInfo!.scope.split(/\s+/).filter(Boolean).map((scope: string) => {
- const Icon = SCOPE_INFO_MAP[scope]
- return (
- <div key={scope} className='body-sm-medium flex items-center gap-2 text-text-secondary'>
- {Icon ? <Icon.icon className='h-4 w-4' /> : <RiAccountCircleLine className='h-4 w-4' />}
- {Icon.label}
- </div>
- )
- })}
- </div>
- )}
-
- <div className='flex flex-col items-center gap-2 pt-4'>
- {!isLoggedIn ? (
- <Button variant='primary' size='large' className='w-full' onClick={onLoginSwitchClick}>{t('oauth.login')}</Button>
- ) : (
- <>
- <Button variant='primary' size='large' className='w-full' onClick={onAuthorize} disabled={!client_id || !redirect_uri || isError || authorizing} loading={authorizing}>{t('oauth.continue')}</Button>
- <Button size='large' className='w-full' onClick={() => router.push('/apps')}>{t('common.operation.cancel')}</Button>
- </>
- )}
- </div>
- <div className='mt-4 py-2'>
- <svg xmlns="http://www.w3.org/2000/svg" width="400" height="1" viewBox="0 0 400 1" fill="none">
- <path d="M0 0.5H400" stroke="url(#paint0_linear_2_5904)" />
- <defs>
- <linearGradient id="paint0_linear_2_5904" x1="400" y1="9.49584" x2="0.000228929" y2="9.17666" gradientUnits="userSpaceOnUse">
- <stop stop-color="white" stop-opacity="0.01" />
- <stop offset="0.505" stop-color="#101828" stop-opacity="0.08" />
- <stop offset="1" stop-color="white" stop-opacity="0.01" />
- </linearGradient>
- </defs>
- </svg>
- </div>
- <div className='system-xs-regular mt-3 text-text-tertiary'>{t('oauth.tips.common')}</div>
- </div>
- )
- }
|