Browse Source

Feature: Refactor authentication module and add MongoDB connection

- Updated package.json to remove bcrypt and add type definitions for bcrypt, cors, and jsonwebtoken.
- Refactored auth actions to improve structure and readability.
- Implemented user registration logic with validation for email, phone, and password strength.
- Enhanced error handling in the registration controller.
- Added MongoDB connection logic in a new db.ts file.
- Created User model with Mongoose schema.
- Introduced Docker Compose configuration for MongoDB and Redis services.
emrecevik106 1 month ago
parent
commit
72c66198ad

+ 22 - 0
docker-compose.yml

@@ -0,0 +1,22 @@
+version: '3.8'
+
+services:
+  mongodb:
+    image: mongo:7
+    container_name: qr-menu-mongo
+    ports:
+      - '27018:27017'
+    volumes:
+      - mongo_data:/data/db
+
+  redis:
+    image: redis:7-alpine
+    container_name: qr-menu-redis
+    ports:
+      - '6379:6379'
+    volumes:
+      - redis_data:/data
+
+volumes:
+  mongo_data:
+  redis_data:

+ 7 - 5
package.json

@@ -15,7 +15,6 @@
   "author": "",
   "license": "ISC",
   "dependencies": {
-    "bcrypt": "6.0.0",
     "cors": "2.8.6",
     "dotenv": "17.4.2",
     "express": "5.2.1",
@@ -25,12 +24,15 @@
   },
   "devDependencies": {
     "@eslint/js": "10.0.1",
+    "@types/bcrypt": "^6.0.0",
+    "@types/cors": "^2.8.19",
     "@types/express": "^4.17.21",
-    "nodemon": "3.1.14",
-    "ts-node": "^10.9.2",
-    "typescript": "^5.5.4",
+    "@types/jsonwebtoken": "^9.0.10",
     "@typescript-eslint/eslint-plugin": "5.0.0",
     "@typescript-eslint/parser": "5.0.0",
-    "eslint": "8.0.1"
+    "eslint": "8.0.1",
+    "nodemon": "3.1.14",
+    "ts-node": "^10.9.2",
+    "typescript": "^5.5.4"
   }
 }

+ 6 - 2
src/actions/auth/index.ts

@@ -1,2 +1,6 @@
-export { default as login } from "./login";
-export { default as register } from "./register";
+export {
+    default as login 
+} from "./login";
+export {
+    default as register 
+} from "./register";

+ 1 - 1
src/actions/auth/login/index.ts

@@ -1,5 +1,5 @@
 const login = async () => {
-
+    return;
 };
 
 export default login;

+ 106 - 3
src/actions/auth/register/index.ts

@@ -1,5 +1,108 @@
-const register = async () => {
-  return "";
+import crypto from "crypto";
+import {
+    User 
+} from "../../../models/User";
+import {
+    RegisterInput, RegisterResult 
+} from "./types";
+
+const isValidEmail = (mail: string): boolean => {
+    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+    return emailRegex.test(mail);
+};
+
+const isValidPhone = (phone: string): boolean => {
+    const phoneRegex = /^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/;
+    return phoneRegex.test(phone);
+};
+
+const isStrongPassword = (password: string): { valid: boolean; message: string } => {
+    if (password.length < 8) return {
+        valid: false, message: "Şifre en az 8 karakter olmalı" 
+    };
+
+    if (!/[A-Z]/.test(password)) return {
+        valid: false, message: "Şifre en az 1 büyük harf içermeli" 
+    };
+
+    if (!/[a-z]/.test(password)) return {
+        valid: false, message: "Şifre en az 1 küçük harf içermeli" 
+    };
+
+    if (!/[0-9]/.test(password)) return {
+        valid: false, message: "Şifre en az 1 sayı içermeli" 
+    };
+
+    if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) return {
+        valid: false, message: "Şifre en az 1 özel karakter içermeli" 
+    };
+
+    return {
+        valid: true, 
+        message: "" 
+    };
 };
 
-export default register;
+export const register = async (input: RegisterInput): Promise<RegisterResult> => {
+    const {
+        firstName, lastName, companyName, mail, phoneNumber, password 
+    } = input;
+
+    if (!isValidEmail(mail)) {
+        return {
+            success: false, message: "Geçersiz email formatı" , statusCode: 400 
+        };
+    }
+
+    if (!isValidPhone(phoneNumber)) {
+        return {
+            success: false, message: "Geçersiz telefon numarası formatı" , statusCode: 400
+        };
+    }
+
+    const passwordCheck = isStrongPassword(password);
+
+    if (!passwordCheck.valid) {
+        return {
+            success: false, message: passwordCheck.message ,statusCode: 400
+        };
+    }
+
+    const existingUser = await User.findOne({
+        mail 
+    });
+
+    if (existingUser) {
+        return {
+            success: false, message: "Bu email zaten kayıtlı" , statusCode: 409
+        };
+    }
+
+    const existingPhone = await User.findOne({
+        phoneNumber 
+    });
+    
+    if (existingPhone) {
+        return {
+            success: false, message: "Bu telefon numarası zaten kayıtlı" , statusCode: 409
+        };
+    }
+    const hashedPassword = crypto.createHash("md5").update(password).digest("hex");
+
+    await User.create({
+        fullName: `${firstName} ${lastName}`,
+        firstName,
+        lastName,
+        companyName,
+        mail,
+        phoneNumber,
+        password: hashedPassword,
+    });
+
+    return {
+        success: true,
+        message: "Kayıt başarılı",
+        statusCode: 201
+    };
+};
+export default register;

