diff --git a/package-lock.json b/package-lock.json index ebebf42..6a315b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,13 @@ "dependencies": { "@tailwindcss/postcss": "^4.1.17", "clsx": "^2.1.1", + "jose": "^6.1.2", "next": "16.0.3", "next-auth": "5.0.0-beta.30", "react": "19.2.0", "react-dom": "19.2.0", - "tailwindcss": "^4.1.17" + "tailwindcss": "^4.1.17", + "zod": "^4.1.13" }, "devDependencies": { "@types/node": "^20", @@ -7013,7 +7015,6 @@ "version": "4.1.13", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 228ac22..17fb2a6 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,13 @@ "dependencies": { "@tailwindcss/postcss": "^4.1.17", "clsx": "^2.1.1", + "jose": "^6.1.2", "next": "16.0.3", "next-auth": "5.0.0-beta.30", "react": "19.2.0", "react-dom": "19.2.0", - "tailwindcss": "^4.1.17" + "tailwindcss": "^4.1.17", + "zod": "^4.1.13" }, "devDependencies": { "@types/node": "^20", diff --git a/src/app/actions/auth.ts b/src/app/actions/auth.ts new file mode 100644 index 0000000..cf3bb84 --- /dev/null +++ b/src/app/actions/auth.ts @@ -0,0 +1,60 @@ +'use server' + +import { SignupFormSchema, FormState } from '@/app/lib/definitions' +import { createSession, deleteSession } from '@/app/lib/session' +import { redirect } from 'next/navigation' + +export async function logout() { + await deleteSession() +} + +export async function authorization( + state: string | undefined, + formData: FormData, +) { + try { + console.log(formData); + } catch (error) { + if (error) { + return 'Something went wrong.'; + } + throw error; + } +} + +export async function registration( + state: string | undefined, + formData: FormData, +) { + const validatedFields = SignupFormSchema.safeParse({ + name: formData.get('full_name'), + email: formData.get('email'), + password: formData.get('password'), + }) + + if (!validatedFields.success) { + return 'Something went wrong from valid.'; + } + + try { + const { name, email, password } = validatedFields.data; + const response = await fetch('http://localhost:8080/api/auth/register', { + method: 'POST', + body: JSON.stringify({ fullName: name, email, password }), + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }); + + let parsed = await response.json(); + await createSession(parsed.token); + } catch (error) { + if (error) { + return 'Something went wrong. error'; + } + throw error; + } + + redirect('/pages/dashboard'); +} \ No newline at end of file diff --git a/src/app/api/action.ts b/src/app/lib/action.ts similarity index 58% rename from src/app/api/action.ts rename to src/app/lib/action.ts index ddfd324..521131c 100644 --- a/src/app/api/action.ts +++ b/src/app/lib/action.ts @@ -19,10 +19,8 @@ export async function testConnect() { const response = await fetch('http://localhost:8080/api/auth/register', { method: 'POST', body: JSON.stringify({ - "firstName": "Serg", - "secondName": "Fedorovich", - "lastName": "Tankan", - "email": "gggpetst2test@e.ru", + "fullName": "Vladislav Sergevich", + "email": "s2@e.ru", "password": "1121231412" }), headers: { @@ -38,32 +36,4 @@ export async function testConnect() { } catch (error) { throw error; } -} - -export async function authenticate( - prevState: string | undefined, - formData: FormData, -) { - try { - console.log(formData); - } catch (error) { - if (error) { - return 'Something went wrong.'; - } - throw error; - } -} - -export async function registration( - prevState: string | undefined, - formData: FormData, -) { - try { - console.log(formData); - } catch (error) { - if (error) { - return 'Something went wrong.'; - } - throw error; - } } \ No newline at end of file diff --git a/src/app/lib/definitions.ts b/src/app/lib/definitions.ts new file mode 100644 index 0000000..f91c601 --- /dev/null +++ b/src/app/lib/definitions.ts @@ -0,0 +1,31 @@ +import * as z from 'zod' + +export const SignupFormSchema = z.object({ + name: z + .string() + .min(2, { error: 'Name must be at least 2 characters long.' }) + .trim(), + email: z.email({ error: 'Please enter a valid email.' }).trim(), + password: z + .string() + .min(5, { error: 'Be at least 5 characters long' }) + .regex(/[a-zA-Z]/, { error: 'Contain at least one letter.' }) + .regex(/[0-9]/, { error: 'Contain at least one number.' }) + .trim(), +}) + +export type FormState = + | { + errors?: { + name?: string[] + email?: string[] + password?: string[] + } + message?: string + } + | undefined + +export type FormAction = ( + state: FormState, + formData: FormData +) => Promise \ No newline at end of file diff --git a/src/app/lib/session.ts b/src/app/lib/session.ts new file mode 100644 index 0000000..fe30b5c --- /dev/null +++ b/src/app/lib/session.ts @@ -0,0 +1,45 @@ +import 'server-only' +import { cookies } from 'next/headers' +import { SignJWT, jwtVerify } from 'jose' +/* import { SessionPayload } from '@/app/lib/definitions' */ + +const secretKey = process.env.SESSION_SECRET +const encodedKey = new TextEncoder().encode(secretKey) + +export async function encrypt(payload: any) { + 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') + } +} + +export async function createSession(userId: string) { + const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + const session = await encrypt({ userId, expiresAt }) + const cookieStore = await cookies() + + cookieStore.set('session', session, { + httpOnly: true, + secure: true, + expires: expiresAt, + sameSite: 'lax', + path: '/', + }) +} + +export async function deleteSession() { + const cookieStore = await cookies() + cookieStore.delete('session') +} \ No newline at end of file diff --git a/src/app/lib/sessions.ts b/src/app/lib/sessions.ts new file mode 100644 index 0000000..5d374b0 --- /dev/null +++ b/src/app/lib/sessions.ts @@ -0,0 +1,23 @@ +import 'server-only' +import { cookies } from 'next/headers' +import { decrypt } from '@/app/lib/session' + +export async function updateSession() { + const session = (await cookies()).get('session')?.value + const payload = await decrypt(session) + + if (!session || !payload) { + return null + } + + const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + + const cookieStore = await cookies() + cookieStore.set('session', session, { + httpOnly: true, + secure: true, + expires: expires, + sameSite: 'lax', + path: '/', + }) +} \ No newline at end of file diff --git a/src/app/ui/button-test.tsx b/src/app/ui/button-test.tsx index 8d54224..4df5d60 100644 --- a/src/app/ui/button-test.tsx +++ b/src/app/ui/button-test.tsx @@ -1,6 +1,6 @@ 'use client' -import { testConnect } from '@/app/api/action'; +import { testConnect } from '@/app/lib/action'; async function clickHandler() { testConnect(); diff --git a/src/app/ui/header/notificationsButton.tsx b/src/app/ui/header/notificationsButton.tsx index 6eaeb10..53b1402 100644 --- a/src/app/ui/header/notificationsButton.tsx +++ b/src/app/ui/header/notificationsButton.tsx @@ -1,7 +1,7 @@ "use client" import { useState, useRef } from 'react'; -import { fetchFruits } from '@/app/api/action'; +import { fetchFruits } from '@/app/lib/action'; import { useClickOutside } from '@/app/hooks/useClickOutside'; import Link from 'next/link'; diff --git a/src/app/ui/login-form.tsx b/src/app/ui/login-form.tsx index b93195a..74e6191 100644 --- a/src/app/ui/login-form.tsx +++ b/src/app/ui/login-form.tsx @@ -2,11 +2,11 @@ import styles from '@/app/styles/login.module.scss' import { useActionState } from 'react'; -import { authenticate } from '@/app/api/action'; +import { authorization } from '@/app/actions/auth'; export default function LoginForm() { const [errorMessage, formAction, isPending] = useActionState( - authenticate, + authorization, undefined, ); diff --git a/src/app/ui/register-form.tsx b/src/app/ui/register-form.tsx index 578d06f..538859c 100644 --- a/src/app/ui/register-form.tsx +++ b/src/app/ui/register-form.tsx @@ -2,7 +2,7 @@ import styles from '@/app/styles/login.module.scss'; import { useActionState } from 'react'; -import { registration } from '@/app/api/action'; +import { registration } from '@/app/actions/auth'; export default function RegisterForm() { const [errorMessage, formAction, isPending] = useActionState( @@ -23,7 +23,7 @@ export default function RegisterForm() {
- +
diff --git a/src/proxy.ts b/src/proxy.ts index b95b6ae..c81b965 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,11 +1,25 @@ import { NextResponse, NextRequest } from 'next/server'; +import { cookies } from 'next/headers' +import { decrypt } from '@/app/lib/session'; -const isLogin = true; +const protectedRoutes = ['/pages'] +const publicRoutes = ['/login', '/register', '/'] -export function proxy(request: NextRequest) { - if (request.nextUrl.pathname !== '/login' && !isLogin) { - return NextResponse.redirect(new URL('/login', request.url)); +export default async function proxy(request: NextRequest) { + const path = request.nextUrl.pathname; + const isProtectedRoute = protectedRoutes.includes(path.slice(0, 6)); + const isPublicRoute = publicRoutes.includes(path); + + const cookie = (await cookies()).get('session')?.value; + console.log("cookie"); + console.log(!!cookie); + const session = await decrypt(cookie); + + if (isProtectedRoute && !session?.userId) { + return NextResponse.redirect(new URL('/login', request.nextUrl)) } + + return NextResponse.next(); } export const config = {