From 0a92da5b3d0d8e5259ca1a93fc8f3ba66dda12e7 Mon Sep 17 00:00:00 2001 From: MarkMelior Date: Wed, 10 Jul 2024 22:54:33 +0300 Subject: [PATCH] add auth example, fsd auth sort --- README.md | 6 - .../app-router-auth/examples/codes.ts | 391 ++++++++++++++++++ .../app-router-auth/examples/services/data.ts | 15 + .../examples/services/definitions.ts | 21 + .../examples/services/login.ts | 47 +++ .../app-router-auth/examples/services/user.ts | 19 + .../examples/ui/form-login.tsx | 41 ++ app/projects/app-router-auth/page.tsx | 147 ++++++- prisma/schema.prisma | 4 +- src/features/auth/api/jwt.ts | 25 ++ .../auth/{services => api}/session.ts | 28 +- src/features/auth/services/login.ts | 10 +- src/features/auth/services/logout.ts | 2 +- src/features/auth/services/signup.ts | 16 +- src/features/auth/services/user.ts | 4 +- .../auth/{services => types}/definitions.ts | 11 +- .../auth/ui/form-login/form-login.tsx | 17 +- .../auth/ui/form-register/form-register.tsx | 2 +- .../auth/ui/logout-button/logout-button.tsx | 19 + src/features/burger/burger.tsx | 2 +- src/features/index.ts | 15 +- src/middleware.ts | 2 +- src/shared/types/programming-language.ts | 6 +- src/shared/ui/code-block/code-block.tsx | 40 +- src/shared/ui/code/code.tsx | 2 +- src/shared/ui/stack-buttons/model/data.tsx | 7 +- src/shared/ui/text/text.tsx | 9 + src/widgets/navbar/navbar.tsx | 2 +- 28 files changed, 815 insertions(+), 95 deletions(-) create mode 100644 app/projects/app-router-auth/examples/codes.ts create mode 100644 app/projects/app-router-auth/examples/services/data.ts create mode 100644 app/projects/app-router-auth/examples/services/definitions.ts create mode 100644 app/projects/app-router-auth/examples/services/login.ts create mode 100644 app/projects/app-router-auth/examples/services/user.ts create mode 100644 app/projects/app-router-auth/examples/ui/form-login.tsx create mode 100644 src/features/auth/api/jwt.ts rename src/features/auth/{services => api}/session.ts (52%) rename src/features/auth/{services => types}/definitions.ts (80%) create mode 100644 src/features/auth/ui/logout-button/logout-button.tsx create mode 100644 src/shared/ui/text/text.tsx diff --git a/README.md b/README.md index 9639a4d..c403366 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,3 @@ -# Next.js: Authentication - -Best Practices for Server Components, Actions, Middleware - ---- - This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). ## Getting Started diff --git a/app/projects/app-router-auth/examples/codes.ts b/app/projects/app-router-auth/examples/codes.ts new file mode 100644 index 0000000..87de7f0 --- /dev/null +++ b/app/projects/app-router-auth/examples/codes.ts @@ -0,0 +1,391 @@ +export const ExampleCodeJwtPath = 'src/features/auth/api/jwt.ts'; +export const ExampleCodeJwt = `import { SignJWT, jwtVerify } from 'jose'; +import { SessionPayload } from '../types/definitions'; + +const secretKey = process.env.SESSION_SECRET; +const encodedKey = new TextEncoder().encode(secretKey); + +export async function encrypt(payload: SessionPayload) { + return new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('7d') + .sign(encodedKey); +} + +export async function decrypt(session: string | undefined = '') { + try { + const { payload } = await jwtVerify(session, encodedKey, { + algorithms: ['HS256'], + }); + return payload; + } catch (error) { + // console.log('Failed to verify session'); + return null; + } +}`; + +export const ExampleCodeLoginPath = 'src/features/auth/services/login.ts'; +export const ExampleCodeLogin = `'use server'; + +import { PrismaClient } from '@prisma/client'; +import bcrypt from 'bcryptjs'; +import { FormState, LoginFormSchema } from './definitions'; +import { createSession } from './session'; + +const prisma = new PrismaClient(); + +export async function login( + state: FormState, + formData: FormData, +): Promise { + // 1. Validate form fields + const validatedFields = LoginFormSchema.safeParse({ + username: formData.get('username'), + password: formData.get('password'), + }); + const errorMessage = { message: 'Invalid login credentials.' }; + + // If any form fields are invalid, return early + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + }; + } + + // 2. Query the database for the user with the given email + const { username, password } = validatedFields.data; + + const user = await prisma.user.findFirst({ + where: { username }, + }); + + if (!user) { + return errorMessage; + } + + // 3. Compare the user's password with the hashed password in the database + const passwordMatch = await bcrypt.compare(password, user.password); + + // If the password does not match, return early + if (!passwordMatch) { + return errorMessage; + } + + // 4. If login successful, create a session for the user and redirect + const userId = user.id.toString(); + await createSession(userId); +}`; + +export const ExampleCodeLogoutPath = 'src/features/auth/services/logout.ts'; +export const ExampleCodeLogout = `'use server'; + +import { deleteSession } from '../api/session'; + +export async function logout() { + deleteSession(); +}`; + +export const ExampleCodeDefinitionsPath = + 'src/features/auth/types/definitions.ts'; +export const ExampleCodeDefinitions = `import { z } from 'zod'; + +export const SignupFormSchema = z.object({ + name: z + .string() + .min(2, { message: 'Name must be at least 2 characters long.' }) + .trim(), + username: z + .string() + .min(4, { message: 'Be at least 4 characters long' }) + .trim(), + password: z + .string() + .min(8, { message: 'Be at least 8 characters long' }) + .regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' }) + .regex(/[0-9]/, { message: 'Contain at least one number.' }) + .regex(/[^a-zA-Z0-9]/, { + message: 'Contain at least one special character.', + }) + .trim(), +}); + +export const LoginFormSchema = z.object({ + username: z.string().min(1, { message: 'Username field must not be empty.' }), + password: z.string().min(1, { message: 'Password field must not be empty.' }), +}); + +export type FormState = + | { + errors?: { + username?: string[]; + password?: string[]; + name?: string[]; + }; + message?: string; + } + | undefined; + +export type SessionPayload = { + userId: string | number; + expiresAt: Date; +}`; + +export const ExampleCodeSessionPath = 'src/features/auth/api/session.ts'; +export const ExampleCodeSession = `import 'server-only'; + +import { cookies } from 'next/headers'; +import { redirect } from 'next/navigation'; +import { decrypt, encrypt } from './jwt'; + +export async function createSession(userId: string) { + const expiresAt = new Date(Date.now() + 7 * 60 * 60 * 1000); + const session = await encrypt({ userId, expiresAt }); + + cookies().set('session', session, { + httpOnly: true, + secure: true, + expires: expiresAt, + sameSite: 'lax', + path: '/', + }); + + redirect('/dashboard'); +} + +export async function verifySession() { + const cookie = cookies().get('session')?.value; + const session = await decrypt(cookie); + + if (!session?.userId) { + redirect('/login'); + return null; + } + + return { isAuth: true, userId: Number(session.userId) }; +} + +export function deleteSession() { + cookies().delete('session'); + redirect('/login'); +}`; + +export const ExampleCodeUserPath = 'src/features/auth/services/user.ts'; +export const ExampleCodeUser = `import 'server-only'; + +import { PrismaClient } from '@prisma/client'; +import { cache } from 'react'; +import { verifySession } from '../api/session'; + +const prisma = new PrismaClient(); + +export const getUser = cache(async () => { + const session = await verifySession(); + if (!session) return null; + + try { + const user = await prisma.user.findFirst({ + where: { + id: session.userId, + }, + select: { + id: true, + username: true, + name: true, + }, + }); + + return user; + } catch (error) { + console.log('Failed to fetch user'); + return null; + } +})`; + +export const ExampleCodeLoginFormPath = + 'src/features/auth/ui/login-form/login-form.tsx'; +export const ExampleCodeLoginForm = `'use client'; + +import { cn } from '@/shared/lib'; +import { Button, Input } from '@nextui-org/react'; +import { useFormState, useFormStatus } from 'react-dom'; +import { login } from '../../services/login'; + +interface FormLoginProps { + className?: string; +} + +export const FormLogin = ({ className }: FormLoginProps) => { + const [state, action] = useFormState(login, undefined); + const { pending } = useFormStatus(); + + return ( +
+ + + +
+ ); +}`; + +export const ExampleCodeLogoutButtonPath = + 'src/features/auth/ui/logout-button/logout-button.tsx'; +export const ExampleCodeLogoutButton = `'use client'; + +import { Button } from '@nextui-org/react'; +import { RxExit } from 'react-icons/rx'; +import { logout } from '../../services/logout'; + +export const LogoutButton = () => { + return ( + + ); +}`; + +export const ExampleCodePrismaPath = 'prisma/schema.prisma'; +export const ExampleCodePrisma = `datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +model User { + id Int @id @default(autoincrement()) + username String @unique + password String + name String +}`; + +export const ExampleCodeSignupPath = 'src/features/auth/services/signup.ts'; +export const ExampleCodeSignup = `'use server'; + +import { PrismaClient } from '@prisma/client'; +import bcrypt from 'bcryptjs'; +import { createSession } from '../api/session'; +import { FormState, SignupFormSchema } from '../types/definitions'; + +const prisma = new PrismaClient(); + +export async function signup( + state: FormState, + formData: FormData, +): Promise { + // 1. Validate form fields + const validatedFields = SignupFormSchema.safeParse({ + name: formData.get('name'), + username: formData.get('username'), + password: formData.get('password'), + }); + + // If any form fields are invalid, return early + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + }; + } + + // 2. Prepare data for insertion into database + const { name, username, password } = validatedFields.data; + + // 3. Check if the user's username already exists + const existingUser = await prisma.user.findFirst({ where: { username } }); + + if (existingUser) { + return { + message: 'Username already exists, please use a different username.', + }; + } + + // Hash the user's password + const hashedPassword = await bcrypt.hash(password, 10); + + // 3. Insert the user into the database or call an Auth Provider's API + const user = await prisma.user.create({ + data: { + name, + username, + password: hashedPassword, + }, + select: { id: true }, + }); + + if (!user) { + return { + message: 'An error occurred while creating your account.', + }; + } + + // 4. Create a session for the user + const userId = user.id.toString(); + await createSession(userId); +}`; + +export const ExampleCodeMiddlewarePath = 'src/middleware.ts'; +export const ExampleCodeMiddleware = `import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { decrypt } from './features/auth/api/jwt'; + +// 1. Specify protected and public routes +const protectedRoutes = ['/dashboard']; +const publicRoutes = ['/login', '/signup', '/']; + +export default async function middleware(req: NextRequest) { + // 2. Check if the current route is protected or public + const path = req.nextUrl.pathname; + const isProtectedRoute = protectedRoutes.includes(path); + const isPublicRoute = publicRoutes.includes(path); + + // 3. Decrypt the session from the cookie + const cookie = cookies().get('session')?.value; + const session = await decrypt(cookie); + + // 5. Redirect to /login if the user is not authenticated + if (isProtectedRoute && !session?.userId) { + return NextResponse.redirect(new URL('/login', req.nextUrl)); + } + + // 6. Redirect to /dashboard if the user is authenticated + if ( + isPublicRoute && + session?.userId && + !req.nextUrl.pathname.startsWith('/dashboard') + ) { + return NextResponse.redirect(new URL('/dashboard', req.nextUrl)); + } + + return NextResponse.next(); +} + +// Routes Middleware should not run on +export const config = { + matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], +}`; diff --git a/app/projects/app-router-auth/examples/services/data.ts b/app/projects/app-router-auth/examples/services/data.ts new file mode 100644 index 0000000..da81036 --- /dev/null +++ b/app/projects/app-router-auth/examples/services/data.ts @@ -0,0 +1,15 @@ +export const usersExample = [ + { + id: 1, + username: 'admin', + password: 'pass', + name: 'Administrator', + role: 'admin', + }, + { + id: 2, + username: 'user', + password: 'pass', + name: 'User', + }, +]; diff --git a/app/projects/app-router-auth/examples/services/definitions.ts b/app/projects/app-router-auth/examples/services/definitions.ts new file mode 100644 index 0000000..49eb84d --- /dev/null +++ b/app/projects/app-router-auth/examples/services/definitions.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; + +export const LoginFormExampleSchema = z.object({ + username: z.string().min(1, { message: 'Username field must not be empty.' }), + password: z.string().min(1, { message: 'Password field must not be empty.' }), +}); + +export type FormState = + | { + errors?: { + username?: string[]; + password?: string[]; + }; + message?: string; + } + | undefined; + +export type SessionPayload = { + userId: string | number; + expiresAt: Date; +}; diff --git a/app/projects/app-router-auth/examples/services/login.ts b/app/projects/app-router-auth/examples/services/login.ts new file mode 100644 index 0000000..e0bf2d9 --- /dev/null +++ b/app/projects/app-router-auth/examples/services/login.ts @@ -0,0 +1,47 @@ +'use server'; + +import { createSession } from '@/features'; +import { usersExample } from './data'; +import { FormState, LoginFormExampleSchema } from './definitions'; + +export async function login( + state: FormState, + formData: FormData, +): Promise { + // 1. Validate form fields + const validatedFields = LoginFormExampleSchema.safeParse({ + username: formData.get('username'), + password: formData.get('password'), + }); + const errorMessage = { message: 'Invalid login credentials.' }; + + // If any form fields are invalid, return early + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + }; + } + + // 2. Query the database for the user with the given email + const { username, password } = validatedFields.data; + + const user = usersExample.find( + (user) => user.username === username && user.password === password, + ); + + if (!user) { + return errorMessage; + } + + // // 3. Compare the user's password with the hashed password in the database + // const passwordMatch = await bcrypt.compare(password, user.password); + + // // If the password does not match, return early + // if (!passwordMatch) { + // return errorMessage; + // } + + // 4. If login successful, create a session for the user and redirect + const userId = user.id.toString(); + await createSession(userId); +} diff --git a/app/projects/app-router-auth/examples/services/user.ts b/app/projects/app-router-auth/examples/services/user.ts new file mode 100644 index 0000000..5d3c4ba --- /dev/null +++ b/app/projects/app-router-auth/examples/services/user.ts @@ -0,0 +1,19 @@ +import 'server-only'; + +import { verifySession } from '@/features'; +import { cache } from 'react'; +import { usersExample } from './data'; + +export const getUserExample = cache(async () => { + const session = await verifySession(); + if (!session) return null; + + try { + const user = usersExample.find((user) => user.id === session.userId); + + return user; + } catch (error) { + console.log('Failed to fetch user'); + return null; + } +}); diff --git a/app/projects/app-router-auth/examples/ui/form-login.tsx b/app/projects/app-router-auth/examples/ui/form-login.tsx new file mode 100644 index 0000000..bc4cbe6 --- /dev/null +++ b/app/projects/app-router-auth/examples/ui/form-login.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { cn } from '@/shared/lib'; +import { Button, Input } from '@nextui-org/react'; +import { useFormState, useFormStatus } from 'react-dom'; +import { login } from '../services/login'; + +interface FormLoginProps { + className?: string; +} + +export const FormLoginExample = ({ className }: FormLoginProps) => { + const [state, action] = useFormState(login, undefined); + const { pending } = useFormStatus(); + + return ( +
+ + + +
+ ); +}; diff --git a/app/projects/app-router-auth/page.tsx b/app/projects/app-router-auth/page.tsx index c46a976..94c4a40 100644 --- a/app/projects/app-router-auth/page.tsx +++ b/app/projects/app-router-auth/page.tsx @@ -1,12 +1,39 @@ -import { FormLogin, FormRegister, getUser } from '@/features'; -import { fetchGitHubFileContent } from '@/shared/lib'; +import { LogoutButton } from '@/features'; import { Code, CodeBlock } from '@/shared/ui'; import { Header } from '@/widgets'; +import { Card } from '@nextui-org/react'; +import Link from 'next/link'; +import { + ExampleCodeDefinitions, + ExampleCodeDefinitionsPath, + ExampleCodeJwt, + ExampleCodeJwtPath, + ExampleCodeLogin, + ExampleCodeLoginForm, + ExampleCodeLoginFormPath, + ExampleCodeLoginPath, + ExampleCodeLogout, + ExampleCodeLogoutButton, + ExampleCodeLogoutButtonPath, + ExampleCodeLogoutPath, + ExampleCodeMiddleware, + ExampleCodeMiddlewarePath, + ExampleCodePrisma, + ExampleCodePrismaPath, + ExampleCodeSession, + ExampleCodeSessionPath, + ExampleCodeSignup, + ExampleCodeSignupPath, + ExampleCodeUser, + ExampleCodeUserPath, +} from './examples/codes'; +import { getUserExample } from './examples/services/user'; +import { FormLoginExample } from './examples/ui/form-login'; export default async function AppRouterAuthPage() { - const user = await getUser(); - const codePath1 = 'src/features/auth/services/login.ts'; - const code1 = await fetchGitHubFileContent({ path: codePath1 }); + const user = await getUserExample(); + // const ExampleCodeLoginPath = 'src/features/auth/services/login.ts'; + // const ExampleCodeLogin = await fetchGitHubFileContent({ path: ExampleCodeLoginPath }); return ( <> @@ -14,25 +41,115 @@ export default async function AppRouterAuthPage() { note='Auth' title='Next.js: Authentication' description='Best practices for Server Components, Actions, Middleware' - tags={['TypeScript', 'Cookie', 'SSR']} + tags={['TypeScript', 'Cookie', 'SSR', 'Prisma']} /> + {user && ( + +

+ You have successfully logged in as {user?.name} +

+ +
+ )} + +
+ + +
+
-