+ 14 - 0
src/actions/auth/register/types.ts

@@ -0,0 +1,14 @@
+export interface RegisterInput {
+  firstName: string;
+  lastName: string;
+  companyName: string;
+  mail: string;
+  phoneNumber: string;
+  password: string;
+}
+
+export interface RegisterResult {
+  success: boolean;
+  message: string;
+  statusCode: number;
+}

+ 11 - 0
src/config/db.ts

@@ -0,0 +1,11 @@
+import mongoose from "mongoose";
+
+export const connectDB = async (): Promise<void> => {
+    try {
+        const conn = await mongoose.connect(process.env.MONGO_URI as string);
+        console.log(`MongoDB Connected: ${conn.connection.host}`);
+    } catch (error) {
+        console.error("MongoDB connection error:", error);
+        process.exit(1);
+    }
+};

+ 36 - 7
src/controllers/authController.ts

@@ -1,9 +1,38 @@
-const _register = async () => {
-  return "";
-};
+import {
+    Request, Response 
+} from "express";
+import {
+    register as _register 
+} from "../actions/auth/register";
 
-const _login = async () => {
-  return "";
-};
+export const register = async (req: Request, res: Response): Promise<void> => {
+    try {
+        const {
+            firstName, lastName, companyName, mail, phoneNumber, password 
+        } = req.body;
 
-export { _register, _login };
+        if (!firstName || !lastName || !companyName || !mail || !phoneNumber || !password) {
+            res.status(400).json({
+                statusCode: 400,
+                message: "Tüm alanlar zorunludur",
+            });
+            return;
+        }
+
+        const result = await _register({
+            firstName, lastName, companyName, mail, phoneNumber, password 
+        });
+
+        res.status(result.statusCode).json({
+            statusCode: result.statusCode,
+            message: result.message,
+        });
+        
+    } catch (error) {
+        console.error("Register error:", error);
+        res.status(500).json({
+            statusCode: 500,
+            message: "Sunucu hatası",
+        });
+    }
+};

+ 20 - 6
src/index.ts

@@ -1,12 +1,26 @@
-require('dotenv').config();
-const express = require('express');
-const cors = require('cors');
-const routes = require('./routes');
+import express from "express";
+import cors from "cors";
+import dotenv from "dotenv";
+import {
+    connectDB 
+} from "./config/db";
+import routes from "./routes";
+
+dotenv.config();
 
 const app = express();
+const PORT = process.env.PORT || 3000;
 
 app.use(cors());
 app.use(express.json());
-app.use('/api', routes);
 
-module.exports = app;
+app.use("/api", routes);
+
+const start = async () => {
+    await connectDB();
+    app.listen(PORT, () => {
+        console.log(`Server running on port ${PORT}`);
+    });
+};
+
+start();

+ 27 - 15
src/middlewares/authMiddleware.ts

@@ -1,19 +1,31 @@
-const jwt = require('jsonwebtoken');
+import {
+    Request, Response, NextFunction 
+} from "express";
+import jwt from "jsonwebtoken";
 
-const authMiddleware = (req, res, next) => {
-  const token = req.headers.authorization?.split(' ')[1];
+export interface AuthRequest extends Request {
+  userId?: string;
+}
 
-  if (!token) {
-    return res.status(401).json({ message: 'Token bulunamadı.' });
-  }
+export const authMiddleware = (req: AuthRequest, res: Response, next: NextFunction): void => {
+    try {
+        const authHeader = req.headers.authorization;
 
-  try {
-    const decoded = jwt.verify(token, process.env.JWT_SECRET);
-    req.user = decoded;
-    next();
-  } catch (error) {
-    res.status(401).json({ message: 'Geçersiz token.' });
-  }
-};
+        if (!authHeader || !authHeader.startsWith("Bearer ")) {
+            res.status(401).json({
+                success: false, message: "Token bulunamadı" 
+            });
+            return;
+        }
 
-module.exports = authMiddleware;
+        const token = authHeader.split(" ")[1];
+        const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { userId: string };
+
+        req.userId = decoded.userId;
+        next();
+    } catch (error) {
+        res.status(401).json({
+            success: false, message: "Geçersiz token" 
+        });
+    }
+};

+ 70 - 0
src/models/User.ts

