Skip to content
6 changes: 6 additions & 0 deletions public/github-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions public/google-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions public/vkontakte-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions public/yandex-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion src/app/providers/AppProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { PropsWithChildren } from 'react';
import { QueryProvider } from './QueryProvider';
import { Toaster, TooltipProvider } from 'shared/ui';
import { FrontendObservability } from 'shared/config/';

export function AppProviders({ children }: PropsWithChildren) {
return (
<>
Expand Down
5 changes: 3 additions & 2 deletions src/entities/auth/api/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,17 @@ export class AuthHttp {
signal,
});
}
static connectedOAuthProviders() {
static connectedOAuthProviders(signal: AbortSignal) {
return api<TAuth.ConnectedOAuthProvidersResponse>({
url: '/auth/oauth/providers/connected',
method: 'GET',
contracts: {
response: SAuth.ConnectedOAuthProvidersResponse,
},
signal,
});
}
static connecteOAuthProvder(provider: TAuth.OAuthProvider) {
static connectOAuthProvder(provider: TAuth.OAuthProvider) {
Comment thread
AlexandrNel marked this conversation as resolved.
return api<TAuth.ConnectOAuthProviderResponse>({
url: `/auth/oauth/${provider}/connect`,
method: 'POST',
Expand Down
10 changes: 9 additions & 1 deletion src/entities/auth/api/queries.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { queryOptions } from '@tanstack/react-query';
import { AuthHttp } from './http';
import { authFabricKeys } from '../model/const';

export class AuthQueries {
static getOAuthProviders() {
return queryOptions({
queryKey: ['oauth-providers'],
queryKey: authFabricKeys.availableProviders(),
queryFn: async ({ signal }) => AuthHttp.oAuthProviders(signal),
staleTime: 60_000 * 360 * 24,
});
}
static getConnectedOAuthProviders() {
return queryOptions({
queryKey: authFabricKeys.connectedProviders(),
queryFn: async ({ signal }) => AuthHttp.connectedOAuthProviders(signal),
staleTime: 60_000 * 360 * 24,
});
}
}
26 changes: 26 additions & 0 deletions src/entities/auth/config/oauth-providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { type TAuth } from 'entities/auth';
import YandexIcon from 'public/yandex-logo.svg';
import VkontakteIcon from 'public/vkontakte-logo.svg';
import GoogleIcon from 'public/google-logo.svg';
import GithubIcon from 'public/github-logo.svg';

export type OAuthProviderMeta = {
iconSrc: string;
buttonClassName?: string;
};

export const OAUTH_PROVIDERS: Record<TAuth.OAuthProvider, OAuthProviderMeta> = {
yandex: {
iconSrc: YandexIcon,
buttonClassName: 'text-[#fc3f1d] hover:text-[#fc3f1d]',
},
vkontakte: {
iconSrc: VkontakteIcon,
buttonClassName: 'bg-[#07f] hover:bg-[#07f]',
},
google: { iconSrc: GoogleIcon },
github: {
iconSrc: GithubIcon,
buttonClassName: 'bg-[#24292f] hover:bg-[#24292f] text-white hover:text-white ',
},
} as const;
2 changes: 2 additions & 0 deletions src/entities/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export type * as TAuth from './model/types';
export * as CAuth from './model/const';
export { AuthHttp } from './api/http';
export { AuthQueries } from './api/queries';
export { authFabricKeys } from './model/const';
export { OAUTH_PROVIDERS } from './config/oauth-providers';
7 changes: 7 additions & 0 deletions src/entities/auth/model/const.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { createEntityKeys } from 'shared/lib/utils';

export const MIN_PASS_LENGTH = 8;
export const MAX_PASS_LENGTH = 32;
export const OTP_LENGTH = 6;

export const MIN_NAME_LENGTH = 2;
export const MAX_NAME_LENGTH = 50;

export const authFabricKeys = createEntityKeys('auth', {
availableProviders: () => ['providers', 'available'],
connectedProviders: () => ['providers', 'connected'],
});
39 changes: 0 additions & 39 deletions src/features/auth/oauth-login/model/consts.ts

This file was deleted.

48 changes: 48 additions & 0 deletions src/features/auth/oauth-login/ui/OAuthButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Link from 'next/link';
import Image from 'next/image';
import { Button } from 'shared/ui';
import { cn } from 'shared/lib/utils';
import { OAUTH_PROVIDERS } from 'entities/auth';
import type { ButtonHTMLAttributes } from 'react';
import type { Route } from 'next';
import type { TAuth } from 'entities/auth';

type OAuthButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'> & {
iconClassName?: string;
href: Route;
data: {
label: string;
value: TAuth.OAuthProvider;
};
};

export function OAuthButton({ className, iconClassName, href, data, ...props }: OAuthButtonProps) {
const { label, value } = data;
const meta = OAUTH_PROVIDERS[value];

if (!meta) return null;

return (
<Button
type="button"
size={meta.iconSrc ? 'icon' : 'default'}
variant={'outline'}
className={cn(meta.buttonClassName, className)}
{...props}
>
<Link href={href} className="text-base">
{meta.iconSrc ? (
<Image
src={meta.iconSrc}
alt={label}
width={24}
height={24}
className={cn('size-6', iconClassName)}
/>
) : (
label
)}
</Link>
</Button>
);
}
56 changes: 0 additions & 56 deletions src/features/auth/oauth-login/ui/OAuthIcons.tsx

This file was deleted.

38 changes: 16 additions & 22 deletions src/features/auth/oauth-login/ui/OAuthLoginButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,32 @@
'use client';
import { Button, Skeleton } from 'shared/ui';
import { OAUTH_PROVIDERS, OAUTH_PROVIDERS_COUNT } from '../model/consts';
import { Skeleton } from 'shared/ui';
import { cn } from 'shared/lib/utils';
import Link from 'next/link';
import { useQuery } from '@tanstack/react-query';
import { AuthQueries } from 'entities/auth';
import { routes } from 'shared/config';
import { OAuthButton } from './OAuthButton';
import { type TAuth } from 'entities/auth';
import { type StartOauthParams } from '../model/types';
import { type Route } from 'next';

export const getRoute = (provider: TAuth.OAuthProvider) => {
const params = new URLSearchParams({
provider,
startOAuth: 'true',
} satisfies Record<keyof StartOauthParams, string>);

return `${routes.auth.oauth()}?${params.toString()}`;
};

export function OAuthLoginButtons({ className }: { className?: string }) {
const { data, isLoading } = useQuery(AuthQueries.getOAuthProviders());

return (
<div className={cn('flex items-center justify-center gap-2', className)}>
{isLoading &&
Array.from({ length: OAUTH_PROVIDERS_COUNT }, (_v, i) => (
<Skeleton className="size-8" key={i} />
))}
{isLoading && Array.from({ length: 3 }, (_v, i) => <Skeleton className="size-8" key={i} />)}

{data?.map((item) => {
const providerConfig = OAUTH_PROVIDERS[item.value];

return (
<Button
asChild
style={{ backgroundColor: providerConfig.color }}
key={item.value}
size={'icon'}
variant={'outline'}
>
<Link href={providerConfig.href as Route}>
<providerConfig.icon className="size-6" />
</Link>
</Button>
);
return <OAuthButton key={item.value} data={item} href={getRoute(item.value) as Route} />;
})}
</div>
);
Expand Down
1 change: 1 addition & 0 deletions src/features/handle-query-params/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { QueryParamsHandler } from './ui/QueryParamsHandler';
30 changes: 30 additions & 0 deletions src/features/handle-query-params/ui/QueryParamsHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';
Comment thread
soorq marked this conversation as resolved.

import { type Route } from 'next';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useRef } from 'react';
import { toast } from 'sonner';

export function QueryParamsHandler() {
const params = useSearchParams();
const isShowToast = useRef(false);
const router = useRouter();

useEffect(() => {
const success = params?.get('success');
const message = params?.get('message');

if (isShowToast.current || !success) return;

if (success === 'true') {
toast.success(message || 'Операция выполнена успешно');
} else if (success === 'false') {
toast.error(message || 'Произошла ошибка. Пожалуйста, попробуйте позже');
}

isShowToast.current = true;
router.replace(location.pathname as Route);
}, [params, router]);

return null;
}
Loading
Loading