From e160fac0ab056e27d72b0097dc3729194a8ef40c Mon Sep 17 00:00:00 2001 From: Vladyslav <46872670+virus231@users.noreply.github.com> Date: Tue, 16 Sep 2025 23:17:38 +0200 Subject: [PATCH 01/48] feat: database init --- backend/api/index.ts | 24 ++++++++++++--- .../20250916210727_init/migration.sql | 30 +++++++++++++++++++ backend/prisma/migrations/migration_lock.toml | 3 ++ backend/prisma/schema.prisma | 30 +++++++++++++++++-- 4 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 backend/prisma/migrations/20250916210727_init/migration.sql create mode 100644 backend/prisma/migrations/migration_lock.toml diff --git a/backend/api/index.ts b/backend/api/index.ts index ad7bf28..1974aa1 100644 --- a/backend/api/index.ts +++ b/backend/api/index.ts @@ -1,19 +1,35 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node'; - -export default function handler(req: VercelRequest, res: VercelResponse) { +export default async function handler(req: any, res: any) { // Health check if (req.url === '/api/health') { return res.status(200).json({ status: 'OK', timestamp: new Date().toISOString(), environment: 'production', + database: 'configured' }); } + // Database test endpoint + if (req.url === '/api/db-test') { + try { + return res.json({ + database_configured: true, + database_url_exists: true, + environment: 'production' + }); + } catch (error: any) { + return res.status(500).json({ + error: 'Database test failed', + message: error?.message || 'Unknown error' + }); + } + } + // Default API response return res.json({ message: 'API is working', method: req.method, - url: req.url + url: req.url, + endpoints: ['/api/health', '/api/db-test'] }); } \ No newline at end of file diff --git a/backend/prisma/migrations/20250916210727_init/migration.sql b/backend/prisma/migrations/20250916210727_init/migration.sql new file mode 100644 index 0000000..f4c0c51 --- /dev/null +++ b/backend/prisma/migrations/20250916210727_init/migration.sql @@ -0,0 +1,30 @@ +-- CreateTable +CREATE TABLE "public"."User" ( + "id" SERIAL NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT, + "password" TEXT NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Post" ( + "id" SERIAL NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "description" TEXT, + "authorId" INTEGER NOT NULL, + + CONSTRAINT "Post_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "public"."User"("email"); + +-- AddForeignKey +ALTER TABLE "public"."Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/migration_lock.toml b/backend/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/backend/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index ee282c7..8a76ea4 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -1,9 +1,6 @@ // This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema -// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? -// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init - generator client { provider = "prisma-client-js" } @@ -12,3 +9,30 @@ datasource db { provider = "postgresql" url = env("DATABASE_URL") } + +// User model for authentication +model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique + name String? + password String + + // Relations + posts Post[] +} + +// Post model for blog posts +model Post { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + content String + description String? + + // Relations + authorId Int + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) +} From 12b641ec51bdbe980a43abb16de3ab64f4e7ff2c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 16 Sep 2025 21:32:53 +0000 Subject: [PATCH 02/48] chore(release): 1.1.0-beta.1 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.1.0-beta.1](https://github.com/virus231/tech-stack/compare/v1.0.4...v1.1.0-beta.1) (2025-09-16) ### ✨ Features * database init ([e160fac](https://github.com/virus231/tech-stack/commit/e160fac0ab056e27d72b0097dc3729194a8ef40c)) ### 🐛 Bug Fixes * vercel config ([38e8c87](https://github.com/virus231/tech-stack/commit/38e8c87000bd28867fb8596ca6515de2e0ae32ee)) --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b995dc9..51c4aff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [1.1.0-beta.1](https://github.com/virus231/tech-stack/compare/v1.0.4...v1.1.0-beta.1) (2025-09-16) + +### ✨ Features + +* database init ([e160fac](https://github.com/virus231/tech-stack/commit/e160fac0ab056e27d72b0097dc3729194a8ef40c)) + +### 🐛 Bug Fixes + +* vercel config ([38e8c87](https://github.com/virus231/tech-stack/commit/38e8c87000bd28867fb8596ca6515de2e0ae32ee)) + ## [1.0.4](https://github.com/virus231/tech-stack/compare/v1.0.3...v1.0.4) (2025-09-16) ### 🐛 Bug Fixes From acefc83ea98a610d05f633845ac5ae51653c761c Mon Sep 17 00:00:00 2001 From: Vladyslav <46872670+virus231@users.noreply.github.com> Date: Tue, 16 Sep 2025 23:53:00 +0200 Subject: [PATCH 03/48] feat: auth endpoints --- backend/package.json | 4 + backend/src/app.ts | 4 + backend/src/controllers/auth.ts | 148 ++++++++++++++++++++++++++++++++ backend/src/middleware/auth.ts | 65 ++++++++++++++ backend/src/routes/auth.ts | 12 +++ backend/src/types/auth.ts | 25 ++++++ backend/src/utils/auth.ts | 58 +++++++++++++ 7 files changed, 316 insertions(+) create mode 100644 backend/src/controllers/auth.ts create mode 100644 backend/src/middleware/auth.ts create mode 100644 backend/src/routes/auth.ts create mode 100644 backend/src/types/auth.ts create mode 100644 backend/src/utils/auth.ts diff --git a/backend/package.json b/backend/package.json index 2f781be..556a3a5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,15 +23,19 @@ "dependencies": { "@prisma/client": "^6.16.1", "@vercel/node": "^3.0.31", + "bcryptjs": "^3.0.2", "cors": "^2.8.5", "dotenv": "^17.2.2", "express": "^5.1.0", "helmet": "^8.1.0", + "jsonwebtoken": "^9.0.2", "morgan": "^1.10.1" }, "devDependencies": { + "@types/bcryptjs": "^3.0.0", "@types/cors": "^2.8.19", "@types/express": "^5.0.3", + "@types/jsonwebtoken": "^9.0.10", "@types/morgan": "^1.9.10", "@types/node": "^24.5.0", "nodemon": "^3.1.10", diff --git a/backend/src/app.ts b/backend/src/app.ts index 24faacc..2b05ac4 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -3,6 +3,7 @@ import cors from "cors"; import express, { type Express } from "express"; import helmet from "helmet"; import morgan from "morgan"; +import authRoutes from "@/routes/auth"; const app: Express = express(); @@ -33,6 +34,9 @@ app.get("/health", (_req, res) => { }); }); +// Auth routes +app.use("/auth", authRoutes); + // API routes app.use("/api", (_req, res) => { res.json({ message: "API is working" }); diff --git a/backend/src/controllers/auth.ts b/backend/src/controllers/auth.ts new file mode 100644 index 0000000..78f145f --- /dev/null +++ b/backend/src/controllers/auth.ts @@ -0,0 +1,148 @@ +import { Request, Response } from 'express'; +import { PrismaClient } from '@prisma/client'; +import { generateToken, hashPassword, comparePassword } from '@/utils/auth'; +import { RegisterRequest, LoginRequest, AuthResponse } from '@/types/auth'; + +const prisma = new PrismaClient(); + +/** + * Register new user + * POST /auth/register + */ +export const register = async (req: Request, res: Response): Promise => { + try { + const { email, password }: RegisterRequest = req.body; + + // Validate input + if (!email || !password) { + res.status(400).json({ + error: 'Validation error', + message: 'Email and password are required' + }); + return; + } + + if (password.length < 6) { + res.status(400).json({ + error: 'Validation error', + message: 'Password must be at least 6 characters long' + }); + return; + } + + // Check if user already exists + const existingUser = await prisma.user.findUnique({ + where: { email } + }); + + if (existingUser) { + res.status(409).json({ + error: 'User already exists', + message: 'A user with this email already exists' + }); + return; + } + + // Hash password and create user + const hashedPassword = await hashPassword(password); + + const user = await prisma.user.create({ + data: { + email, + password: hashedPassword, + name: null + } + }); + + // Generate token + const token = generateToken({ + userId: user.id, + email: user.email + }); + + const response: AuthResponse = { + user: { + id: user.id, + email: user.email, + name: user.name, + createdAt: user.createdAt + }, + token + }; + + res.status(201).json(response); + } catch (error) { + console.error('Registration error:', error); + res.status(500).json({ + error: 'Internal server error', + message: 'Failed to create user' + }); + } +}; + +/** + * Login user + * POST /auth/login + */ +export const login = async (req: Request, res: Response): Promise => { + try { + const { email, password }: LoginRequest = req.body; + + // Validate input + if (!email || !password) { + res.status(400).json({ + error: 'Validation error', + message: 'Email and password are required' + }); + return; + } + + // Find user + const user = await prisma.user.findUnique({ + where: { email } + }); + + if (!user) { + res.status(401).json({ + error: 'Authentication failed', + message: 'Invalid email or password' + }); + return; + } + + // Verify password + const isValidPassword = await comparePassword(password, user.password); + + if (!isValidPassword) { + res.status(401).json({ + error: 'Authentication failed', + message: 'Invalid email or password' + }); + return; + } + + // Generate token + const token = generateToken({ + userId: user.id, + email: user.email + }); + + const response: AuthResponse = { + user: { + id: user.id, + email: user.email, + name: user.name, + createdAt: user.createdAt + }, + token + }; + + res.status(200).json(response); + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ + error: 'Internal server error', + message: 'Failed to authenticate user' + }); + } +}; \ No newline at end of file diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts new file mode 100644 index 0000000..ed40d2c --- /dev/null +++ b/backend/src/middleware/auth.ts @@ -0,0 +1,65 @@ +import { Request, Response, NextFunction } from 'express'; +import { verifyToken, extractTokenFromHeader, JWTPayload } from '@/utils/auth'; + +// Extend Request interface to include user +declare global { + namespace Express { + interface Request { + user?: JWTPayload; + } + } +} + +/** + * Middleware to verify JWT token and authenticate user + */ +export const authenticateToken = ( + req: Request, + res: Response, + next: NextFunction +): void => { + try { + const token = extractTokenFromHeader(req.headers.authorization); + + if (!token) { + res.status(401).json({ + error: 'Authentication required', + message: 'No token provided' + }); + return; + } + + const decoded = verifyToken(token); + req.user = decoded; + + next(); + } catch (error) { + res.status(401).json({ + error: 'Authentication failed', + message: error instanceof Error ? error.message : 'Invalid token' + }); + } +}; + +/** + * Optional auth middleware - doesn't fail if no token + */ +export const optionalAuth = ( + req: Request, + res: Response, + next: NextFunction +): void => { + try { + const token = extractTokenFromHeader(req.headers.authorization); + + if (token) { + const decoded = verifyToken(token); + req.user = decoded; + } + + next(); + } catch (error) { + // Ignore auth errors in optional auth + next(); + } +}; \ No newline at end of file diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts new file mode 100644 index 0000000..6e3e2ee --- /dev/null +++ b/backend/src/routes/auth.ts @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import { register, login } from '@/controllers/auth'; + +const router = Router(); + +// POST /auth/register +router.post('/register', register); + +// POST /auth/login +router.post('/login', login); + +export default router; \ No newline at end of file diff --git a/backend/src/types/auth.ts b/backend/src/types/auth.ts new file mode 100644 index 0000000..be819be --- /dev/null +++ b/backend/src/types/auth.ts @@ -0,0 +1,25 @@ +export interface RegisterRequest { + email: string; + password: string; +} + +export interface LoginRequest { + email: string; + password: string; +} + +export interface AuthResponse { + user: { + id: number; + email: string; + name: string | null; + createdAt: Date; + }; + token: string; +} + +export interface ApiError { + error: string; + message: string; + details?: string[]; +} \ No newline at end of file diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts new file mode 100644 index 0000000..92422fc --- /dev/null +++ b/backend/src/utils/auth.ts @@ -0,0 +1,58 @@ +import jwt from 'jsonwebtoken'; +import bcrypt from 'bcryptjs'; +import { config } from '@/config/config'; + +export interface JWTPayload { + userId: number; + email: string; +} + +/** + * Generate JWT token for user + */ +export const generateToken = (payload: JWTPayload): string => { + return jwt.sign(payload, config.jwtSecret, { + expiresIn: '24h', + }); +}; + +/** + * Verify and decode JWT token + */ +export const verifyToken = (token: string): JWTPayload => { + try { + const decoded = jwt.verify(token, config.jwtSecret) as JWTPayload; + return decoded; + } catch (error) { + throw new Error('Invalid or expired token'); + } +}; + +/** + * Hash password using bcrypt + */ +export const hashPassword = async (password: string): Promise => { + const saltRounds = 12; + return await bcrypt.hash(password, saltRounds); +}; + +/** + * Compare password with hash + */ +export const comparePassword = async ( + password: string, + hash: string +): Promise => { + return await bcrypt.compare(password, hash); +}; + +/** + * Extract token from Authorization header + */ +export const extractTokenFromHeader = (authHeader?: string): string | null => { + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return null; + } + + return authHeader.substring(7); // Remove 'Bearer ' prefix +}; \ No newline at end of file From d4156b47ee468b7ae2e352ff8767512a1204c51b Mon Sep 17 00:00:00 2001 From: Vladyslav <46872670+virus231@users.noreply.github.com> Date: Tue, 16 Sep 2025 23:56:42 +0200 Subject: [PATCH 04/48] fix: type errors --- backend/src/routes/auth.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 6e3e2ee..3127301 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -1,12 +1,12 @@ -import { Router } from 'express'; -import { register, login } from '@/controllers/auth'; +import { login, register } from "@/controllers/auth"; +import { Router } from "express"; -const router = Router(); +const router: Router = Router(); // POST /auth/register -router.post('/register', register); +router.post("/register", register); -// POST /auth/login -router.post('/login', login); +// POST /auth/login +router.post("/login", login); -export default router; \ No newline at end of file +export default router; From 5e3031a4920f167bf2d58a935dd7076f05ae04b7 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 16 Sep 2025 21:59:03 +0000 Subject: [PATCH 05/48] chore(release): 1.1.0-beta.2 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.1.0-beta.2](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.1...v1.1.0-beta.2) (2025-09-16) ### ✨ Features * auth endpoints ([acefc83](https://github.com/virus231/tech-stack/commit/acefc83ea98a610d05f633845ac5ae51653c761c)) ### 🐛 Bug Fixes * type errors ([d4156b4](https://github.com/virus231/tech-stack/commit/d4156b47ee468b7ae2e352ff8767512a1204c51b)) --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51c4aff..75c27fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [1.1.0-beta.2](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.1...v1.1.0-beta.2) (2025-09-16) + +### ✨ Features + +* auth endpoints ([acefc83](https://github.com/virus231/tech-stack/commit/acefc83ea98a610d05f633845ac5ae51653c761c)) + +### 🐛 Bug Fixes + +* type errors ([d4156b4](https://github.com/virus231/tech-stack/commit/d4156b47ee468b7ae2e352ff8767512a1204c51b)) + ## [1.1.0-beta.1](https://github.com/virus231/tech-stack/compare/v1.0.4...v1.1.0-beta.1) (2025-09-16) ### ✨ Features From 81a953462b343fba7cfd4c4e4d2c181394f06bf3 Mon Sep 17 00:00:00 2001 From: Vladyslav <46872670+virus231@users.noreply.github.com> Date: Wed, 17 Sep 2025 00:06:09 +0200 Subject: [PATCH 06/48] feat: connect full Express app to Vercel for production auth --- backend/api/index.ts | 36 ++---------------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/backend/api/index.ts b/backend/api/index.ts index 1974aa1..1e0e40c 100644 --- a/backend/api/index.ts +++ b/backend/api/index.ts @@ -1,35 +1,3 @@ -export default async function handler(req: any, res: any) { - // Health check - if (req.url === '/api/health') { - return res.status(200).json({ - status: 'OK', - timestamp: new Date().toISOString(), - environment: 'production', - database: 'configured' - }); - } +import app from '../src/app'; - // Database test endpoint - if (req.url === '/api/db-test') { - try { - return res.json({ - database_configured: true, - database_url_exists: true, - environment: 'production' - }); - } catch (error: any) { - return res.status(500).json({ - error: 'Database test failed', - message: error?.message || 'Unknown error' - }); - } - } - - // Default API response - return res.json({ - message: 'API is working', - method: req.method, - url: req.url, - endpoints: ['/api/health', '/api/db-test'] - }); -} \ No newline at end of file +export default app; \ No newline at end of file From 2a77bb04e7e1d612287668d5390dae4a4832db94 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 16 Sep 2025 22:08:13 +0000 Subject: [PATCH 07/48] chore(release): 1.1.0-beta.3 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.1.0-beta.3](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.2...v1.1.0-beta.3) (2025-09-16) ### ✨ Features * connect full Express app to Vercel for production auth ([81a9534](https://github.com/virus231/tech-stack/commit/81a953462b343fba7cfd4c4e4d2c181394f06bf3)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75c27fc..04d1077 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.1.0-beta.3](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.2...v1.1.0-beta.3) (2025-09-16) + +### ✨ Features + +* connect full Express app to Vercel for production auth ([81a9534](https://github.com/virus231/tech-stack/commit/81a953462b343fba7cfd4c4e4d2c181394f06bf3)) + ## [1.1.0-beta.2](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.1...v1.1.0-beta.2) (2025-09-16) ### ✨ Features From b9bb60ac91ba5c2f234a77b6b375d99c61285f58 Mon Sep 17 00:00:00 2001 From: Vladyslav <46872670+virus231@users.noreply.github.com> Date: Wed, 17 Sep 2025 00:18:26 +0200 Subject: [PATCH 08/48] Update index.ts --- backend/api/index.ts | 66 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/backend/api/index.ts b/backend/api/index.ts index 1e0e40c..d3e30db 100644 --- a/backend/api/index.ts +++ b/backend/api/index.ts @@ -1,3 +1,65 @@ -import app from '../src/app'; +import cors from "cors"; +import express from "express"; +import helmet from "helmet"; +import morgan from "morgan"; +import { config } from "../src/config/config"; +import { login, register } from "../src/controllers/auth"; -export default app; \ No newline at end of file +const app = express(); + +// Security middleware +app.use(helmet()); + +// CORS configuration +app.use( + cors({ + origin: config.corsOrigin, + credentials: true, + }) +); + +// Logging middleware +app.use(morgan("combined")); + +// Body parsing middleware +app.use(express.json({ limit: "10mb" })); +app.use(express.urlencoded({ extended: true, limit: "10mb" })); + +// Health check endpoint +app.get("/api/health", (_req, res) => { + res.status(200).json({ + status: "OK", + timestamp: new Date().toISOString(), + environment: config.nodeEnv, + }); +}); + +// Auth routes +app.post("/api/auth/register", register); +app.post("/api/auth/login", login); + +// API routes +app.use("/api", (_req, res) => { + res.json({ message: "API is working" }); +}); + +// 404 handler +app.use((req, res) => { + res.status(404).json({ + error: "Route not found", + message: `Cannot ${req.method} ${req.originalUrl}`, + }); +}); + +// Global error handler +app.use((err: Error, _req: any, res: any, _next: any) => { + console.error("Error:", err); + + res.status(500).json({ + error: "Internal server error", + message: + config.nodeEnv === "development" ? err.message : "Something went wrong", + }); +}); + +export default app; From 7b4617253321b0b65d8def99d0c66957d4c94eb3 Mon Sep 17 00:00:00 2001 From: Vladyslav <46872670+virus231@users.noreply.github.com> Date: Wed, 17 Sep 2025 01:15:48 +0200 Subject: [PATCH 09/48] test: api request --- vercel.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/vercel.json b/vercel.json index daaf89d..4944b15 100644 --- a/vercel.json +++ b/vercel.json @@ -7,9 +7,18 @@ }, { "src": "backend/api/index.ts", - "use": "@vercel/node" + "use": "@vercel/node", + "config": { + "includeFiles": "backend/src/**", + "maxLambdaSize": "50mb" + } } ], + "functions": { + "backend/api/index.ts": { + "includeFiles": "backend/{src,prisma}/**" + } + }, "routes": [ { "src": "/api/(.*)", From cf93b1284aeba05cbc3c0a3fb744f99596e99ae5 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 16 Sep 2025 23:16:51 +0000 Subject: [PATCH 10/48] chore(release): 1.1.0-beta.4 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.1.0-beta.4](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.3...v1.1.0-beta.4) (2025-09-16) ### 🚨 Tests * api request ([7b46172](https://github.com/virus231/tech-stack/commit/7b4617253321b0b65d8def99d0c66957d4c94eb3)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04d1077..0e948cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.1.0-beta.4](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.3...v1.1.0-beta.4) (2025-09-16) + +### 🚨 Tests + +* api request ([7b46172](https://github.com/virus231/tech-stack/commit/7b4617253321b0b65d8def99d0c66957d4c94eb3)) + ## [1.1.0-beta.3](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.2...v1.1.0-beta.3) (2025-09-16) ### ✨ Features From 28c4ae364c1ef8ec09f65b83cb63ecf17ba15dca Mon Sep 17 00:00:00 2001 From: Vladyslav <46872670+virus231@users.noreply.github.com> Date: Wed, 17 Sep 2025 01:27:49 +0200 Subject: [PATCH 11/48] test: fix api --- backend/api/index.ts | 64 +------------------------------------------- vercel.json | 2 +- 2 files changed, 2 insertions(+), 64 deletions(-) diff --git a/backend/api/index.ts b/backend/api/index.ts index d3e30db..4993bdb 100644 --- a/backend/api/index.ts +++ b/backend/api/index.ts @@ -1,65 +1,3 @@ -import cors from "cors"; -import express from "express"; -import helmet from "helmet"; -import morgan from "morgan"; -import { config } from "../src/config/config"; -import { login, register } from "../src/controllers/auth"; - -const app = express(); - -// Security middleware -app.use(helmet()); - -// CORS configuration -app.use( - cors({ - origin: config.corsOrigin, - credentials: true, - }) -); - -// Logging middleware -app.use(morgan("combined")); - -// Body parsing middleware -app.use(express.json({ limit: "10mb" })); -app.use(express.urlencoded({ extended: true, limit: "10mb" })); - -// Health check endpoint -app.get("/api/health", (_req, res) => { - res.status(200).json({ - status: "OK", - timestamp: new Date().toISOString(), - environment: config.nodeEnv, - }); -}); - -// Auth routes -app.post("/api/auth/register", register); -app.post("/api/auth/login", login); - -// API routes -app.use("/api", (_req, res) => { - res.json({ message: "API is working" }); -}); - -// 404 handler -app.use((req, res) => { - res.status(404).json({ - error: "Route not found", - message: `Cannot ${req.method} ${req.originalUrl}`, - }); -}); - -// Global error handler -app.use((err: Error, _req: any, res: any, _next: any) => { - console.error("Error:", err); - - res.status(500).json({ - error: "Internal server error", - message: - config.nodeEnv === "development" ? err.message : "Something went wrong", - }); -}); +import app from "../src/app"; export default app; diff --git a/vercel.json b/vercel.json index 4944b15..5e9055f 100644 --- a/vercel.json +++ b/vercel.json @@ -9,7 +9,7 @@ "src": "backend/api/index.ts", "use": "@vercel/node", "config": { - "includeFiles": "backend/src/**", + "includeFiles": "backend/{src,prisma,tsconfig.json}/**", "maxLambdaSize": "50mb" } } From 3858b33e923b247578de7a1e808b7f4f63ba3ee7 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 16 Sep 2025 23:28:54 +0000 Subject: [PATCH 12/48] chore(release): 1.1.0-beta.5 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.1.0-beta.5](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.4...v1.1.0-beta.5) (2025-09-16) ### 🚨 Tests * fix api ([28c4ae3](https://github.com/virus231/tech-stack/commit/28c4ae364c1ef8ec09f65b83cb63ecf17ba15dca)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e948cd..22d5eec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.1.0-beta.5](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.4...v1.1.0-beta.5) (2025-09-16) + +### 🚨 Tests + +* fix api ([28c4ae3](https://github.com/virus231/tech-stack/commit/28c4ae364c1ef8ec09f65b83cb63ecf17ba15dca)) + ## [1.1.0-beta.4](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.3...v1.1.0-beta.4) (2025-09-16) ### 🚨 Tests From 2f641b5a3ee320a1dc72594459c52382088d15a8 Mon Sep 17 00:00:00 2001 From: Vladyslav <46872670+virus231@users.noreply.github.com> Date: Wed, 17 Sep 2025 01:33:44 +0200 Subject: [PATCH 13/48] test: fix api --- backend/package.json | 3 ++- vercel.json | 11 +---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/backend/package.json b/backend/package.json index 556a3a5..36d636f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,7 +13,7 @@ "prisma:migrate": "prisma migrate dev", "prisma:migrate:prod": "ts-node prisma/migrate-prod.ts", "prisma:studio": "prisma studio", - "vercel-build": "pnpm prisma:generate && pnpm build", + "vercel-build": "pnpm prisma:generate && tsc && tsc-alias", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], @@ -41,6 +41,7 @@ "nodemon": "^3.1.10", "prisma": "^6.16.1", "ts-node": "^10.9.2", + "tsc-alias": "^1.8.8", "tsconfig-paths": "^4.2.0", "typescript": "^5.9.2" } diff --git a/vercel.json b/vercel.json index 5e9055f..daaf89d 100644 --- a/vercel.json +++ b/vercel.json @@ -7,18 +7,9 @@ }, { "src": "backend/api/index.ts", - "use": "@vercel/node", - "config": { - "includeFiles": "backend/{src,prisma,tsconfig.json}/**", - "maxLambdaSize": "50mb" - } + "use": "@vercel/node" } ], - "functions": { - "backend/api/index.ts": { - "includeFiles": "backend/{src,prisma}/**" - } - }, "routes": [ { "src": "/api/(.*)", From 19a9cc49fe1309851caddfb8b87d10df214c255a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 16 Sep 2025 23:34:59 +0000 Subject: [PATCH 14/48] chore(release): 1.1.0-beta.6 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.1.0-beta.6](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.5...v1.1.0-beta.6) (2025-09-16) ### 🚨 Tests * fix api ([2f641b5](https://github.com/virus231/tech-stack/commit/2f641b5a3ee320a1dc72594459c52382088d15a8)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22d5eec..5fa5bae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.1.0-beta.6](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.5...v1.1.0-beta.6) (2025-09-16) + +### 🚨 Tests + +* fix api ([2f641b5](https://github.com/virus231/tech-stack/commit/2f641b5a3ee320a1dc72594459c52382088d15a8)) + ## [1.1.0-beta.5](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.4...v1.1.0-beta.5) (2025-09-16) ### 🚨 Tests From 6ff87b174ce711deca43853a879458d72943a6d3 Mon Sep 17 00:00:00 2001 From: Vladyslav <46872670+virus231@users.noreply.github.com> Date: Wed, 17 Sep 2025 01:41:28 +0200 Subject: [PATCH 15/48] fix: api --- backend/package.json | 3 +-- backend/src/app.ts | 4 ++-- backend/src/controllers/auth.ts | 4 ++-- backend/src/middleware/auth.ts | 2 +- backend/src/routes/auth.ts | 2 +- backend/src/server.ts | 4 ++-- backend/src/utils/auth.ts | 2 +- 7 files changed, 10 insertions(+), 11 deletions(-) diff --git a/backend/package.json b/backend/package.json index 36d636f..eda11c4 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,8 +13,7 @@ "prisma:migrate": "prisma migrate dev", "prisma:migrate:prod": "ts-node prisma/migrate-prod.ts", "prisma:studio": "prisma studio", - "vercel-build": "pnpm prisma:generate && tsc && tsc-alias", - "test": "echo \"Error: no test specified\" && exit 1" + "vercel-build": "pnpm prisma:generate && tsc && tsc-alias" }, "keywords": [], "author": "", diff --git a/backend/src/app.ts b/backend/src/app.ts index 2b05ac4..360db29 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,9 +1,9 @@ -import { config } from "@/config/config"; +import { config } from "./config/config"; import cors from "cors"; import express, { type Express } from "express"; import helmet from "helmet"; import morgan from "morgan"; -import authRoutes from "@/routes/auth"; +import authRoutes from "./routes/auth"; const app: Express = express(); diff --git a/backend/src/controllers/auth.ts b/backend/src/controllers/auth.ts index 78f145f..15ee568 100644 --- a/backend/src/controllers/auth.ts +++ b/backend/src/controllers/auth.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import { PrismaClient } from '@prisma/client'; -import { generateToken, hashPassword, comparePassword } from '@/utils/auth'; -import { RegisterRequest, LoginRequest, AuthResponse } from '@/types/auth'; +import { generateToken, hashPassword, comparePassword } from '../utils/auth'; +import { RegisterRequest, LoginRequest, AuthResponse } from '../types/auth'; const prisma = new PrismaClient(); diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index ed40d2c..3e9a733 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -1,5 +1,5 @@ import { Request, Response, NextFunction } from 'express'; -import { verifyToken, extractTokenFromHeader, JWTPayload } from '@/utils/auth'; +import { verifyToken, extractTokenFromHeader, JWTPayload } from '../utils/auth'; // Extend Request interface to include user declare global { diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 3127301..ee30e11 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -1,4 +1,4 @@ -import { login, register } from "@/controllers/auth"; +import { login, register } from "../controllers/auth"; import { Router } from "express"; const router: Router = Router(); diff --git a/backend/src/server.ts b/backend/src/server.ts index 2d22719..e8e997e 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -1,6 +1,6 @@ import app from './app'; -import { config } from '@/config/config'; -import { prisma } from '@/config/database'; +import { config } from './config/config'; +import { prisma } from './config/database'; const startServer = async () => { try { diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index 92422fc..86cdce7 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -1,6 +1,6 @@ import jwt from 'jsonwebtoken'; import bcrypt from 'bcryptjs'; -import { config } from '@/config/config'; +import { config } from '../config/config'; export interface JWTPayload { userId: number; From e64bbe189e6b1fb298f9244eb9ddeb5d7cda66ae Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 16 Sep 2025 23:42:57 +0000 Subject: [PATCH 16/48] chore(release): 1.1.0-beta.7 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.1.0-beta.7](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.6...v1.1.0-beta.7) (2025-09-16) ### 🐛 Bug Fixes * api ([6ff87b1](https://github.com/virus231/tech-stack/commit/6ff87b174ce711deca43853a879458d72943a6d3)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa5bae..7031a61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.1.0-beta.7](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.6...v1.1.0-beta.7) (2025-09-16) + +### 🐛 Bug Fixes + +* api ([6ff87b1](https://github.com/virus231/tech-stack/commit/6ff87b174ce711deca43853a879458d72943a6d3)) + ## [1.1.0-beta.6](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.5...v1.1.0-beta.6) (2025-09-16) ### 🚨 Tests From 492e2461c6fc095242c49002109837615df35308 Mon Sep 17 00:00:00 2001 From: Vladyslav <46872670+virus231@users.noreply.github.com> Date: Wed, 17 Sep 2025 01:52:59 +0200 Subject: [PATCH 17/48] fix: api path url --- backend/src/app.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 360db29..b9f9f85 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -26,7 +26,7 @@ app.use(express.json({ limit: "10mb" })); app.use(express.urlencoded({ extended: true, limit: "10mb" })); // Health check endpoint -app.get("/health", (_req, res) => { +app.get("/api/health", (_req, res) => { res.status(200).json({ status: "OK", timestamp: new Date().toISOString(), @@ -35,7 +35,7 @@ app.get("/health", (_req, res) => { }); // Auth routes -app.use("/auth", authRoutes); +app.use("/api/auth", authRoutes); // API routes app.use("/api", (_req, res) => { From a7f90c8eefa65f9ecef6669676253b254f489ed4 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 16 Sep 2025 23:54:08 +0000 Subject: [PATCH 18/48] chore(release): 1.1.0-beta.8 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.1.0-beta.8](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.7...v1.1.0-beta.8) (2025-09-16) ### 🐛 Bug Fixes * api path url ([492e246](https://github.com/virus231/tech-stack/commit/492e2461c6fc095242c49002109837615df35308)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7031a61..1c4aee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.1.0-beta.8](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.7...v1.1.0-beta.8) (2025-09-16) + +### 🐛 Bug Fixes + +* api path url ([492e246](https://github.com/virus231/tech-stack/commit/492e2461c6fc095242c49002109837615df35308)) + ## [1.1.0-beta.7](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.6...v1.1.0-beta.7) (2025-09-16) ### 🐛 Bug Fixes From 58aa7b03a8e4fd7e465a5b3ebce5d3cdae5ce4ac Mon Sep 17 00:00:00 2001 From: Vladyslav <46872670+virus231@users.noreply.github.com> Date: Wed, 17 Sep 2025 15:43:44 +0200 Subject: [PATCH 19/48] fix: change folder --- backend/api/index.ts | 3 --- backend/index.ts | 0 vercel.json | 6 +++--- 3 files changed, 3 insertions(+), 6 deletions(-) delete mode 100644 backend/api/index.ts delete mode 100644 backend/index.ts diff --git a/backend/api/index.ts b/backend/api/index.ts deleted file mode 100644 index 4993bdb..0000000 --- a/backend/api/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import app from "../src/app"; - -export default app; diff --git a/backend/index.ts b/backend/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/vercel.json b/vercel.json index daaf89d..faa7d65 100644 --- a/vercel.json +++ b/vercel.json @@ -6,18 +6,18 @@ "use": "@vercel/next" }, { - "src": "backend/api/index.ts", + "src": "backend/src/app.ts", "use": "@vercel/node" } ], "routes": [ { "src": "/api/(.*)", - "dest": "backend/api/index.ts" + "dest": "backend/src/app.ts" }, { "src": "/(.*)", "dest": "frontend/$1" } ] -} \ No newline at end of file +} From 94f770c26f1869dce22811d15de0402c3aa23c99 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 17 Sep 2025 13:44:49 +0000 Subject: [PATCH 20/48] chore(release): 1.1.0-beta.9 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.1.0-beta.9](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.8...v1.1.0-beta.9) (2025-09-17) ### 🐛 Bug Fixes * change folder ([58aa7b0](https://github.com/virus231/tech-stack/commit/58aa7b03a8e4fd7e465a5b3ebce5d3cdae5ce4ac)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c4aee7..a67bf21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.1.0-beta.9](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.8...v1.1.0-beta.9) (2025-09-17) + +### 🐛 Bug Fixes + +* change folder ([58aa7b0](https://github.com/virus231/tech-stack/commit/58aa7b03a8e4fd7e465a5b3ebce5d3cdae5ce4ac)) + ## [1.1.0-beta.8](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.7...v1.1.0-beta.8) (2025-09-16) ### 🐛 Bug Fixes From e3159854bdf33b155920a3a36b95c8ca1f15eadc Mon Sep 17 00:00:00 2001 From: Vladyslav <46872670+virus231@users.noreply.github.com> Date: Wed, 17 Sep 2025 15:49:01 +0200 Subject: [PATCH 21/48] fix: releaser config --- .releaserc.json | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/.releaserc.json b/.releaserc.json index d885024..b51e9d5 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -115,16 +115,9 @@ [ "@semantic-release/github", { - "assets": [ - { - "path": "backend/dist/**/*", - "label": "Backend Build" - }, - { - "path": "frontend/dist/**/*", - "label": "Frontend Build" - } - ] + "failComment": false, + "failTitle": false, + "successComment": false } ], [ From ebbadb4c78b91eebaae8e827c01b66b71590dfec Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 17 Sep 2025 13:50:15 +0000 Subject: [PATCH 22/48] chore(release): 1.1.0-beta.10 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.1.0-beta.10](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.9...v1.1.0-beta.10) (2025-09-17) ### 🐛 Bug Fixes * releaser config ([e315985](https://github.com/virus231/tech-stack/commit/e3159854bdf33b155920a3a36b95c8ca1f15eadc)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a67bf21..1877742 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.1.0-beta.10](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.9...v1.1.0-beta.10) (2025-09-17) + +### 🐛 Bug Fixes + +* releaser config ([e315985](https://github.com/virus231/tech-stack/commit/e3159854bdf33b155920a3a36b95c8ca1f15eadc)) + ## [1.1.0-beta.9](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.8...v1.1.0-beta.9) (2025-09-17) ### 🐛 Bug Fixes From f97fad778935223a4392c8fd7a513c3580700056 Mon Sep 17 00:00:00 2001 From: Vladyslav <46872670+virus231@users.noreply.github.com> Date: Wed, 17 Sep 2025 16:07:29 +0200 Subject: [PATCH 23/48] feat: posts api --- backend/src/app.ts | 4 + backend/src/controllers/posts.ts | 173 +++++++++++++++++++++++++++++++ backend/src/routes/posts.ts | 16 +++ 3 files changed, 193 insertions(+) create mode 100644 backend/src/controllers/posts.ts create mode 100644 backend/src/routes/posts.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index b9f9f85..6aaaf83 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -4,6 +4,7 @@ import express, { type Express } from "express"; import helmet from "helmet"; import morgan from "morgan"; import authRoutes from "./routes/auth"; +import postsRoutes from "./routes/posts"; const app: Express = express(); @@ -37,6 +38,9 @@ app.get("/api/health", (_req, res) => { // Auth routes app.use("/api/auth", authRoutes); +// Posts routes +app.use("/api/posts", postsRoutes); + // API routes app.use("/api", (_req, res) => { res.json({ message: "API is working" }); diff --git a/backend/src/controllers/posts.ts b/backend/src/controllers/posts.ts new file mode 100644 index 0000000..dac7fc0 --- /dev/null +++ b/backend/src/controllers/posts.ts @@ -0,0 +1,173 @@ +import { Request, Response } from 'express'; +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +interface AuthRequest extends Request { + user?: { + userId: number; + email: string; + }; +} + +interface CreatePostRequest { + title: string; + content: string; + description?: string; +} + +/** + * Get all posts + * GET /posts + */ +export const getAllPosts = async (req: Request, res: Response): Promise => { + try { + const posts = await prisma.post.findMany({ + include: { + author: { + select: { + id: true, + name: true, + email: true, + createdAt: true + } + } + }, + orderBy: { + createdAt: 'desc' + } + }); + + res.status(200).json({ + posts, + total: posts.length + }); + } catch (error) { + console.error('Get all posts error:', error); + res.status(500).json({ + error: 'Internal server error', + message: 'Failed to fetch posts' + }); + } +}; + +/** + * Get post by ID + * GET /posts/:id + */ +export const getPostById = async (req: Request, res: Response): Promise => { + try { + const { id } = req.params; + const postId = parseInt(id, 10); + + if (isNaN(postId)) { + res.status(400).json({ + error: 'Validation error', + message: 'Invalid post ID' + }); + return; + } + + const post = await prisma.post.findUnique({ + where: { id: postId }, + include: { + author: { + select: { + id: true, + name: true, + email: true, + createdAt: true + } + } + } + }); + + if (!post) { + res.status(404).json({ + error: 'Post not found', + message: 'Post with this ID does not exist' + }); + return; + } + + res.status(200).json(post); + } catch (error) { + console.error('Get post by ID error:', error); + res.status(500).json({ + error: 'Internal server error', + message: 'Failed to fetch post' + }); + } +}; + +/** + * Create new post + * POST /posts + * Requires authentication + */ +export const createPost = async (req: AuthRequest, res: Response): Promise => { + try { + if (!req.user) { + res.status(401).json({ + error: 'Authentication required', + message: 'You must be logged in to create a post' + }); + return; + } + + const { title, content, description }: CreatePostRequest = req.body; + + // Validate input + if (!title || !content) { + res.status(400).json({ + error: 'Validation error', + message: 'Title and content are required' + }); + return; + } + + if (title.trim().length < 3) { + res.status(400).json({ + error: 'Validation error', + message: 'Title must be at least 3 characters long' + }); + return; + } + + if (content.trim().length < 10) { + res.status(400).json({ + error: 'Validation error', + message: 'Content must be at least 10 characters long' + }); + return; + } + + // Create post + const post = await prisma.post.create({ + data: { + title: title.trim(), + content: content.trim(), + description: description?.trim() || null, + authorId: req.user.userId + }, + include: { + author: { + select: { + id: true, + name: true, + email: true, + createdAt: true + } + } + } + }); + + res.status(201).json(post); + } catch (error) { + console.error('Create post error:', error); + res.status(500).json({ + error: 'Internal server error', + message: 'Failed to create post' + }); + } +}; \ No newline at end of file diff --git a/backend/src/routes/posts.ts b/backend/src/routes/posts.ts new file mode 100644 index 0000000..0ca0439 --- /dev/null +++ b/backend/src/routes/posts.ts @@ -0,0 +1,16 @@ +import { Router } from 'express'; +import { getAllPosts, getPostById, createPost } from '../controllers/posts'; +import { authenticateToken } from '../middleware/auth'; + +const router: Router = Router(); + +// GET /posts - отримати всі пости +router.get('/', getAllPosts); + +// GET /posts/:id - отримати конкретний пост +router.get('/:id', getPostById); + +// POST /posts - створити новий пост (тільки авторизований користувач) +router.post('/', authenticateToken, createPost); + +export default router; \ No newline at end of file From 143830fd77ac99fc2dcad035b206b30f568b38cc Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 17 Sep 2025 14:10:48 +0000 Subject: [PATCH 24/48] chore(release): 1.1.0-beta.11 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.1.0-beta.11](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.10...v1.1.0-beta.11) (2025-09-17) ### ✨ Features * posts api ([f97fad7](https://github.com/virus231/tech-stack/commit/f97fad778935223a4392c8fd7a513c3580700056)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1877742..23bf554 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.1.0-beta.11](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.10...v1.1.0-beta.11) (2025-09-17) + +### ✨ Features + +* posts api ([f97fad7](https://github.com/virus231/tech-stack/commit/f97fad778935223a4392c8fd7a513c3580700056)) + ## [1.1.0-beta.10](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.9...v1.1.0-beta.10) (2025-09-17) ### 🐛 Bug Fixes From fab05d5bedea427442a8cc78d384f0e25d1a1cf0 Mon Sep 17 00:00:00 2001 From: Vladyslav <46872670+virus231@users.noreply.github.com> Date: Wed, 17 Sep 2025 16:20:25 +0200 Subject: [PATCH 25/48] feat: users api --- backend/src/app.ts | 4 + backend/src/controllers/users.ts | 274 +++++++++++++++++++++++++++++++ backend/src/routes/users.ts | 19 +++ 3 files changed, 297 insertions(+) create mode 100644 backend/src/controllers/users.ts create mode 100644 backend/src/routes/users.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index 6aaaf83..f05a512 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -5,6 +5,7 @@ import helmet from "helmet"; import morgan from "morgan"; import authRoutes from "./routes/auth"; import postsRoutes from "./routes/posts"; +import usersRoutes from "./routes/users"; const app: Express = express(); @@ -41,6 +42,9 @@ app.use("/api/auth", authRoutes); // Posts routes app.use("/api/posts", postsRoutes); +// Users routes +app.use("/api/users", usersRoutes); + // API routes app.use("/api", (_req, res) => { res.json({ message: "API is working" }); diff --git a/backend/src/controllers/users.ts b/backend/src/controllers/users.ts new file mode 100644 index 0000000..9bb5bc0 --- /dev/null +++ b/backend/src/controllers/users.ts @@ -0,0 +1,274 @@ +import { Request, Response } from 'express'; +import { PrismaClient } from '@prisma/client'; +import { hashPassword, comparePassword } from '../utils/auth'; + +const prisma = new PrismaClient(); + +interface AuthRequest extends Request { + user?: { + userId: number; + email: string; + }; +} + +interface UpdateUserRequest { + name?: string; + email?: string; + password?: string; + currentPassword?: string; +} + +/** + * Get current user information + * GET /users/me + */ +export const getCurrentUser = async (req: AuthRequest, res: Response): Promise => { + try { + if (!req.user) { + res.status(401).json({ + error: 'Authentication required', + message: 'You must be logged in to access this resource' + }); + return; + } + + const user = await prisma.user.findUnique({ + where: { id: req.user.userId }, + select: { + id: true, + email: true, + name: true, + createdAt: true, + updatedAt: true, + _count: { + select: { + posts: true + } + } + } + }); + + if (!user) { + res.status(404).json({ + error: 'User not found', + message: 'User account no longer exists' + }); + return; + } + + res.status(200).json(user); + } catch (error) { + console.error('Get current user error:', error); + res.status(500).json({ + error: 'Internal server error', + message: 'Failed to fetch user information' + }); + } +}; + +/** + * Update current user information + * PUT /users/me + */ +export const updateCurrentUser = async (req: AuthRequest, res: Response): Promise => { + try { + if (!req.user) { + res.status(401).json({ + error: 'Authentication required', + message: 'You must be logged in to update your profile' + }); + return; + } + + const { name, email, password, currentPassword }: UpdateUserRequest = req.body; + + // Validate that at least one field is provided + if (!name && !email && !password) { + res.status(400).json({ + error: 'Validation error', + message: 'At least one field (name, email, or password) must be provided' + }); + return; + } + + // Get current user + const currentUser = await prisma.user.findUnique({ + where: { id: req.user.userId } + }); + + if (!currentUser) { + res.status(404).json({ + error: 'User not found', + message: 'User account no longer exists' + }); + return; + } + + // Prepare update data + const updateData: any = {}; + + // Update name + if (name !== undefined) { + if (name.trim().length < 2) { + res.status(400).json({ + error: 'Validation error', + message: 'Name must be at least 2 characters long' + }); + return; + } + updateData.name = name.trim(); + } + + // Update email + if (email !== undefined) { + if (!email.includes('@')) { + res.status(400).json({ + error: 'Validation error', + message: 'Please provide a valid email address' + }); + return; + } + + // Check if email is already taken by another user + const existingUser = await prisma.user.findUnique({ + where: { email } + }); + + if (existingUser && existingUser.id !== req.user.userId) { + res.status(409).json({ + error: 'Email already exists', + message: 'This email is already registered to another account' + }); + return; + } + + updateData.email = email.toLowerCase().trim(); + } + + // Update password + if (password !== undefined) { + if (!currentPassword) { + res.status(400).json({ + error: 'Validation error', + message: 'Current password is required to set a new password' + }); + return; + } + + // Verify current password + const isValidCurrentPassword = await comparePassword(currentPassword, currentUser.password); + + if (!isValidCurrentPassword) { + res.status(400).json({ + error: 'Authentication failed', + message: 'Current password is incorrect' + }); + return; + } + + if (password.length < 6) { + res.status(400).json({ + error: 'Validation error', + message: 'New password must be at least 6 characters long' + }); + return; + } + + updateData.password = await hashPassword(password); + } + + // Update user + const updatedUser = await prisma.user.update({ + where: { id: req.user.userId }, + data: updateData, + select: { + id: true, + email: true, + name: true, + createdAt: true, + updatedAt: true, + _count: { + select: { + posts: true + } + } + } + }); + + res.status(200).json({ + message: 'Profile updated successfully', + user: updatedUser + }); + } catch (error) { + console.error('Update user error:', error); + res.status(500).json({ + error: 'Internal server error', + message: 'Failed to update user information' + }); + } +}; + +/** + * Delete current user account + * DELETE /users/me + */ +export const deleteCurrentUser = async (req: AuthRequest, res: Response): Promise => { + try { + if (!req.user) { + res.status(401).json({ + error: 'Authentication required', + message: 'You must be logged in to delete your account' + }); + return; + } + + const { password } = req.body; + + if (!password) { + res.status(400).json({ + error: 'Validation error', + message: 'Password confirmation is required to delete your account' + }); + return; + } + + // Get current user + const currentUser = await prisma.user.findUnique({ + where: { id: req.user.userId } + }); + + if (!currentUser) { + res.status(404).json({ + error: 'User not found', + message: 'User account no longer exists' + }); + return; + } + + // Verify password + const isValidPassword = await comparePassword(password, currentUser.password); + + if (!isValidPassword) { + res.status(400).json({ + error: 'Authentication failed', + message: 'Password is incorrect' + }); + return; + } + + // Delete user (this will cascade delete all posts due to schema setup) + await prisma.user.delete({ + where: { id: req.user.userId } + }); + + res.status(200).json({ + message: 'Account deleted successfully' + }); + } catch (error) { + console.error('Delete user error:', error); + res.status(500).json({ + error: 'Internal server error', + message: 'Failed to delete user account' + }); + } +}; \ No newline at end of file diff --git a/backend/src/routes/users.ts b/backend/src/routes/users.ts new file mode 100644 index 0000000..869e5b1 --- /dev/null +++ b/backend/src/routes/users.ts @@ -0,0 +1,19 @@ +import { Router } from 'express'; +import { getCurrentUser, updateCurrentUser, deleteCurrentUser } from '../controllers/users'; +import { authenticateToken } from '../middleware/auth'; + +const router: Router = Router(); + +// All user routes require authentication +router.use(authenticateToken); + +// GET /users/me - отримати інформацію про поточного авторизованого користувача +router.get('/me', getCurrentUser); + +// PUT /users/me - оновити інформацію (ім'я, email, пароль) +router.put('/me', updateCurrentUser); + +// DELETE /users/me - видалити свій акаунт (опціонально) +router.delete('/me', deleteCurrentUser); + +export default router; \ No newline at end of file From a8dd8ea2d36ba2607fed70420c59467c2effcbe7 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 17 Sep 2025 14:21:57 +0000 Subject: [PATCH 26/48] chore(release): 1.1.0-beta.12 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.1.0-beta.12](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.11...v1.1.0-beta.12) (2025-09-17) ### ✨ Features * users api ([fab05d5](https://github.com/virus231/tech-stack/commit/fab05d5bedea427442a8cc78d384f0e25d1a1cf0)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23bf554..3c983ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.1.0-beta.12](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.11...v1.1.0-beta.12) (2025-09-17) + +### ✨ Features + +* users api ([fab05d5](https://github.com/virus231/tech-stack/commit/fab05d5bedea427442a8cc78d384f0e25d1a1cf0)) + ## [1.1.0-beta.11](https://github.com/virus231/tech-stack/compare/v1.1.0-beta.10...v1.1.0-beta.11) (2025-09-17) ### ✨ Features From 469894a899eb4e42490653691193a78c5659980e Mon Sep 17 00:00:00 2001 From: Vladyslav <46872670+virus231@users.noreply.github.com> Date: Wed, 17 Sep 2025 16:28:33 +0200 Subject: [PATCH 27/48] add: env var --- backend/src/config/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/config/config.ts b/backend/src/config/config.ts index c923178..629a4cf 100644 --- a/backend/src/config/config.ts +++ b/backend/src/config/config.ts @@ -24,7 +24,7 @@ export const config: Config = { }; // Validate required environment variables -const requiredEnvVars = ["DATABASE_URL"]; +const requiredEnvVars = ["DATABASE_URL", "JWT_SECRET"]; for (const envVar of requiredEnvVars) { if (!process.env[envVar]) { From d2b9ce5002917ea1f25dc1b0332c587cb49b703f Mon Sep 17 00:00:00 2001 From: Vladyslav <46872670+virus231@users.noreply.github.com> Date: Wed, 17 Sep 2025 17:02:26 +0200 Subject: [PATCH 28/48] feat: shadcn setup, auth setup with tanstack query and axios --- frontend/components.json | 22 + frontend/package.json | 20 +- frontend/pnpm-lock.yaml | 375 ++++++++++++++++++ frontend/src/app/auth/login/page.tsx | 146 +++++++ frontend/src/app/auth/register/page.tsx | 194 +++++++++ frontend/src/app/globals.css | 122 +++++- frontend/src/app/layout.tsx | 14 +- frontend/src/app/page.tsx | 72 +++- .../components/providers/query-provider.tsx | 30 ++ frontend/src/components/ui/button.tsx | 59 +++ frontend/src/components/ui/card.tsx | 92 +++++ frontend/src/components/ui/form.tsx | 167 ++++++++ frontend/src/components/ui/input.tsx | 21 + frontend/src/components/ui/label.tsx | 24 ++ frontend/src/contexts/auth-context.tsx | 74 ++++ frontend/src/hooks/use-auth.ts | 99 +++++ frontend/src/lib/api.ts | 142 +++++++ frontend/src/lib/utils.ts | 6 + 18 files changed, 1656 insertions(+), 23 deletions(-) create mode 100644 frontend/components.json create mode 100644 frontend/src/app/auth/login/page.tsx create mode 100644 frontend/src/app/auth/register/page.tsx create mode 100644 frontend/src/components/providers/query-provider.tsx create mode 100644 frontend/src/components/ui/button.tsx create mode 100644 frontend/src/components/ui/card.tsx create mode 100644 frontend/src/components/ui/form.tsx create mode 100644 frontend/src/components/ui/input.tsx create mode 100644 frontend/src/components/ui/label.tsx create mode 100644 frontend/src/contexts/auth-context.tsx create mode 100644 frontend/src/hooks/use-auth.ts create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/src/lib/utils.ts diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..edcaef2 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/frontend/package.json b/frontend/package.json index 9240b8e..c466780 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,17 +10,29 @@ "format": "biome format --write" }, "dependencies": { + "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@tanstack/react-query": "^5.89.0", + "axios": "^1.12.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.544.0", + "next": "15.5.3", "react": "19.1.0", "react-dom": "19.1.0", - "next": "15.5.3" + "react-hook-form": "^7.62.0", + "tailwind-merge": "^3.3.1", + "zod": "^4.1.9" }, "devDependencies": { - "typescript": "^5", + "@biomejs/biome": "2.2.0", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", "tailwindcss": "^4", - "@biomejs/biome": "2.2.0" + "tw-animate-css": "^1.3.8", + "typescript": "^5" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 61a0eb2..36a3852 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -8,6 +8,30 @@ importers: .: dependencies: + '@hookform/resolvers': + specifier: ^5.2.2 + version: 5.2.2(react-hook-form@7.62.0(react@19.1.0)) + '@radix-ui/react-label': + specifier: ^2.1.7 + version: 2.1.7(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.3(@types/react@19.1.13)(react@19.1.0) + '@tanstack/react-query': + specifier: ^5.89.0 + version: 5.89.0(react@19.1.0) + axios: + specifier: ^1.12.2 + version: 1.12.2 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lucide-react: + specifier: ^0.544.0 + version: 0.544.0(react@19.1.0) next: specifier: 15.5.3 version: 15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -17,6 +41,15 @@ importers: react-dom: specifier: 19.1.0 version: 19.1.0(react@19.1.0) + react-hook-form: + specifier: ^7.62.0 + version: 7.62.0(react@19.1.0) + tailwind-merge: + specifier: ^3.3.1 + version: 3.3.1 + zod: + specifier: ^4.1.9 + version: 4.1.9 devDependencies: '@biomejs/biome': specifier: 2.2.0 @@ -36,6 +69,9 @@ importers: tailwindcss: specifier: ^4 version: 4.1.13 + tw-animate-css: + specifier: ^1.3.8 + version: 1.3.8 typescript: specifier: ^5 version: 5.9.2 @@ -102,6 +138,11 @@ packages: '@emnapi/runtime@1.5.0': resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + '@hookform/resolvers@5.2.2': + resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} + peerDependencies: + react-hook-form: ^7.55.0 + '@img/sharp-darwin-arm64@0.34.3': resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -295,6 +336,53 @@ packages: cpu: [x64] os: [win32] + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -386,6 +474,14 @@ packages: '@tailwindcss/postcss@4.1.13': resolution: {integrity: sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==} + '@tanstack/query-core@5.89.0': + resolution: {integrity: sha512-joFV1MuPhSLsKfTzwjmPDrp8ENfZ9N23ymFu07nLfn3JCkSHy0CFgsyhHTJOmWaumC/WiNIKM0EJyduCF/Ih/Q==} + + '@tanstack/react-query@5.89.0': + resolution: {integrity: sha512-SXbtWSTSRXyBOe80mszPxpEbaN4XPRUp/i0EfQK1uyj3KCk/c8FuPJNIRwzOVe/OU3rzxrYtiNabsAmk1l714A==} + peerDependencies: + react: ^18 || ^19 + '@types/node@20.19.15': resolution: {integrity: sha512-W3bqcbLsRdFDVcmAM5l6oLlcl67vjevn8j1FPZ4nx+K5jNoWCh+FC/btxFoBPnvQlrHHDwfjp1kjIEDfwJ0Mog==} @@ -397,6 +493,16 @@ packages: '@types/react@19.1.13': resolution: {integrity: sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.12.2: + resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001741: resolution: {integrity: sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==} @@ -404,9 +510,16 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -421,20 +534,88 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + detect-libc@2.1.0: resolution: {integrity: sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==} engines: {node: '>=8'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + is-arrayish@0.3.4: resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} @@ -506,9 +687,26 @@ packages: resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} engines: {node: '>= 12.0.0'} + lucide-react@0.544.0: + resolution: {integrity: sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} @@ -559,11 +757,20 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + react-dom@19.1.0: resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} peerDependencies: react: ^19.1.0 + react-hook-form@7.62.0: + resolution: {integrity: sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react@19.1.0: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} @@ -600,6 +807,9 @@ packages: babel-plugin-macros: optional: true + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwindcss@4.1.13: resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==} @@ -614,6 +824,9 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tw-animate-css@1.3.8: + resolution: {integrity: sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw==} + typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} @@ -626,6 +839,9 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} + zod@4.1.9: + resolution: {integrity: sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -670,6 +886,11 @@ snapshots: tslib: 2.8.1 optional: true + '@hookform/resolvers@5.2.2(react-hook-form@7.62.0(react@19.1.0))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.62.0(react@19.1.0) + '@img/sharp-darwin-arm64@0.34.3': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.2.0 @@ -805,6 +1026,39 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.3': optional: true + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.13)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.13 + + '@radix-ui/react-label@2.1.7(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.13 + '@types/react-dom': 19.1.9(@types/react@19.1.13) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.13)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.13 + '@types/react-dom': 19.1.9(@types/react@19.1.13) + + '@radix-ui/react-slot@1.2.3(@types/react@19.1.13)(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.13)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.13 + + '@standard-schema/utils@0.3.0': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -881,6 +1135,13 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.13 + '@tanstack/query-core@5.89.0': {} + + '@tanstack/react-query@5.89.0(react@19.1.0)': + dependencies: + '@tanstack/query-core': 5.89.0 + react: 19.1.0 + '@types/node@20.19.15': dependencies: undici-types: 6.21.0 @@ -893,12 +1154,33 @@ snapshots: dependencies: csstype: 3.1.3 + asynckit@0.4.0: {} + + axios@1.12.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + caniuse-lite@1.0.30001741: {} chownr@3.0.0: {} + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + client-only@0.0.1: {} + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -919,17 +1201,86 @@ snapshots: color-string: 1.9.1 optional: true + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + csstype@3.1.3: {} + delayed-stream@1.0.0: {} + detect-libc@2.1.0: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 tapable: 2.2.3 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + follow-redirects@1.15.11: {} + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + graceful-fs@4.2.11: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + is-arrayish@0.3.4: optional: true @@ -980,10 +1331,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.1 lightningcss-win32-x64-msvc: 1.30.1 + lucide-react@0.544.0(react@19.1.0): + dependencies: + react: 19.1.0 + magic-string@0.30.19: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minipass@7.1.2: {} minizlib@3.0.2: @@ -1031,11 +1394,17 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + proxy-from-env@1.1.0: {} + react-dom@19.1.0(react@19.1.0): dependencies: react: 19.1.0 scheduler: 0.26.0 + react-hook-form@7.62.0(react@19.1.0): + dependencies: + react: 19.1.0 + react@19.1.0: {} scheduler@0.26.0: {} @@ -1085,6 +1454,8 @@ snapshots: client-only: 0.0.1 react: 19.1.0 + tailwind-merge@3.3.1: {} + tailwindcss@4.1.13: {} tapable@2.2.3: {} @@ -1100,8 +1471,12 @@ snapshots: tslib@2.8.1: {} + tw-animate-css@1.3.8: {} + typescript@5.9.2: {} undici-types@6.21.0: {} yallist@5.0.0: {} + + zod@4.1.9: {} diff --git a/frontend/src/app/auth/login/page.tsx b/frontend/src/app/auth/login/page.tsx new file mode 100644 index 0000000..1464d5e --- /dev/null +++ b/frontend/src/app/auth/login/page.tsx @@ -0,0 +1,146 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; + +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; + +import { useLogin } from '@/hooks/use-auth'; +import { useAuth } from '@/contexts/auth-context'; +import { useEffect } from 'react'; + +const loginSchema = z.object({ + email: z.string().email('Введіть правильний email'), + password: z.string().min(6, 'Пароль повинен містити мінімум 6 символів'), +}); + +type LoginFormValues = z.infer; + +export default function LoginPage() { + const router = useRouter(); + const { isAuthenticated, isLoading } = useAuth(); + const loginMutation = useLogin(); + const [error, setError] = useState(''); + + const form = useForm({ + resolver: zodResolver(loginSchema), + defaultValues: { + email: '', + password: '', + }, + }); + + // Redirect if already authenticated + useEffect(() => { + if (!isLoading && isAuthenticated) { + router.push('/'); + } + }, [isAuthenticated, isLoading, router]); + + const onSubmit = async (data: LoginFormValues) => { + try { + setError(''); + await loginMutation.mutateAsync(data); + } catch (err: any) { + const errorMessage = err.response?.data?.message || 'Помилка входу'; + setError(errorMessage); + } + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (isAuthenticated) { + return null; // Will redirect + } + + return ( +
+ + + Вхід + + Введіть свої дані для входу в аккаунт + + + +
+ + ( + + Email + + + + + + )} + /> + ( + + Пароль + + + + + + )} + /> + + {error && ( +
+ {error} +
+ )} + + + + +
+ +

