From b763690381f171bc89247ae02997f1727a7ec512 Mon Sep 17 00:00:00 2001 From: TeaCoder52 Date: Sat, 5 Apr 2025 12:34:08 +0300 Subject: [PATCH 1/5] feat: update AuthWrapper layout with responsive design enhancements --- src/app/layout.tsx | 2 +- .../account/settings/display-name-form.tsx | 2 +- src/components/auth/auth-wrapper.tsx | 110 ++++++++++-------- src/components/auth/login-form.tsx | 7 +- src/components/auth/new-password-form.tsx | 5 +- src/components/auth/register-form.tsx | 9 +- src/components/auth/reset-password-form.tsx | 5 +- src/components/shared/logo.tsx | 14 ++- 8 files changed, 87 insertions(+), 67 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5d72954..c9d5ecc 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -69,7 +69,7 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: ReactNode }) { return ( - +
diff --git a/src/components/auth/auth-wrapper.tsx b/src/components/auth/auth-wrapper.tsx index e0c7abe..cc2eda3 100644 --- a/src/components/auth/auth-wrapper.tsx +++ b/src/components/auth/auth-wrapper.tsx @@ -1,69 +1,81 @@ import Link from 'next/link' import type { ReactNode } from 'react' -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle -} from '../ui/card' - -interface Props { +interface AuthWrapperProps { children: ReactNode heading: string description?: string - backButtonLabel?: string - backButtonHref?: string + bottomText?: string + bottomLinkText?: string + bottomLinkHref?: string } export function AuthWrapper({ children, heading, description, - backButtonLabel, - backButtonHref -}: Props) { + bottomText, + bottomLinkText, + bottomLinkHref +}: AuthWrapperProps) { return ( -
-
- - - {heading} - {description} - - - {/* {isShowSocial && } */} - {children} - - - {/* {backButtonLabel && backButtonHref && ( - - )} */} -

- Нажимая продолжить, вы соглашаетесь с нашим{' '} - - Пользовательским соглашением - {' '} - и{' '} +

+
+
+
+

{heading}

+

+ {description} +

+
+
{children}
+ {/*

+ Нажимая продолжить, вы соглашаетесь с нашим{' '} + + Пользовательским соглашением + {' '} + и{' '} + + Политикой Конфиденциальности + + . +

*/} + {bottomText && bottomLinkText && bottomLinkHref && ( +

+ {bottomText}{' '} - Политикой Конфиденциальности + {bottomLinkText} - .

- - + )} +
+
+
+ +
+
+
+
+
+
+
+
{' '} + {/*
+ + + +
*/}
) diff --git a/src/components/auth/login-form.tsx b/src/components/auth/login-form.tsx index bfa585c..41eb29d 100644 --- a/src/components/auth/login-form.tsx +++ b/src/components/auth/login-form.tsx @@ -97,8 +97,9 @@ export function LoginForm() {
@@ -111,7 +112,7 @@ export function LoginForm() { Почта diff --git a/src/components/auth/new-password-form.tsx b/src/components/auth/new-password-form.tsx index 914dc97..e7fe024 100644 --- a/src/components/auth/new-password-form.tsx +++ b/src/components/auth/new-password-form.tsx @@ -72,8 +72,9 @@ export function NewPasswordForm() { @@ -97,7 +98,7 @@ export function RegisterForm() { Имя @@ -114,7 +115,7 @@ export function RegisterForm() { Почта diff --git a/src/components/auth/reset-password-form.tsx b/src/components/auth/reset-password-form.tsx index e9fe28c..3ff27cc 100644 --- a/src/components/auth/reset-password-form.tsx +++ b/src/components/auth/reset-password-form.tsx @@ -73,8 +73,9 @@ export function ResetPasswordForm() { @@ -30,7 +34,7 @@ export function Logo({ className = 'size-14' }: Props) { From 58f85171b666e149b330d7077c999a76e3635671 Mon Sep 17 00:00:00 2001 From: TeaCoder52 Date: Sun, 6 Apr 2025 03:07:28 +0300 Subject: [PATCH 2/5] feat: add email verification page layout and start linked accounts page --- src/app/account/integrations/page.tsx | 11 +++ src/app/auth/verify/[token]/page.tsx | 11 +++ .../account/integrations/integrations.tsx | 90 +++++++++++++++++++ src/components/account/progress/progress.tsx | 2 +- .../account/sessions/session-item.tsx | 5 -- src/components/account/sessions/sessions.tsx | 2 +- .../account/settings/email-form.tsx | 34 ++++--- src/components/auth/auth-social.tsx | 22 +++++ src/components/auth/auth-wrapper.tsx | 24 ++--- src/components/auth/login-form.tsx | 3 +- src/components/auth/verify-email.tsx | 17 ++++ src/components/layout/user-navigation.tsx | 13 ++- src/components/shared/captcha.tsx | 26 +++--- src/generated/accountResponse.ts | 18 ++-- src/middleware.ts | 9 ++ 15 files changed, 237 insertions(+), 50 deletions(-) create mode 100644 src/app/account/integrations/page.tsx create mode 100644 src/app/auth/verify/[token]/page.tsx create mode 100644 src/components/account/integrations/integrations.tsx create mode 100644 src/components/auth/auth-social.tsx create mode 100644 src/components/auth/verify-email.tsx diff --git a/src/app/account/integrations/page.tsx b/src/app/account/integrations/page.tsx new file mode 100644 index 0000000..975fbe2 --- /dev/null +++ b/src/app/account/integrations/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from 'next' + +import { Integrations } from '@/src/components/account/integrations/integrations' + +export const metadata: Metadata = { + title: 'Сторонние сервисы' +} + +export default function IntegrationsPage() { + return +} diff --git a/src/app/auth/verify/[token]/page.tsx b/src/app/auth/verify/[token]/page.tsx new file mode 100644 index 0000000..31a70fe --- /dev/null +++ b/src/app/auth/verify/[token]/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from 'next' + +import { VerifyEmail } from '@/src/components/auth/verify-email' + +export const metadata: Metadata = { + title: 'Верификация почты' +} + +export default async function VerifyEmailPage() { + return +} diff --git a/src/components/account/integrations/integrations.tsx b/src/components/account/integrations/integrations.tsx new file mode 100644 index 0000000..a1b0d5a --- /dev/null +++ b/src/components/account/integrations/integrations.tsx @@ -0,0 +1,90 @@ +'use client' + +import { FaGithub, FaGoogle, FaTelegram, FaYandex } from 'react-icons/fa6' + +import { Heading } from '../../shared/heading' +import { Button } from '../../ui/button' +import { Card, CardContent } from '../../ui/card' + +export function Integrations() { + return ( +
+
+ +
+ + +
+
+ +
+
+

Google

+

+ Set up a single sign-on with Google and + login with one click. +

+
+
+ +
+
+ + +
+
+ +
+
+

Github

+

+ Set up a single sign-on with Github and + login with one click. +

+
+
+ +
+
+ + +
+
+ +
+
+

Yandex

+

+ Set up a single sign-on with Yandex and + login with one click. +

+
+
+ +
+
+ + +
+
+ +
+
+

Telegram

+

+ Set up a single sign-on with Telegram + and login with one click. +

+
+
+ +
+
+
+
+
+ ) +} diff --git a/src/components/account/progress/progress.tsx b/src/components/account/progress/progress.tsx index 80b3587..1fb05e6 100644 --- a/src/components/account/progress/progress.tsx +++ b/src/components/account/progress/progress.tsx @@ -23,7 +23,7 @@ export function Progress() {
diff --git a/src/components/account/settings/email-form.tsx b/src/components/account/settings/email-form.tsx index 6ddd7f6..a3ac5e6 100644 --- a/src/components/account/settings/email-form.tsx +++ b/src/components/account/settings/email-form.tsx @@ -1,11 +1,12 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { Mail } from 'lucide-react' +import { Check, CheckCircle, Mail, MoreHorizontal, Pencil } from 'lucide-react' import { useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' +import { Badge } from '../../ui/badge' import { Button } from '../../ui/button' import { Dialog, @@ -17,6 +18,13 @@ import { DialogTitle, DialogTrigger } from '../../ui/dialog' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger +} from '../../ui/dropdown-menu' import { Form, FormControl, @@ -82,11 +90,11 @@ export function EmailForm({ user }: EmailFormProps) {

Почта

- {/* {user?.isEmailVerified ? ( + {user?.isEmailVerified ? ( Подтверждёна ) : ( Не подтверждёна - )} */} + )}

Ваша учетная запись привязана к адресу{' '} @@ -99,7 +107,7 @@ export function EmailForm({ user }: EmailFormProps) {

- {/* +
) diff --git a/src/components/auth/auth-social.tsx b/src/components/auth/auth-social.tsx new file mode 100644 index 0000000..c3d849d --- /dev/null +++ b/src/components/auth/auth-social.tsx @@ -0,0 +1,22 @@ +import { FaGithub, FaGoogle, FaTelegram, FaYandex } from 'react-icons/fa6' + +import { Button } from '../ui/button' + +export function AuthSocial() { + return ( +
+ + + + +
+ ) +} diff --git a/src/components/auth/auth-wrapper.tsx b/src/components/auth/auth-wrapper.tsx index cc2eda3..76eb6b5 100644 --- a/src/components/auth/auth-wrapper.tsx +++ b/src/components/auth/auth-wrapper.tsx @@ -1,6 +1,8 @@ import Link from 'next/link' import type { ReactNode } from 'react' +import { AuthSocial } from './auth-social' + interface AuthWrapperProps { children: ReactNode heading: string @@ -8,6 +10,7 @@ interface AuthWrapperProps { bottomText?: string bottomLinkText?: string bottomLinkHref?: string + isShowSocial?: boolean } export function AuthWrapper({ @@ -16,7 +19,8 @@ export function AuthWrapper({ description, bottomText, bottomLinkText, - bottomLinkHref + bottomLinkHref, + isShowSocial }: AuthWrapperProps) { return (
@@ -24,10 +28,13 @@ export function AuthWrapper({

{heading}

-

- {description} -

+ {description && ( +

+ {description} +

+ )}
+ {isShowSocial && }
{children}
{/*

Нажимая продолжить, вы соглашаетесь с нашим{' '} @@ -53,7 +60,7 @@ export function AuthWrapper({ {bottomText}{' '} {bottomLinkText} @@ -70,12 +77,7 @@ export function AuthWrapper({

-
{' '} - {/*
- - - -
*/} +
) diff --git a/src/components/auth/login-form.tsx b/src/components/auth/login-form.tsx index 41eb29d..5156ce4 100644 --- a/src/components/auth/login-form.tsx +++ b/src/components/auth/login-form.tsx @@ -100,6 +100,7 @@ export function LoginForm() { bottomText='Еще нет аккаунта?' bottomLinkText='Регистрация' bottomLinkHref='/auth/register' + isShowSocial >
@@ -151,7 +152,7 @@ export function LoginForm() { control={form.control} name='captcha' render={({ field }) => ( - + diff --git a/src/components/auth/verify-email.tsx b/src/components/auth/verify-email.tsx new file mode 100644 index 0000000..abc8a36 --- /dev/null +++ b/src/components/auth/verify-email.tsx @@ -0,0 +1,17 @@ +import { Button } from '../ui/button' + +import { AuthWrapper } from './auth-wrapper' + +export function VerifyEmail() { + return ( + +

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Velit, + est? +

+ +
+ ) +} diff --git a/src/components/layout/user-navigation.tsx b/src/components/layout/user-navigation.tsx index f1d2292..3b8c104 100644 --- a/src/components/layout/user-navigation.tsx +++ b/src/components/layout/user-navigation.tsx @@ -1,6 +1,12 @@ 'use client' -import { ChartArea, MonitorSmartphone, Route, Settings } from 'lucide-react' +import { + ChartArea, + MonitorSmartphone, + Plug, + Route, + Settings +} from 'lucide-react' import Link from 'next/link' import { usePathname } from 'next/navigation' @@ -19,6 +25,11 @@ export const links = [ href: '/account/settings', icon: Settings }, + { + title: 'Сторонние сервисы', + href: '/account/integrations', + icon: Plug + }, { title: 'Устройства', href: '/account/sessions', diff --git a/src/components/shared/captcha.tsx b/src/components/shared/captcha.tsx index 5bb1e2f..bfc6038 100644 --- a/src/components/shared/captcha.tsx +++ b/src/components/shared/captcha.tsx @@ -9,17 +9,19 @@ export function Captcha({ onVerify, ...props }: CaptchaProps) { const { resolvedTheme } = useTheme() return ( - +
+ +
) } diff --git a/src/generated/accountResponse.ts b/src/generated/accountResponse.ts index aaf6972..7b17963 100644 --- a/src/generated/accountResponse.ts +++ b/src/generated/accountResponse.ts @@ -7,12 +7,14 @@ */ export interface AccountResponse { - /** Unique identifier */ - id: string; - /** Display name */ - displayName: string; - /** Email address */ - email: string; - /** Identifier of the user avatar */ - avatar: string; + /** Unique identifier */ + id: string + /** Display name */ + displayName: string + /** Email address */ + email: string + /** Identifier of the user avatar */ + avatar: string + /** Indicates whether the user has verified their email address */ + isEmailVerified: boolean } diff --git a/src/middleware.ts b/src/middleware.ts index 3d2b157..f80828e 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -4,7 +4,16 @@ export default async function middleware(request: NextRequest) { const { cookies, url } = request const token = cookies.get('token')?.value + const isAuthPage = url.includes('/auth') + const isVerifyPage = url.includes('/auth/verify') + + if (isVerifyPage) { + if (!token) { + return NextResponse.redirect(new URL('/auth/login', url)) + } + return NextResponse.next() + } if (isAuthPage) { if (token) { From 57101e3a030e909797a8a07e1832f991a8a978c3 Mon Sep 17 00:00:00 2001 From: TeaCoder52 Date: Fri, 18 Apr 2025 07:04:40 +0300 Subject: [PATCH 3/5] feat: add social login and unlinking logic for Google and GitHub --- orval.config.ts | 2 +- src/api/external.ts | 24 ++++++ src/app/auth/callback/page.tsx | 26 +++++++ .../account/integrations/integrations.tsx | 77 +++++++++++++------ .../account/integrations/unlink-provider.tsx | 49 ++++++++++++ src/components/auth/auth-social.tsx | 49 +++++++++--- src/components/layout/user-navigation.tsx | 18 ++--- src/components/shared/captcha.tsx | 27 ++++--- src/generated/accountResponse.ts | 20 ++--- src/generated/activeRestrictionResponse.ts | 16 ++-- src/generated/externalConnectResponse.ts | 12 +++ .../externalControllerCallbackParams.ts | 11 +++ src/generated/externalStatusResponse.ts | 14 ++++ src/generated/index.ts | 6 ++ src/generated/registrationsResponse.ts | 14 ++++ .../sessionControllerLoginAdmin200.ts | 11 +++ src/generated/statisticsResponse.ts | 18 +++++ src/generated/userResponse.ts | 19 ++++- 18 files changed, 331 insertions(+), 82 deletions(-) create mode 100644 src/api/external.ts create mode 100644 src/app/auth/callback/page.tsx create mode 100644 src/components/account/integrations/unlink-provider.tsx create mode 100644 src/generated/externalConnectResponse.ts create mode 100644 src/generated/externalControllerCallbackParams.ts create mode 100644 src/generated/externalStatusResponse.ts create mode 100644 src/generated/registrationsResponse.ts create mode 100644 src/generated/sessionControllerLoginAdmin200.ts create mode 100644 src/generated/statisticsResponse.ts diff --git a/orval.config.ts b/orval.config.ts index 1531c29..650f668 100644 --- a/orval.config.ts +++ b/orval.config.ts @@ -5,7 +5,7 @@ config({ path: '.env' }) export default defineConfig({ client: { - input: `${process.env['API_DOCS']}/openapi.json`, + input: process.env['API_DOCS'], output: { schemas: './src/generated' } diff --git a/src/api/external.ts b/src/api/external.ts new file mode 100644 index 0000000..2cea826 --- /dev/null +++ b/src/api/external.ts @@ -0,0 +1,24 @@ +import type { + ExternalConnectResponse, + ExternalStatusResponse +} from '../generated' + +import { instance } from './instance' + +export const getAuthUrl = async (provider: 'google' | 'github') => + await instance + .post(`/auth/external/login/${provider}`) + .then(response => response.data) + +export const fetchExternalStatus = async () => + await instance + .get('/auth/external') + .then(response => response.data) + +export const getConnectUrl = async (provider: 'google' | 'github') => + await instance + .post(`/auth/external/connect/${provider}`) + .then(response => response.data) + +export const unlinkAccount = async (provider: 'google' | 'github') => + await instance.delete(`/auth/external/${provider}`) diff --git a/src/app/auth/callback/page.tsx b/src/app/auth/callback/page.tsx new file mode 100644 index 0000000..4925471 --- /dev/null +++ b/src/app/auth/callback/page.tsx @@ -0,0 +1,26 @@ +'use client' + +import { useRouter, useSearchParams } from 'next/navigation' +import { useEffect } from 'react' + +import { instance } from '@/src/api' +import { setSessionToken } from '@/src/lib/cookies/session' + +export default function AuthCallbackPage() { + const router = useRouter() + const searchParams = useSearchParams() + + useEffect(() => { + const token = searchParams.get('token') + + if (typeof token === 'string') { + setSessionToken(token) + + instance.defaults.headers['X-Session-Token'] = token + + router.push('/account/settings') + } + }, [router]) + + return
Пожалуйста, подождите, выполняется вход...
+} diff --git a/src/components/account/integrations/integrations.tsx b/src/components/account/integrations/integrations.tsx index a1b0d5a..8529076 100644 --- a/src/components/account/integrations/integrations.tsx +++ b/src/components/account/integrations/integrations.tsx @@ -1,12 +1,40 @@ 'use client' +import { useMutation, useQuery } from '@tanstack/react-query' +import { useRouter } from 'next/navigation' import { FaGithub, FaGoogle, FaTelegram, FaYandex } from 'react-icons/fa6' +import { toast } from 'sonner' import { Heading } from '../../shared/heading' import { Button } from '../../ui/button' import { Card, CardContent } from '../../ui/card' +import { UnlinkProvider } from './unlink-provider' +import { fetchExternalStatus, getConnectUrl } from '@/src/api/external' + export function Integrations() { + const router = useRouter() + + const { data, isLoading } = useQuery({ + queryKey: ['fetch external status'], + queryFn: () => fetchExternalStatus() + }) + + const { mutate, isPending } = useMutation({ + mutationKey: ['connect external account'], + mutationFn: (provider: 'google' | 'github') => getConnectUrl(provider), + onSuccess(data) { + router.push(data.url) + }, + onError(error: any) { + toast.error( + error.response?.data?.message ?? 'Ошибка при подключении' + ) + } + }) + + if (isLoading) return
Loading status...
+ return (
@@ -29,7 +57,17 @@ export function Integrations() {

- + {data?.google ? ( + + ) : ( + + )} @@ -46,10 +84,20 @@ export function Integrations() {

- + {data?.github ? ( + + ) : ( + + )}
- + {/*
@@ -63,26 +111,11 @@ export function Integrations() {

- +
-
- - -
-
- -
-
-

Telegram

-

- Set up a single sign-on with Telegram - and login with one click. -

-
-
- -
-
+
*/} diff --git a/src/components/account/integrations/unlink-provider.tsx b/src/components/account/integrations/unlink-provider.tsx new file mode 100644 index 0000000..e79fa24 --- /dev/null +++ b/src/components/account/integrations/unlink-provider.tsx @@ -0,0 +1,49 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useState } from 'react' +import { toast } from 'sonner' + +import { ConfirmDialog } from '../../shared/confirm-dialog' +import { Button } from '../../ui/button' + +import { unlinkAccount } from '@/src/api/external' + +interface UnlinkProviderProps { + provider: 'google' | 'github' +} + +export function UnlinkProvider({ provider }: UnlinkProviderProps) { + const [isOpen, setIsOpen] = useState(false) + + const queryClient = useQueryClient() + + const { mutate, isPending } = useMutation({ + mutationKey: ['unlink account'], + mutationFn: () => unlinkAccount(provider), + onSuccess() { + queryClient.invalidateQueries({ + queryKey: ['fetch external status'] + }) + setIsOpen(false) + }, + onError(error: any) { + toast.error( + error.response?.data?.message ?? 'Ошибка при отключении' + ) + } + }) + + return ( + mutate()} + isLoading={isPending} + open={isOpen} + onOpenChange={setIsOpen} + > + + + ) +} diff --git a/src/components/auth/auth-social.tsx b/src/components/auth/auth-social.tsx index c3d849d..d0126d9 100644 --- a/src/components/auth/auth-social.tsx +++ b/src/components/auth/auth-social.tsx @@ -1,22 +1,49 @@ -import { FaGithub, FaGoogle, FaTelegram, FaYandex } from 'react-icons/fa6' +'use client' + +import { useMutation } from '@tanstack/react-query' +import { useRouter } from 'next/navigation' +import { FaGithub } from 'react-icons/fa6' +import { FcGoogle } from 'react-icons/fc' +import { toast } from 'sonner' import { Button } from '../ui/button' +import { getAuthUrl } from '@/src/api/external' + export function AuthSocial() { + const router = useRouter() + + const { mutate, isPending } = useMutation({ + mutationKey: ['oauth login'], + mutationFn: (provider: 'google' | 'github') => getAuthUrl(provider), + onSuccess(data) { + router.push(data.url) + }, + onError(error: any) { + toast.error( + error.response?.data?.message ?? 'Ошибка при создании URL' + ) + } + }) + return ( -
- - - -
) } diff --git a/src/components/layout/user-navigation.tsx b/src/components/layout/user-navigation.tsx index 3b8c104..572a076 100644 --- a/src/components/layout/user-navigation.tsx +++ b/src/components/layout/user-navigation.tsx @@ -1,12 +1,6 @@ 'use client' -import { - ChartArea, - MonitorSmartphone, - Plug, - Route, - Settings -} from 'lucide-react' +import { ChartArea, MonitorSmartphone, Settings, Users } from 'lucide-react' import Link from 'next/link' import { usePathname } from 'next/navigation' @@ -25,15 +19,15 @@ export const links = [ href: '/account/settings', icon: Settings }, - { - title: 'Сторонние сервисы', - href: '/account/integrations', - icon: Plug - }, { title: 'Устройства', href: '/account/sessions', icon: MonitorSmartphone + }, + { + title: 'Связанные аккаунты', + href: '/account/integrations', + icon: Users } ] diff --git a/src/components/shared/captcha.tsx b/src/components/shared/captcha.tsx index bfc6038..1bdc15b 100644 --- a/src/components/shared/captcha.tsx +++ b/src/components/shared/captcha.tsx @@ -1,4 +1,5 @@ import { useTheme } from 'next-themes' +import { useEffect } from 'react' import Turnstile, { type TurnstileProps } from 'react-turnstile' interface CaptchaProps extends Omit { @@ -9,19 +10,17 @@ export function Captcha({ onVerify, ...props }: CaptchaProps) { const { resolvedTheme } = useTheme() return ( -
- -
+ ) } diff --git a/src/generated/accountResponse.ts b/src/generated/accountResponse.ts index 7b17963..0de67da 100644 --- a/src/generated/accountResponse.ts +++ b/src/generated/accountResponse.ts @@ -7,14 +7,14 @@ */ export interface AccountResponse { - /** Unique identifier */ - id: string - /** Display name */ - displayName: string - /** Email address */ - email: string - /** Identifier of the user avatar */ - avatar: string - /** Indicates whether the user has verified their email address */ - isEmailVerified: boolean + /** Unique identifier */ + id: string; + /** Display name */ + displayName: string; + /** Email address */ + email: string; + /** Identifier of the user avatar */ + avatar: string; + /** Indicates whether the user has verified their email address */ + isEmailVerified: boolean; } diff --git a/src/generated/activeRestrictionResponse.ts b/src/generated/activeRestrictionResponse.ts index 5e63626..850fb1f 100644 --- a/src/generated/activeRestrictionResponse.ts +++ b/src/generated/activeRestrictionResponse.ts @@ -7,12 +7,12 @@ */ export interface ActiveRestrictionResponse { - /** Date of restriction creation */ - createdAt: string - /** Reason for the user restriction */ - reason: string - /** End date of the restriction, if temporary */ - until?: string - /** Information on whether the ban is permanent */ - isPermanent: boolean + /** Date of restriction creation */ + createdAt: string; + /** Reason for the user restriction */ + reason: string; + /** End date of the restriction, if temporary */ + until?: string; + /** Information on whether the ban is permanent */ + isPermanent: boolean; } diff --git a/src/generated/externalConnectResponse.ts b/src/generated/externalConnectResponse.ts new file mode 100644 index 0000000..75ee019 --- /dev/null +++ b/src/generated/externalConnectResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * TeaCoder API + * API for Teacoder educational platform + * OpenAPI spec version: 1.0.0 + */ + +export interface ExternalConnectResponse { + /** The URL for authorization via the external provider (e.g., Google, GitHub) */ + url: string; +} diff --git a/src/generated/externalControllerCallbackParams.ts b/src/generated/externalControllerCallbackParams.ts new file mode 100644 index 0000000..b5d879c --- /dev/null +++ b/src/generated/externalControllerCallbackParams.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * TeaCoder API + * API for Teacoder educational platform + * OpenAPI spec version: 1.0.0 + */ + +export type ExternalControllerCallbackParams = { +code: string; +}; diff --git a/src/generated/externalStatusResponse.ts b/src/generated/externalStatusResponse.ts new file mode 100644 index 0000000..f4a941f --- /dev/null +++ b/src/generated/externalStatusResponse.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * TeaCoder API + * API for Teacoder educational platform + * OpenAPI spec version: 1.0.0 + */ + +export interface ExternalStatusResponse { + /** Indicates whether the GitHub account is connected */ + github: boolean; + /** Indicates whether the Google account is connected */ + google: boolean; +} diff --git a/src/generated/index.ts b/src/generated/index.ts index 33b4cbb..6e969ad 100644 --- a/src/generated/index.ts +++ b/src/generated/index.ts @@ -22,6 +22,9 @@ export * from './createRestrictionRequest'; export * from './createRestrictionRequestReason'; export * from './createUserRequest'; export * from './createUserResponse'; +export * from './externalConnectResponse'; +export * from './externalControllerCallbackParams'; +export * from './externalStatusResponse'; export * from './lastLessonResponse'; export * from './leaderResponse'; export * from './lessonResponse'; @@ -37,9 +40,12 @@ export * from './mfaVerifyRequest'; export * from './passwordResetRequest'; export * from './patchUserRequest'; export * from './progressResponse'; +export * from './registrationsResponse'; export * from './sendPasswordResetRequest'; export * from './sessionControllerLogin200'; +export * from './sessionControllerLoginAdmin200'; export * from './sessionResponse'; +export * from './statisticsResponse'; export * from './totpDisableRequest'; export * from './totpEnableRequest'; export * from './totpGenerateSecretResponse'; diff --git a/src/generated/registrationsResponse.ts b/src/generated/registrationsResponse.ts new file mode 100644 index 0000000..5f665ac --- /dev/null +++ b/src/generated/registrationsResponse.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * TeaCoder API + * API for Teacoder educational platform + * OpenAPI spec version: 1.0.0 + */ + +export interface RegistrationsResponse { + /** Date of user registrations in YYYY-MM-DD format */ + date: string; + /** Number of users registered on the given date */ + users: number; +} diff --git a/src/generated/sessionControllerLoginAdmin200.ts b/src/generated/sessionControllerLoginAdmin200.ts new file mode 100644 index 0000000..f8cabb1 --- /dev/null +++ b/src/generated/sessionControllerLoginAdmin200.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * TeaCoder API + * API for Teacoder educational platform + * OpenAPI spec version: 1.0.0 + */ +import type { LoginSessionResponse } from './loginSessionResponse'; +import type { LoginMfaResponse } from './loginMfaResponse'; + +export type SessionControllerLoginAdmin200 = LoginSessionResponse | LoginMfaResponse; diff --git a/src/generated/statisticsResponse.ts b/src/generated/statisticsResponse.ts new file mode 100644 index 0000000..07a22c6 --- /dev/null +++ b/src/generated/statisticsResponse.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * TeaCoder API + * API for Teacoder educational platform + * OpenAPI spec version: 1.0.0 + */ + +export interface StatisticsResponse { + /** Total number of users */ + users: number; + /** Total number of courses */ + courses: number; + /** Total number of views across all course */ + views: number; + /** Total number of lessons */ + lessons: number; +} diff --git a/src/generated/userResponse.ts b/src/generated/userResponse.ts index eaa0b6e..49fa43a 100644 --- a/src/generated/userResponse.ts +++ b/src/generated/userResponse.ts @@ -9,10 +9,21 @@ export interface UserResponse { /** Unique identifier */ id: string; - /** Display name */ - displayName: string; + /** Account creation date */ + createdAt: string; /** Email address */ email: string; - /** Identifier of the user avatar */ - avatar: string; + /** Username */ + username: string; + /** Display name */ + displayName: string; + /** + * Identifier of the user avatar + * @nullable + */ + avatar: string | null; + /** Indicates whether the user is banned */ + isBanned: boolean; + /** Indicates whether multi-factor authentication is enabled */ + isMfaEnabled: boolean; } From 57e7c3288a4b358929e68c5439c77def67626bce Mon Sep 17 00:00:00 2001 From: TeaCoder52 Date: Mon, 28 Apr 2025 07:38:38 +0300 Subject: [PATCH 4/5] refactor: extract routes to a separate file and rename integrations page to connections --- .../(public)/document/privacy-policy/page.tsx | 6 +++-- .../(public)/document/terms-of-use/page.tsx | 6 +++-- src/app/account/connections/page.tsx | 11 ++++++++ src/app/account/integrations/page.tsx | 11 -------- src/app/auth/callback/page.tsx | 15 ++++++----- src/app/layout.tsx | 2 +- src/app/loading.tsx | 9 +++++++ src/app/not-found.tsx | 3 ++- .../connections.tsx} | 2 +- .../unlink-provider.tsx | 0 .../account/progress/courses-tab.tsx | 5 +++- src/components/auth/login-form.tsx | 5 ++-- src/components/auth/new-password-form.tsx | 3 ++- src/components/auth/register-form.tsx | 3 ++- src/components/auth/reset-password-form.tsx | 3 ++- src/components/course/course-card.tsx | 3 ++- src/components/course/course-lessons.tsx | 7 +++--- src/components/course/course-summary.tsx | 5 ++-- src/components/home/hero.tsx | 4 ++- src/components/layout/footer.tsx | 10 +++++--- src/components/layout/header.tsx | 7 +++--- src/components/layout/mobile-nav.tsx | 9 ++++--- src/components/layout/nav-links.tsx | 6 +++-- src/components/layout/user-menu.tsx | 5 ++-- src/components/layout/user-navigation.tsx | 13 +++++----- src/components/lesson/lesson-sidebar.tsx | 7 +++--- src/components/shared/captcha.tsx | 4 +++ src/constants/index.ts | 1 + src/constants/routes.ts | 25 +++++++++++++++++++ 29 files changed, 129 insertions(+), 61 deletions(-) create mode 100644 src/app/account/connections/page.tsx delete mode 100644 src/app/account/integrations/page.tsx create mode 100644 src/app/loading.tsx rename src/components/account/{integrations/integrations.tsx => connections/connections.tsx} (99%) rename src/components/account/{integrations => connections}/unlink-provider.tsx (100%) create mode 100644 src/constants/routes.ts diff --git a/src/app/(public)/document/privacy-policy/page.tsx b/src/app/(public)/document/privacy-policy/page.tsx index 72a314d..c42d7e7 100644 --- a/src/app/(public)/document/privacy-policy/page.tsx +++ b/src/app/(public)/document/privacy-policy/page.tsx @@ -1,6 +1,8 @@ import type { Metadata } from 'next' import Link from 'next/link' +import { ROUTES } from '@/src/constants' + export const metadata: Metadata = { title: 'Политика конфиденциальности' } @@ -29,7 +31,7 @@ export default function PrivacyPage() { Пользовательского соглашения (далее — «Соглашение»), размещённого и/или доступного в сети Интернет по адресу:{' '} - + https://teacoder.ru/document/terms-of-use , а также иных заключаемых с Пользователем @@ -289,7 +291,7 @@ export default function PrivacyPage() {

6.2. Действующая редакция Политики находится на Сайте в сети Интернет по адресу - + https://teacoder.ru/document/privacy-policy . diff --git a/src/app/(public)/document/terms-of-use/page.tsx b/src/app/(public)/document/terms-of-use/page.tsx index 02d640c..23872a1 100644 --- a/src/app/(public)/document/terms-of-use/page.tsx +++ b/src/app/(public)/document/terms-of-use/page.tsx @@ -1,6 +1,8 @@ import type { Metadata } from 'next' import Link from 'next/link' +import { ROUTES } from '@/src/constants' + export const metadata: Metadata = { title: 'Пользовательское соглашение' } @@ -96,7 +98,7 @@ export default function AgreementPage() { 1.4.1. Политики конфиденциальности, размещённой и/или доступной на Сайте в сети Интернет по адресу{' '} - + https://teacoder.ru/document/privacy-policy . @@ -129,7 +131,7 @@ export default function AgreementPage() {

1.6. Действующая редакция Соглашения находится на Сайте в сети Интернет по адресу - + https://teacoder.ru/document/terms-of-use . diff --git a/src/app/account/connections/page.tsx b/src/app/account/connections/page.tsx new file mode 100644 index 0000000..0c274ff --- /dev/null +++ b/src/app/account/connections/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from 'next' + +import { Connections } from '@/src/components/account/connections/connections' + +export const metadata: Metadata = { + title: 'Сторонние сервисы' +} + +export default function ConnectionsPage() { + return +} diff --git a/src/app/account/integrations/page.tsx b/src/app/account/integrations/page.tsx deleted file mode 100644 index 975fbe2..0000000 --- a/src/app/account/integrations/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { Metadata } from 'next' - -import { Integrations } from '@/src/components/account/integrations/integrations' - -export const metadata: Metadata = { - title: 'Сторонние сервисы' -} - -export default function IntegrationsPage() { - return -} diff --git a/src/app/auth/callback/page.tsx b/src/app/auth/callback/page.tsx index 4925471..90cd515 100644 --- a/src/app/auth/callback/page.tsx +++ b/src/app/auth/callback/page.tsx @@ -4,23 +4,26 @@ import { useRouter, useSearchParams } from 'next/navigation' import { useEffect } from 'react' import { instance } from '@/src/api' +import { EllipsisLoader } from '@/src/components/shared/ellipsis-loader' import { setSessionToken } from '@/src/lib/cookies/session' export default function AuthCallbackPage() { const router = useRouter() - const searchParams = useSearchParams() useEffect(() => { - const token = searchParams.get('token') + const hash = window.location.hash + const token = new URLSearchParams(hash.slice(1)).get('token') - if (typeof token === 'string') { + if (token) { setSessionToken(token) - instance.defaults.headers['X-Session-Token'] = token - router.push('/account/settings') } }, [router]) - return

Пожалуйста, подождите, выполняется вход...
+ return ( +
+ +
+ ) } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c9d5ecc..858c95d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -73,7 +73,7 @@ export default function RootLayout({ children }: { children: ReactNode }) { diff --git a/src/app/loading.tsx b/src/app/loading.tsx new file mode 100644 index 0000000..604befc --- /dev/null +++ b/src/app/loading.tsx @@ -0,0 +1,9 @@ +import { EllipsisLoader } from '../components/shared/ellipsis-loader' + +export default function LoadingPage() { + return ( +
+ +
+ ) +} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 211f231..b3b2926 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -3,6 +3,7 @@ import type { Metadata } from 'next' import Link from 'next/link' import { Button } from '../components/ui/button' +import { ROUTES } from '../constants/routes' export const metadata: Metadata = { title: 'Страница не найдена' @@ -18,7 +19,7 @@ export default function NotFoundPage() {

diff --git a/src/components/layout/mobile-nav.tsx b/src/components/layout/mobile-nav.tsx index 45ff30c..39c9d28 100644 --- a/src/components/layout/mobile-nav.tsx +++ b/src/components/layout/mobile-nav.tsx @@ -17,6 +17,7 @@ import { import { navLinks } from './nav-links' import { logout } from '@/src/api' +import { ROUTES } from '@/src/constants' import { useAuth } from '@/src/hooks' export function MobileNav() { @@ -113,23 +114,23 @@ export function MobileNav() { <> )} diff --git a/src/components/layout/nav-links.tsx b/src/components/layout/nav-links.tsx index 6cc9a89..18881a1 100644 --- a/src/components/layout/nav-links.tsx +++ b/src/components/layout/nav-links.tsx @@ -2,14 +2,16 @@ import Link from 'next/link' import { Button } from '../ui/button' +import { ROUTES } from '@/src/constants' + interface NavLink { title: string href: string } export const navLinks: NavLink[] = [ - { title: 'Курсы', href: '/courses' }, - { title: 'Об основателе', href: '/about' } + { title: 'Курсы', href: ROUTES.courses }, + { title: 'Об основателе', href: ROUTES.about } ] export function NavLinks() { diff --git a/src/components/layout/user-menu.tsx b/src/components/layout/user-menu.tsx index 066d62a..d831709 100644 --- a/src/components/layout/user-menu.tsx +++ b/src/components/layout/user-menu.tsx @@ -19,6 +19,7 @@ import { } from '../ui/dropdown-menu' import { logout } from '@/src/api' +import { ROUTES } from '@/src/constants' import { useCurrent } from '@/src/hooks/use-current' import { getMediaSource } from '@/src/lib/utils' @@ -70,13 +71,13 @@ export function UserMenu() { - + Мой прогресс - + Настройки diff --git a/src/components/layout/user-navigation.tsx b/src/components/layout/user-navigation.tsx index 572a076..cfccdf3 100644 --- a/src/components/layout/user-navigation.tsx +++ b/src/components/layout/user-navigation.tsx @@ -1,33 +1,34 @@ 'use client' -import { ChartArea, MonitorSmartphone, Settings, Users } from 'lucide-react' +import { ChartArea, LinkIcon, MonitorSmartphone, Settings } from 'lucide-react' import Link from 'next/link' import { usePathname } from 'next/navigation' import { buttonVariants } from '../ui/button' +import { ROUTES } from '@/src/constants' import { cn } from '@/src/lib/utils' export const links = [ { title: 'Мой прогресс', - href: '/account', + href: ROUTES.progress, icon: ChartArea }, { title: 'Настройки аккаунта', - href: '/account/settings', + href: ROUTES.settings, icon: Settings }, { title: 'Устройства', - href: '/account/sessions', + href: ROUTES.connections, icon: MonitorSmartphone }, { title: 'Связанные аккаунты', - href: '/account/integrations', - icon: Users + href: ROUTES.connections, + icon: LinkIcon } ] diff --git a/src/components/lesson/lesson-sidebar.tsx b/src/components/lesson/lesson-sidebar.tsx index 8870f6f..ebc4ebc 100644 --- a/src/components/lesson/lesson-sidebar.tsx +++ b/src/components/lesson/lesson-sidebar.tsx @@ -14,6 +14,7 @@ import { CourseProgress } from '../shared/course-progress' import { buttonVariants } from '../ui/button' import { ScrollArea } from '../ui/scroll-area' +import { ROUTES } from '@/src/constants' import type { CourseResponse, LessonResponse } from '@/src/generated' import { cn } from '@/src/lib/utils' @@ -38,7 +39,7 @@ export function LessonSidebar({
@@ -70,7 +71,7 @@ export function LessonSidebar({ lesson.id ) const isActive = - pathname === `/lesson/${lesson.slug}` + pathname === ROUTES.lesson(lesson.slug) const Icon = isActive ? PauseCircle @@ -81,7 +82,7 @@ export function LessonSidebar({ return ( ) diff --git a/src/constants/index.ts b/src/constants/index.ts index 1bc33d4..a7e12d1 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,2 +1,3 @@ export * from './app' export * from './seo' +export * from './routes' diff --git a/src/constants/routes.ts b/src/constants/routes.ts new file mode 100644 index 0000000..a985154 --- /dev/null +++ b/src/constants/routes.ts @@ -0,0 +1,25 @@ +export const ROUTES = { + // Common + home: '/', + about: '/about', + + // Auth + login: '/auth/login', + register: '/auth/register', + recovery: '/auth/recovery', + + // Documents + privacy: '/document/privacy-policy', + terms: '/document/terms-of-use', + + // Courses + courses: '/courses', + course: (slug: string) => `/courses/${slug}`, + lesson: (slug: string) => `/lesson/${slug}`, + + // Account + progress: '/account', + settings: '/account/settings', + sessions: '/account/sessions', + connections: '/account/connections' +} From 584124b7c8552530a63c1e3f925e219b0023719b Mon Sep 17 00:00:00 2001 From: TeaCoder52 Date: Tue, 6 May 2025 11:31:12 +0300 Subject: [PATCH 5/5] feat: improve responsiveness and add email verification --- LICENSE | 661 ++++++++++++++++++ bun.lockb | Bin 265276 -> 270340 bytes package.json | 146 ++-- public/opengraph.png | Bin 0 -> 7935 bytes src/api/account.ts | 5 + src/app/layout.tsx | 4 +- .../account/connections/connection-error.tsx | 79 +++ .../account/connections/connections.tsx | 154 ++-- .../account/settings/email-form.tsx | 37 +- .../account/settings/password-form.tsx | 48 +- .../account/settings/two-step-auth-form.tsx | 2 +- src/components/auth/auth-wrapper.tsx | 2 +- src/components/auth/verify-email.tsx | 35 +- src/components/layout/footer.tsx | 11 +- src/components/layout/user-navigation.tsx | 2 +- src/components/ui/switch.tsx | 33 + src/generated/changePasswordRequest.ts | 8 +- .../externalControllerCallbackParams.ts | 5 +- src/generated/mfaStatusResponse.ts | 2 - src/hooks/use-auth.ts | 15 +- 20 files changed, 1028 insertions(+), 221 deletions(-) create mode 100644 LICENSE create mode 100644 public/opengraph.png create mode 100644 src/components/account/connections/connection-error.tsx create mode 100644 src/components/ui/switch.tsx diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..edfaadd --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + Frontend + Copyright (C) 2024 TEACODER + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/bun.lockb b/bun.lockb index 091407dd5ab717e970e09bbf8e8b33e30468d59c..48ba82b686183cab35d5a26f865a44b582af8e68 100644 GIT binary patch delta 48202 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 { return response.data } +export const sendEmailVerification = () => instance.post('/auth/account/verify') + +export const verifyEmail = (code: string) => + instance.post(`/auth/account/verify/${code}`) + export const sendPasswordReset = (data: SendPasswordResetRequest) => api.post('/auth/account/reset_password', data) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 858c95d..01962e2 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -42,7 +42,7 @@ export const metadata: Metadata = { locale: 'ru_RU', images: [ { - url: new URL(APP_CONFIG.baseUrl + '/touch-icons/512x512.png'), + url: new URL(APP_CONFIG.baseUrl + '/opengraph.png'), width: 512, height: 512, alt: SEO.name @@ -56,7 +56,7 @@ export const metadata: Metadata = { description: SEO.description, images: [ { - url: new URL(APP_CONFIG.baseUrl + '/touch-icons/512x512.png'), + url: new URL(APP_CONFIG.baseUrl + '/opengraph.png'), width: 512, height: 512, alt: SEO.name diff --git a/src/components/account/connections/connection-error.tsx b/src/components/account/connections/connection-error.tsx new file mode 100644 index 0000000..d48b6eb --- /dev/null +++ b/src/components/account/connections/connection-error.tsx @@ -0,0 +1,79 @@ +'use client' + +import { AlertCircle, AlertTriangle } from 'lucide-react' +import { useSearchParams } from 'next/navigation' +import { useEffect, useState } from 'react' + +import { Button } from '../../ui/button' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle +} from '../../ui/card' + +export function ConnectionError() { + const searchParams = useSearchParams() + const [isVisible, setIsVisible] = useState(false) + const [errorInfo, setErrorInfo] = useState<{ + title: string + description: string + details: string + } | null>(null) + + useEffect(() => { + const error = searchParams.get('error') + + if (error === 'already-linked') { + setErrorInfo({ + title: 'Аккаунт уже привязан', + description: + 'Этот аккаунт уже привязан к другому пользователю.', + details: + 'Пожалуйста, используйте другой аккаунт или свяжитесь с поддержкой по адресу support@teacoder.ru, чтобы решить эту проблему.' + }) + setIsVisible(true) + } else if (error === 'email-taken') { + setErrorInfo({ + title: 'Почта уже используется', + description: + 'Указанная почта уже используется другим аккаунтом.', + details: + 'Попробуйте использовать другой адрес электронной почты или восстановить доступ к старому аккаунту.' + }) + setIsVisible(true) + } + }, [searchParams]) + + if (!isVisible || !errorInfo) return null + + return ( +
+ + +
+ + {errorInfo.title} +
+ {errorInfo.description} +
+ +
+

{errorInfo.details}

+
+
+ + + +
+
+ ) +} diff --git a/src/components/account/connections/connections.tsx b/src/components/account/connections/connections.tsx index b78f9ad..7c5373d 100644 --- a/src/components/account/connections/connections.tsx +++ b/src/components/account/connections/connections.tsx @@ -1,17 +1,43 @@ 'use client' import { useMutation, useQuery } from '@tanstack/react-query' -import { useRouter } from 'next/navigation' -import { FaGithub, FaGoogle, FaTelegram, FaYandex } from 'react-icons/fa6' +import { useRouter, useSearchParams } from 'next/navigation' +import { type ReactNode, useEffect } from 'react' +import { FaGithub, FaGoogle } from 'react-icons/fa6' import { toast } from 'sonner' import { Heading } from '../../shared/heading' import { Button } from '../../ui/button' import { Card, CardContent } from '../../ui/card' +import { ConnectionError } from './connection-error' import { UnlinkProvider } from './unlink-provider' import { fetchExternalStatus, getConnectUrl } from '@/src/api/external' +interface Provider { + name: string + icon: ReactNode + key: 'google' | 'github' + description: string +} + +export const providers: Provider[] = [ + { + name: 'Google', + icon: , + key: 'google', + description: + 'Настройте вход через Google для удобной и быстрой авторизации' + }, + { + name: 'Github', + icon: , + key: 'github', + description: + 'Настройте вход через Github для удобной и быстрой авторизации' + } +] + export function Connections() { const router = useRouter() @@ -36,88 +62,50 @@ export function Connections() { if (isLoading) return
Loading status...
return ( -
-
- -
- - -
-
- -
-
-

Google

-

- Set up a single sign-on with Google and - login with one click. -

-
-
- {data?.google ? ( - - ) : ( - - )} -
-
- - -
-
- -
-
-

Github

-

- Set up a single sign-on with Github and - login with one click. -

-
-
- {data?.github ? ( - - ) : ( - - )} -
-
- {/* - -
-
- -
-
-

Yandex

-

- Set up a single sign-on with Yandex and - login with one click. -

-
-
- -
-
*/} + <> +
+
+ +
+ {providers.map((provider, index) => ( + + +
+
+ {provider.icon} +
+
+

+ {provider.name} +

+

+ {provider.description} +

+
+
+ {data?.[provider.key] ? ( + + ) : ( + + )} +
+
+ ))} +
-
+ + ) } diff --git a/src/components/account/settings/email-form.tsx b/src/components/account/settings/email-form.tsx index a3ac5e6..61d25f6 100644 --- a/src/components/account/settings/email-form.tsx +++ b/src/components/account/settings/email-form.tsx @@ -35,7 +35,7 @@ import { } from '../../ui/form' import { Input } from '../../ui/input' -import { changeEmail } from '@/src/api' +import { changeEmail, sendEmailVerification } from '@/src/api' import type { AccountResponse } from '@/src/generated' const emailSchema = z.object({ @@ -56,12 +56,28 @@ export function EmailForm({ user }: EmailFormProps) { const queryClient = useQueryClient() + const { mutate: send } = useMutation({ + mutationKey: ['send email verification'], + mutationFn: () => sendEmailVerification(), + onSuccess() { + toast.success( + 'Ссылка c подтверждением была отправлена на выш почтовый адрес' + ) + }, + onError(error: any) { + toast.error( + error.response?.data?.message ?? 'Ошибка при отправке письма' + ) + } + }) + const { mutateAsync, isPending } = useMutation({ mutationKey: ['change email'], mutationFn: (data: Email) => changeEmail(data), onSuccess() { - queryClient.invalidateQueries({ queryKey: ['get current'] }) + form.reset() setIsOpen(false) + queryClient.invalidateQueries({ queryKey: ['get current'] }) }, onError(error: any) { toast.error( @@ -116,18 +132,12 @@ export function EmailForm({ user }: EmailFormProps) { {!user?.isEmailVerified && ( - - toast.success( - 'Ссылка c подтверждением была отправлена на выш почтовый адрес' - ) - } - > + send()}> Подтвердить )} - + setIsOpen(true)}> Изменить @@ -135,16 +145,13 @@ export function EmailForm({ user }: EmailFormProps) { - {/* { form.reset() setIsOpen(state) }} > - - - Обновление почты @@ -191,7 +198,7 @@ export function EmailForm({ user }: EmailFormProps) { - */} +
) diff --git a/src/components/account/settings/password-form.tsx b/src/components/account/settings/password-form.tsx index f46d96b..7a5fcc9 100644 --- a/src/components/account/settings/password-form.tsx +++ b/src/components/account/settings/password-form.tsx @@ -29,22 +29,22 @@ import { Input } from '../../ui/input' import { changePassword } from '@/src/api' -const passwordSchema = z.object({ - currentPassword: z - .string() - .min(6, { - message: 'Текущий пароль должен содержать хотя бы 6 символов' - }) - .max(128, { - message: 'Текущий пароль должен содержать не более 128 символов' - }), - newPassword: z - .string() - .min(6, { message: 'Новый пароль должен содержать хотя бы 6 символов' }) - .max(128, { - message: 'Новый пароль должен содержать не более 128 символов' - }) -}) +const passwordSchema = z + .object({ + newPassword: z + .string() + .min(6, { + message: 'Новый пароль должен содержать хотя бы 6 символов' + }) + .max(128, { + message: 'Новый пароль должен содержать не более 128 символов' + }), + confirmPassword: z.string() + }) + .refine(data => data.newPassword === data.confirmPassword, { + message: 'Пароли не совпадают', + path: ['confirmPassword'] + }) export type Password = z.infer @@ -67,8 +67,8 @@ export function PasswordForm() { const form = useForm({ resolver: zodResolver(passwordSchema), defaultValues: { - currentPassword: '', - newPassword: '' + newPassword: '', + confirmPassword: '' } }) @@ -120,12 +120,10 @@ export function PasswordForm() { > ( - - Текущий пароль - + Новый пароль ( - Новый пароль + + Подтвердите новый пароль + -
+

{heading}

diff --git a/src/components/auth/verify-email.tsx b/src/components/auth/verify-email.tsx index abc8a36..6c118df 100644 --- a/src/components/auth/verify-email.tsx +++ b/src/components/auth/verify-email.tsx @@ -1,15 +1,42 @@ +'use client' + +import { useMutation } from '@tanstack/react-query' +import { useParams, useRouter } from 'next/navigation' +import { toast } from 'sonner' + import { Button } from '../ui/button' import { AuthWrapper } from './auth-wrapper' +import { verifyEmail } from '@/src/api' export function VerifyEmail() { + const router = useRouter() + const { token } = useParams<{ token: string }>() + + const { mutate, isPending } = useMutation({ + mutationKey: ['verify email'], + mutationFn: () => verifyEmail(token), + onSuccess() { + router.push('/account/settings') + }, + onError(error: any) { + toast.error( + error.response?.data?.message ?? 'Ошибка при верификации' + ) + } + }) + return ( -

- Lorem ipsum dolor sit amet, consectetur adipisicing elit. Velit, - est? +

+ Чтобы завершить подтверждение почты, нажми на кнопку ниже.

-
diff --git a/src/components/layout/footer.tsx b/src/components/layout/footer.tsx index 427f972..ea33f7e 100644 --- a/src/components/layout/footer.tsx +++ b/src/components/layout/footer.tsx @@ -65,7 +65,16 @@ export function Footer() {

TeaCoder

- Образовательная платформа по веб-разработке + Образовательная платформа по веб-разработке. +
В случае возникновения вопросов, обращайтесь + на почту{' '} + + support@teacoder.ru + + .

diff --git a/src/components/layout/user-navigation.tsx b/src/components/layout/user-navigation.tsx index cfccdf3..d7512dd 100644 --- a/src/components/layout/user-navigation.tsx +++ b/src/components/layout/user-navigation.tsx @@ -22,7 +22,7 @@ export const links = [ }, { title: 'Устройства', - href: ROUTES.connections, + href: ROUTES.sessions, icon: MonitorSmartphone }, { diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx new file mode 100644 index 0000000..b533bcb --- /dev/null +++ b/src/components/ui/switch.tsx @@ -0,0 +1,33 @@ +'use client' + +import * as SwitchPrimitives from '@radix-ui/react-switch' +import { + type ComponentPropsWithoutRef, + type ComponentRef, + forwardRef +} from 'react' + +import { cn } from '@/src/lib/utils' + +const Switch = forwardRef< + ComponentRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/src/generated/changePasswordRequest.ts b/src/generated/changePasswordRequest.ts index 0d491f1..aaf5f1b 100644 --- a/src/generated/changePasswordRequest.ts +++ b/src/generated/changePasswordRequest.ts @@ -8,15 +8,15 @@ export interface ChangePasswordRequest { /** - * Current password + * New password * @minLength 6 * @maxLength 128 */ - currentPassword: string; + newPassword: string; /** - * New password + * Confirmation of the new password * @minLength 6 * @maxLength 128 */ - newPassword: string; + confirmPassword: string; } diff --git a/src/generated/externalControllerCallbackParams.ts b/src/generated/externalControllerCallbackParams.ts index b5d879c..c632b49 100644 --- a/src/generated/externalControllerCallbackParams.ts +++ b/src/generated/externalControllerCallbackParams.ts @@ -7,5 +7,6 @@ */ export type ExternalControllerCallbackParams = { -code: string; -}; + code: string + state: string +} diff --git a/src/generated/mfaStatusResponse.ts b/src/generated/mfaStatusResponse.ts index 0c72daa..ee4802a 100644 --- a/src/generated/mfaStatusResponse.ts +++ b/src/generated/mfaStatusResponse.ts @@ -9,8 +9,6 @@ export interface MfaStatusResponse { /** Indicates if TOTP MFA is enabled for the account */ totpMfa: boolean; - /** Indicates if Passkey MFA is enabled for the account */ - passkeyMfa: boolean; /** Indicates if recovery codes are active for the account */ recoveryActive: boolean; } diff --git a/src/hooks/use-auth.ts b/src/hooks/use-auth.ts index 97a2eef..f20e199 100644 --- a/src/hooks/use-auth.ts +++ b/src/hooks/use-auth.ts @@ -1,17 +1,8 @@ -import { useEffect, useState } from 'react' - import { getSessionToken } from '../lib/cookies/session' export function useAuth() { - const [isAuthorized, setIsAuthorized] = useState(false) - - useEffect(() => { - const token = getSessionToken() - - setIsAuthorized(token !== undefined && token !== '') - }, []) + const token = getSessionToken() + const isAuthorized = token !== undefined && token !== '' - return { - isAuthorized - } + return { isAuthorized } }