diff --git a/backend/.env.example b/backend/.env.example index 457ec81..917665a 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -3,3 +3,7 @@ # Database configuration DATABASE_URL="postgresql://USER:PASSWORD@localhost:5432/arcana_db?schema=public" + +# JWT Configuration +JWT_SECRET="your-super-secret-jwt-key-change-this-in-production" +PORT=3001 diff --git a/backend/API.md b/backend/API.md new file mode 100644 index 0000000..8ab2325 --- /dev/null +++ b/backend/API.md @@ -0,0 +1,141 @@ +# Arcana Backend API Documentation + +## Base URL +`http://localhost:3001` + +--- + +## Authentication Endpoints + +### Register User +**POST** `/api/users/register` + +Request Body: +```json +{ + "username": "john_doe", + "email": "john@example.com", + "password": "securepassword123" +} +``` + +Response (201 Created): +```json +{ + "user": { + "id": "cuid...", + "username": "john_doe", + "email": "john@example.com", + "profile": null + }, + "token": "jwt_token_here" +} +``` + +### Login User +**POST** `/api/users/login` + +Request Body: +```json +{ + "email": "john@example.com", + "password": "securepassword123" +} +``` + +Response (200 OK): +```json +{ + "user": { + "id": "cuid...", + "username": "john_doe", + "email": "john@example.com", + "profile": null + }, + "token": "jwt_token_here" +} +``` + +### Get All Users +**GET** `/api/users` + +Response (200 OK): +```json +{ + "users": [ + { + "id": "cuid...", + "username": "john_doe", + "email": "john@example.com", + "profile": null, + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } + ] +} +``` + +### Get User By ID +**GET** `/api/users/:id` + +--- + +## Transaction Endpoints + +### Create Transaction +**POST** `/api/transactions` + +Request Body: +```json +{ + "userId": "cuid...", + "type": "deposit", + "amount": 100.50, + "currency": "ETH", + "status": "completed", + "hash": "0x..." +} +``` + +Transaction Types: `deposit`, `withdraw`, `transfer`, `swap` +Transaction Status: `pending`, `completed`, `failed`, `cancelled` + +Response (201 Created): +```json +{ + "transaction": { + "id": "cuid...", + "userId": "cuid...", + "type": "deposit", + "amount": 100.5, + "currency": "ETH", + "status": "completed", + "hash": "0x...", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } +} +``` + +### Get All Transactions +**GET** `/api/transactions` + +### Get Transaction By ID +**GET** `/api/transactions/:id` + +### Get Transactions By User ID +**GET** `/api/transactions/user/:userId` + +--- + +## Health Check +**GET** `/health` + +Response: +```json +{ + "status": "ok", + "database": "connected", + "uptime": 123.45 +} +``` diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 4528562..2cd2141 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -11,6 +11,7 @@ model User { id String @id @default(cuid()) username String @unique email String @unique + password String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt profile Profile? diff --git a/backend/src/controllers/transaction.controller.ts b/backend/src/controllers/transaction.controller.ts new file mode 100644 index 0000000..3803a55 --- /dev/null +++ b/backend/src/controllers/transaction.controller.ts @@ -0,0 +1,57 @@ +import { Request, Response } from 'express'; +import prisma from '../lib/prisma'; +import { CreateTransactionInput } from '../validations/transaction.validations'; + +export const createTransaction = async ( + req: Request<{}, {}, CreateTransactionInput>, + res: Response +) => { + const transaction = await prisma.transaction.create({ + data: req.body, + include: { + user: true, + }, + }); + + res.status(201).json({ transaction }); +}; + +export const getTransactions = async (_req: Request, res: Response) => { + const transactions = await prisma.transaction.findMany({ + include: { + user: true, + }, + }); + + res.json({ transactions }); +}; + +export const getTransactionById = async (req: Request, res: Response) => { + const { id } = req.params; + + const transaction = await prisma.transaction.findUnique({ + where: { id }, + include: { + user: true, + }, + }); + + if (!transaction) { + return res.status(404).json({ error: 'Transaction not found' }); + } + + res.json({ transaction }); +}; + +export const getTransactionsByUserId = async (req: Request, res: Response) => { + const { userId } = req.params; + + const transactions = await prisma.transaction.findMany({ + where: { userId }, + include: { + user: true, + }, + }); + + res.json({ transactions }); +}; diff --git a/backend/src/controllers/user.controller.ts b/backend/src/controllers/user.controller.ts new file mode 100644 index 0000000..4f62556 --- /dev/null +++ b/backend/src/controllers/user.controller.ts @@ -0,0 +1,106 @@ +import { Request, Response } from 'express'; +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; +import prisma from '../lib/prisma'; +import { RegisterUserInput, LoginUserInput } from '../validations/user.validations'; + +const JWT_SECRET = process.env.JWT_SECRET || 'arcana-secret-key-change-in-production'; + +export const registerUser = async (req: Request<{}, {}, RegisterUserInput>, res: Response) => { + const { username, email, password } = req.body; + + const existingUser = await prisma.user.findFirst({ + where: { + OR: [{ email }, { username }], + }, + }); + + if (existingUser) { + return res.status(409).json({ error: 'User with email or username already exists' }); + } + + const hashedPassword = await bcrypt.hash(password, 12); + + const user = await prisma.user.create({ + data: { + username, + email, + password: hashedPassword, + }, + include: { + profile: true, + }, + }); + + const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' }); + + res.status(201).json({ + user: { + id: user.id, + username: user.username, + email: user.email, + profile: user.profile, + }, + token, + }); +}; + +export const loginUser = async (req: Request<{}, {}, LoginUserInput>, res: Response) => { + const { email, password } = req.body; + + const user = await prisma.user.findUnique({ + where: { email }, + include: { + profile: true, + }, + }); + + if (!user) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + const passwordMatch = await bcrypt.compare(password, user.password); + + if (!passwordMatch) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' }); + + res.json({ + user: { + id: user.id, + username: user.username, + email: user.email, + profile: user.profile, + }, + token, + }); +}; + +export const getUsers = async (_req: Request, res: Response) => { + const users = await prisma.user.findMany({ + include: { + profile: true, + }, + }); + + res.json({ users }); +}; + +export const getUserById = async (req: Request, res: Response) => { + const { id } = req.params; + + const user = await prisma.user.findUnique({ + where: { id }, + include: { + profile: true, + }, + }); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + res.json({ user }); +}; diff --git a/backend/src/index.ts b/backend/src/index.ts index ff509c4..1bc3459 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,16 +1,21 @@ -import 'dotenv/config' +import 'dotenv/config'; import express from 'express'; import morgan from 'morgan'; +import cors from 'cors'; import prisma from './lib/prisma'; +import userRoutes from './routes/user.routes'; +import transactionRoutes from './routes/transaction.routes'; +import { errorHandler } from './middleware/error.middleware'; const app = express(); -const PORT = 3001; +const PORT = process.env.PORT || 3001; +app.use(cors()); app.use(morgan('dev')); app.use(express.json()); app.get('/', (_req, res) => { - res.send('Arcana Backend - Database Integrated'); + res.send('Arcana Backend - API v1'); }); app.get('/health', async (_req, res) => { @@ -22,45 +27,10 @@ app.get('/health', async (_req, res) => { } }); -app.get('/users', async (_req, res) => { - try { - const users = await prisma.user.findMany({ include: { profile: true } }); - res.json({ users }); - } catch (error) { - res.status(500).json({ error: String(error) }); - } -}); - -app.post('/users', async (req, res) => { - try { - const { username, email } = req.body; - const user = await prisma.user.create({ data: { username, email } }); - res.json({ user }); - } catch (error) { - res.status(500).json({ error: String(error) }); - } -}); +app.use('/api/users', userRoutes); +app.use('/api/transactions', transactionRoutes); -app.get('/transactions', async (_req, res) => { - try { - const transactions = await prisma.transaction.findMany(); - res.json({ transactions }); - } catch (error) { - res.status(500).json({ error: String(error) }); - } -}); - -app.post('/transactions', async (req, res) => { - try { - const { userId, type, amount, currency, status } = req.body; - const transaction = await prisma.transaction.create({ - data: { userId, type, amount, currency, status } - }); - res.json({ transaction }); - } catch (error) { - res.status(500).json({ error: String(error) }); - } -}); +app.use(errorHandler); app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); diff --git a/backend/src/middleware/error.middleware.ts b/backend/src/middleware/error.middleware.ts new file mode 100644 index 0000000..fd72fb3 --- /dev/null +++ b/backend/src/middleware/error.middleware.ts @@ -0,0 +1,20 @@ +import { Request, Response, NextFunction } from 'express'; + +export const errorHandler = ( + error: any, + _req: Request, + res: Response, + _next: NextFunction +) => { + console.error('Error:', error); + + if (error.code === 'P2002') { + return res.status(409).json({ error: 'Resource already exists' }); + } + + if (error.code === 'P2025') { + return res.status(404).json({ error: 'Resource not found' }); + } + + res.status(500).json({ error: 'Internal server error' }); +}; diff --git a/backend/src/middleware/validation.middleware.ts b/backend/src/middleware/validation.middleware.ts new file mode 100644 index 0000000..19685fc --- /dev/null +++ b/backend/src/middleware/validation.middleware.ts @@ -0,0 +1,16 @@ +import { Request, Response, NextFunction } from 'express'; +import { ZodSchema } from 'zod'; + +export const validateRequest = (schema: ZodSchema) => { + return (req: Request, res: Response, next: NextFunction) => { + try { + schema.parse(req.body); + next(); + } catch (error: any) { + res.status(400).json({ + error: 'Validation failed', + details: error.errors, + }); + } + }; +}; diff --git a/backend/src/routes/transaction.routes.ts b/backend/src/routes/transaction.routes.ts new file mode 100644 index 0000000..db14c31 --- /dev/null +++ b/backend/src/routes/transaction.routes.ts @@ -0,0 +1,18 @@ +import express from 'express'; +import { + createTransaction, + getTransactions, + getTransactionById, + getTransactionsByUserId, +} from '../controllers/transaction.controller'; +import { validateRequest } from '../middleware/validation.middleware'; +import { createTransactionSchema } from '../validations/transaction.validations'; + +const router = express.Router(); + +router.post('/', validateRequest(createTransactionSchema), createTransaction); +router.get('/', getTransactions); +router.get('/:id', getTransactionById); +router.get('/user/:userId', getTransactionsByUserId); + +export default router; diff --git a/backend/src/routes/user.routes.ts b/backend/src/routes/user.routes.ts new file mode 100644 index 0000000..6700a41 --- /dev/null +++ b/backend/src/routes/user.routes.ts @@ -0,0 +1,18 @@ +import express from 'express'; +import { + registerUser, + loginUser, + getUsers, + getUserById, +} from '../controllers/user.controller'; +import { validateRequest } from '../middleware/validation.middleware'; +import { registerUserSchema, loginUserSchema } from '../validations/user.validations'; + +const router = express.Router(); + +router.post('/register', validateRequest(registerUserSchema), registerUser); +router.post('/login', validateRequest(loginUserSchema), loginUser); +router.get('/', getUsers); +router.get('/:id', getUserById); + +export default router; diff --git a/backend/src/validations/transaction.validations.ts b/backend/src/validations/transaction.validations.ts new file mode 100644 index 0000000..bafc7e5 --- /dev/null +++ b/backend/src/validations/transaction.validations.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +export const createTransactionSchema = z.object({ + userId: z.string(), + type: z.enum(['deposit', 'withdraw', 'transfer', 'swap']), + amount: z.number().positive(), + currency: z.string().min(1), + status: z.enum(['pending', 'completed', 'failed', 'cancelled']), + hash: z.string().optional(), + metadata: z.any().optional(), +}); + +export type CreateTransactionInput = z.infer; diff --git a/backend/src/validations/user.validations.ts b/backend/src/validations/user.validations.ts new file mode 100644 index 0000000..349b3f9 --- /dev/null +++ b/backend/src/validations/user.validations.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +export const registerUserSchema = z.object({ + username: z.string().min(3).max(50), + email: z.string().email(), + password: z.string().min(8), +}); + +export const loginUserSchema = z.object({ + email: z.string().email(), + password: z.string().min(8), +}); + +export type RegisterUserInput = z.infer; +export type LoginUserInput = z.infer;