+


+

+ + + + + + + Реализация + +

+

Получение данных пользователя:{' '}

+ + + + + + + + + +
- - {user &&

Hi, {user.name}!

} - - - ); } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7ea895c..bc59691 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -9,7 +9,7 @@ generator client { model User { id Int @id @default(autoincrement()) - name String - email String @unique + username String @unique password String + name String } diff --git a/src/features/auth/api/jwt.ts b/src/features/auth/api/jwt.ts new file mode 100644 index 0000000..342d9dc --- /dev/null +++ b/src/features/auth/api/jwt.ts @@ -0,0 +1,25 @@ +import { SignJWT, jwtVerify } from 'jose'; +import { SessionPayload } from '../types/definitions'; + +const secretKey = process.env.SESSION_SECRET; +const encodedKey = new TextEncoder().encode(secretKey); + +export async function encrypt(payload: SessionPayload) { + return new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('7d') + .sign(encodedKey); +} + +export async function decrypt(session: string | undefined = '') { + try { + const { payload } = await jwtVerify(session, encodedKey, { + algorithms: ['HS256'], + }); + return payload; + } catch (error) { + // console.log('Failed to verify session'); + return null; + } +} diff --git a/src/features/auth/services/session.ts b/src/features/auth/api/session.ts similarity index 52% rename from src/features/auth/services/session.ts rename to src/features/auth/api/session.ts index b6155fc..52e073b 100644 --- a/src/features/auth/services/session.ts +++ b/src/features/auth/api/session.ts @@ -1,33 +1,7 @@ import 'server-only'; -import { SignJWT, jwtVerify } from 'jose'; import { cookies } from 'next/headers'; -import { SessionPayload } from './definitions'; - -const secretKey = process.env.SESSION_SECRET; -const encodedKey = new TextEncoder().encode(secretKey); - -export async function encrypt(payload: SessionPayload) { - return new SignJWT(payload) - .setProtectedHeader({ alg: 'HS256' }) - .setIssuedAt() - .setExpirationTime('7d') - .sign(encodedKey); -} - -export async function decrypt(session: string | undefined = '') { - try { - const { payload } = await jwtVerify(session, encodedKey, { - algorithms: ['HS256'], - }); - return payload; - } catch (error) { - // console.log('Failed to verify session'); - return null; - } -} - -// * --- +import { decrypt, encrypt } from './jwt'; export async function createSession(userId: string) { const expiresAt = new Date(Date.now() + 7 * 60 * 60 * 1000); diff --git a/src/features/auth/services/login.ts b/src/features/auth/services/login.ts index a17cc72..db988c6 100644 --- a/src/features/auth/services/login.ts +++ b/src/features/auth/services/login.ts @@ -2,8 +2,8 @@ import { PrismaClient } from '@prisma/client'; import bcrypt from 'bcryptjs'; -import { FormState, LoginFormSchema } from './definitions'; -import { createSession } from './session'; +import { createSession } from '../api/session'; +import { FormState, LoginFormSchema } from '../types/definitions'; const prisma = new PrismaClient(); @@ -13,7 +13,7 @@ export async function login( ): Promise { // 1. Validate form fields const validatedFields = LoginFormSchema.safeParse({ - email: formData.get('email'), + username: formData.get('username'), password: formData.get('password'), }); const errorMessage = { message: 'Invalid login credentials.' }; @@ -26,10 +26,10 @@ export async function login( } // 2. Query the database for the user with the given email - const { email, password } = validatedFields.data; + const { username, password } = validatedFields.data; const user = await prisma.user.findFirst({ - where: { email }, + where: { username }, }); if (!user) { diff --git a/src/features/auth/services/logout.ts b/src/features/auth/services/logout.ts index e5cbd05..18e3777 100644 --- a/src/features/auth/services/logout.ts +++ b/src/features/auth/services/logout.ts @@ -1,6 +1,6 @@ 'use server'; -import { deleteSession } from './session'; +import { deleteSession } from '../api/session'; export async function logout() { deleteSession(); diff --git a/src/features/auth/services/signup.ts b/src/features/auth/services/signup.ts index 35e365f..4da122c 100644 --- a/src/features/auth/services/signup.ts +++ b/src/features/auth/services/signup.ts @@ -2,8 +2,8 @@ import { PrismaClient } from '@prisma/client'; import bcrypt from 'bcryptjs'; -import { FormState, SignupFormSchema } from './definitions'; -import { createSession } from './session'; +import { createSession } from '../api/session'; +import { FormState, SignupFormSchema } from '../types/definitions'; const prisma = new PrismaClient(); @@ -14,7 +14,7 @@ export async function signup( // 1. Validate form fields const validatedFields = SignupFormSchema.safeParse({ name: formData.get('name'), - email: formData.get('email'), + username: formData.get('username'), password: formData.get('password'), }); @@ -26,14 +26,14 @@ export async function signup( } // 2. Prepare data for insertion into database - const { name, email, password } = validatedFields.data; + const { name, username, password } = validatedFields.data; - // 3. Check if the user's email already exists - const existingUser = await prisma.user.findFirst({ where: { email } }); + // 3. Check if the user's username already exists + const existingUser = await prisma.user.findFirst({ where: { username } }); if (existingUser) { return { - message: 'Email already exists, please use a different email or login.', + message: 'Username already exists, please use a different username.', }; } @@ -44,7 +44,7 @@ export async function signup( const user = await prisma.user.create({ data: { name, - email, + username, password: hashedPassword, }, select: { id: true }, diff --git a/src/features/auth/services/user.ts b/src/features/auth/services/user.ts index c448a29..010a26c 100644 --- a/src/features/auth/services/user.ts +++ b/src/features/auth/services/user.ts @@ -2,7 +2,7 @@ import 'server-only'; import { PrismaClient } from '@prisma/client'; import { cache } from 'react'; -import { verifySession } from './session'; +import { verifySession } from '../api/session'; const prisma = new PrismaClient(); @@ -17,8 +17,8 @@ export const getUser = cache(async () => { }, select: { id: true, + username: true, name: true, - email: true, }, }); diff --git a/src/features/auth/services/definitions.ts b/src/features/auth/types/definitions.ts similarity index 80% rename from src/features/auth/services/definitions.ts rename to src/features/auth/types/definitions.ts index 1643448..5bfd182 100644 --- a/src/features/auth/services/definitions.ts +++ b/src/features/auth/types/definitions.ts @@ -5,7 +5,10 @@ export const SignupFormSchema = z.object({ .string() .min(2, { message: 'Name must be at least 2 characters long.' }) .trim(), - email: z.string().email({ message: 'Please enter a valid email.' }).trim(), + username: z + .string() + .min(4, { message: 'Be at least 4 characters long' }) + .trim(), password: z .string() .min(8, { message: 'Be at least 8 characters long' }) @@ -18,16 +21,16 @@ export const SignupFormSchema = z.object({ }); export const LoginFormSchema = z.object({ - email: z.string().email({ message: 'Please enter a valid email.' }), + username: z.string().min(1, { message: 'Username field must not be empty.' }), password: z.string().min(1, { message: 'Password field must not be empty.' }), }); export type FormState = | { errors?: { - name?: string[]; - email?: string[]; + username?: string[]; password?: string[]; + name?: string[]; }; message?: string; } diff --git a/src/features/auth/ui/form-login/form-login.tsx b/src/features/auth/ui/form-login/form-login.tsx index c3ad934..320a502 100644 --- a/src/features/auth/ui/form-login/form-login.tsx +++ b/src/features/auth/ui/form-login/form-login.tsx @@ -1,20 +1,25 @@ 'use client'; +import { cn } from '@/shared/lib'; import { Button, Input } from '@nextui-org/react'; import { useFormState, useFormStatus } from 'react-dom'; import { login } from '../../services/login'; -export const FormLogin = () => { +interface FormLoginProps { + className?: string; +} + +export const FormLogin = ({ className }: FormLoginProps) => { const [state, action] = useFormState(login, undefined); const { pending } = useFormStatus(); return ( -
+ { const { pending } = useFormStatus(); return ( - + { + return ( + + ); +}; diff --git a/src/features/burger/burger.tsx b/src/features/burger/burger.tsx index 89a1425..02607e5 100644 --- a/src/features/burger/burger.tsx +++ b/src/features/burger/burger.tsx @@ -20,7 +20,7 @@ export const Burger = () => { -
+
diff --git a/src/features/index.ts b/src/features/index.ts index beaecca..6ff380b 100644 --- a/src/features/index.ts +++ b/src/features/index.ts @@ -1,7 +1,20 @@ +import { createSession, verifySession } from './auth/api/session'; +import { logout } from './auth/services/logout'; import { getUser } from './auth/services/user'; import { FormLogin } from './auth/ui/form-login/form-login'; import { FormRegister } from './auth/ui/form-register/form-register'; +import { LogoutButton } from './auth/ui/logout-button/logout-button'; import { Burger } from './burger/burger'; import { ThemeSwitcher } from './theme-switcher/theme-switcher'; -export { Burger, FormLogin, FormRegister, getUser, ThemeSwitcher }; +export { + Burger, + createSession, + FormLogin, + FormRegister, + getUser, + logout, + LogoutButton, + ThemeSwitcher, + verifySession, +}; diff --git a/src/middleware.ts b/src/middleware.ts index 880ebd1..d8e0587 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,6 +1,6 @@ import { cookies } from 'next/headers'; import { NextRequest, NextResponse } from 'next/server'; -import { decrypt } from './features/auth/services/session'; +import { decrypt } from './features/auth/api/jwt'; // 1. Specify protected and public routes const protectedRoutes = ['/dashboard']; diff --git a/src/shared/types/programming-language.ts b/src/shared/types/programming-language.ts index 3fa6497..3e371e8 100644 --- a/src/shared/types/programming-language.ts +++ b/src/shared/types/programming-language.ts @@ -1 +1,5 @@ -export type ProgrammingLanguage = 'TypeScript' | 'JavaScript'; +export type ProgrammingLanguage = + | 'TypeScript' + | 'JavaScript' + | 'Markdown' + | 'Prisma'; diff --git a/src/shared/ui/code-block/code-block.tsx b/src/shared/ui/code-block/code-block.tsx index 1864ed8..e156913 100644 --- a/src/shared/ui/code-block/code-block.tsx +++ b/src/shared/ui/code-block/code-block.tsx @@ -1,15 +1,16 @@ 'use client'; -import { gitHubRepoLink } from '@/shared/lib'; +import { cn, gitHubRepoLink } from '@/shared/lib'; import { GitHubPath } from '@/shared/types/github-path'; import { ProgrammingLanguage } from '@/shared/types/programming-language'; import { Theme } from '@/shared/types/theme'; -import { Button, Skeleton, Tooltip } from '@nextui-org/react'; +import { Button, Link, Skeleton, Tooltip } from '@nextui-org/react'; import { useTheme } from 'next-themes'; -import Link from 'next/link'; import { FC, useEffect, useMemo, useState } from 'react'; import { BiLogoJavascript, BiLogoTypescript } from 'react-icons/bi'; import { LuEye } from 'react-icons/lu'; +import { PiMarkdownLogo } from 'react-icons/pi'; +import { SiPrisma } from 'react-icons/si'; import { TbFileUnknown } from 'react-icons/tb'; import SyntaxHighlighter from 'react-syntax-highlighter'; import { @@ -25,6 +26,9 @@ interface CodeBlockProps { language?: ProgrammingLanguage; linesLength?: number; github?: GitHubPath; + variant?: 'small'; + className?: string; + disableLineNumbers?: boolean; } export const CodeBlock: FC = ({ @@ -32,7 +36,10 @@ export const CodeBlock: FC = ({ fileName, github, language = 'TypeScript', - linesLength = 15, + linesLength = 10, + variant, + className, + disableLineNumbers, }) => { const [isExpanded, setIsExpanded] = useState(false); const lines = text.split('\n'); @@ -51,21 +58,32 @@ export const CodeBlock: FC = ({ return ; case 'JavaScript': return ; + case 'Markdown': + return ; + case 'Prisma': + return ; default: return ; } }, [language]); if (!mounted) { - return ; + return ( + + ); } return ( -
-
- {/* */} +
+
{renderIcon} - {/* */} {fileName ? fileName : language}
{github?.path && ( @@ -92,7 +110,7 @@ export const CodeBlock: FC = ({ @@ -102,7 +120,7 @@ export const CodeBlock: FC = ({ {isLong && (
{!isExpanded && ( -
+
)}