@@ -0,0 +1,70 @@
+import mongoose, {
+    Document, Schema 
+} from "mongoose";
+
+export interface IUser extends Document {
+  fullName: string;
+  firstName: string;
+  lastName: string;
+  companyName: string;
+  mail: string;
+  phoneNumber: string;
+  password: string;
+  isActive: boolean;
+  isMailVerified: boolean;
+  isPhoneVerified: boolean;
+  isDeleted: boolean;
+  refreshToken?: string;
+  deleteAccountDate?: Date;
+  createdAt: Date;
+  updatedAt: Date;
+}
+
+const UserSchema = new Schema<IUser>(
+    {
+        fullName: {
+            type: String, required: true, trim: true 
+        },
+        firstName: {
+            type: String, required: true, trim: true 
+        },
+        lastName: {
+            type: String, required: true, trim: true 
+        },
+        companyName: {
+            type: String, required: true, trim: true 
+        },
+        mail: {
+            type: String, required: true, unique: true, lowercase: true, trim: true 
+        },
+        phoneNumber: {
+            type: String, required: true, unique: true, trim: true 
+        },
+        password: {
+            type: String, required: true 
+        },
+        isActive: {
+            type: Boolean, default: true 
+        },
+        isMailVerified: {
+            type: Boolean, default: false 
+        },
+        isPhoneVerified: {
+            type: Boolean, default: false 
+        },
+        isDeleted: {
+            type: Boolean, default: false 
+        },
+        refreshToken: {
+            type: String 
+        },
+        deleteAccountDate: {
+            type: Date 
+        },
+    },
+    {
+        timestamps: true 
+    }
+);
+
+export const User = mongoose.model<IUser>("User", UserSchema);

+ 10 - 10
src/routes/authRoutes.ts

@@ -1,12 +1,12 @@
-const express = require('express');
-const router = express.Router();
-const { register, login } = require('../controllers/authController');
-const authMiddleware = require('../middlewares/authMiddleware');
+import {
+    Router 
+} from "express";
+import {
+    register 
+} from "../controllers/authController";
 
-router.post('/register', register);
-router.post('/login', login);
-router.get('/me', authMiddleware, (req, res) => {
-  res.json({ message: 'Giriş yapıldı!', user: req.user });
-});
+const router = Router();
 
-module.exports = router;
+router.post("/register", register);
+
+export default router;

+ 8 - 5
src/routes/index.ts

@@ -1,7 +1,10 @@
-const express = require('express');
-const router = express.Router();
-const authRoutes = require('./authRoutes');
+import {
+    Router 
+} from "express";
+import authRoutes from "./authRoutes";
 
-router.use('/auth', authRoutes);
+const router = Router();
 
-module.exports = router;
+router.use("/auth", authRoutes);
+
+export default router;

+ 27 - 18
yarn.lock

@@ -114,6 +114,13 @@
   resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
   integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
 
+"@types/bcrypt@^6.0.0":
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-6.0.0.tgz#0d20587924663607fb59ae373d3d6fbc7b339a92"
+  integrity sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==
+  dependencies:
+    "@types/node" "*"
+
 "@types/body-parser@*":
   version "1.19.6"
   resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474"
@@ -129,6 +136,13 @@
   dependencies:
     "@types/node" "*"
 
+"@types/cors@^2.8.19":
+  version "2.8.19"
+  resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342"
+  integrity sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==
+  dependencies:
+    "@types/node" "*"
+
 "@types/express-serve-static-core@^4.17.33":
   version "4.19.8"
   resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz#99b960322a4d576b239a640ab52ef191989b036f"
@@ -159,11 +173,24 @@
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
   integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
 
+"@types/jsonwebtoken@^9.0.10":
+  version "9.0.10"
+  resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz#a7932a47177dcd4283b6146f3bd5c26d82647f09"
+  integrity sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==
+  dependencies:
+    "@types/ms" "*"
+    "@types/node" "*"
+
 "@types/mime@^1":
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
   integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==
 
+"@types/ms@*":
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78"
+  integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
+
 "@types/node@*":
   version "25.6.0"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.0.tgz#4e09bad9b469871f2d0f68140198cbd714f4edca"
@@ -372,14 +399,6 @@ balanced-match@^4.0.2:
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a"
   integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==
 
-bcrypt@6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-6.0.0.tgz#86643fddde9bcd0ad91400b063003fa4b0312835"
-  integrity sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==
-  dependencies:
-    node-addon-api "^8.3.0"
-    node-gyp-build "^4.8.4"
-
 binary-extensions@^2.0.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
@@ -1361,16 +1380,6 @@ negotiator@^1.0.0:
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a"
   integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==
 
-node-addon-api@^8.3.0:
-  version "8.7.0"
-  resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-8.7.0.tgz#f64f8413456ecbe900221305a3f883c37666473f"
-  integrity sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==
-
-node-gyp-build@^4.8.4:
-  version "4.8.4"
-  resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8"
-  integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==
-
 nodemon@3.1.14:
   version "3.1.14"
   resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.14.tgz#8487ca379c515301d221ec007f27f24ecafa2b51"