+ Немає аккаунту?{' '} + + Зареєструватися + +

+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/app/auth/register/page.tsx b/frontend/src/app/auth/register/page.tsx new file mode 100644 index 0000000..4aa04da --- /dev/null +++ b/frontend/src/app/auth/register/page.tsx @@ -0,0 +1,194 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; + +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; + +import { useRegister } from '@/hooks/use-auth'; +import { useAuth } from '@/contexts/auth-context'; +import { useEffect } from 'react'; + +const registerSchema = z.object({ + email: z.string().email('Введіть правильний email'), + password: z.string().min(6, 'Пароль повинен містити мінімум 6 символів'), + confirmPassword: z.string().min(6, 'Підтвердження пароля обов\'язкове'), + name: z.string().min(2, 'Ім\'я повинно містити мінімум 2 символи').optional().or(z.literal('')), +}).refine((data) => data.password === data.confirmPassword, { + message: 'Паролі не співпадають', + path: ['confirmPassword'], +}); + +type RegisterFormValues = z.infer; + +export default function RegisterPage() { + const router = useRouter(); + const { isAuthenticated, isLoading } = useAuth(); + const registerMutation = useRegister(); + const [error, setError] = useState(''); + + const form = useForm({ + resolver: zodResolver(registerSchema), + defaultValues: { + email: '', + password: '', + confirmPassword: '', + name: '', + }, + }); + + // Redirect if already authenticated + useEffect(() => { + if (!isLoading && isAuthenticated) { + router.push('/'); + } + }, [isAuthenticated, isLoading, router]); + + const onSubmit = async (data: RegisterFormValues) => { + try { + setError(''); + const registerData = { + email: data.email, + password: data.password, + name: data.name || undefined, + }; + await registerMutation.mutateAsync(registerData); + } catch (err: any) { + const errorMessage = err.response?.data?.message || 'Помилка реєстрації'; + setError(errorMessage); + } + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (isAuthenticated) { + return null; // Will redirect + } + + return ( +
+ + + Реєстрація + + Створіть новий аккаунт для доступу до блогу + + + +
+ + ( + + Ім'я (опціонально) + + + + + + )} + /> + ( + + Email + + + + + + )} + /> + ( + + Пароль + + + + + + )} + /> + ( + + Підтвердження пароля + + + + + + )} + /> + + {error && ( +
+ {error} +
+ )} + + + + +
+ +

