Compare commits

...

2 Commits

Author SHA1 Message Date
vahidrezvani cb0cb9106e تغییرات جدید 2025-07-19 11:58:27 +03:30
vahidrezvani 6a26ea9b78 اپلود روی لیارا 2025-07-08 10:10:52 +03:30
54 changed files with 1357 additions and 140 deletions

40
Dockerfile Normal file
View File

@ -0,0 +1,40 @@
# ---- Base Stage ----
FROM node:20-slim AS base
# ---- Build Stage ----
FROM base AS build
WORKDIR /app
COPY package.json package-lock.json ./
# Install ALL dependencies (dev and prod)
# This is cached and only re-runs if the lockfile changes.
RUN npm ci
# Copy the rest of the source code
COPY . .
# Build the application
RUN npm run build
# ---- Production Stage ----
# Create the final, lean production image by copying only what's needed.
# This stage is very fast as it only contains COPY commands.
FROM base AS production
WORKDIR /app
# Copy the pruned production dependencies, the built app, and package.json
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./package.json
# Expose the port the app runs on
EXPOSE 3000
# Run the application as a non-root user for better security
USER node
# Command to run the application
CMD ["node", "dist/main"]

103
package-lock.json generated
View File

@ -18,7 +18,10 @@
"@nestjs/mongoose": "^11.0.3",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@types/kavenegar": "^1.1.3",
"@types/passport-google-oauth20": "^2.0.16",
"aws-sdk": "^2.1692.0",
"axios": "^1.10.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"dotenv": "^17.0.0",
@ -5228,7 +5231,6 @@
"version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/connect": "*",
@ -5239,7 +5241,6 @@
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
@ -5285,7 +5286,6 @@
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz",
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
@ -5297,7 +5297,6 @@
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz",
"integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@ -5327,7 +5326,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/istanbul-lib-coverage": {
@ -5384,6 +5382,12 @@
"@types/node": "*"
}
},
"node_modules/@types/kavenegar": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@types/kavenegar/-/kavenegar-1.1.3.tgz",
"integrity": "sha512-va7j5bjW+ic1rmTH2kLOItgGflZ/ZI4G7rPQAo+Ysiccb0mFQhK5Dz/HvdNCMZ1TlAQNWb+QCfBsPaktqwYjuA==",
"license": "MIT"
},
"node_modules/@types/methods": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@ -5395,7 +5399,6 @@
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/multer": {
@ -5417,25 +5420,62 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/oauth": {
"version": "0.9.6",
"resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.6.tgz",
"integrity": "sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/passport": {
"version": "1.0.17",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
"integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==",
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/passport-google-oauth20": {
"version": "2.0.16",
"resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.16.tgz",
"integrity": "sha512-ayXK2CJ7uVieqhYOc6k/pIr5pcQxOLB6kBev+QUGS7oEZeTgIs1odDobXRqgfBPvXzl0wXCQHftV5220czZCPA==",
"license": "MIT",
"dependencies": {
"@types/express": "*",
"@types/passport": "*",
"@types/passport-oauth2": "*"
}
},
"node_modules/@types/passport-oauth2": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
"integrity": "sha512-6//z+4orIOy/g3zx17HyQ71GSRK4bs7Sb+zFasRoc2xzlv7ZCJ+vkDBYFci8U6HY+or6Zy7ajf4mz4rK7nsWJQ==",
"license": "MIT",
"dependencies": {
"@types/express": "*",
"@types/oauth": "*",
"@types/passport": "*"
}
},
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/send": {
"version": "0.17.5",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
"integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
@ -5446,7 +5486,6 @@
"version": "1.15.8",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz",
"integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
@ -6807,7 +6846,6 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/available-typed-arrays": {
@ -6882,6 +6920,17 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/axios": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/b4a": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
@ -7787,7 +7836,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@ -8131,7 +8179,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@ -8388,7 +8435,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -9142,6 +9188,26 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@ -9206,7 +9272,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@ -9233,7 +9298,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@ -9243,7 +9307,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
@ -12401,6 +12464,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

@ -29,7 +29,10 @@
"@nestjs/mongoose": "^11.0.3",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@types/kavenegar": "^1.1.3",
"@types/passport-google-oauth20": "^2.0.16",
"aws-sdk": "^2.1692.0",
"axios": "^1.10.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"dotenv": "^17.0.0",

View File

@ -13,13 +13,16 @@ import { ConfigModule } from '@nestjs/config';
import { ProductModule } from './product/product.module';
import { DiscountModule } from './discount/discount.module';
import { DiscountTypeModule } from './discount-type/discount-type.module';
import { SmsModule } from './sms/sms.module';
import { UserLoginModule } from './user-login/user-login.module';
import { UserModule } from './user/user.module';
@Module({
imports: [
CategoryModule,UploadModule,LoginModule,ShopModule,ProductModule,DiscountModule,
MongooseModule.forRoot('mongodb://localhost/fartak'),
ConfigModule.forRoot({ isGlobal: true }), DiscountTypeModule
MongooseModule.forRoot('mongodb://root:FcpaOAxvZd2IiqK5Vl5QRMTE@fartakdatabase:27017/my-app?authSource=admin'),
ConfigModule.forRoot({ isGlobal: true }), DiscountTypeModule, SmsModule, UserLoginModule, UserModule
],
controllers: [AppController],
providers: [AppService],

View File

@ -1,6 +1,52 @@
import { Controller } from '@nestjs/common';
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { JwtService } from '@nestjs/jwt';
import { Response, Request } from 'express';
import { JwtAuthGuard } from './jwt-auth.guard';
import { AuthenticatedRequest } from 'src/interfaces/request';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private jwtService: JwtService,private readonly authService:AuthService) {}
@Get('google')
@UseGuards(AuthGuard('google'))
async googleAuth() {
// Redirects to Google
}
@Get('google/redirect')
@UseGuards(AuthGuard('google'))
async googleRedirect(@Req() req: Request, @Res() res: Response) {
const user = req.user as any;
// You can store user in DB here if needed
const payload = {
email: user.email,
name: `${user.firstName} ${user.lastName}`,
picture: user.picture,
};
const token = this.jwtService.sign(payload);
// ✅ Option 1: Send token as JSON
// res.json({ token });
// ✅ Option 2: Redirect to frontend with token
res.redirect(`http://localhost:4200/login/success?token=${token}`);
}
@UseGuards(JwtAuthGuard)
@Get('/get')
async getListDiscountShop(
@Req() req: AuthenticatedRequest,
@Res() res: Response,
) {
const createdShop = await this.authService.verifyauth(req.user.user_ID);
console.log(createdShop)
return res.status(createdShop.status).json(createdShop.data);
}
}

View File

