From 16c21a4e490aa8be99b48e8a7285040a0d4a5576 Mon Sep 17 00:00:00 2001 From: sbilketay Date: Wed, 26 Nov 2025 18:57:18 +0300 Subject: [PATCH] first commit --- .gitignore | 7 ++ .vscode/settings.json | 3 + README.md | 39 +++++++ backend/.dockerignore | 4 + backend/.env.example | 6 ++ backend/Dockerfile | 12 +++ backend/package.json | 28 +++++ backend/src/config/env.ts | 16 +++ backend/src/index.ts | 70 +++++++++++++ backend/src/middleware/authMiddleware.ts | 23 +++++ backend/src/routes/auth.ts | 30 ++++++ backend/tsconfig.json | 17 ++++ docker-compose.yml | 39 +++++++ frontend/.dockerignore | 4 + frontend/.env.example | 1 + frontend/Dockerfile | 12 +++ frontend/index.html | 13 +++ frontend/package.json | 35 +++++++ frontend/postcss.config.js | 6 ++ frontend/src/App.tsx | 18 ++++ frontend/src/api/client.ts | 27 +++++ frontend/src/components/ProtectedRoute.tsx | 21 ++++ frontend/src/components/ThemeToggle.tsx | 13 +++ frontend/src/components/ui/button.tsx | 51 ++++++++++ frontend/src/components/ui/card.tsx | 51 ++++++++++ frontend/src/components/ui/input.tsx | 21 ++++ frontend/src/components/ui/label.tsx | 16 +++ frontend/src/components/ui/switch.tsx | 24 +++++ frontend/src/components/ui/toaster.tsx | 16 +++ frontend/src/index.css | 57 +++++++++++ frontend/src/lib/utils.ts | 6 ++ frontend/src/main.tsx | 21 ++++ frontend/src/pages/AdminPage.tsx | 113 +++++++++++++++++++++ frontend/src/pages/LoginPage.tsx | 72 +++++++++++++ frontend/src/providers/auth-provider.tsx | 58 +++++++++++ frontend/src/providers/theme-provider.tsx | 37 +++++++ frontend/src/vite-env.d.ts | 1 + frontend/tailwind.config.js | 53 ++++++++++ frontend/tsconfig.json | 8 ++ frontend/tsconfig.node.json | 16 +++ frontend/vite.config.ts | 10 ++ 41 files changed, 1075 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 backend/.dockerignore create mode 100644 backend/.env.example create mode 100644 backend/Dockerfile create mode 100644 backend/package.json create mode 100644 backend/src/config/env.ts create mode 100644 backend/src/index.ts create mode 100644 backend/src/middleware/authMiddleware.ts create mode 100644 backend/src/routes/auth.ts create mode 100644 backend/tsconfig.json create mode 100644 docker-compose.yml create mode 100644 frontend/.dockerignore create mode 100644 frontend/.env.example create mode 100644 frontend/Dockerfile create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/components/ProtectedRoute.tsx create mode 100644 frontend/src/components/ThemeToggle.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/input.tsx create mode 100644 frontend/src/components/ui/label.tsx create mode 100644 frontend/src/components/ui/switch.tsx create mode 100644 frontend/src/components/ui/toaster.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/lib/utils.ts create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/AdminPage.tsx create mode 100644 frontend/src/pages/LoginPage.tsx create mode 100644 frontend/src/providers/auth-provider.tsx create mode 100644 frontend/src/providers/theme-provider.tsx create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ac1c1d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +pnpm-lock.yaml +yarn.lock +package-lock.json +dist +.env +.DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..13ee2b0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "nuxt.isNuxtApp": false +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..80fe8a7 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# WisecoltCI Monorepo Starter + +Minimal, üretime hazır bir full-stack başlangıç kiti. React (Vite, TypeScript, shadcn/ui, Tailwind), Express + Socket.io (TypeScript), MongoDB ve Docker Compose ile dev ortamında hot-reload destekler. + +## Gereksinimler +- Docker ve Docker Compose + +## Kurulum +1. Ortam dosyalarını oluşturun: + ```bash + cp backend/.env.example backend/.env + cp frontend/.env.example frontend/.env + ``` + İstediğiniz admin bilgilerini `.env` dosyalarına girin. + +2. Servisleri başlatın: + ```bash + docker compose up --build + ``` + +3. Uygulamaya erişin: + - Frontend: http://localhost:5173 + - Backend API: http://localhost:4000 + - MongoDB: localhost:27017 + +## Giriş Bilgisi +`.env` dosyasındaki değerleri kullanın (varsayılanlar): +- Kullanıcı adı: `admin` +- Şifre: `supersecret` + +## Özellikler +- **Auth**: `/auth/login` ile .env'deki kimlik bilgilerini kontrol eder, JWT döner; `/auth/me` korumalı. +- **Socket.io**: Login sonrası frontend token ile bağlanır, basit `ping/pong` olayı mevcut. +- **Tema**: shadcn/ui teması, Tailwind sınıf stratejisi, localStorage kalıcılığı. +- **Hot Reload**: Backend `tsx watch`, Frontend Vite dev server. + +## Notlar +- Frontend API adresi `frontend/.env` içindeki `VITE_API_URL` ile ayarlanır. +- Docker bind mount sayesinde kod değişiklikleri konteynerde otomatik yansır. diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..25ab276 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,4 @@ +node_modules +npm-debug.log +dist +.env diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..5caf63c --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,6 @@ +PORT=4000 +MONGO_URI=mongodb://mongo:27017/wisecoltci +ADMIN_USERNAME=admin +ADMIN_PASSWORD=supersecret +JWT_SECRET=change-me +CLIENT_ORIGIN=http://localhost:5173 diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..414976f --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json . +RUN npm install + +COPY tsconfig.json . +COPY src ./src + +EXPOSE 4000 +CMD ["npm", "run", "dev"] diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..d919fe1 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,28 @@ +{ + "name": "backend", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/index.js" + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", + "mongoose": "^7.6.3", + "socket.io": "^4.7.2" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.5", + "@types/node": "^20.9.0", + "ts-node": "^10.9.2", + "tsx": "^4.7.1", + "typescript": "^5.2.2" + } +} diff --git a/backend/src/config/env.ts b/backend/src/config/env.ts new file mode 100644 index 0000000..3d38a81 --- /dev/null +++ b/backend/src/config/env.ts @@ -0,0 +1,16 @@ +import dotenv from "dotenv"; + +dotenv.config(); + +export const config = { + port: parseInt(process.env.PORT || "4000", 10), + mongoUri: process.env.MONGO_URI || "mongodb://mongo:27017/wisecoltci", + adminUsername: process.env.ADMIN_USERNAME || "admin", + adminPassword: process.env.ADMIN_PASSWORD || "password", + jwtSecret: process.env.JWT_SECRET || "changeme", + clientOrigin: process.env.CLIENT_ORIGIN || "http://localhost:5173" +}; + +if (!config.jwtSecret) { + throw new Error("JWT_SECRET is required"); +} diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..e3b5db2 --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,70 @@ +import http from "http"; +import express from "express"; +import cors from "cors"; +import mongoose from "mongoose"; +import { Server } from "socket.io"; +import authRoutes from "./routes/auth.js"; +import { config } from "./config/env.js"; +import jwt from "jsonwebtoken"; + +const app = express(); + +app.use( + cors({ + origin: config.clientOrigin, + credentials: true + }) +); +app.use(express.json()); + +app.get("/health", (_req, res) => { + res.json({ status: "ok" }); +}); + +app.use("/auth", authRoutes); + +const server = http.createServer(app); + +const io = new Server(server, { + cors: { + origin: config.clientOrigin, + methods: ["GET", "POST"] + } +}); + +io.use((socket, next) => { + const token = socket.handshake.auth?.token as string | undefined; + if (!token) { + return next(new Error("Yetkisiz")); + } + try { + jwt.verify(token, config.jwtSecret); + next(); + } catch (err) { + next(new Error("Geçersiz token")); + } +}); + +io.on("connection", (socket) => { + socket.emit("hello", "Socket bağlantısı kuruldu"); + + socket.on("ping", () => { + socket.emit("pong", "pong"); + }); +}); + +async function start() { + try { + await mongoose.connect(config.mongoUri); + console.log("MongoDB'ye bağlanıldı"); + + server.listen(config.port, () => { + console.log(`Sunucu ${config.port} portunda çalışıyor`); + }); + } catch (err) { + console.error("Başlatma hatası", err); + process.exit(1); + } +} + +start(); diff --git a/backend/src/middleware/authMiddleware.ts b/backend/src/middleware/authMiddleware.ts new file mode 100644 index 0000000..9ff70f0 --- /dev/null +++ b/backend/src/middleware/authMiddleware.ts @@ -0,0 +1,23 @@ +import { Request, Response, NextFunction } from "express"; +import jwt from "jsonwebtoken"; +import { config } from "../config/env.js"; + +export interface AuthRequest extends Request { + user?: { username: string }; +} + +export const authMiddleware = (req: AuthRequest, res: Response, next: NextFunction) => { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).json({ message: "Yetkisiz" }); + } + + const token = authHeader.split(" ")[1]; + try { + const decoded = jwt.verify(token, config.jwtSecret) as { username: string }; + req.user = { username: decoded.username }; + next(); + } catch (err) { + return res.status(401).json({ message: "Geçersiz token" }); + } +}; diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts new file mode 100644 index 0000000..07e8fe3 --- /dev/null +++ b/backend/src/routes/auth.ts @@ -0,0 +1,30 @@ +import { Router } from "express"; +import jwt from "jsonwebtoken"; +import { authMiddleware, AuthRequest } from "../middleware/authMiddleware.js"; +import { config } from "../config/env.js"; + +const router = Router(); + +router.post("/login", (req, res) => { + const { username, password } = req.body as { username?: string; password?: string }; + + if (!username || !password) { + return res.status(400).json({ message: "Kullanıcı adı ve şifre gerekli" }); + } + + if (username !== config.adminUsername || password !== config.adminPassword) { + return res.status(401).json({ message: "Geçersiz kimlik bilgileri" }); + } + + const token = jwt.sign({ username }, config.jwtSecret, { expiresIn: "1h" }); + return res.json({ token, username }); +}); + +router.get("/me", authMiddleware, (req: AuthRequest, res) => { + if (!req.user) { + return res.status(401).json({ message: "Yetkisiz" }); + } + return res.json({ username: req.user.username }); +}); + +export default router; diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..e2fd91f --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Node", + "outDir": "dist", + "rootDir": "src", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "types": ["node"] + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4e981a1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +version: "3.9" + +services: + mongo: + image: mongo:7 + restart: unless-stopped + ports: + - "27017:27017" + volumes: + - mongo-data:/data/db + + backend: + build: ./backend + command: npm run dev + volumes: + - ./backend:/app + - /app/node_modules + env_file: + - ./backend/.env + ports: + - "4000:4000" + depends_on: + - mongo + + frontend: + build: ./frontend + command: npm run dev -- --host --port 5173 + volumes: + - ./frontend:/app + - /app/node_modules + env_file: + - ./frontend/.env + ports: + - "5173:5173" + depends_on: + - backend + +volumes: + mongo-data: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..cd44c15 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,4 @@ +node_modules +dist +npm-debug.log +.env diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..1fad084 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +VITE_API_URL=http://localhost:4000 diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..fb9db10 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json . +RUN npm install + +COPY tsconfig.json tsconfig.node.json vite.config.ts tailwind.config.js postcss.config.js index.html . +COPY src ./src + +EXPOSE 5173 +CMD ["npm", "run", "dev"] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..2320bdf --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Wisecolt Starter + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..a6bde70 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,35 @@ +{ + "name": "frontend", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host --port 5173", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-slot": "^1.0.2", + "axios": "^1.5.1", + "class-variance-authority": "^0.7.0", + "clsx": "^2.0.0", + "lucide-react": "^0.292.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.0", + "socket.io-client": "^4.7.2", + "sonner": "^1.4.0", + "tailwind-merge": "^1.14.0" + }, + "devDependencies": { + "@types/node": "^20.9.0", + "@types/react": "^18.2.28", + "@types/react-dom": "^18.2.12", + "@vitejs/plugin-react": "^4.1.1", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.31", + "tailwindcss": "^3.3.3", + "typescript": "^5.2.2", + "vite": "^4.4.9" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..ba80730 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..2b7e0c4 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,18 @@ +import { Navigate, Route, Routes } from "react-router-dom"; +import { LoginPage } from "./pages/LoginPage"; +import { AdminPage } from "./pages/AdminPage"; +import { ProtectedRoute } from "./components/ProtectedRoute"; + +function App() { + return ( + + } /> + }> + } /> + + } /> + + ); +} + +export default App; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..60f4b61 --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,27 @@ +import axios from "axios"; + +const apiBaseURL = import.meta.env.VITE_API_URL || "http://localhost:4000"; + +export const apiClient = axios.create({ + baseURL: apiBaseURL +}); + +export function setAuthToken(token?: string) { + if (token) { + apiClient.defaults.headers.common["Authorization"] = `Bearer ${token}`; + localStorage.setItem("token", token); + } else { + delete apiClient.defaults.headers.common["Authorization"]; + localStorage.removeItem("token"); + } +} + +export async function loginRequest(username: string, password: string) { + const { data } = await apiClient.post("/auth/login", { username, password }); + return data as { token: string; username: string }; +} + +export async function fetchMe() { + const { data } = await apiClient.get("/auth/me"); + return data as { username: string }; +} diff --git a/frontend/src/components/ProtectedRoute.tsx b/frontend/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..f834714 --- /dev/null +++ b/frontend/src/components/ProtectedRoute.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { Navigate, Outlet } from "react-router-dom"; +import { useAuth } from "../providers/auth-provider"; + +export function ProtectedRoute() { + const { token, loading } = useAuth(); + + if (loading) { + return ( +
+ Yükleniyor... +
+ ); + } + + if (!token) { + return ; + } + + return ; +} diff --git a/frontend/src/components/ThemeToggle.tsx b/frontend/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..e717702 --- /dev/null +++ b/frontend/src/components/ThemeToggle.tsx @@ -0,0 +1,13 @@ +import { Moon, Sun } from "lucide-react"; +import { Button } from "./ui/button"; +import { useTheme } from "../providers/theme-provider"; + +export function ThemeToggle() { + const { theme, toggleTheme } = useTheme(); + + return ( + + ); +} diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..a1f3397 --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,51 @@ +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 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-background", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline" + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10" + } + }, + defaultVariants: { + variant: "default", + size: "default" + } + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button.displayName = "Button"; + +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..37e1068 --- /dev/null +++ b/frontend/src/components/ui/card.tsx @@ -0,0 +1,51 @@ +import * as React from "react"; +import { cn } from "../../lib/utils"; + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +CardFooter.displayName = "CardFooter"; + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..693052f --- /dev/null +++ b/frontend/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; +import { cn } from "../../lib/utils"; + +export interface InputProps extends React.InputHTMLAttributes {} + +const Input = React.forwardRef(({ className, type, ...props }, ref) => { + return ( + + ); +}); +Input.displayName = "Input"; + +export { Input }; diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx new file mode 100644 index 0000000..fd68198 --- /dev/null +++ b/frontend/src/components/ui/label.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; +import { cn } from "../../lib/utils"; + +const Label = React.forwardRef< + HTMLLabelElement, + React.LabelHTMLAttributes +>(({ className, ...props }, ref) => ( +