+ Вже маєте аккаунт?{' '} + + Увійти + +

+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index a2dc41e..dc98be7 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -1,26 +1,122 @@ @import "tailwindcss"; +@import "tw-animate-css"; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index f7fa87e..61ef3a0 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import { QueryProvider } from "@/components/providers/query-provider"; +import { AuthProvider } from "@/contexts/auth-context"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -13,8 +15,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Міні-блог", + description: "Простий блог з авторизацією", }; export default function RootLayout({ @@ -23,11 +25,15 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + - {children} + + + {children} + + ); diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 0de028b..c5f142e 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,7 +1,75 @@ +'use client'; + +import { useAuth } from '@/contexts/auth-context'; +import { useRouter } from 'next/navigation'; +import { useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { useLogout } from '@/hooks/use-auth'; + export default function Home() { + const { isAuthenticated, isLoading, user } = useAuth(); + const router = useRouter(); + const logout = useLogout(); + + useEffect(() => { + if (!isLoading && !isAuthenticated) { + router.push('/auth/login'); + } + }, [isAuthenticated, isLoading, router]); + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!isAuthenticated) { + return null; // Will redirect + } + return ( -
- kkkk +
+ {/* Header */} +
+
+
+

Міні-блог

+
+ + Привіт, {user?.name || user?.email}! + + +
+
+
+
+ + {/* Main Content */} +
+
+

+ Ласкаво просимо до міні-блогу! +

+

+ Тут буде список постів та навігація +

+
+ + + +
+
+
); } diff --git a/frontend/src/components/providers/query-provider.tsx b/frontend/src/components/providers/query-provider.tsx new file mode 100644 index 0000000..ad9b9ed --- /dev/null +++ b/frontend/src/components/providers/query-provider.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactNode, useState } from 'react'; + +export function QueryProvider({ children }: { children: ReactNode }) { + const [queryClient] = useState(() => new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, // 1 minute + retry: (failureCount, error: any) => { + // Don't retry on 401/403 errors + if (error?.response?.status === 401 || error?.response?.status === 403) { + return false; + } + return failureCount < 3; + }, + }, + mutations: { + retry: false, + }, + }, + })); + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..a2df8dc --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/frontend/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/frontend/src/components/ui/form.tsx b/frontend/src/components/ui/form.tsx new file mode 100644 index 0000000..524b986 --- /dev/null +++ b/frontend/src/components/ui/form.tsx @@ -0,0 +1,167 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState } = useFormContext() + const formState = useFormState({ name: fieldContext.name }) + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId() + + return ( + +
+ + ) +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField() + + return ( +