Jelajahi Sumber

Merge branch 'feature/categories' into develop

emrecevik106 1 bulan lalu
induk
melakukan
654bdf3e39

+ 2 - 2
src/actions/auth/finishMailVerify/types.ts

@@ -4,12 +4,12 @@ import {
 } from "class-validator";
 
 export class FinishMailVerifyInput {
-    @IsNotEmpty({ message: "userID-is-required" })
     @IsString()
+    @IsNotEmpty({ message: "userID-is-required" })
     userID?: string;
 
-    @IsNotEmpty({ message: "code-is-required" })
     @IsString()
+    @IsNotEmpty({ message: "code-is-required" })
     code?: string;
 }
 

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

@@ -78,7 +78,7 @@ const refreshToken = async (input: RefreshTokenInput): Promise<RefreshTokenResul
             message: "token-refreshed",
             payload: {
                 refreshToken: newRefreshToken,
-                accessToken: newAccessToken,
+                accessToken: newAccessToken
             },
         };
     } catch (error) {

+ 1 - 1
src/actions/auth/refreshToken/types.ts

@@ -4,8 +4,8 @@ import {
 } from "class-validator";
 
 export class RefreshTokenInput {
-    @IsNotEmpty({ message: "refreshToken-required" })
     @IsString({ message: "refreshToken-must-be-string" })
+    @IsNotEmpty({ message: "refreshToken-required" })
     refreshToken?: string;
 }
 export interface RefreshTokenResult {

+ 16 - 0
src/actions/auth/register/index.ts

@@ -6,6 +6,21 @@ import {
     RegisterResult 
 } from "./types";
 
+const generateSlug = (companyName: string): string => {
+    return companyName
+        .toLowerCase()
+        .replace(/ğ/g, "g")
+        .replace(/ü/g, "u")
+        .replace(/ş/g, "s")
+        .replace(/ı/g, "i")
+        .replace(/ö/g, "o")
+        .replace(/ç/g, "c")
+        .replace(/[^a-z0-9\s-]/g, "")
+        .trim()
+        .replace(/\s+/g, "-")
+        .replace(/-+/g, "-");
+};
+
 const register = async (input: RegisterInput): Promise<RegisterResult> => {
     const {
         companyName,
@@ -38,6 +53,7 @@ const register = async (input: RegisterInput): Promise<RegisterResult> => {
 
     await User.create({
         fullName: `${firstName} ${lastName}`,
+        slug: generateSlug(companyName),
         phoneNumber,
         companyName,
         firstName,

+ 5 - 4
src/actions/auth/register/types.ts

@@ -6,23 +6,24 @@ import {
 } from "class-validator";
 
 export class RegisterInput {
-    @IsNotEmpty({ message: "First name is required" })
     @IsString()
+    @IsNotEmpty({ message: "First name is required" })
         firstName!: string;
 
-    @IsNotEmpty({ message: "Last name is required" })
     @IsString()
+    @IsNotEmpty({ message: "Last name is required" })
         lastName!: string;
 
-    @IsNotEmpty({ message: "Company name is required" })
     @IsString()
+    @IsNotEmpty({ message: "Company name is required" })
         companyName!: string;
 
     @IsEmail({}, { message: "Invalid email format" })
+    @IsNotEmpty({ message: "Email is required" })
         mail!: string;
 
-    @IsNotEmpty({ message: "Phone number is required" })
     @Matches(/^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/, { message: "Invalid phone number format" })
+    @IsNotEmpty({ message: "Phone number is required" })
         phoneNumber!: string;
 
     @IsNotEmpty({ message: "Password is required" })

+ 55 - 0
src/actions/menu/addCategory/index.ts

@@ -0,0 +1,55 @@
+import {
+    Category 
+} from "../../../models/Category";
+import {
+    AddCategoryInput, 
+    AddCategoryResult
+} from "./types";
+
+const addCategory = async (userID: string, input: AddCategoryInput): Promise<AddCategoryResult> => {
+    try {
+        const {
+            title,
+            index
+        } = input;
+
+        const existing = await Category.findOne({
+            userID,
+            index
+        });
+        if (existing) {
+            return {
+                message: "index-already-in-use", 
+                code: 409 
+            };
+        }
+
+        const newCategory = await Category.create({
+            userID,
+            title,
+            index
+        });
+
+        return {
+            message: "category-created",
+            code: 201,
+            payload: {
+                category: {
+                    _id: newCategory._id.toString(),
+                    isActive: newCategory.isActive,
+                    title: newCategory.title,
+                    index: newCategory.index
+                }
+            }
+        };
+
+    } catch (error) {
+        console.error("CreateCategory error:", error);
+        return {
+            message: "internal-server-error",
+            code: 500 
+        };
+    }
+};
+
+export default addCategory;

+ 28 - 0
src/actions/menu/addCategory/types.ts

@@ -0,0 +1,28 @@
+import {
+    IsNotEmpty, 
+    IsString, 
+    IsNumber 
+} from "class-validator";
+
+export class AddCategoryInput {
+    @IsString()
+    @IsNotEmpty()
+    title!: string;
+
+    @IsNumber()
+    @IsNotEmpty()
+    index!: number;
+}
+
+export interface AddCategoryResult {
+    message: string;
+    code: number;
+    payload?: {
+        category: {
+            _id: string;
+            title: string;
+            index: number;
+            isActive: boolean;
+        }
+    };
+}

+ 41 - 0
src/actions/menu/deleteCategory/index.ts

@@ -0,0 +1,41 @@
+import {
+    Category 
+} from "../../../models/Category";
+import {
+    DeleteCategoryInput,
+    DeleteCategoryResult 
+} from "./types";
+
+const deleteCategory = async (userID: string, input: DeleteCategoryInput): Promise<DeleteCategoryResult> => {
+    try {
+        const {
+            categoryID 
+        } = input;  
+
+        const category = await Category.findOne({
+            _id: categoryID,
+            userID
+        });
+        if (!category) {
+            return {
+                message: "category-not-found",
+                code: 404 
+            };
+        }
+
+        await Category.findByIdAndDelete(categoryID);
+
+        return {
+            message: "category-deleted",
+            code: 200
+        };
+    } catch (error) {
+        console.error("DeleteCategory error:", error);
+        return {
+            message: "internal-server-error",
+            code: 500 
+        };
+    }
+};
+
+export default deleteCategory;

+ 18 - 0
src/actions/menu/deleteCategory/types.ts

@@ -0,0 +1,18 @@
+import {
+    IsNotEmpty, IsString, 
+    Validate
+} from "class-validator";
+
+export class DeleteCategoryInput {
+    @IsString()
+    @IsNotEmpty()
+    categoryID!: string;
+}
+
+export interface DeleteCategoryResult {
+    message: string;
+    code: number;
+    payload?: {
+        categoryID: string;
+    };
+}

+ 37 - 0
src/actions/menu/getCategories/index.ts

@@ -0,0 +1,37 @@
+import {
+    Category 
+} from "../../../models/Category";
+import {
+    GetCategoriesResult 
+} from "./types";
+
+const getCategories = async (userID: string): Promise<GetCategoriesResult> => {
+    try {
+        const categories = await Category.find({
+            userID
+        }).sort({
+            index: 1
+        });
+
+        return {
+            message: "categories-retrieved",
+            code: 200,
+            payload: {
+                categories: categories.map(c => ({
+                    _id: c._id.toString(),
+                    isActive: c.isActive,
+                    title: c.title,
+                    index: c.index
+                }))
+            }
+        };
+    } catch (error) {
+        console.error("GetCategories error:", error);
+        return {
+            message: "internal-server-error",
+            code: 500 
+        };
+    }
+};
+
+export default getCategories;

+ 12 - 0
src/actions/menu/getCategories/types.ts

@@ -0,0 +1,12 @@
+export interface GetCategoriesResult {
+    message: string;
+    code: number;
+    payload?: {
+        categories: {
+            isActive: boolean;
+            title: string;
+            index: number;
+            _id: string;
+        }[];
+    };
+}

+ 12 - 0
src/actions/menu/index.ts

@@ -0,0 +1,12 @@
+export {
+    default as addCategory 
+} from "./addCategory";
+export {
+    default as deleteCategory 
+} from "./deleteCategory";
+export {
+    default as getCategories 
+} from "./getCategories";
+export {
+    default as updateCategory 
+} from "./updateCategory";

+ 9 - 0
src/actions/menu/types/index.ts

@@ -0,0 +1,9 @@
+export {
+    AddCategoryInput 
+} from "../addCategory/types";
+export {
+    DeleteCategoryInput
+} from "../deleteCategory/types";
+export {
+    UpdateCategoryInput
+} from "../updateCategory/types";

+ 64 - 0
src/actions/menu/updateCategory/index.ts

@@ -0,0 +1,64 @@
+import {
+    Category 
+} from "../../../models/Category";
+import {
+    UpdateCategoryInput, 
+    UpdateCategoryResult 
+} from "./types";
+
+const updateCategory = async (userID: string, input: UpdateCategoryInput): Promise<UpdateCategoryResult> => {
+    try {
+        const {
+            categoryID,
+            ...updateFields
+        } = input;
+
+        const category = await Category.findOne({
+            _id: categoryID,
+            userID
+        });
+
+        if (!category) {
+            return {
+                message: "category-not-found",
+                code: 404
+            };
+        }
+
+        const updatedCategory = await Category.findByIdAndUpdate(
+            categoryID,
+            updateFields,
+            {
+                new: true //güncelledikten sonra döndürmesi için
+            }
+        );
+        
+        if (!updatedCategory){
+            return {
+                message: "category-not-found",
+                code: 404
+            };
+        }
+
+        return {
+            message: "category-updated",
+            code: 200,
+            payload: {
+                category: {
+                    _id: updatedCategory._id.toString(),
+                    isActive: updatedCategory.isActive,
+                    title: updatedCategory.title,
+                    index: updatedCategory.index
+                }
+            }
+        };
+    } catch (error) {
+        console.error("UpdateCategory error:", error);
+        return {
+            message: "internal-server-error",
+            code: 500 
+        };
+    }
+};
+
+export default updateCategory;

+ 39 - 0
src/actions/menu/updateCategory/types.ts

@@ -0,0 +1,39 @@
+import {
+    IsNotEmpty,
+    IsOptional,
+    IsString,
+    IsNumber,
+    IsBoolean,
+    Validate
+} from "class-validator";
+
+export class UpdateCategoryInput {
+    @IsString()
+    @IsNotEmpty()
+    categoryID!: string;
+
+    @IsString()
+    @IsOptional()
+    title?: string;
+
+    @IsNumber()
+    @IsOptional()
+    index?: number;
+
+    @IsBoolean()
+    @IsOptional()
+    isActive?: boolean;
+}
+
+export interface UpdateCategoryResult {
+    message: string;
+    code: number;
+    payload?: {
+        category: {
+            _id: string;
+            title: string;
+            index: number;
+            isActive: boolean;
+        }
+    };
+}

+ 55 - 0
src/controllers/menuController.ts

@@ -0,0 +1,55 @@
+import {
+    Response 
+} from "express";
+import {
+    AuthRequest 
+} from "../middlewares/authMiddleware";
+import {
+    deleteCategory as _deleteCategory,
+    updateCategory as _updateCategory,
+    getCategories as _getCategories,
+    addCategory as _addCategory
+} from "../actions/menu";
+
+export const addCategory = async (req: AuthRequest, res: Response): Promise<void> => {
+    const result = await _addCategory(req.context!.userID, req.body);
+
+    res.status(result.code)
+        .json({
+            message: result.message, 
+            code: result.code 
+        });
+};
+
+export const deleteCategory = async (req: AuthRequest, res: Response): Promise<void> => {
+    const result = await _deleteCategory(req.context!.userID, req.body);
+
+    res.status(result.code)
+        .json({
+            message: result.message, 
+            code: result.code 
+        });
+};
+
+export const updateCategory = async (req: AuthRequest, res: Response): Promise<void> => {
+    const result = await _updateCategory(req.context!.userID, req.body);
+    
+    res.status(result.code)
+        .json({
+            message: result.message, 
+            code: result.code 
+        });
+};
+
+export const getCategories = async (req: AuthRequest, res: Response): Promise<void> => {
+    const result = await _getCategories(req.context!.userID);
+
+    res.status(result.code)
+        .json({
+            message: result.message,
+            code: result.code,
+            ...(result.payload && {
+                payload: result.payload 
+            }),
+        });
+};

+ 4 - 1
src/middlewares/authMiddleware.ts

@@ -1,11 +1,14 @@
 import {
-    Request, Response, NextFunction
+    NextFunction,
+    Response,
+    Request
 } from "express";
 import jwt from "jsonwebtoken";
 import redis from "../config/redis";
 import {
     User
 } from "../models/User";
+
 export interface AuthRequest extends Request {
     context?: {
         companyName: string;

+ 33 - 0
src/models/Category.ts

@@ -0,0 +1,33 @@
+import mongoose, {
+    Document,
+    Schema 
+} from "mongoose";
+
+export interface ICategory extends Document {
+    userID: mongoose.Types.ObjectId;
+    isActive: boolean;
+    createdAt: Date;
+    updatedAt: Date;
+    title: string;
+    index: number;
+}
+
+const CategorySchema = new Schema<ICategory>(
+    {
+        userID: mongoose.Schema.Types.ObjectId,
+        title: {
+            type: String,
+            trim: true
+        },
+        index: Number,
+        isActive: {
+            type: Boolean,
+            default: true
+        }
+    },
+    {
+        timestamps: true
+    }
+);
+
+export const Category = mongoose.model<ICategory>("Category", CategorySchema);

+ 22 - 15
src/models/User.ts

@@ -1,11 +1,13 @@
 import mongoose, {
-    Document, Schema 
+    Document,
+    Schema 
 } from "mongoose";
 
 export interface IUser extends Document {
     deleteAccountDate?: Date;
     isPhoneVerified: boolean;
     isMailVerified: boolean;
+    coverPhotoPath?: string;
     refreshToken?: string;
     companyName: string;
     phoneNumber: string;
@@ -18,6 +20,7 @@ export interface IUser extends Document {
     createdAt: Date;
     updatedAt: Date;
     mail: string;
+    slug: string;
 }
 
 const UserSchema = new Schema<IUser>(
@@ -28,50 +31,51 @@ const UserSchema = new Schema<IUser>(
             trim: true 
         },
         firstName: {
-            required: true,
             type: String, 
             trim: true 
         },
         lastName: {
-            required: true, 
             type: String, 
             trim: true 
         },
         companyName: {
-            required: true, 
             type: String, 
             trim: true 
         },
+        slug: {
+            required: true,
+            type: String,
+            unique: true,
+            trim: true
+        },
         mail: {
             lowercase: true, 
-            required: true, 
             type: String, 
             unique: true, 
-            trim: true 
+            trim: true
         },
         phoneNumber: {
-            required: true, 
-            type: String, 
-            unique: true, 
-            trim: true 
+            type: String,
+            unique: true,
+            trim: true
         },
         password: {
-            required: true, 
-            type: String
+            type: String,
+            trim: true 
         },
         isActive: {
             type: Boolean, 
             default: true 
         },
-        isMailVerified: {
+        isDeleted: {
             type: Boolean, 
             default: false 
         },
-        isPhoneVerified: {
+        isMailVerified: {
             type: Boolean, 
             default: false 
         },
-        isDeleted: {
+        isPhoneVerified: {
             type: Boolean, 
             default: false 
         },
@@ -81,6 +85,9 @@ const UserSchema = new Schema<IUser>(
         deleteAccountDate: {
             type: Date 
         },
+        coverPhotoPath: {
+            type: String
+        }
     },
     {
         timestamps: true 

+ 4 - 4
src/routes/authRoutes.ts

@@ -27,14 +27,14 @@ import {
 
 const router = Router();
 
-router.post("/finish-mail-verify", validateBody(FinishMailVerifyInput), finishMailVerify);
-router.post("/start-mail-verify", validateBody(StartMailVerifyInput), startMailVerify);
-router.post("/refresh-token", validateBody(RefreshTokenInput), refreshToken);
+router.post("/finishMailVerify", validateBody(FinishMailVerifyInput), finishMailVerify);
+router.post("/startMailVerify", validateBody(StartMailVerifyInput), startMailVerify);
+router.post("/refreshToken", validateBody(RefreshTokenInput), refreshToken);
 router.post("/register", validateBody(RegisterInput), register);
 router.post("/login", validateBody(LoginInput), login);
 router.post("/logout", authMiddleware, logout);
 
-router.get("/validate-token", authMiddleware, (req: AuthRequest, res) => {
+router.get("/validateToken", authMiddleware, (req: AuthRequest, res) => {
     res.status(200)
         .json({
             message: "token-valid",

+ 2 - 0
src/routes/index.ts

@@ -2,9 +2,11 @@ import {
     Router 
 } from "express";
 import authRoutes from "./authRoutes";
+import menuRoutes from "./menuRoutes";
 
 const router = Router();
 
 router.use("/auth", authRoutes);
+router.use("/menu", menuRoutes);
 
 export default router;

+ 29 - 0
src/routes/menuRoutes.ts

@@ -0,0 +1,29 @@
+import {
+    Router 
+} from "express";
+import {
+    deleteCategory,
+    updateCategory,
+    getCategories,
+    addCategory
+} from "../controllers/menuController";
+import {
+    authMiddleware 
+} from "../middlewares/authMiddleware";
+import {
+    validateBody 
+} from "../middlewares/validateBody";
+import {
+    AddCategoryInput,
+    DeleteCategoryInput,
+    UpdateCategoryInput
+} from "../actions/menu/types";
+
+const router = Router();
+
+router.delete("/deleteCategory", authMiddleware, validateBody(DeleteCategoryInput), deleteCategory);
+router.put("/updateCategory", authMiddleware, validateBody(UpdateCategoryInput), updateCategory);
+router.post("/addCategory", authMiddleware, validateBody(AddCategoryInput), addCategory);
+router.get("/getCategories", authMiddleware, getCategories);
+
+export default router;