@ -5,11 +5,13 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { AuthController } from './auth.controller';
import { SellerModule } from 'src/seller/seller.module';
@Module({
imports: [
ConfigModule,
PassportModule,
SellerModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],

View File

@ -1,21 +1,23 @@
// auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { CreateSellerResponse } from 'src/interfaces/request';
import { SellerService } from 'src/seller/seller.service';
@Injectable()
export class AuthService {
constructor(private jwtService: JwtService) {}
constructor(private jwtService: JwtService,private readonly sellerService:SellerService) {}
async generateTokens(payload:any) {
console.log(process.env.JWT_SECRET)
const accessToken = await this.jwtService.signAsync(payload, {
secret: process.env.JWT_SECRET,
expiresIn: '15m',
expiresIn: '1d',
});
const refreshToken = await this.jwtService.signAsync(payload, {
secret: process.env.JWT_REFRESH_SECRET,
expiresIn: '7d',
expiresIn: '30d',
});
return {
@ -24,9 +26,9 @@ export class AuthService {
};
}
async verifyRefreshToken(token: string) {
async verifyToken(token: string) {
return this.jwtService.verifyAsync(token, {
secret: process.env.JWT_REFRESH_SECRET,
secret: process.env.JWT_SECRET,
});
}
async loginWithGoogle(user: any) {
@ -37,5 +39,25 @@ export class AuthService {
};
}
async verifyauth(user: string):Promise <CreateSellerResponse>{
const dataSeller = await this.sellerService.findSellerwidtID(user)
console.log("data")
console.log(dataSeller)
if(dataSeller){
return {
"message":"فروشنده موجود است",
"status":200,
"data":dataSeller
}
}else{
return {
"message":"فروشنده موجود نیست",
"status":404,
"data":null
}
}
}
}

View File

@ -1,22 +1,26 @@
// auth/google.strategy.ts
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Strategy, VerifyCallback, StrategyOptions } from 'passport-google-oauth20';
import { Injectable } from '@nestjs/common';
import * as dotenv from 'dotenv';
dotenv.config();
import { ConfigService } from '@nestjs/config';
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor() {
constructor(private configService: ConfigService) {
super({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: 'http://localhost:3000/auth/google/redirect',
clientID: configService.get<string>('GOOGLE_CLIENT_ID'),
clientSecret: configService.get<string>('GOOGLE_CLIENT_SECRET'),
callbackURL: configService.get<string>('GOOGLE_CALLBACK_URL'),
scope: ['email', 'profile'],
});
} as StrategyOptions); // Fixes the typing error
}
async validate(accessToken: string, refreshToken: string, profile: any, done: VerifyCallback): Promise<any> {
async validate(
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
const { name, emails, photos } = profile;
const user = {

View File

@ -1,21 +1,27 @@
import { Injectable } from '@nestjs/common';
import { Injectable,UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { UserService } from 'src/user/user.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
constructor(private configService: ConfigService,private readonly userService:UserService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_REFRESH_SECRET'),
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
async validate(payload: any) {
console.log("payload")
console.log("payload")
console.log(payload)
return { userId: payload.sellerid};
const user = await this.userService.findUserwidtID(payload.userID)
console.log("user")
console.log(user)
if (user.status != 200) {
throw new UnauthorizedException('کاربر وارد نشده است');
}
return { userId: user.data._id,user_ID: user.data.ID};
}
}

View File

@ -1,4 +1,4 @@
import { Body, Controller, Res, Post } from '@nestjs/common';
import { Body, Controller, Res, Post, Get } from '@nestjs/common';
import { Response } from 'express';
import { CategoryService } from './category.service';
import { CreatCategoryDto } from './dto/creatCategory.dto';
@ -12,4 +12,10 @@ export class CategoryController {
const result = await this.categoryService.creatCategory(dto);
return res.status(result.status).json(result);
}
@Get('/all')
async findAll(@Res() res: Response) {
const result = await this.categoryService.findAllCategory();
return res.status(result.status).json(result);
}
}

View File

@ -24,7 +24,8 @@ export class CategoryService {
const newCategory = new this.categoryModel({
Name:creatCategory.Name,
Description:creatCategory.Description,
ID:newUuid
ID:newUuid,
Code:creatCategory.Code,
})
const result = newCategory.save()
@ -63,6 +64,14 @@ export class CategoryService {
}
}
}
async findAllCategory() :Promise <CreateSellerResponse> {
const categories = await this.categoryModel.find().select('-_id')
return {
"message":"همه دسته بندی ها",
"status":200,
"data":categories
}
}
}

View File

@ -1,4 +1,4 @@
import { IsNotEmpty, IsString } from "class-validator";
import { IsNotEmpty, IsString,IsNumberString,Length} from "class-validator";
export class CreatCategoryDto {
@IsNotEmpty({message:"نام دسته بندی نباید خالی باشد . "})
@ -9,6 +9,13 @@ export class CreatCategoryDto {
@IsString({message:"نام دسته بندی باید متن باشد . "})
Description:string
@IsNotEmpty({ message: 'کد دسته بندی نمی‌تواند خالی باشد.' })
@IsNumberString({},{ message: 'کد دسته بندی باید فقط شامل اعداد باشد.' })
Code: number;
}

View File

@ -1,4 +1,4 @@
import { Body, Controller, Res, Post } from '@nestjs/common';
import { Body, Controller, Res, Post,Get} from '@nestjs/common';
import { Response } from 'express';
import { DiscountTypeService } from './discount-type.service';
import { CreatDiscountTypeDto } from './dto/discountType.dto';
@ -12,4 +12,10 @@ export class DiscountTypeController {
const result = await this.discountTypeService.creatDiscountType(dto);
return res.status(result.status).json(result);
}
@Get('/all')
async findAll(@Res() res: Response) {
const result = await this.discountTypeService.findAllDiscountType();
return res.status(result.status).json(result);
}
}

View File

@ -20,7 +20,8 @@ export class DiscountTypeService {
const newDiscountType = new this.discountTypeModel({
Name:creatDiscountType.Name,
Description:creatDiscountType.Description,
ID:newUuid
ID:newUuid,
Code:creatDiscountType.Code
})
const result = newDiscountType.save()
@ -41,7 +42,6 @@ export class DiscountTypeService {
}
async findDiscountType(id:string) :Promise <CreateSellerResponse> {
const category = await this.discountTypeModel.findOne({ID:id})
@ -60,5 +60,15 @@ export class DiscountTypeService {
}
}
async findAllDiscountType() :Promise <CreateSellerResponse> {
const categories = await this.discountTypeModel.find().select('-_id')
return {
"message":"همه دسته بندی های تخفیف",
"status":200,
"data":categories
}
}
}

View File

@ -1,14 +1,19 @@
import { IsNotEmpty, IsString } from "class-validator";
import { IsNotEmpty, IsString,IsNumberString } from "class-validator";
export class CreatDiscountTypeDto {
@IsNotEmpty({message:ام دسته بندی نباید خالی باشد . "})
@IsString({message:ام دسته بندی باید متن باشد . "})
@IsNotEmpty({message:وع تخفیف نباید خالی باشد . "})
@IsString({message:وع تخفیف باید متن باشد . "})
Name:string;
@IsNotEmpty({message:ام دسته بندی نباید خالی باشد . "})
@IsString({message:ام دسته بندی باید متن باشد . "})
@IsNotEmpty({message:وع تخفیف نباید خالی باشد . "})
@IsString({message:وع تخفیف باید متن باشد . "})
Description:string
@IsNotEmpty({ message: 'نوع تخفیف نمی‌تواند خالی باشد.' })
@IsNumberString({},{ message: 'نوع تخفیف باید فقط شامل اعداد باشد.' })
Code: number;
}

View File

@ -5,28 +5,139 @@ import {
UseGuards,
Res,
Req,
UploadedFiles,
BadRequestException,
UseInterceptors,
Param,
Get,
Query
} from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { FilesInterceptor } from '@nestjs/platform-express';
import { DiscountService } from './discount.service';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { AuthenticatedRequest } from 'src/interfaces/request';
import { CreateDiscountDto } from './dto/discount.dto';
import { EditDiscountDto } from './dto/editDiscount.dto';
import { UploadService } from 'src/upload/upload.service';
import { PaginationQueryDiscountDto } from './dto/discountfind.dto';
import { Request } from 'express';
import { Response } from 'express';
@Controller('discount')
export class DiscountController {
constructor(
private readonly upload: UploadService,
private readonly discountService: DiscountService,
) {}
@UseGuards(JwtAuthGuard)
@Post('/add')
@UseGuards(JwtAuthGuard)
@Post('/add')
@UseInterceptors(
FilesInterceptor('Images', 3, {
limits: {
fileSize: 2 * 1024 * 1024, // 2MB
},
fileFilter: (req, file, cb) => {
const allowedMimeTypes = ['image/png', 'image/jpeg', 'image/jpg'];
cb(null, true);
},
}))
async create(
@Body() dto:CreateDiscountDto,
@UploadedFiles() files: Express.Multer.File[],
@Req() req: AuthenticatedRequest,
@Res() res: Response,
) {
const creatDiscount = await this.discountService.creatDiscount(dto,req.user.userId);
let fileIds: string[] = [];
if (files && files.length > 0) {
for (const file of files) {
const result = await this.upload.uploadFileSingle(file);
if (result.status !== 200) {
return res.status(400).json({
message: 'آپلود فایل با خطا مواجه شد',
status: 400,
});
}
fileIds.push(result.data._id);
}
}
const creatDiscount = await this.discountService.creatDiscount(dto,fileIds,req.user.userId);
return res.status(creatDiscount.status).json(creatDiscount);
}
@UseGuards(JwtAuthGuard)
@Get('/get')
async getListDiscountShop(
@Query() query :PaginationQueryDiscountDto,
@Req() req: AuthenticatedRequest,
@Res() res: Response,
) {
const createdShop = await this.discountService.findAlldiscount(req.user.userId,query);
return res.status(createdShop.status).json(createdShop);
}
@UseGuards(JwtAuthGuard)
@Get('/get/:id')
async getSingleDiscountwithID(
@Param('id') id: string,
@Req() req: AuthenticatedRequest,
@Res() res: Response,
) {
const createdShop = await this.discountService.findDiscountWithID(id);
return res.status(createdShop.status).json(createdShop);
}
@UseGuards(JwtAuthGuard)
@Get('/delete/:id')
async deleteSingleDiscountwithID(
@Param('id') id: string,
@Req() req: AuthenticatedRequest,
@Res() res: Response,
) {
const createdShop = await this.discountService.deleteDiscountWithID(id);
return res.status(createdShop.status).json(createdShop);
}
@UseGuards(JwtAuthGuard)
@Post('/edit/:id')
@UseInterceptors(
FilesInterceptor('Images', 3, {
limits: {
fileSize: 2 * 1024 * 1024, // 2MB
},
fileFilter: (req, file, cb) => {
const allowedMimeTypes = ['image/png', 'image/jpeg', 'image/jpg'];
cb(null, true);
},
}))
async editDiscountWithID(
@Param('id') id: string,
@Body() dto:EditDiscountDto,
@UploadedFiles() files: Express.Multer.File[],
@Req() req: AuthenticatedRequest,
@Res() res: Response,
) {
let fileIds: string[] = [];
if (files && files.length > 0) {
for (const file of files) {
const result = await this.upload.uploadFileSingle(file);
if (result.status !== 200) {
return res.status(400).json({
message: 'آپلود فایل با خطا مواجه شد',
status: 400,
});
}
fileIds.push(result.data._id);
}
}
const createdShop = await this.discountService.editDiscount(id,dto,fileIds,req.user.userId);
return res.status(createdShop.status).json(createdShop);
}
}

View File

@ -6,11 +6,13 @@ import { DiscountSchema } from 'src/schemas/discount.schema';
import { ProductModule } from 'src/product/product.module';
import { ShopModule } from 'src/shop/shop.module';
import { DiscountTypeModule } from 'src/discount-type/discount-type.module';
import { UploadModule } from 'src/upload/upload.module';
import { UploadService } from 'src/upload/upload.service';
@Module({
imports:[
MongooseModule.forFeature([
{ name: 'Discount', schema: DiscountSchema },
]),ProductModule,DiscountTypeModule,ShopModule
]),UploadModule,DiscountTypeModule,ShopModule
],

View File

@ -9,26 +9,32 @@ import { CreateDiscountDto } from './dto/discount.dto';
import { CreateSellerResponse } from 'src/interfaces/request';
import { v4 as uuidv4 } from 'uuid';
import { ShopService } from 'src/shop/shop.service';
import { UploadService } from 'src/upload/upload.service';
import { PaginationQueryDiscountDto } from './dto/discountfind.dto';
import { EditDiscountDto } from './dto/editDiscount.dto';
@Injectable()
export class DiscountService {
constructor(
@InjectModel(Discount.name)
private readonly discountModel: Model<DiscountDocument>,
private readonly shopService: ShopService,
private readonly productService: ProductService,
private readonly discountTypeService: DiscountTypeService,
) {}
private readonly upload: UploadService
) { }
private convertToMinutes(time: string): number {
const [hours, minutes] = time.split(':').map(Number);
return hours * 60 + minutes;
}
async creatDiscount(
creatDiscountDto: CreateDiscountDto,
images: (string[] | null),
user: any,
): Promise<CreateSellerResponse> {
const newUuid: string = uuidv4();
console.log("creatDiscountDto")
console.log(creatDiscountDto)
console.log(creatDiscountDto)
const existCategory = await this.discountTypeService.findDiscountType(creatDiscountDto.Type);
if (existCategory.status !== 200 || !existCategory.data) {
return {
@ -38,7 +44,7 @@ export class DiscountService {
};
}
const existShop = await this.shopService.findShopSingle(creatDiscountDto.Shop);
const existShop = await this.shopService.findShopWithSeller(user);
if (existShop.status !== 200 || !existShop.data) {
return {
message: 'فروشگاه ثبت نشده است',
@ -46,50 +52,43 @@ export class DiscountService {
data: null,
};
}
if (!existShop.data.Seller) {
return {
message: 'اطلاعات فروشنده فروشگاه ناقص است',
status: 500,
data: null,
};
}
const sellerId = new Types.ObjectId(user);
const shopSellerId = existShop.data.Seller;
if (!sellerId.equals(shopSellerId)) {
const start = this.convertToMinutes(creatDiscountDto.StartTime);
const end = this.convertToMinutes(creatDiscountDto.EndTime);
if (end <= start) {
return {
message: 'این فروشنده مجاز به ثبت تخفیف برای این فروشگاه نیست',
status: 403,
data: null,
};
}
const existProduct = await this.productService.findProductSingle(creatDiscountDto.Product);
if (existProduct.status !== 200 || !existProduct.data) {
return {
message: 'محصول ثبت نشده است',
message: 'تایم شروع نباید از تایم پایان بزرگتر باشد',
status: 404,
data: null,
};
}
if (!existProduct.data.Shop.equals(existShop.data._id)) {
const sellerId = new Types.ObjectId(user);
const shopSellerId = existShop.data.Seller._id;
console.log("shopSellerId")
console.log(existShop)
/*if (!sellerId.equals(shopSellerId)) {
return {
message: 'این محصول در این فروشگاه ثبت نشده است',
status: 400,
message: 'این فروشنده مجاز به ثبت تخفیف برای این فروشگاه نیست',
status: 403,
data: null,
};
}
*/
try {
const newDiscount = new this.discountModel({
Name: creatDiscountDto.Name,
Shop: existShop.data._id,
Product: existProduct.data._id,
Images: images,
Type: existCategory.data._id,
ProductDescription: "محصول جدید",
Description: creatDiscountDto.Description,
Price: creatDiscountDto.Price,
NPrice: creatDiscountDto.NPrice,
StartDate: creatDiscountDto.Start,
EndDate: creatDiscountDto.End,
StartTime: creatDiscountDto.StartTime,
@ -116,4 +115,204 @@ export class DiscountService {
};
}
}
async findAlldiscount(
user: any,
query: PaginationQueryDiscountDto,
): Promise<CreateSellerResponse> {
const existShop = await this.shopService.findShopWithSeller(user);
if (existShop.status !== 200 || !existShop.data) {
return {
message: 'فروشگاهی ثبت نشده است',
status: 404,
data: null,
};
}
const filter: any = {};
const now = new Date();
filter.Shop = existShop.data._id;
if (query.search) {
filter.Name = { $regex: query.search, $options: 'i' };
}
if (Number(query.status) === 1) {
filter.StartDate = { $lte: now };
filter.EndDate = { $gte: now };
} else if (Number(query.status) === 0) {
filter.$or = [
{ StartDate: { $gt: now } },
{ EndDate: { $lt: now } },
];
}
const page = Number(query.page) || 1;
const limit = Number(query.limit) || 10;
const skip = (page - 1) * limit;
console.log("query1111")
console.log(query)
const [discounts, total] = await Promise.all([
this.discountModel
.find(filter)
.skip(skip)
.limit(limit)
.populate({ path: 'Images' })
.populate({ path: 'Shop', select: 'ID Name' })
.populate({ path: 'Type' }),
this.discountModel.countDocuments(filter),
]);
console.log("discounts")
console.log(discounts)
return {
message: 'لیست تخفیفات',
status: 200,
data: {
discounts,
total,
page,
lastPage: Math.ceil(total / limit),
},
};
}
async findDiscountWithID(id: string): Promise<CreateSellerResponse> {
const discount = await this.discountModel.findOne({ ID: id }).populate({ path: 'Images' }).populate({ path: 'Shop', select: 'Name ID' }).populate({ path: 'Type' })
if (discount) {
return {
"message": "تخفیف موجود می باشد",
"status": 200,
"data": discount
}
} else {
return {
"message": "تخفیف موجود نمی باشد",
"status": 401,
"data": null
}
}
}
async deleteDiscountWithID(id: string): Promise<CreateSellerResponse> {
const discount = await this.discountModel.findOne({ ID: id })
if (!discount) {
return {
message: 'تخفیف موجود نمی‌باشد',
status: 404,
data: null,
};
}
const deleteResult = await this.discountModel.deleteOne({ _id: discount._id });
if (deleteResult.deletedCount !== 1) {
return {
message: 'خطا در حذف تخفیف',
status: 500,
data: null,
};
}
return {
message: 'تخفیف با موفقیت حذف شد',
status: 200,
data: null,
};
}
async editDiscount(
id: string,
editDiscountDto: EditDiscountDto,
images: (string[] | null),
user: string,
): Promise<CreateSellerResponse> {
const discount = await this.findDiscountWithID(id)
if (discount.status !== 200) {
return {
"data": "",
"message": "تخفیف با این شناسه ثبت نشده است",
"status": 404
}
}
const existCategory = await this.discountTypeService.findDiscountType(editDiscountDto.Type);
if (existCategory.status !== 200 || !existCategory.data) {
return {
message: 'نوع تخفیف ثبت نشده است',
status: 404,
data: null,
};
}
const start = this.convertToMinutes(editDiscountDto.StartTime);
const end = this.convertToMinutes(editDiscountDto.EndTime);
if (end <= start) {
return {
message: 'تایم شروع نباید از تایم پایان بزرگتر باشد',
status: 404,
data: null,
};
}
var discountImages = discount.data.Images;
if (images && images.length > 0) {
var changeImages = discountImages.filter(item => !editDiscountDto.Image.includes(item));
images.forEach(item => {
if (!changeImages.includes(item)) {
changeImages.push(item);
}
});
}
const updateDiscount = await this.discountModel.updateOne(
{_id:discount.data._id},
{$set: {
"Name":editDiscountDto.Name,
"Images":changeImages,
"Type":editDiscountDto.Type,
"Description":editDiscountDto.Description,
"Price":editDiscountDto.Price,
"NPrice":editDiscountDto.NPrice,
"StartDate":editDiscountDto.Start,
"EndDate":editDiscountDto.End,
"StartTime":editDiscountDto.StartTime,
"EndTime":editDiscountDto.EndTime,
"Radius":editDiscountDto.Radius
}}
)
if (updateDiscount.modifiedCount === 0) {
return {
message: 'آپدیت تخفیف با شکست مواجه شد',
data: null,
status: 500,
};
}
return {
message: 'تخفیف با موفقیت آپدیت شد',
data: null,
status: 200,
};
}
}

View File

@ -3,24 +3,26 @@ import { Type } from 'class-transformer';
export class CreateDiscountDto {
@IsNotEmpty({ message: 'شناسه فروشگاه نباید خالی باشد' })
@IsString({ message: 'شناسه فروشگاه باید شامل متن باشد' })
Shop: string;
@IsNotEmpty({ message: 'نام تخفیف نباید خالی باشد' })
@IsString({ message: 'نام تخفیف باید شامل متن باشد' })
Name: string;
@IsNotEmpty({ message: 'نوع تخفیف نباید خالی باشد' })
@IsString({ message: 'نوع تخفیف باید شامل متن باشد' })
Type: string;
@IsNotEmpty({ message: 'شناسه محصول نباید خالی باشد' })
@IsString({ message: 'شناسه محصول باید شامل متن باشد' })
Product: string;
@IsNotEmpty({ message: 'توضیحات تخفیف نباید خالی باشد' })
@IsNotEmpty({ message: 'توضیحات تخفیف نباید خالی باشد' })
@IsString({ message: 'توضیحات تخفیف باید شامل متن باشد' })
@Length(0, 250, { message: 'طول توضیحات تخفیف حداکثر 250 حرف می تواند باشد' })
Description: string;
@IsNotEmpty({ message: 'قیمت محصول نباید خالی باشد' })
@IsNumberString({},{ message: 'قیمت محصول باید شامل عدد باشد' })
Price: number;
@IsNotEmpty({ message: 'قیمت محصول با احتساب تخفیف نباید خالی باشد' })
@IsNumberString({},{ message: 'قیمت محصول با احتساب تخفیف باید شامل عدد باشد' })
NPrice: number;
@IsNotEmpty({ message: 'تاریخ شروع نباید خالی باشد' })
@IsDateString({},{ message: 'تاریخ شروع باید به فرمت تاریخ باشد' })
@ -30,12 +32,11 @@ export class CreateDiscountDto {
@IsDateString({},{ message: 'تاریخ پایان باید به فرمت تاریخ باشد' })
End: string;
@IsNotEmpty({ message: 'ساعت شروع نباید خالی باشد' })
@Matches(/^([01]\d|2[0-3]):([0-5]\d)$/, {
message: 'فرمت تایم شروع صحیح نیست',
})
StartTime: string;
StartTime: string;
@IsNotEmpty({ message: 'ساعت پایان نباید خالی باشد' })
@Matches(/^([01]\d|2[0-3]):([0-5]\d)$/, {
@ -43,7 +44,6 @@ export class CreateDiscountDto {
})
EndTime: string;
@IsNotEmpty({ message: 'شعاع ارسال اعلان نباید خالی باشد' })
@IsNumberString({},{ message: 'شعاع ارسال اعلان باید شامل عدد باشد' })
Radius: number;

View File

@ -0,0 +1,21 @@
// dto/pagination-query.dto.ts
import { IsOptional, IsEnum, IsString, IsNumberString, IsNumber } from 'class-validator';
import { Type } from 'class-transformer';
export class PaginationQueryDiscountDto {
@IsOptional()
@IsNumberString()
page: number;
@IsOptional()
@IsNumberString()
limit: number;
@IsOptional()
@IsString()
search: string;
@IsOptional()
@IsNumberString()
status: number;
}

View File

@ -0,0 +1,58 @@
import { IsNotEmpty,IsDateString, IsNumberString, IsString,ArrayMinSize, Matches,Length,ArrayNotEmpty, IsOptional,IsArray,ValidateNested,Validate, IsDate} from 'class-validator';
import { Type } from 'class-transformer';
export class EditDiscountDto {
@IsNotEmpty({ message: 'نام تخفیف نباید خالی باشد' })
@IsString({ message: 'نام تخفیف باید شامل متن باشد' })
Name: string;
@IsNotEmpty({ message: 'نوع تخفیف نباید خالی باشد' })
@IsString({ message: 'نوع تخفیف باید شامل متن باشد' })
Type: string;
@IsNotEmpty({ message: 'توضیحات تخفیف نباید خالی باشد' })
@IsString({ message: 'توضیحات تخفیف باید شامل متن باشد' })
@Length(0, 250, { message: 'طول توضیحات تخفیف حداکثر 250 حرف می تواند باشد' })
Description: string;
@IsNotEmpty({ message: 'قیمت محصول نباید خالی باشد' })
@IsNumberString({},{ message: 'قیمت محصول باید شامل عدد باشد' })
Price: number;
@IsNotEmpty({ message: 'قیمت محصول با احتساب تخفیف نباید خالی باشد' })
@IsNumberString({},{ message: 'قیمت محصول با احتساب تخفیف باید شامل عدد باشد' })
NPrice: number;
@IsArray({ message: 'تصاویر باید به صورت آرایه ارسال شوند' })
@IsString({ each: true, message: 'هر تصویر باید به صورت رشته باشد' })
Image: string[];
@IsNotEmpty({ message: 'تاریخ شروع نباید خالی باشد' })
@IsDateString({},{ message: 'تاریخ شروع باید به فرمت تاریخ باشد' })
Start: string;
@IsNotEmpty({ message: 'تاریخ پایان نباید خالی باشد' })
@IsDateString({},{ message: 'تاریخ پایان باید به فرمت تاریخ باشد' })
End: string;
@IsNotEmpty({ message: 'ساعت شروع نباید خالی باشد' })
@Matches(/^([01]\d|2[0-3]):([0-5]\d)$/, {
message: 'فرمت تایم شروع صحیح نیست',
})
StartTime: string;
@IsNotEmpty({ message: 'ساعت پایان نباید خالی باشد' })
@Matches(/^([01]\d|2[0-3]):([0-5]\d)$/, {
message: 'فرمت تایم پایان صحیح نیست',
})
EndTime: string;
@IsNotEmpty({ message: 'شعاع ارسال اعلان نباید خالی باشد' })
@IsNumberString({},{ message: 'شعاع ارسال اعلان باید شامل عدد باشد' })
Radius: number;
}

View File

@ -3,6 +3,7 @@ import { Request } from "express";
export interface AuthenticatedRequest extends Request {
user: {
userId: string;
user_ID:string;
};
}

View File

@ -16,6 +16,4 @@ export class SendOTPCodeDto {
@IsNumberString({},{ message: 'کد ارسالی باید فقط شامل اعداد باشد.' })
@Length(5, 5, { message: 'عدد باید دقیقاً ۵ رقم باشد' })
OTP: number;
}

View File

@ -8,7 +8,7 @@ export class LoginController {
constructor(private readonly loginService:LoginService){}
@Post('/sendcode')
@Post('/sendcode')
async sendCode(@Body() dto: CreateSellerDto,@Res() res: Response) {
const result = await this.loginService.sendCode(dto);
return res.status(result.status).json(result);

View File

@ -4,8 +4,9 @@ import { LoginService } from './login.service';
import { SellerService } from 'src/seller/seller.service';
import { AuthModule } from 'src/auth/auth.module';
import { SellerModule } from 'src/seller/seller.module';
import { SmsModule } from 'src/sms/sms.module';
@Module({
imports:[SellerModule,AuthModule],
imports:[SellerModule,AuthModule,SmsModule],
controllers: [LoginController],
providers: [LoginService]
})

View File

@ -5,6 +5,7 @@ import redis from '../redis/redis.provider';
import { SellerService } from 'src/seller/seller.service';
import {SendOTPCodeDto} from './dto/sendcode.dto'
import { AuthService } from '../auth/auth.service'
import { SmsService } from 'src/sms/sms.service';
interface CreateSellerResponse {
message: string;
@ -16,15 +17,19 @@ interface CreateSellerResponse {
export class LoginService {
constructor(private readonly sellerService: SellerService,private readonly authService: AuthService) {}
constructor(private readonly sellerService: SellerService,private readonly authService: AuthService,private readonly smsService:SmsService) {}
async sendCode(createSellerDto: CreateSellerDto): Promise<CreateSellerResponse> {
async sendCode(createSellerDto: CreateSellerDto): Promise<CreateSellerResponse> {
const code = await generateCode();
console.log(`verified code is : ${code}`);
const getSeller = await this.sellerService.findSellerwidtPhone(createSellerDto.Code,createSellerDto.Phone);
if (!getSeller) {
throw new NotFoundException('User not found');
}
const sendSms = await this.smsService.sendMessage(`0${createSellerDto.Phone}`, Number(code));
console.log("sendSms")
console.log(sendSms)
await redis.set(`login-code:${createSellerDto.Phone}`, Number(code), 'EX', 60);
return {
message: 'کد یکبار مصرف ارسال شد',
@ -55,9 +60,6 @@ async getcode(sendOTPCodeDto: SendOTPCodeDto): Promise<CreateSellerResponse> {
console.log("getSeller")
console.log(getSeller)
if (!getSeller) {
throw new NotFoundException('User not found');
}
if(getSeller.status == 404){
const creatSeller = await this.sellerService.createWithPhone({Phone:sendOTPCodeDto.Phone,Code:sendOTPCodeDto.Code})
if(creatSeller.status == 200){

View File

@ -1,9 +1,5 @@
// src/redis.client.ts
import Redis from 'ioredis';
const redis = new Redis({
host: 'localhost', // یا IP سرور Redis
port: 6379, // پورت پیش‌فرض Redis
});
const redis = new Redis('redis://:eCwxwRr7YX9q0ynso4uR6HNf@fartakredis:6379/0');
export default redis;

View File

@ -15,6 +15,9 @@ export class Category {
@Prop({required:true,unique: true})
ID: string;
@Prop()
Code:number;
}
export const CategorySchema = SchemaFactory.createForClass(Category);

View File

@ -0,0 +1,31 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { HydratedDocument } from 'mongoose';
export type CommentDocument = HydratedDocument<Comment>;
@Schema({ timestamps: true })
export class Comment {
@Prop({ type: { type: mongoose.Schema.Types.ObjectId, ref: 'Shop' } })
Shops: mongoose.Schema.Types.ObjectId;
@Prop({ type: { type: mongoose.Schema.Types.ObjectId, ref: 'Discount' } })
Discount: mongoose.Schema.Types.ObjectId;
@Prop({ type: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } })
User: mongoose.Schema.Types.ObjectId;
@Prop({ required: true })
Text: string;
@Prop({ default: null })
Score: number;
@Prop({ required: true, unique: true })
ID: string
}
export const commentSchema = SchemaFactory.createForClass(Comment);
commentSchema.index({ ID: 1 });

View File

@ -7,18 +7,31 @@ export type DiscountDocument = HydratedDocument<Discount>;
@Schema({ timestamps: true })
export class Discount {
@Prop({required:true})
Name: string;
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Shop', default: null,required:true})
Shop: mongoose.Schema.Types.ObjectId;
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Product', default: null,required:true })
Product: mongoose.Schema.Types.ObjectId;
@Prop({ type: [mongoose.Schema.Types.ObjectId], ref: 'File', default: [] })
Images: mongoose.Schema.Types.ObjectId[];
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'DiscountType', default: null,required:true })
Type: mongoose.Schema.Types.ObjectId;
@Prop()
ProductDescription: string
@Prop({})
Description: string;
@Prop()
Price: number;
@Prop()
NPrice: number;
@Prop({required:true})
StartDate:Date

View File

@ -15,7 +15,9 @@ export class DiscountType {
@Prop({required:true,unique: true})
ID: string;
@Prop()
Code:number;
}
export const DiscountTypeSchema = SchemaFactory.createForClass(DiscountType);

View File

@ -7,8 +7,8 @@ export type FileDocument = HydratedDocument<File>;
@Schema({ timestamps: true })
export class File {
@Prop({ required: true,enum:['image','video','document']})
FileType: string;
@Prop()
FileType: string;
@Prop({required:false})
Title:string

View File

@ -6,7 +6,7 @@ export type SellerDocument = HydratedDocument<Seller>;
@Schema({ timestamps: true })
export class Seller {
@Prop({ unique: true })
@Prop()
Email: string;
@Prop({ unique: true })
@ -35,6 +35,9 @@ export class Seller {
@Prop()
CountryCode: number;
@Prop({ default: null })
TokenFireBase: string;
}
export const SellerSchema = SchemaFactory.createForClass(Seller);

View File

@ -19,6 +19,16 @@ class ShopSchedule {
Status: boolean;
}
@Schema()
class UserInfo {
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true })
User: mongoose.Schema.Types.ObjectId;
@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'File' }], default: [] })
Images: mongoose.Schema.Types.ObjectId[];
}
@Schema({ timestamps: true })
export class Shop {
@Prop({required:true})
@ -75,8 +85,11 @@ export class Shop {
@Prop()
ShopNumber: number;
@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'File' }], default: [] })
Images: mongoose.Schema.Types.ObjectId[];
@Prop()
Phone: number;
@Prop({ type: [UserInfo], default: [] })
Images: UserInfo[];
@Prop({ type: [ShopSchedule], default: [] })
Schedule: ShopSchedule[];
@ -86,6 +99,10 @@ export class Shop {
@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Order' }], default: [] })
Orders: mongoose.Schema.Types.ObjectId[];
@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }], default: [] })
Comments: mongoose.Schema.Types.ObjectId[];
@Prop({required:true,unique:true})
ID:string

View File

@ -0,0 +1,72 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { HydratedDocument } from 'mongoose';
export enum Gender {
Male = 'male',
Female = 'female',
None = 'none',
}
export type UserDocument = HydratedDocument<User>;
@Schema()
export class GpsPoint {
@Prop({ type: String, enum: ['Point'], required: true, default: 'Point' })
type: 'Point';
@Prop({ type: [Number], required: true })
coordinates: [number, number];
}
export const GpsPointSchema = SchemaFactory.createForClass(GpsPoint);
@Schema({ timestamps: true })
export class User {
@Prop()
Name: string;
@Prop({ type: String, enum: Gender })
Gender: Gender;
@Prop()
Email: string;
@Prop({ unique: true })
ID: string;
@Prop({ required: true, unique: true })
Phone: number;
@Prop({ required: false })
verify: boolean;
@Prop({ default: null })
LastLogin: Date;
@Prop({ default: null })
IpAddress: string;
@Prop({ type: [GpsPointSchema] })
Gps: GpsPoint[];
@Prop({ type: GpsPointSchema })
CurrentGps: GpsPoint;
@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Category' }], default: [] })
FCategory: mongoose.Schema.Types.ObjectId[];
@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Category' }], default: [] })
NotifCategory: mongoose.Schema.Types.ObjectId[];
@Prop()
CountryCode: number;
@Prop({ default: null })
TokenFireBase: string;
}
export const UserSchema = SchemaFactory.createForClass(User);

View File

@ -0,0 +1,7 @@
import { IsNotEmpty, IsString,IsNumberString,Length} from "class-validator";
export class CreatTokenFireBase {
@IsNotEmpty({message:"توکن نباید خالی باشد. "})
@IsString({message:"توکن باید متن باشد . "})
Token:string;
}

View File

@ -1,4 +1,35 @@
import { Controller } from '@nestjs/common';
import { Controller,
Post,
UploadedFile,
UseInterceptors,
Body,
UseGuards,
Res,
Req,
BadRequestException,
Get } from '@nestjs/common';
import { SellerService } from './seller.service';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { AuthenticatedRequest } from 'src/interfaces/request';
import { CreatTokenFireBase } from './dto/token-firebase.dto';
import { Response } from 'express';
@Controller('seller')
export class SellerController {}
export class SellerController {
constructor (private readonly sellerService:SellerService){}
@UseGuards(JwtAuthGuard)
@Post('/firebaseUpdate')
async updateFireBaseToken (
@Body() body:CreatTokenFireBase,
@Req() req: AuthenticatedRequest,
@Res() res: Response,
){
const updateToken = await this.sellerService.updateTokenFireBase(body.Token,req.user.userId)
return res.status(updateToken.status).json(updateToken.data)
}
}

View File

@ -30,7 +30,8 @@ export class SellerService {
"LastLogin": null,
"IpAddress": null,
"Shops": [],
"CountryCode": 98
"CountryCode": 98,
"TokenFireBase":""
});
const savedSeller = await createdSeller.save();
@ -107,6 +108,35 @@ export class SellerService {
}
}
async updateTokenFireBase (token:string,user:string) : Promise <CreateSellerResponse> {
const seller = await this.findSellerwidtID(user)
if(!seller){
return {
"data":null,
"status": 404,
"message": "فروشنده موجود نیست"
}
}
const updateSeller = await this.sellerModel.updateOne({_id:seller.data._id},{$set:{TokenFireBase:token}})
if (updateSeller.modifiedCount === 0) {
return {
"data":null,
"status": 500,
"message": "آپدیت با شکست مواجه شد"
}
}
return {
data: null,
status: 200,
message: "به‌روزرسانی انجام شد"
}
}

View File

@ -17,7 +17,7 @@ export class CreateShopDto {
//@Validate(IsCategoryExists)
Category: string;
@IsNotEmpty({ message: 'نام استان نباید خالی باشد' })
@IsNotEmpty({ message: 'نام استان نباید خالی باشد' })
@IsString({ message: 'نام استان باید شامل متن باشد' })
@Length(0, 20, { message: 'طول نام استان حداکثر 20 حرف می تواند باشد' })
Province: string;
@ -43,9 +43,14 @@ export class CreateShopDto {
@IsOptional()
@IsNumberString({},{ message: 'شماره تلفن باید فقط شامل اعداد باشد.' })
@Length(1, 12, { message: 'شماره تلفن باید حداکثر 12 رقم باشد.' })
ShopNumber: string;
@IsNumberString({},{ message: 'تلفن فروشگاه باید فقط شامل اعداد باشد.' })
@Length(1, 12, { message: 'تلفن فروشگاه باید حداکثر 12 رقم باشد.' })
Phone: string;
@IsOptional()
@IsNumberString({},{ message: 'پلاک باید فقط شامل اعداد باشد.' })
ShopNumber: number;
@IsNotEmpty({message: 'کد پستی نباید خالی باشد.'})
@IsNumberString({},{ message: 'کد پستی باید فقط شامل اعداد باشد.' })

View File

@ -8,6 +8,7 @@ import {
Res,
Req,
BadRequestException,
Get,
} from '@nestjs/common';
import { Response } from 'express';
import { UploadService } from '../upload/upload.service';
@ -31,11 +32,7 @@ export class ShopController {
},
fileFilter: (req, file, cb) => {
const allowedMimeTypes = ['image/png', 'image/jpeg', 'image/jpg'];
if (!allowedMimeTypes.includes(file.mimetype)) {
cb(new BadRequestException('فقط فرمت PNG یا JPG مجاز است'), false);
} else {
cb(null, true);
}
cb(null, true);
},
}),
)
@ -58,6 +55,7 @@ export class ShopController {
if (file) {
const result = await this.upload.uploadFileSingle(file);
console.log(result)
if (result.status !== 200) {
return res.status(400).json({
message: 'آپلود لوگو با خطا مواجه شد',
@ -70,4 +68,14 @@ export class ShopController {
const createdShop = await this.shopService.creatshop(shopData, fileId, req.user.userId);
return res.status(createdShop.status).json(createdShop);
}
@UseGuards(JwtAuthGuard)
@Get('/get')
async getDataShopSeller(
@Req() req: AuthenticatedRequest,
@Res() res: Response,
) {
const createdShop = await this.shopService.findShopWithSeller(req.user.userId);
return res.status(createdShop.status).json(createdShop);
}
}

View File

@ -23,6 +23,14 @@ export class ShopService {
"data":null
}
}
const existShop = await this.shopModel.findOne({Seller:user}).select('-_id')
if(existShop){
return {
"message":"فروشگاهی قبلا ثبت شده است",
"status":409,
"data":existShop
}
}
try {
const newShop = new this.shopModel({
"Name":creatshopDto.Name,
@ -32,7 +40,7 @@ export class ShopService {
"Address":creatshopDto.Address,
"Map":{
"type":'Point',
"coordinates":[creatshopDto.Coordinates.latitude,creatshopDto.Coordinates.latitude]
"coordinates":[creatshopDto.Coordinates.latitude,creatshopDto.Coordinates.longitude]
},
"Property":creatshopDto.Property,
"BusinessLicense":creatshopDto.BusinessLicense,
@ -41,6 +49,7 @@ export class ShopService {
"Province":creatshopDto.Province,
"PostalCode":creatshopDto.PostalCode,
"ShopNumber":creatshopDto.ShopNumber,
"Phone":creatshopDto.Phone,
"Images":null,
"Schedule":creatshopDto.Schedule,
"Discounts":[],
@ -101,7 +110,27 @@ export class ShopService {
async findShopSingle(id:string) : Promise<CreateSellerResponse>{
const shop = await this.shopModel.findOne({ID:id})
const shop = await this.shopModel.findOne({ID:id}).select('-_id')
if(shop){
return {
"message":"فروشگاه ثبت شده است",
"status":200,
"data":shop
}
}else {
return {
"message":"فروشگاهی با این شناسه موجود نیست",
"status":404,
"data":null
}
}
}
async findShopWithSeller(id:string) : Promise<CreateSellerResponse>{
const shop = await this.shopModel.findOne({Seller:id}).populate({path:'Seller',select:'-_id'}).populate({path:'Category',select:'-_id'})
if(shop){
return {
"message":"فروشگاه ثبت شده است",

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SmsController } from './sms.controller';
describe('SmsController', () => {
let controller: SmsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SmsController],
}).compile();
controller = module.get<SmsController>(SmsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('sms')
export class SmsController {}

10
src/sms/sms.module.ts Normal file
View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { SmsController } from './sms.controller';
import { SmsService } from './sms.service';
@Module({
providers: [SmsService],
exports: [SmsService],
})
export class SmsModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SmsService } from './sms.service';
describe('SmsService', () => {
let service: SmsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [SmsService],
}).compile();
service = module.get<SmsService>(SmsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

36
src/sms/sms.service.ts Normal file
View File

@ -0,0 +1,36 @@
import { Injectable, Logger } from '@nestjs/common';
import axios from 'axios';
@Injectable()
export class SmsService {
private readonly logger = new Logger(SmsService.name);
private readonly API_KEY = '346446725341592F2F4554514846714A366A6349327A376671316779412F63474F48684D6348372B326A553D';
private readonly TEMPLATE = 'verify';
/**
* ارسال پیامک تاییدیه با قالب کاوهنگار
* @param receptor شماره گیرنده (مثلاً: "09123456789")
* @param token1 مقدار توکن (مثلاً: 123456)
*/
async sendMessage(receptor: string, token1: number): Promise<void> {
const token2 = receptor;
const url = `https://api.kavenegar.com/v1/${this.API_KEY}/verify/lookup.json`;
try {
const response = await axios.post(url, null, {
params: {
receptor,
template: this.TEMPLATE,
token: token1,
token2,
},
});
console.log(`پیام با موفقیت برای ${receptor} ارسال شد.`);
console.log(response.data);
} catch (error: any) {
console.error(`خطا در ارسال پیام برای ${receptor}:`, error?.response?.data || error.message);
}
}
}

View File

@ -61,8 +61,10 @@ const keyWithExt = `${key}${extension}`;
ContentType: file.mimetype,
});
await this.s3.send(command);
const dataupload = await this.s3.send(command);
const mimeType = file.mimetype;
console.log("dataupload")
console.log(dataupload)
let fileType: 'image' | 'video' | 'document' | 'other' = 'other';
@ -72,7 +74,7 @@ const keyWithExt = `${key}${extension}`;
fileType = 'video';
} else if (
mimeType === 'application/pdf' ||
mimeType === 'application/msword' ||
mimeType === 'application/msword' || 'application/octet-stream' ||
mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
) {
fileType = 'document';
@ -80,6 +82,8 @@ const keyWithExt = `${key}${extension}`;
try {
console.log("file")
console.log(file)
const newFile = new this.fileModel({
"FileType":fileType,
"Title":"avatar",
@ -88,6 +92,8 @@ try {
"Url":urlFile,
"ID":key
})
console.log("newFile")
console.log(newFile)
const savedSeller = await newFile.save();
return {
"message":"فایل با موفقیت آپلود شد .",

View File

@ -0,0 +1,13 @@
import { IsNotEmpty, IsNumberString, Length, Matches } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty({ message: 'کد کشور نمی‌تواند خالی باشد.' })
@IsNumberString({},{ message: 'کد کشور باید فقط شامل اعداد باشد.' })
@Length(1, 4, { message: 'کد کشور باید بین 1 تا 4 رقم باشد.' })
Code: number;
@IsNotEmpty({ message: 'شماره تلفن نمی‌تواند خالی باشد.' })
@IsNumberString({},{ message: 'شماره تلفن باید فقط شامل اعداد باشد.' })
@Length(10, 10, { message: 'شماره تلفن باید دقیقاً 10 رقم باشد.' })
Phone: number;
}

View File

@ -0,0 +1,7 @@
import { IsNotEmpty, IsString,IsNumberString,Length} from "class-validator";
export class CreatTokenFireBase {
@IsNotEmpty({message:"توکن نباید خالی باشد. "})
@IsString({message:"توکن باید متن باشد . "})
Token:string;
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SellerController } from './user.controller';
describe('SellerController', () => {
let controller: SellerController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SellerController],
}).compile();
controller = module.get<SellerController>(SellerController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,35 @@
import { Controller,
Post,
UploadedFile,
UseInterceptors,
Body,
UseGuards,
Res,
Req,
BadRequestException,
Get } from '@nestjs/common';
import { UserService } from './user.service';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { AuthenticatedRequest } from 'src/interfaces/request';
import { CreatTokenFireBase } from './dto/token-firebase.dto';
import { Response } from 'express';
@Controller('seller')
export class UserController {
constructor (private readonly userService:UserService){}
@UseGuards(JwtAuthGuard)
@Post('/firebaseUpdate')
async updateFireBaseToken (
@Body() body:CreatTokenFireBase,
@Req() req: AuthenticatedRequest,
@Res() res: Response,
){
const updateToken = await this.userService.updateTokenFireBase(body.Token,req.user.userId)
return res.status(updateToken.status).json(updateToken.data)
}
}

16
src/user/user.module.ts Normal file
View File

@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { MongooseModule } from '@nestjs/mongoose';
import { UserSchema } from 'src/schemas/user.schema';
@Module({
imports:[
MongooseModule.forFeature([
{ name:'User', schema: UserSchema },
])],
controllers: [UserController],
providers: [UserService],
exports:[UserService]
})
export class UserModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SellerService } from './user.service';
describe('SellerService', () => {
let service: SellerService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [SellerService],
}).compile();
service = module.get<SellerService>(SellerService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

140
src/user/user.service.ts Normal file
View File

@ -0,0 +1,140 @@
import { Injectable, NotFoundException, InternalServerErrorException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from '../schemas/user.schema';
import { CreateUserDto } from './dto/creat-seller.dto';
import { v4 as uuidv4 } from 'uuid';
interface CreateSellerResponse {
message: string;
status: number;
data: any;
}
@Injectable()
export class UserService {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
) {}
async createWithPhone(createUserDto: CreateUserDto): Promise<CreateSellerResponse> {
const newUuid: string = uuidv4();
try {
const createdUser = new this.userModel({
"Email": null,
"ID": newUuid,
"Phone": createUserDto.Phone,
"verify": false,
"LastLogin":new Date(),
"IpAddress": null,
"CountryCode": 98,
"TokenFireBase":""
});
const savedUser = await createdUser.save();
return {
"message": "کاربر با موفقیت ساخته شد",
"status": 200,
"data": {
"_id":createdUser._id,
"ID":createdUser.ID
}
};
} catch (e) {
console.error("Error saving seller:", e);
throw new InternalServerErrorException("ذخیره دیتا با شکست مواجه شد");
}
}
async findUserwidtID(id: string): Promise<CreateSellerResponse> {
try {
const user = await this.userModel.findOne({ ID: id }).exec();
if (!user) {
return {
"message" : "کاربری ای با این شناسه ایدی موجود نیست",
"status":404,
"data":null
}
}
return {
"message":"اطلاعات کاربر",
"status":200,
"data":{
"ID":user.ID,
"Phone":user.Phone
}
}
} catch (e) {
console.error("Error finding user:", e);
return {
"message":"خطا در دریافت اطلاعات کاربر",
"status":502,
"data":null
}
}
}
async findUserwidtPhone(code:number,phone:number): Promise<CreateSellerResponse> {
try {
const user = await this.userModel.findOne({CountryCode: code,Phone:phone }).exec();
if (!user) {
return {
"message" : "کاربری ای با این شناسه ایدی موجود نیست",
"status":404,
"data":null
}
}
return {
"message":"اطلاعات کاربر",
"status":200,
"data":{
"ID":user.ID,
"Phone":user.Phone,
"_id":user._id
}
}
} catch (e) {
console.error("Error finding seller:", e);
return {
"message":"خطا در دریافت اطلاعات کاربر",
"status":502,
"data":null
}
}
}
async updateTokenFireBase (token:string,user:string) : Promise <CreateSellerResponse> {
const userfind = await this.findUserwidtID(user)
if(!userfind){
return {
"data":null,
"status": 404,
"message": "کاربر موجود نیست"
}
}
const updateUser = await this.userModel.updateOne({_id:userfind.data._id},{$set:{TokenFireBase:token}})
if (updateUser.modifiedCount === 0) {
return {
"data":null,
"status": 500,
"message": "آپدیت با شکست مواجه شد"
}
}
return {
data: null,
status: 200,
message: "به‌روزرسانی انجام شد"
}
}
}