init
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
name: deploy to danak
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- production
|
||||
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
DANAK_SERVER: "https://captain.dev.danakcorp.com"
|
||||
APP_TOKEN: e7d4e8ae6b8164d3c1669c161f7cab295549b6fdfaea90eaf833f24122380841
|
||||
APP_NAME: dmenuplus-api-production
|
||||
GITHUB_TOKEN: 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
|
||||
|
||||
steps:
|
||||
- name: Check out repositorys
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: zmihamid
|
||||
password: ${{ env.GITHUB_TOKEN }}
|
||||
|
||||
- name: Preset Image Name
|
||||
run: echo "IMAGE_URL=$(echo ghcr.io/zmihamid/${{ github.event.repository.name }}:$(echo ${{ github.sha }} | cut -c1-7) | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.IMAGE_URL }}
|
||||
|
||||
- name: Install CapRover CLI
|
||||
run: npm install -g caprover
|
||||
|
||||
- name: deploy to server
|
||||
run: |
|
||||
caprover deploy -a $APP_NAME -u $DANAK_SERVER --appToken $APP_TOKEN -i "$IMAGE_URL"
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
/build
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# temp directory
|
||||
.temp
|
||||
.tmp
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
/react/*
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"insertPragma": false,
|
||||
"bracketSameLine": false,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 120,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false
|
||||
}
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"eslint.format.enable": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
# Stage 1: Base
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
# Install timezone support
|
||||
RUN apk add --no-cache tzdata
|
||||
RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
|
||||
|
||||
# Set workdir for clarity
|
||||
WORKDIR /app
|
||||
|
||||
# Stage 2: Dependencies
|
||||
FROM base AS deps
|
||||
WORKDIR /temp-deps
|
||||
COPY package*.json ./
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
# Stage 3: Build
|
||||
FROM base AS builder
|
||||
WORKDIR /build
|
||||
COPY . ./
|
||||
COPY --from=deps /temp-deps/node_modules ./node_modules
|
||||
RUN if [ -f package.json ] && grep -q '"build":' package.json; then npm run build; fi
|
||||
|
||||
# Stage 4: Runner
|
||||
FROM base AS runner
|
||||
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||
WORKDIR /app
|
||||
|
||||
COPY . ./
|
||||
COPY --from=builder --chown=appuser:appgroup /build/ ./
|
||||
RUN chown -R appuser:appgroup /app
|
||||
|
||||
USER appuser
|
||||
ENV NODE_ENV=production
|
||||
EXPOSE 4000
|
||||
|
||||
CMD ["npm", "start"]
|
||||
|
||||
# # Stage 1: Build Stage
|
||||
# FROM node:22-alpine AS base
|
||||
|
||||
# RUN npm install -g corepack@latest
|
||||
# RUN corepack enable && corepack prepare pnpm@9 --activate
|
||||
|
||||
# # Install tzdata to support timezone settings
|
||||
# RUN apk add --no-cache tzdata
|
||||
|
||||
# # Set the timezone to Asia/Tehran
|
||||
# RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
|
||||
|
||||
|
||||
# FROM base AS deps
|
||||
# WORKDIR /temp-deps
|
||||
# COPY package*.json pnpm-lock.yaml ./
|
||||
# RUN pnpm install --frozen-lockfile --loglevel info
|
||||
|
||||
|
||||
# FROM base AS builder
|
||||
# WORKDIR /build
|
||||
# COPY . ./
|
||||
# COPY --from=deps /temp-deps/node_modules ./node_modules
|
||||
# RUN if [ -f package.json ] && grep -q '"build":' package.json; then pnpm run build; fi
|
||||
# RUN pnpm install --prod --frozen-lockfile --loglevel info
|
||||
|
||||
# FROM base AS runner
|
||||
# RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||
# WORKDIR /app
|
||||
|
||||
# COPY . ./
|
||||
# COPY --from=builder --chown=appuser:appgroup /build/ ./
|
||||
|
||||
# RUN chown -R appuser:appgroup /app
|
||||
|
||||
# USER appuser
|
||||
|
||||
# ENV NODE_ENV=production
|
||||
# EXPOSE 3000
|
||||
|
||||
# CMD ["npm", "start"]
|
||||
@@ -1 +1,98 @@
|
||||
# negareh-api
|
||||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Project setup
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
## Compile and run the project
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ npm run start
|
||||
|
||||
# watch mode
|
||||
$ npm run start:dev
|
||||
|
||||
# production mode
|
||||
$ npm run start:prod
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ npm run test
|
||||
|
||||
# e2e tests
|
||||
$ npm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ npm run test:cov
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||
|
||||
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||
|
||||
```bash
|
||||
$ npm install -g @nestjs/mau
|
||||
$ mau deploy
|
||||
```
|
||||
|
||||
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||
|
||||
## Resources
|
||||
|
||||
Check out a few resources that may come in handy when working with NestJS:
|
||||
|
||||
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20260105085439 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table "point_transactions" drop constraint if exists "point_transactions_type_check";`);
|
||||
|
||||
this.addSql(`alter table "categories" add column "order" int null;`);
|
||||
|
||||
this.addSql(`alter table "point_transactions" add constraint "point_transactions_type_check" check("type" in (''));`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`create table "user_wallets" ("id" char(26) not null, "created_at" timestamptz(6) not null default now(), "updated_at" timestamptz(6) not null default now(), "deleted_at" timestamptz(6) null, "restaurant_id" char(26) not null, "user_id" char(26) not null, "wallet" int4 not null default 0, "points" int4 not null default 0, constraint "user_wallets_pkey" primary key ("id"));`);
|
||||
this.addSql(`alter table "user_wallets" add constraint "unique_user_restaurant" unique ("user_id", "restaurant_id");`);
|
||||
this.addSql(`create index "user_wallets_created_at_index" on "user_wallets" ("created_at");`);
|
||||
this.addSql(`create index "user_wallets_deleted_at_index" on "user_wallets" ("deleted_at");`);
|
||||
this.addSql(`create index "user_wallets_restaurant_id_index" on "user_wallets" ("restaurant_id");`);
|
||||
this.addSql(`create index "user_wallets_user_id_index" on "user_wallets" ("user_id");`);
|
||||
this.addSql(`create index "user_wallets_user_id_restaurant_id_index" on "user_wallets" ("user_id", "restaurant_id");`);
|
||||
|
||||
this.addSql(`alter table "point_transactions" drop constraint if exists "point_transactions_type_check";`);
|
||||
|
||||
this.addSql(`alter table "point_transactions" add constraint "point_transactions_type_check" check("type" in (''));`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20260106054151 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_id" type varchar(255) using ("subscription_id"::varchar(255));`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_id" drop not null;`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_end_date" type timestamptz using ("subscription_end_date"::timestamptz);`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_end_date" drop not null;`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_start_date" type timestamptz using ("subscription_start_date"::timestamptz);`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_start_date" drop not null;`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_id" type varchar(255) using ("subscription_id"::varchar(255));`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_id" set not null;`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_end_date" type timestamptz using ("subscription_end_date"::timestamptz);`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_end_date" set not null;`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_start_date" type timestamptz using ("subscription_start_date"::timestamptz);`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_start_date" set not null;`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20260106192946 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table "contacts" add column "restaurant_id" char(26) null;`);
|
||||
this.addSql(`alter table "contacts" add constraint "contacts_restaurant_id_foreign" foreign key ("restaurant_id") references "restaurants" ("id") on update cascade on delete set null;`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`alter table "contacts" drop constraint "contacts_restaurant_id_foreign";`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# Icon Naming Process
|
||||
|
||||
## Files Created:
|
||||
- `icons.csv` - Updated with "icon_name" column
|
||||
- `icon_viewer.html` - Web interface to view and name icons
|
||||
- `populate_icon_names.js` - Script to add names to CSV
|
||||
|
||||
## Step-by-Step Process:
|
||||
|
||||
### 1. View the Icons
|
||||
- Open `icon_viewer.html` in your web browser
|
||||
- You'll see all 138 icons in a grid layout
|
||||
- Each icon shows its number and has an input field for naming
|
||||
|
||||
### 2. Name the Icons
|
||||
- Look at each icon and enter appropriate names in the input fields
|
||||
- The progress counter shows how many you've named
|
||||
- Names should be descriptive (e.g., "hamburger", "pizza", "coffee", "delivery")
|
||||
|
||||
### 3. Export Named Icons (Optional)
|
||||
- Click "Export Named Icons" to download a CSV of only the named icons
|
||||
- This helps you review your naming before finalizing
|
||||
|
||||
### 4. Update the Main CSV
|
||||
- Edit `populate_icon_names.js`
|
||||
- Replace the placeholder names in the `iconNames` array with your actual names
|
||||
- Run the script: `node populate_icon_names.js`
|
||||
|
||||
## Example Icon Names:
|
||||
- Food items: "pizza", "burger", "pasta", "salad"
|
||||
- Drinks: "coffee", "soda", "juice", "beer"
|
||||
- Categories: "fast_food", "italian", "asian", "desserts"
|
||||
- Actions: "delivery", "pickup", "favorite", "cart"
|
||||
|
||||
## Tips:
|
||||
- Use consistent naming conventions (snake_case or camelCase)
|
||||
- Keep names short but descriptive
|
||||
- Group similar icons with consistent prefixes if applicable
|
||||
- Review all names before running the populate script
|
||||
|
||||
The final CSV will have all original columns plus the new "icon_name" column with your custom names.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to dump icon and icon_groups tables from PostgreSQL database
|
||||
# Usage: ./dump-icons.sh [output_file]
|
||||
|
||||
set -e
|
||||
|
||||
# Load environment variables if .env file exists
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
# Source .env file, handling comments and empty lines
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
# Remove leading/trailing whitespace
|
||||
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
# Skip empty lines and lines starting with #
|
||||
[[ -z "$line" ]] && continue
|
||||
[[ "$line" =~ ^# ]] && continue
|
||||
# Remove inline comments (everything after # that's not in quotes)
|
||||
line=$(echo "$line" | sed 's/#.*$//' | sed 's/[[:space:]]*$//')
|
||||
# Skip if line is empty after removing comments
|
||||
[[ -z "$line" ]] && continue
|
||||
# Export the variable (only if it looks like KEY=VALUE)
|
||||
if [[ "$line" =~ ^[[:alpha:]_][[:alnum:]_]*= ]]; then
|
||||
export "$line" 2>/dev/null || true
|
||||
fi
|
||||
done < .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
# Get database connection details from environment variables
|
||||
DB_HOST="${DB_HOST:-localhost}"
|
||||
DB_PORT="${DB_PORT:-5432}"
|
||||
DB_NAME="${DB_NAME:-dmenu}"
|
||||
DB_USER="${DB_USER:-postgres}"
|
||||
|
||||
# Output file name (default: icons_dump_YYYYMMDD_HHMMSS.sql)
|
||||
if [ -z "$1" ]; then
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
OUTPUT_FILE="icons_dump_${TIMESTAMP}.sql"
|
||||
else
|
||||
OUTPUT_FILE="$1"
|
||||
fi
|
||||
|
||||
echo "Dumping icon_groups and icons tables..."
|
||||
echo "Database: ${DB_NAME}@${DB_HOST}:${DB_PORT}"
|
||||
echo "Output file: ${OUTPUT_FILE}"
|
||||
|
||||
# Dump icon_groups table first (parent table)
|
||||
echo "Dumping icon_groups table..."
|
||||
PGPASSWORD="${DB_PASS}" pg_dump \
|
||||
-h "${DB_HOST}" \
|
||||
-p "${DB_PORT}" \
|
||||
-U "${DB_USER}" \
|
||||
-d "${DB_NAME}" \
|
||||
-t icon_groups \
|
||||
--data-only \
|
||||
--column-inserts \
|
||||
--no-owner \
|
||||
--no-privileges \
|
||||
> "${OUTPUT_FILE}"
|
||||
|
||||
# Append icons table to the dump file
|
||||
echo "Dumping icons table..."
|
||||
PGPASSWORD="${DB_PASS}" pg_dump \
|
||||
-h "${DB_HOST}" \
|
||||
-p "${DB_PORT}" \
|
||||
-U "${DB_USER}" \
|
||||
-d "${DB_NAME}" \
|
||||
-t icons \
|
||||
--data-only \
|
||||
--column-inserts \
|
||||
--no-owner \
|
||||
--no-privileges \
|
||||
>> "${OUTPUT_FILE}"
|
||||
|
||||
echo "Dump completed successfully!"
|
||||
echo "File saved to: ${OUTPUT_FILE}"
|
||||
echo ""
|
||||
echo "To restore, run: ./restore-icons.sh ${OUTPUT_FILE}"
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Icon Viewer - Name the Icons</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.header {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
.icon-item {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.icon-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.icon-img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.icon-id {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.icon-name {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.input-field {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.save-btn {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.save-btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.progress {
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Icon Name Editor</h1>
|
||||
<p>Review each icon below and enter appropriate names. Names will be saved back to the CSV file.</p>
|
||||
<div class="progress">
|
||||
<span id="completed-count">0</span> of <span id="total-count">138</span> icons named
|
||||
</div>
|
||||
<button class="save-btn" onclick="saveToCSV()">Save Names to CSV</button>
|
||||
<button class="save-btn" onclick="exportNamedIcons()" style="margin-left: 10px; background-color: #28a745;">Export Named Icons</button>
|
||||
</div>
|
||||
|
||||
<div class="icon-grid" id="iconGrid">
|
||||
<!-- Icons will be loaded here -->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const icons = [
|
||||
"https://storage.danakcorp.com/images/1766554365832-411750172.svg",
|
||||
"https://storage.danakcorp.com/images/1766554379149-104970938.svg",
|
||||
"https://storage.danakcorp.com/images/1766554389090-593863597.svg",
|
||||
"https://storage.danakcorp.com/images/1766554404968-905984091.svg",
|
||||
"https://storage.danakcorp.com/images/1766554415325-873800591.svg",
|
||||
"https://storage.danakcorp.com/images/1766554428814-77027818.svg",
|
||||
"https://storage.danakcorp.com/images/1766554438632-45072052.svg",
|
||||
"https://storage.danakcorp.com/images/1766554446340-463157109.svg",
|
||||
"https://storage.danakcorp.com/images/1766554456605-280166176.svg",
|
||||
"https://storage.danakcorp.com/images/1766554471887-315640495.svg",
|
||||
"https://storage.danakcorp.com/images/1766554485634-242317100.svg",
|
||||
"https://storage.danakcorp.com/images/1766554504553-157481346.svg",
|
||||
"https://storage.danakcorp.com/images/1766554528198-919877647.svg",
|
||||
"https://storage.danakcorp.com/images/1766554544140-886744157.svg",
|
||||
"https://storage.danakcorp.com/images/1766554565614-684869010.svg",
|
||||
"https://storage.danakcorp.com/images/1766554578855-318583145.svg",
|
||||
"https://storage.danakcorp.com/images/1766554593272-362758712.svg",
|
||||
"https://storage.danakcorp.com/images/1766554606166-877198688.svg",
|
||||
"https://storage.danakcorp.com/images/1766554624133-21439996.svg",
|
||||
"https://storage.danakcorp.com/images/1766554641691-535696200.svg",
|
||||
"https://storage.danakcorp.com/images/1766554658481-114730426.svg",
|
||||
"https://storage.danakcorp.com/images/1766554671760-634916570.svg",
|
||||
"https://storage.danakcorp.com/images/1766554967381-660612838.svg",
|
||||
"https://storage.danakcorp.com/images/1766555106925-267924006.svg",
|
||||
"https://storage.danakcorp.com/images/1766555231674-387319393.svg",
|
||||
"https://storage.danakcorp.com/images/1766555290519-155637622.svg",
|
||||
"https://storage.danakcorp.com/images/1766555308301-230359494.svg",
|
||||
"https://storage.danakcorp.com/images/1766555420851-186440015.svg",
|
||||
"https://storage.danakcorp.com/images/1766555590828-644527730.svg",
|
||||
"https://storage.danakcorp.com/images/1766555606997-571682462.svg",
|
||||
"https://storage.danakcorp.com/images/1766555663659-433052581.svg",
|
||||
"https://storage.danakcorp.com/images/1766555713098-988242030.svg",
|
||||
"https://storage.danakcorp.com/images/1766555726158-680948882.svg",
|
||||
"https://storage.danakcorp.com/images/1766555746650-426516324.svg",
|
||||
"https://storage.danakcorp.com/images/1766555765798-127083646.svg",
|
||||
"https://storage.danakcorp.com/images/1766555784837-243715698.svg",
|
||||
"https://storage.danakcorp.com/images/1766555799191-594035927.svg",
|
||||
"https://storage.danakcorp.com/images/1766555894624-524350777.svg",
|
||||
"https://storage.danakcorp.com/images/1766555932114-485954688.svg",
|
||||
"https://storage.danakcorp.com/images/1766555949650-909996041.svg",
|
||||
"https://storage.danakcorp.com/images/1766555966861-75517122.svg",
|
||||
"https://storage.danakcorp.com/images/1766556006875-239689681.svg",
|
||||
"https://storage.danakcorp.com/images/1766556257163-459958715.svg",
|
||||
"https://storage.danakcorp.com/images/1766556293812-665835248.svg",
|
||||
"https://storage.danakcorp.com/images/1766556307082-162280793.svg",
|
||||
"https://storage.danakcorp.com/images/1766556340402-744632392.svg",
|
||||
"https://storage.danakcorp.com/images/1766556458335-857124841.svg",
|
||||
"https://storage.danakcorp.com/images/1766556479530-161860621.svg",
|
||||
"https://storage.danakcorp.com/images/1766556498215-91175544.svg",
|
||||
"https://storage.danakcorp.com/images/1766556677355-108854669.svg",
|
||||
"https://storage.danakcorp.com/images/1766556736283-691587656.svg",
|
||||
"https://storage.danakcorp.com/images/1766556759367-58066921.svg",
|
||||
"https://storage.danakcorp.com/images/1766556777353-490756125.svg",
|
||||
"https://storage.danakcorp.com/images/1766556870740-118553581.svg",
|
||||
"https://storage.danakcorp.com/images/1766556890334-962678132.svg",
|
||||
"https://storage.danakcorp.com/images/1766556921029-865260745.svg",
|
||||
"https://storage.danakcorp.com/images/1766556946363-320833501.svg",
|
||||
"https://storage.danakcorp.com/images/1766556973771-832565827.svg",
|
||||
"https://storage.danakcorp.com/images/1766557056358-727862983.svg",
|
||||
"https://storage.danakcorp.com/images/1766557199259-72955190.svg",
|
||||
"https://storage.danakcorp.com/images/1766557252452-988016525.svg",
|
||||
"https://storage.danakcorp.com/images/1766557274778-439028456.svg",
|
||||
"https://storage.danakcorp.com/images/1766557289094-415104854.svg",
|
||||
"https://storage.danakcorp.com/images/1766557300923-302277606.svg",
|
||||
"https://storage.danakcorp.com/images/1766557317091-602116315.svg",
|
||||
"https://storage.danakcorp.com/images/1766557647999-47333939.svg",
|
||||
"https://storage.danakcorp.com/images/1766557669282-172512141.svg",
|
||||
"https://storage.danakcorp.com/images/1766557705283-692546793.svg",
|
||||
"https://storage.danakcorp.com/images/1766557729175-762576743.svg",
|
||||
"https://storage.danakcorp.com/images/1766557756487-665300947.svg",
|
||||
"https://storage.danakcorp.com/images/1766557776067-804149228.svg",
|
||||
"https://storage.danakcorp.com/images/1766557797509-101726352.svg",
|
||||
"https://storage.danakcorp.com/images/1766557816484-357699340.svg",
|
||||
"https://storage.danakcorp.com/images/1766557837292-326521286.svg",
|
||||
"https://storage.danakcorp.com/images/1766559083575-564721059.svg",
|
||||
"https://storage.danakcorp.com/images/1766559099561-720966516.svg",
|
||||
"https://storage.danakcorp.com/images/1766559116496-834964990.svg",
|
||||
"https://storage.danakcorp.com/images/1766563021426-600579018.svg",
|
||||
"https://storage.danakcorp.com/images/1766563038525-727531190.svg",
|
||||
"https://storage.danakcorp.com/images/1766563051485-824602001.svg",
|
||||
"https://storage.danakcorp.com/images/1766563066017-372210647.svg",
|
||||
"https://storage.danakcorp.com/images/1766563124296-331607800.svg",
|
||||
"https://storage.danakcorp.com/images/1766563142133-359920423.svg",
|
||||
"https://storage.danakcorp.com/images/1766563164270-350101986.svg",
|
||||
"https://storage.danakcorp.com/images/1766563181587-86630830.svg",
|
||||
"https://storage.danakcorp.com/images/1766563204966-651499380.svg",
|
||||
"https://storage.danakcorp.com/images/1766563220596-103699525.svg",
|
||||
"https://storage.danakcorp.com/images/1766563265634-373953449.svg",
|
||||
"https://storage.danakcorp.com/images/1766563275517-958045207.svg",
|
||||
"https://storage.danakcorp.com/images/1766563286526-622849562.svg",
|
||||
"https://storage.danakcorp.com/images/1766564695339-504553420.svg",
|
||||
"https://storage.danakcorp.com/images/1766564706032-159367016.svg",
|
||||
"https://storage.danakcorp.com/images/1766564720683-887818252.svg",
|
||||
"https://storage.danakcorp.com/images/1766564734432-483528219.svg",
|
||||
"https://storage.danakcorp.com/images/1766564750099-854146988.svg",
|
||||
"https://storage.danakcorp.com/images/1766564758573-696060786.svg",
|
||||
"https://storage.danakcorp.com/images/1766564805823-829938380.svg",
|
||||
"https://storage.danakcorp.com/images/1766564891137-468827500.svg",
|
||||
"https://storage.danakcorp.com/images/1766564907215-921380792.svg",
|
||||
"https://storage.danakcorp.com/images/1766564926872-672612697.svg",
|
||||
"https://storage.danakcorp.com/images/1766565459604-638026702.svg",
|
||||
"https://storage.danakcorp.com/images/1766565476771-42527917.svg",
|
||||
"https://storage.danakcorp.com/images/1766565491264-946731491.svg",
|
||||
"https://storage.danakcorp.com/images/1766565502475-288189136.svg",
|
||||
"https://storage.danakcorp.com/images/1766565530628-800474580.svg",
|
||||
"https://storage.danakcorp.com/images/1766565547983-878658938.svg",
|
||||
"https://storage.danakcorp.com/images/1766565563263-891688321.svg",
|
||||
"https://storage.danakcorp.com/images/1766565600547-704332792.svg",
|
||||
"https://storage.danakcorp.com/images/1766565625437-528383713.svg",
|
||||
"https://storage.danakcorp.com/images/1766565641620-53176909.svg",
|
||||
"https://storage.danakcorp.com/images/1766565664427-433669317.svg",
|
||||
"https://storage.danakcorp.com/images/1766565971515-159903323.svg",
|
||||
"https://storage.danakcorp.com/images/1766565995424-288101561.svg",
|
||||
"https://storage.danakcorp.com/images/1766566026942-868807643.svg",
|
||||
"https://storage.danakcorp.com/images/1766566041525-973019175.svg",
|
||||
"https://storage.danakcorp.com/images/1766566064741-271844433.svg",
|
||||
"https://storage.danakcorp.com/images/1766566084368-438871956.svg",
|
||||
"https://storage.danakcorp.com/images/1766566106954-47179607.svg",
|
||||
"https://storage.danakcorp.com/images/1766566122836-486871124.svg",
|
||||
"https://storage.danakcorp.com/images/1766566143171-668309076.svg",
|
||||
"https://storage.danakcorp.com/images/1766566161551-844376214.svg",
|
||||
"https://storage.danakcorp.com/images/1766566176755-30806596.svg",
|
||||
"https://storage.danakcorp.com/images/1766566193720-339416210.svg",
|
||||
"https://storage.danakcorp.com/images/1766566293736-981836756.svg",
|
||||
"https://storage.danakcorp.com/images/1766566304318-193301989.svg",
|
||||
"https://storage.danakcorp.com/images/1766566327768-2675508.svg",
|
||||
"https://storage.danakcorp.com/images/1766566341717-462491426.svg",
|
||||
"https://storage.danakcorp.com/images/1766566357028-432111036.svg",
|
||||
"https://storage.danakcorp.com/images/1766566371354-279596850.svg",
|
||||
"https://storage.danakcorp.com/images/1766566385674-569493499.svg",
|
||||
"https://storage.danakcorp.com/images/1766566413058-975642584.svg",
|
||||
"https://storage.danakcorp.com/images/1766566427440-982841441.svg",
|
||||
"https://storage.danakcorp.com/images/1766566442808-761588425.svg",
|
||||
"https://storage.danakcorp.com/images/1766566468108-519151895.svg",
|
||||
"https://storage.danakcorp.com/images/1766566482278-661272876.svg",
|
||||
"https://storage.danakcorp.com/images/1766566494050-408184442.svg",
|
||||
"https://storage.danakcorp.com/images/1766566504760-709260995.svg"
|
||||
];
|
||||
|
||||
const iconNames = new Array(icons.length).fill('');
|
||||
|
||||
function loadIcons() {
|
||||
const grid = document.getElementById('iconGrid');
|
||||
icons.forEach((url, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'icon-item';
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="icon-id">Icon ${index + 1}</div>
|
||||
<img src="${url}" alt="Icon ${index + 1}" class="icon-img" onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiBmaWxsPSIjZGRkIi8+Cjx0ZXh0IHg9IjI0IiB5PSIyNCIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBmaWxsPSIjOTk5IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iMC4zNWVtIj5ObyBJY29uPC90ZXh0Pgo8L3N2Zz4='">
|
||||
<input type="text" class="input-field" placeholder="Enter icon name..." oninput="updateName(${index}, this.value)">
|
||||
`;
|
||||
|
||||
grid.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function updateName(index, name) {
|
||||
iconNames[index] = name.trim();
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
const completed = iconNames.filter(name => name.length > 0).length;
|
||||
document.getElementById('completed-count').textContent = completed;
|
||||
}
|
||||
|
||||
function saveToCSV() {
|
||||
// This will create a downloadable CSV with the names
|
||||
let csvContent = '"id","created_at","updated_at","deleted_at","url","group_id","icon_name"\n';
|
||||
|
||||
// You'll need to load the original CSV data here and add the names
|
||||
// For now, this is a placeholder - you'd need to combine with original data
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'icons_with_names.csv';
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
alert('CSV template downloaded. You\'ll need to manually add the icon names to the original CSV file.');
|
||||
}
|
||||
|
||||
function exportNamedIcons() {
|
||||
const namedIcons = [];
|
||||
iconNames.forEach((name, index) => {
|
||||
if (name.trim()) {
|
||||
namedIcons.push({
|
||||
index: index + 1,
|
||||
name: name.trim(),
|
||||
url: icons[index]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const csvContent = 'Index,Name,URL\n' +
|
||||
namedIcons.map(icon => `${icon.index},"${icon.name}","${icon.url}"`).join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'named_icons.csv';
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Load icons when page loads
|
||||
window.onload = loadIcons;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
"id","created_at","updated_at","deleted_at","url","group_id","icon_name"
|
||||
"01KD7DGWH6TDB47V4710FDT7SR","2025-12-24 05:32:46.503+00","2025-12-24 05:32:46.503+00",NULL,"https://storage.danakcorp.com/images/1766554365832-411750172.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DH9F6BN4TEM96TA2QT4GJ","2025-12-24 05:32:59.751+00","2025-12-24 05:32:59.751+00",NULL,"https://storage.danakcorp.com/images/1766554379149-104970938.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DHK6ZTEGPRBNXJPV49TE6","2025-12-24 05:33:09.727+00","2025-12-24 05:33:09.727+00",NULL,"https://storage.danakcorp.com/images/1766554389090-593863597.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DJ2NVEHVQMDCAM31VKKGK","2025-12-24 05:33:25.564+00","2025-12-24 05:33:25.564+00",NULL,"https://storage.danakcorp.com/images/1766554404968-905984091.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DJCQY1V84DAVXPV097HE6","2025-12-24 05:33:35.871+00","2025-12-24 05:33:35.871+00",NULL,"https://storage.danakcorp.com/images/1766554415325-873800591.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DJSZ2XVX971FGNFRRDQV6","2025-12-24 05:33:49.411+00","2025-12-24 05:33:49.411+00",NULL,"https://storage.danakcorp.com/images/1766554428814-77027818.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DK3HJAD9SMDD8CM1RPNQS","2025-12-24 05:33:59.218+00","2025-12-24 05:33:59.218+00",NULL,"https://storage.danakcorp.com/images/1766554438632-45072052.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DKB23B6XWJ8CMAKQ5R8HS","2025-12-24 05:34:06.915+00","2025-12-24 05:34:06.915+00",NULL,"https://storage.danakcorp.com/images/1766554446340-463157109.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DKN32VCDQDF2D407MGKF0","2025-12-24 05:34:17.186+00","2025-12-24 05:34:17.186+00",NULL,"https://storage.danakcorp.com/images/1766554456605-280166176.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DM4AEQ2FCJGZM390N7YVJ","2025-12-24 05:34:32.782+00","2025-12-24 05:34:32.782+00",NULL,"https://storage.danakcorp.com/images/1766554471887-315640495.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DMHKWJSHX6BR41FTDYVHE","2025-12-24 05:34:46.396+00","2025-12-24 05:34:46.396+00",NULL,"https://storage.danakcorp.com/images/1766554485634-242317100.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DN3XNDW2JQ0RGM711R9Q5","2025-12-24 05:35:05.141+00","2025-12-24 05:35:05.141+00",NULL,"https://storage.danakcorp.com/images/1766554504553-157481346.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DNV0F9CPMKQ4R482MTVKJ","2025-12-24 05:35:28.783+00","2025-12-24 05:35:28.783+00",NULL,"https://storage.danakcorp.com/images/1766554528198-919877647.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DPAKCPXD2GK0FCZ7N3XXP","2025-12-24 05:35:44.749+00","2025-12-24 05:35:44.749+00",NULL,"https://storage.danakcorp.com/images/1766554544140-886744157.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DPZGZFZPS5AJ8RNM3W16F","2025-12-24 05:36:06.175+00","2025-12-24 05:36:06.175+00",NULL,"https://storage.danakcorp.com/images/1766554565614-684869010.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DQCMAD39T5QB5728M6PFP","2025-12-24 05:36:19.594+00","2025-12-24 05:36:19.594+00",NULL,"https://storage.danakcorp.com/images/1766554578855-318583145.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DQTQHGWP5J77V327Y4GHZ","2025-12-24 05:36:34.034+00","2025-12-24 05:36:34.034+00",NULL,"https://storage.danakcorp.com/images/1766554593272-362758712.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DR75VSDY0B28AFTB6QP9M","2025-12-24 05:36:46.779+00","2025-12-24 05:36:46.779+00",NULL,"https://storage.danakcorp.com/images/1766554606166-877198688.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DRRT3B7BAA714MMYVVHN4","2025-12-24 05:37:04.835+00","2025-12-24 05:37:04.835+00",NULL,"https://storage.danakcorp.com/images/1766554624133-21439996.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DS9TQY1KWPM2HY7VC7THR","2025-12-24 05:37:22.263+00","2025-12-24 05:37:22.263+00",NULL,"https://storage.danakcorp.com/images/1766554641691-535696200.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DST85ZZ0SKSVDYJWN67VQ","2025-12-24 05:37:39.078+00","2025-12-24 05:37:39.078+00",NULL,"https://storage.danakcorp.com/images/1766554658481-114730426.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DT75Y84N1ECNV2WW6VQ37","2025-12-24 05:37:52.318+00","2025-12-24 05:37:52.318+00",NULL,"https://storage.danakcorp.com/images/1766554671760-634916570.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7E3837FJ9TDDKPF8S6PHK6","2025-12-24 05:42:48.167+00","2025-12-24 05:42:48.167+00",NULL,"https://storage.danakcorp.com/images/1766554967381-660612838.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7E7G89J5KH2ZT45F0MT5X3","2025-12-24 05:45:07.593+00","2025-12-24 05:45:07.593+00",NULL,"https://storage.danakcorp.com/images/1766555106925-267924006.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EBA6ATBBFVMGWQHACZ1G7","2025-12-24 05:47:12.459+00","2025-12-24 05:47:12.459+00",NULL,"https://storage.danakcorp.com/images/1766555231674-387319393.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7ED3NRKEX3Z8G6CTHK5QAB","2025-12-24 05:48:11.32+00","2025-12-24 05:48:11.32+00",NULL,"https://storage.danakcorp.com/images/1766555290519-155637622.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EDN07W94SGG04BRNRNRWT","2025-12-24 05:48:29.063+00","2025-12-24 05:48:29.063+00",NULL,"https://storage.danakcorp.com/images/1766555308301-230359494.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EH2W75B2H8JGMHR9P76ZE","2025-12-24 05:50:21.576+00","2025-12-24 05:50:21.576+00",NULL,"https://storage.danakcorp.com/images/1766555420851-186440015.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EP8V3KQYMDCDMXYP1QKPK","2025-12-24 05:53:11.523+00","2025-12-24 05:53:11.523+00",NULL,"https://storage.danakcorp.com/images/1766555590828-644527730.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EPRJWQAQX00F0J02FV05J","2025-12-24 05:53:27.644+00","2025-12-24 05:53:27.644+00",NULL,"https://storage.danakcorp.com/images/1766555606997-571682462.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7ERGH1ABTPGSJN1WFTESP6","2025-12-24 05:54:24.929+00","2025-12-24 05:54:24.929+00",NULL,"https://storage.danakcorp.com/images/1766555663659-433052581.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7ET0394XN31KFT3340R04H","2025-12-24 05:55:13.641+00","2025-12-24 05:55:13.641+00",NULL,"https://storage.danakcorp.com/images/1766555713098-988242030.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7ETCZBFV8ZZF1GFDKP7DXA","2025-12-24 05:55:26.827+00","2025-12-24 05:55:26.827+00",NULL,"https://storage.danakcorp.com/images/1766555726158-680948882.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EV0VW1M18RGBB54C4YDAC","2025-12-24 05:55:47.196+00","2025-12-24 05:55:47.196+00",NULL,"https://storage.danakcorp.com/images/1766555746650-426516324.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EVMP392QF71V6QFGK9XQA","2025-12-24 05:56:07.491+00","2025-12-24 05:56:07.491+00",NULL,"https://storage.danakcorp.com/images/1766555765798-127083646.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EW6MQX0VFPS21TJPSEBAH","2025-12-24 05:56:25.879+00","2025-12-24 05:56:25.879+00",NULL,"https://storage.danakcorp.com/images/1766555784837-243715698.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EWMGEHYQRKBM6XBS9EKTF","2025-12-24 05:56:40.078+00","2025-12-24 05:56:40.078+00",NULL,"https://storage.danakcorp.com/images/1766555799191-594035927.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EZHSEJ4G27VT3HS7JZ8YN","2025-12-24 05:58:15.599+00","2025-12-24 05:58:15.599+00",NULL,"https://storage.danakcorp.com/images/1766555894624-524350777.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7F0PCEADF1HWJ47V8RP1ZP","2025-12-24 05:58:53.07+00","2025-12-24 05:58:53.07+00",NULL,"https://storage.danakcorp.com/images/1766555932114-485954688.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7F184CHB129A900K2V6N94","2025-12-24 05:59:11.244+00","2025-12-24 05:59:11.244+00",NULL,"https://storage.danakcorp.com/images/1766555949650-909996041.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7F1R0JNNRP8QW8NHANZB01","2025-12-24 05:59:27.506+00","2025-12-24 05:59:27.506+00",NULL,"https://storage.danakcorp.com/images/1766555966861-75517122.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7F2Z1V0T1S47R7VWBEPY66","2025-12-24 06:00:07.483+00","2025-12-24 06:00:07.483+00",NULL,"https://storage.danakcorp.com/images/1766556006875-239689681.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FAP3S04D5CW51W98YZRHE","2025-12-24 06:04:20.473+00","2025-12-24 06:04:20.473+00",NULL,"https://storage.danakcorp.com/images/1766556257163-459958715.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FBQJAV9HJMG4EVRKPSAC5","2025-12-24 06:04:54.73+00","2025-12-24 06:04:54.73+00",NULL,"https://storage.danakcorp.com/images/1766556293812-665835248.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FC48FRARBP7ZXN7SPGP8K","2025-12-24 06:05:07.727+00","2025-12-24 06:05:07.727+00",NULL,"https://storage.danakcorp.com/images/1766556307082-162280793.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FD4SW2G73QX0YY6KNK7V4","2025-12-24 06:05:41.052+00","2025-12-24 06:05:41.052+00",NULL,"https://storage.danakcorp.com/images/1766556340402-744632392.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FGRC85J6JXEYNBGCPY0S4","2025-12-24 06:07:39.4+00","2025-12-24 06:07:39.4+00",NULL,"https://storage.danakcorp.com/images/1766556458335-857124841.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FHCW86SPVBSC8N62YEY5R","2025-12-24 06:08:00.393+00","2025-12-24 06:08:00.393+00",NULL,"https://storage.danakcorp.com/images/1766556479530-161860621.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FHYWZ73T36XKTDQ5FEN7G","2025-12-24 06:08:18.847+00","2025-12-24 06:08:18.847+00",NULL,"https://storage.danakcorp.com/images/1766556498215-91175544.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FQDYPGWD05HHATWBC499Z","2025-12-24 06:11:18.103+00","2025-12-24 06:11:18.103+00",NULL,"https://storage.danakcorp.com/images/1766556677355-108854669.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FS7QGJJ3YRSM10Y5YWD6C","2025-12-24 06:12:17.264+00","2025-12-24 06:12:17.264+00",NULL,"https://storage.danakcorp.com/images/1766556736283-691587656.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FSY2XKBREKJ9HCNJN924C","2025-12-24 06:12:40.157+00","2025-12-24 06:12:40.157+00",NULL,"https://storage.danakcorp.com/images/1766556759367-58066921.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FTFZ25MHQATRC41Y3EPQS","2025-12-24 06:12:58.468+00","2025-12-24 06:12:58.468+00",NULL,"https://storage.danakcorp.com/images/1766556777353-490756125.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FXARH84SJYESXVAG0AH3Q","2025-12-24 06:14:31.441+00","2025-12-24 06:14:31.441+00",NULL,"https://storage.danakcorp.com/images/1766556870740-118553581.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FXXV4DP74NGWJC0J43MDP","2025-12-24 06:14:50.98+00","2025-12-24 06:14:50.98+00",NULL,"https://storage.danakcorp.com/images/1766556890334-962678132.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FYVRNQAAA0DQZ8NZ02F2R","2025-12-24 06:15:21.621+00","2025-12-24 06:15:21.621+00",NULL,"https://storage.danakcorp.com/images/1766556921029-865260745.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FZMJ4QGTG6NZEDKWP7V0W","2025-12-24 06:15:47.013+00","2025-12-24 06:15:47.013+00",NULL,"https://storage.danakcorp.com/images/1766556946363-320833501.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7G0F8GTY7KZ530Y9XF8YXF","2025-12-24 06:16:14.352+00","2025-12-24 06:16:14.352+00",NULL,"https://storage.danakcorp.com/images/1766556973771-832565827.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7G300731YZQDT3FVAP0KHH","2025-12-24 06:17:37.031+00","2025-12-24 06:17:37.031+00",NULL,"https://storage.danakcorp.com/images/1766557056358-727862983.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7G7BHM8WEXFHHKD09MG10J","2025-12-24 06:19:59.925+00","2025-12-24 06:19:59.925+00",NULL,"https://storage.danakcorp.com/images/1766557199259-72955190.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7G8ZM2FPZ9H5BVGD983CTS","2025-12-24 06:20:53.25+00","2025-12-24 06:20:53.25+00",NULL,"https://storage.danakcorp.com/images/1766557252452-988016525.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7G9N7J647RH94K5WQB1Q3B","2025-12-24 06:21:15.378+00","2025-12-24 06:21:15.378+00",NULL,"https://storage.danakcorp.com/images/1766557274778-439028456.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GA36DDJBQJ15XT4D0E2D6","2025-12-24 06:21:29.678+00","2025-12-24 06:21:29.678+00",NULL,"https://storage.danakcorp.com/images/1766557289094-415104854.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GAEZKQKRC80EP7F03K22K","2025-12-24 06:21:41.747+00","2025-12-24 06:21:41.747+00",NULL,"https://storage.danakcorp.com/images/1766557300923-302277606.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GAYJAHXAZV84PN3X2N4FY","2025-12-24 06:21:57.707+00","2025-12-24 06:21:57.707+00",NULL,"https://storage.danakcorp.com/images/1766557317091-602116315.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GN1W5Q0WZWCJR8V62AD88","2025-12-24 06:27:28.774+00","2025-12-24 06:27:28.774+00",NULL,"https://storage.danakcorp.com/images/1766557647999-47333939.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GNPEX4CMZHRVYB4AZYSK3","2025-12-24 06:27:49.853+00","2025-12-24 06:27:49.853+00",NULL,"https://storage.danakcorp.com/images/1766557669282-172512141.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GPSY27QGH5XSPXWHD8VJ5","2025-12-24 06:28:26.179+00","2025-12-24 06:28:26.179+00",NULL,"https://storage.danakcorp.com/images/1766557705283-692546793.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GQGZK52YM87E283013JS4","2025-12-24 06:28:49.779+00","2025-12-24 06:28:49.779+00",NULL,"https://storage.danakcorp.com/images/1766557729175-762576743.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GRBRZTFFHDS1A6MH2CMJ4","2025-12-24 06:29:17.215+00","2025-12-24 06:29:17.215+00",NULL,"https://storage.danakcorp.com/images/1766557756487-665300947.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GRYXHE220H9C3VH4VAC8E","2025-12-24 06:29:36.817+00","2025-12-24 06:29:36.817+00",NULL,"https://storage.danakcorp.com/images/1766557776067-804149228.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GSKMZH8WAH5VYAS4GH02C","2025-12-24 06:29:58.048+00","2025-12-24 06:29:58.048+00",NULL,"https://storage.danakcorp.com/images/1766557797509-101726352.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GT6A83SX8NKPVHYXAKJ3H","2025-12-24 06:30:17.16+00","2025-12-24 06:30:17.16+00",NULL,"https://storage.danakcorp.com/images/1766557816484-357699340.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GTTKT3H3EDFTRCY8P2AE9","2025-12-24 06:30:37.946+00","2025-12-24 06:30:37.946+00",NULL,"https://storage.danakcorp.com/images/1766557837292-326521286.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7J0VP01FCP0TEHYG0MD191","2025-12-24 06:51:24.224+00","2025-12-24 06:51:24.224+00",NULL,"https://storage.danakcorp.com/images/1766559083575-564721059.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7J1BMTMBMV009EYC7N4G7D","2025-12-24 06:51:40.57+00","2025-12-24 06:51:40.57+00",NULL,"https://storage.danakcorp.com/images/1766559099561-720966516.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7J1W90CASGWZKBFMN75STB","2025-12-24 06:51:57.601+00","2025-12-24 06:51:57.601+00",NULL,"https://storage.danakcorp.com/images/1766559116496-834964990.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NS1HJ2C707G3KXRRZCN7Y","2025-12-24 07:57:02.387+00","2025-12-24 07:57:02.387+00",NULL,"https://storage.danakcorp.com/images/1766563021426-600579018.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NSHYZ6XESEJR7MCKS7CY0","2025-12-24 07:57:19.199+00","2025-12-24 07:57:19.199+00",NULL,"https://storage.danakcorp.com/images/1766563038525-727531190.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NSYX5W066SDN0RYBKXHQ3","2025-12-24 07:57:32.454+00","2025-12-24 07:57:32.454+00",NULL,"https://storage.danakcorp.com/images/1766563051485-824602001.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NTCZK4NYE3KCYDM1247GP","2025-12-24 07:57:46.867+00","2025-12-24 07:57:46.867+00",NULL,"https://storage.danakcorp.com/images/1766563066017-372210647.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NW5KZEW4M2ZJX93T53G41","2025-12-24 07:58:44.863+00","2025-12-24 07:58:44.863+00",NULL,"https://storage.danakcorp.com/images/1766563124296-331607800.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NWQ7YFJ7HDNWRB5F1V47Q","2025-12-24 07:59:02.91+00","2025-12-24 07:59:02.91+00",NULL,"https://storage.danakcorp.com/images/1766563142133-359920423.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NXCZSEN7M2TN0DT0XVVR9","2025-12-24 07:59:25.177+00","2025-12-24 07:59:25.177+00",NULL,"https://storage.danakcorp.com/images/1766563164270-350101986.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NXXMYZFMAWMRS23GRZSKP","2025-12-24 07:59:42.238+00","2025-12-24 07:59:42.238+00",NULL,"https://storage.danakcorp.com/images/1766563181587-86630830.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NYP6M0HNC3A4XCXHK4W4M","2025-12-24 08:00:07.38+00","2025-12-24 08:00:07.38+00",NULL,"https://storage.danakcorp.com/images/1766563204966-651499380.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NZ3PDZX4WTVV3KC5MTD8K","2025-12-24 08:00:21.198+00","2025-12-24 08:00:21.198+00",NULL,"https://storage.danakcorp.com/images/1766563220596-103699525.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7P0FWPK3HTYVX14XMGCHTC","2025-12-24 08:01:06.455+00","2025-12-24 08:01:06.455+00",NULL,"https://storage.danakcorp.com/images/1766563265634-373953449.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7P0SAN0PA6KAGE3ABAXSWF","2025-12-24 08:01:16.117+00","2025-12-24 08:01:16.117+00",NULL,"https://storage.danakcorp.com/images/1766563275517-958045207.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7P1429PQT8NFDFRZEDNH1P","2025-12-24 08:01:27.113+00","2025-12-24 08:01:27.113+00",NULL,"https://storage.danakcorp.com/images/1766563286526-622849562.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7QC4M3D2SF26ZH0S0SDWBD","2025-12-24 08:24:56.708+00","2025-12-24 08:24:56.708+00",NULL,"https://storage.danakcorp.com/images/1766564695339-504553420.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QCEDA0T3CGZ01NGBKWZ9S","2025-12-24 08:25:06.73+00","2025-12-24 08:25:06.73+00",NULL,"https://storage.danakcorp.com/images/1766564706032-159367016.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QCWQ00TVCK7BAAHSWA09H","2025-12-24 08:25:21.376+00","2025-12-24 08:25:21.376+00",NULL,"https://storage.danakcorp.com/images/1766564720683-887818252.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QDA1R9WTTV2JWQ2Y3WGST","2025-12-24 08:25:35.032+00","2025-12-24 08:25:35.032+00",NULL,"https://storage.danakcorp.com/images/1766564734432-483528219.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QDSCQNW2JC4SAQCK0QBNW","2025-12-24 08:25:50.743+00","2025-12-24 08:25:50.743+00",NULL,"https://storage.danakcorp.com/images/1766564750099-854146988.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QE1NXGG08JQ5F2BC35NFC","2025-12-24 08:25:59.23+00","2025-12-24 08:25:59.23+00",NULL,"https://storage.danakcorp.com/images/1766564758573-696060786.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QFFXDVTSB5VAXB640HR7N","2025-12-24 08:26:46.574+00","2025-12-24 08:26:46.574+00",NULL,"https://storage.danakcorp.com/images/1766564805823-829938380.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QJ37DT4NCD6QY9MNGCZXZ","2025-12-24 08:28:11.885+00","2025-12-24 08:28:11.885+00",NULL,"https://storage.danakcorp.com/images/1766564891137-468827500.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QJJZX6Y7CR8R60W3TNKRS","2025-12-24 08:28:28.029+00","2025-12-24 08:28:28.029+00",NULL,"https://storage.danakcorp.com/images/1766564907215-921380792.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QK69ZH6B4S602YKZCBVMB","2025-12-24 08:28:47.807+00","2025-12-24 08:28:47.807+00",NULL,"https://storage.danakcorp.com/images/1766564926872-672612697.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R3EM739KW6PV431V6P19S","2025-12-24 08:37:40.616+00","2025-12-24 08:37:40.616+00",NULL,"https://storage.danakcorp.com/images/1766565459604-638026702.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R3YZ6SAQWZ4CMC4GCJ1V4","2025-12-24 08:37:57.351+00","2025-12-24 08:37:57.351+00",NULL,"https://storage.danakcorp.com/images/1766565476771-42527917.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R4D5BVGPWKXE6A7FNVNRA","2025-12-24 08:38:11.883+00","2025-12-24 08:38:11.883+00",NULL,"https://storage.danakcorp.com/images/1766565491264-946731491.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R4RFYFFQ4GV5TQ3DH6BW6","2025-12-24 08:38:23.486+00","2025-12-24 08:38:23.486+00",NULL,"https://storage.danakcorp.com/images/1766565502475-288189136.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R5KMSAGCXZCTQPTVYDTA7","2025-12-24 08:38:51.289+00","2025-12-24 08:38:51.289+00",NULL,"https://storage.danakcorp.com/images/1766565530628-800474580.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R64HYQD37W8ASB2Q817MW","2025-12-24 08:39:08.606+00","2025-12-24 08:39:08.606+00",NULL,"https://storage.danakcorp.com/images/1766565547983-878658938.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R6KF9YWZBZ3WPAKXQS2CH","2025-12-24 08:39:23.881+00","2025-12-24 08:39:23.881+00",NULL,"https://storage.danakcorp.com/images/1766565563263-891688321.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R7RM02R8FZ4DN65WMJVE8","2025-12-24 08:40:01.92+00","2025-12-24 08:40:01.92+00",NULL,"https://storage.danakcorp.com/images/1766565600547-704332792.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7R8G53XBC6AG6KAVSBW94G","2025-12-24 08:40:26.019+00","2025-12-24 08:40:26.019+00",NULL,"https://storage.danakcorp.com/images/1766565625437-528383713.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R901TPNAX9YYRG6W0GWHF","2025-12-24 08:40:42.298+00","2025-12-24 08:40:42.298+00",NULL,"https://storage.danakcorp.com/images/1766565641620-53176909.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R9PKF9G1DD1HMS2QQQN9V","2025-12-24 08:41:05.391+00","2025-12-24 08:41:05.391+00",NULL,"https://storage.danakcorp.com/images/1766565664427-433669317.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RK2G6E5JKEDK9P95W79PR","2025-12-24 08:46:12.487+00","2025-12-24 08:46:12.487+00",NULL,"https://storage.danakcorp.com/images/1766565971515-159903323.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RKSM9QD6RA2H6WGE23KEC","2025-12-24 08:46:36.169+00","2025-12-24 08:46:36.169+00",NULL,"https://storage.danakcorp.com/images/1766565995424-288101561.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RMR9DRA1TG2BJ9Q8QAZ15","2025-12-24 08:47:07.565+00","2025-12-24 08:47:07.565+00",NULL,"https://storage.danakcorp.com/images/1766566026942-868807643.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RN6FH0TX69RZTKJKM6MEN","2025-12-24 08:47:22.097+00","2025-12-24 08:47:22.097+00",NULL,"https://storage.danakcorp.com/images/1766566041525-973019175.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RNX4EE92QR2C24577BJCW","2025-12-24 08:47:45.294+00","2025-12-24 08:47:45.294+00",NULL,"https://storage.danakcorp.com/images/1766566064741-271844433.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RPHHCKNXNQH29WSG84D2Z","2025-12-24 08:48:06.189+00","2025-12-24 08:48:06.189+00",NULL,"https://storage.danakcorp.com/images/1766566084368-438871956.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RQ6MFA5CDQ4J55NG3N7PX","2025-12-24 08:48:27.791+00","2025-12-24 08:48:27.791+00",NULL,"https://storage.danakcorp.com/images/1766566106954-47179607.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RQNXJNRXS8MXV57R50PKN","2025-12-24 08:48:43.443+00","2025-12-24 08:48:43.443+00",NULL,"https://storage.danakcorp.com/images/1766566122836-486871124.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RR9W6B9BTWJ0N2H11TWM1","2025-12-24 08:49:03.879+00","2025-12-24 08:49:03.879+00",NULL,"https://storage.danakcorp.com/images/1766566143171-668309076.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RRVQWYHSHN3M7R25GNGHS","2025-12-24 08:49:22.172+00","2025-12-24 08:49:22.172+00",NULL,"https://storage.danakcorp.com/images/1766566161551-844376214.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RSAKAP1TJ59GN11DEAXD9","2025-12-24 08:49:37.386+00","2025-12-24 08:49:37.386+00",NULL,"https://storage.danakcorp.com/images/1766566176755-30806596.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RSV35PY1A6193H4GDW1ZW","2025-12-24 08:49:54.277+00","2025-12-24 08:49:54.277+00",NULL,"https://storage.danakcorp.com/images/1766566193720-339416210.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RWX3D4ZZE35W345HDK35S","2025-12-24 08:51:34.638+00","2025-12-24 08:51:34.638+00",NULL,"https://storage.danakcorp.com/images/1766566293736-981836756.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RX79BVR30WGKXAZQ3D0CM","2025-12-24 08:51:45.068+00","2025-12-24 08:51:45.068+00",NULL,"https://storage.danakcorp.com/images/1766566304318-193301989.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RXY99C8PV6SAR0AJ00F16","2025-12-24 08:52:08.618+00","2025-12-24 08:52:08.618+00",NULL,"https://storage.danakcorp.com/images/1766566327768-2675508.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RYBPV273KCR350ESK2FN1","2025-12-24 08:52:22.364+00","2025-12-24 08:52:22.364+00",NULL,"https://storage.danakcorp.com/images/1766566341717-462491426.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RYTNTE20YS29X2XJT7GY3","2025-12-24 08:52:37.69+00","2025-12-24 08:52:37.69+00",NULL,"https://storage.danakcorp.com/images/1766566357028-432111036.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RZ8K86GCJSW1E0EC5T4B2","2025-12-24 08:52:51.944+00","2025-12-24 08:52:51.944+00",NULL,"https://storage.danakcorp.com/images/1766566371354-279596850.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RZPNGASPYFP9XKMSY208B","2025-12-24 08:53:06.352+00","2025-12-24 08:53:06.352+00",NULL,"https://storage.danakcorp.com/images/1766566385674-569493499.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7S0HEQZ4AY5TTFKF36HS6F","2025-12-24 08:53:33.783+00","2025-12-24 08:53:33.783+00",NULL,"https://storage.danakcorp.com/images/1766566413058-975642584.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7S0ZEQ6BH163HCX0MQZHBJ","2025-12-24 08:53:48.119+00","2025-12-24 08:53:48.119+00",NULL,"https://storage.danakcorp.com/images/1766566427440-982841441.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7S1EF6R6J6AMMC6HP5J0AA","2025-12-24 08:54:03.494+00","2025-12-24 08:54:03.494+00",NULL,"https://storage.danakcorp.com/images/1766566442808-761588425.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7S27EFH2CDEK71GVQC839G","2025-12-24 08:54:29.074+00","2025-12-24 08:54:29.074+00",NULL,"https://storage.danakcorp.com/images/1766566468108-519151895.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7S2N705Z754YS3YFQW4AGQ","2025-12-24 08:54:43.169+00","2025-12-24 08:54:43.169+00",NULL,"https://storage.danakcorp.com/images/1766566482278-661272876.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7S31HNKPAAK58CX77BNJCA","2025-12-24 08:54:55.798+00","2025-12-24 08:54:55.798+00",NULL,"https://storage.danakcorp.com/images/1766566494050-408184442.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7S3AX9VW5B830MYJYGXQ0B","2025-12-24 08:55:05.385+00","2025-12-24 08:55:05.385+00",NULL,"https://storage.danakcorp.com/images/1766566504760-709260995.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
|
@@ -0,0 +1,205 @@
|
||||
--
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
\restrict acoqslHA6I152rhJbwpzhi5E4Cp5r1Bq6kFOAPp82JWutcjW9v0rh7ptBQwHz6l
|
||||
|
||||
-- Dumped from database version 17.6 (Debian 17.6-2.pgdg13+1)
|
||||
-- Dumped by pg_dump version 17.7 (Ubuntu 17.7-3.pgdg24.04+1)
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET transaction_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
--
|
||||
-- Data for Name: icon_groups; Type: TABLE DATA; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
INSERT INTO public.icon_groups (id, created_at, updated_at, deleted_at, name) VALUES ('01KD7DG4VCTWZRSA54M3PEJ6NW', '2025-12-24 05:32:22.252+00', '2025-12-24 05:32:22.252+00', NULL, 'پک آیکون 1');
|
||||
INSERT INTO public.icon_groups (id, created_at, updated_at, deleted_at, name) VALUES ('01KD7DGBDFX76VKXMKQKR51RJK', '2025-12-24 05:32:28.976+00', '2025-12-24 05:32:28.976+00', NULL, 'پک آیکون 2');
|
||||
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
--
|
||||
|
||||
\unrestrict acoqslHA6I152rhJbwpzhi5E4Cp5r1Bq6kFOAPp82JWutcjW9v0rh7ptBQwHz6l
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
\restrict 4pvnNoptyhScdLjLBpsonU6ZnOH502ap2y2am0YHcRG7WkY1NxnW43VtUD3Gq85
|
||||
|
||||
-- Dumped from database version 17.6 (Debian 17.6-2.pgdg13+1)
|
||||
-- Dumped by pg_dump version 17.7 (Ubuntu 17.7-3.pgdg24.04+1)
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET transaction_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
--
|
||||
-- Data for Name: icons; Type: TABLE DATA; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DGWH6TDB47V4710FDT7SR', '2025-12-24 05:32:46.503+00', '2025-12-24 05:32:46.503+00', NULL, 'https://storage.danakcorp.com/images/1766554365832-411750172.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DH9F6BN4TEM96TA2QT4GJ', '2025-12-24 05:32:59.751+00', '2025-12-24 05:32:59.751+00', NULL, 'https://storage.danakcorp.com/images/1766554379149-104970938.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DHK6ZTEGPRBNXJPV49TE6', '2025-12-24 05:33:09.727+00', '2025-12-24 05:33:09.727+00', NULL, 'https://storage.danakcorp.com/images/1766554389090-593863597.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DJ2NVEHVQMDCAM31VKKGK', '2025-12-24 05:33:25.564+00', '2025-12-24 05:33:25.564+00', NULL, 'https://storage.danakcorp.com/images/1766554404968-905984091.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DJCQY1V84DAVXPV097HE6', '2025-12-24 05:33:35.871+00', '2025-12-24 05:33:35.871+00', NULL, 'https://storage.danakcorp.com/images/1766554415325-873800591.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DJSZ2XVX971FGNFRRDQV6', '2025-12-24 05:33:49.411+00', '2025-12-24 05:33:49.411+00', NULL, 'https://storage.danakcorp.com/images/1766554428814-77027818.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DK3HJAD9SMDD8CM1RPNQS', '2025-12-24 05:33:59.218+00', '2025-12-24 05:33:59.218+00', NULL, 'https://storage.danakcorp.com/images/1766554438632-45072052.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DKB23B6XWJ8CMAKQ5R8HS', '2025-12-24 05:34:06.915+00', '2025-12-24 05:34:06.915+00', NULL, 'https://storage.danakcorp.com/images/1766554446340-463157109.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DKN32VCDQDF2D407MGKF0', '2025-12-24 05:34:17.186+00', '2025-12-24 05:34:17.186+00', NULL, 'https://storage.danakcorp.com/images/1766554456605-280166176.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DM4AEQ2FCJGZM390N7YVJ', '2025-12-24 05:34:32.782+00', '2025-12-24 05:34:32.782+00', NULL, 'https://storage.danakcorp.com/images/1766554471887-315640495.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DMHKWJSHX6BR41FTDYVHE', '2025-12-24 05:34:46.396+00', '2025-12-24 05:34:46.396+00', NULL, 'https://storage.danakcorp.com/images/1766554485634-242317100.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DN3XNDW2JQ0RGM711R9Q5', '2025-12-24 05:35:05.141+00', '2025-12-24 05:35:05.141+00', NULL, 'https://storage.danakcorp.com/images/1766554504553-157481346.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DNV0F9CPMKQ4R482MTVKJ', '2025-12-24 05:35:28.783+00', '2025-12-24 05:35:28.783+00', NULL, 'https://storage.danakcorp.com/images/1766554528198-919877647.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DPAKCPXD2GK0FCZ7N3XXP', '2025-12-24 05:35:44.749+00', '2025-12-24 05:35:44.749+00', NULL, 'https://storage.danakcorp.com/images/1766554544140-886744157.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DPZGZFZPS5AJ8RNM3W16F', '2025-12-24 05:36:06.175+00', '2025-12-24 05:36:06.175+00', NULL, 'https://storage.danakcorp.com/images/1766554565614-684869010.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DQCMAD39T5QB5728M6PFP', '2025-12-24 05:36:19.594+00', '2025-12-24 05:36:19.594+00', NULL, 'https://storage.danakcorp.com/images/1766554578855-318583145.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DQTQHGWP5J77V327Y4GHZ', '2025-12-24 05:36:34.034+00', '2025-12-24 05:36:34.034+00', NULL, 'https://storage.danakcorp.com/images/1766554593272-362758712.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DR75VSDY0B28AFTB6QP9M', '2025-12-24 05:36:46.779+00', '2025-12-24 05:36:46.779+00', NULL, 'https://storage.danakcorp.com/images/1766554606166-877198688.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DRRT3B7BAA714MMYVVHN4', '2025-12-24 05:37:04.835+00', '2025-12-24 05:37:04.835+00', NULL, 'https://storage.danakcorp.com/images/1766554624133-21439996.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DS9TQY1KWPM2HY7VC7THR', '2025-12-24 05:37:22.263+00', '2025-12-24 05:37:22.263+00', NULL, 'https://storage.danakcorp.com/images/1766554641691-535696200.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DST85ZZ0SKSVDYJWN67VQ', '2025-12-24 05:37:39.078+00', '2025-12-24 05:37:39.078+00', NULL, 'https://storage.danakcorp.com/images/1766554658481-114730426.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DT75Y84N1ECNV2WW6VQ37', '2025-12-24 05:37:52.318+00', '2025-12-24 05:37:52.318+00', NULL, 'https://storage.danakcorp.com/images/1766554671760-634916570.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7E3837FJ9TDDKPF8S6PHK6', '2025-12-24 05:42:48.167+00', '2025-12-24 05:42:48.167+00', NULL, 'https://storage.danakcorp.com/images/1766554967381-660612838.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7E7G89J5KH2ZT45F0MT5X3', '2025-12-24 05:45:07.593+00', '2025-12-24 05:45:07.593+00', NULL, 'https://storage.danakcorp.com/images/1766555106925-267924006.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EBA6ATBBFVMGWQHACZ1G7', '2025-12-24 05:47:12.459+00', '2025-12-24 05:47:12.459+00', NULL, 'https://storage.danakcorp.com/images/1766555231674-387319393.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7ED3NRKEX3Z8G6CTHK5QAB', '2025-12-24 05:48:11.32+00', '2025-12-24 05:48:11.32+00', NULL, 'https://storage.danakcorp.com/images/1766555290519-155637622.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EDN07W94SGG04BRNRNRWT', '2025-12-24 05:48:29.063+00', '2025-12-24 05:48:29.063+00', NULL, 'https://storage.danakcorp.com/images/1766555308301-230359494.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EH2W75B2H8JGMHR9P76ZE', '2025-12-24 05:50:21.576+00', '2025-12-24 05:50:21.576+00', NULL, 'https://storage.danakcorp.com/images/1766555420851-186440015.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EP8V3KQYMDCDMXYP1QKPK', '2025-12-24 05:53:11.523+00', '2025-12-24 05:53:11.523+00', NULL, 'https://storage.danakcorp.com/images/1766555590828-644527730.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EPRJWQAQX00F0J02FV05J', '2025-12-24 05:53:27.644+00', '2025-12-24 05:53:27.644+00', NULL, 'https://storage.danakcorp.com/images/1766555606997-571682462.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7ERGH1ABTPGSJN1WFTESP6', '2025-12-24 05:54:24.929+00', '2025-12-24 05:54:24.929+00', NULL, 'https://storage.danakcorp.com/images/1766555663659-433052581.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7ET0394XN31KFT3340R04H', '2025-12-24 05:55:13.641+00', '2025-12-24 05:55:13.641+00', NULL, 'https://storage.danakcorp.com/images/1766555713098-988242030.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7ETCZBFV8ZZF1GFDKP7DXA', '2025-12-24 05:55:26.827+00', '2025-12-24 05:55:26.827+00', NULL, 'https://storage.danakcorp.com/images/1766555726158-680948882.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EV0VW1M18RGBB54C4YDAC', '2025-12-24 05:55:47.196+00', '2025-12-24 05:55:47.196+00', NULL, 'https://storage.danakcorp.com/images/1766555746650-426516324.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EVMP392QF71V6QFGK9XQA', '2025-12-24 05:56:07.491+00', '2025-12-24 05:56:07.491+00', NULL, 'https://storage.danakcorp.com/images/1766555765798-127083646.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EW6MQX0VFPS21TJPSEBAH', '2025-12-24 05:56:25.879+00', '2025-12-24 05:56:25.879+00', NULL, 'https://storage.danakcorp.com/images/1766555784837-243715698.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EWMGEHYQRKBM6XBS9EKTF', '2025-12-24 05:56:40.078+00', '2025-12-24 05:56:40.078+00', NULL, 'https://storage.danakcorp.com/images/1766555799191-594035927.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EZHSEJ4G27VT3HS7JZ8YN', '2025-12-24 05:58:15.599+00', '2025-12-24 05:58:15.599+00', NULL, 'https://storage.danakcorp.com/images/1766555894624-524350777.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7F0PCEADF1HWJ47V8RP1ZP', '2025-12-24 05:58:53.07+00', '2025-12-24 05:58:53.07+00', NULL, 'https://storage.danakcorp.com/images/1766555932114-485954688.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7F184CHB129A900K2V6N94', '2025-12-24 05:59:11.244+00', '2025-12-24 05:59:11.244+00', NULL, 'https://storage.danakcorp.com/images/1766555949650-909996041.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7F1R0JNNRP8QW8NHANZB01', '2025-12-24 05:59:27.506+00', '2025-12-24 05:59:27.506+00', NULL, 'https://storage.danakcorp.com/images/1766555966861-75517122.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7F2Z1V0T1S47R7VWBEPY66', '2025-12-24 06:00:07.483+00', '2025-12-24 06:00:07.483+00', NULL, 'https://storage.danakcorp.com/images/1766556006875-239689681.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FAP3S04D5CW51W98YZRHE', '2025-12-24 06:04:20.473+00', '2025-12-24 06:04:20.473+00', NULL, 'https://storage.danakcorp.com/images/1766556257163-459958715.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FBQJAV9HJMG4EVRKPSAC5', '2025-12-24 06:04:54.73+00', '2025-12-24 06:04:54.73+00', NULL, 'https://storage.danakcorp.com/images/1766556293812-665835248.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FC48FRARBP7ZXN7SPGP8K', '2025-12-24 06:05:07.727+00', '2025-12-24 06:05:07.727+00', NULL, 'https://storage.danakcorp.com/images/1766556307082-162280793.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FD4SW2G73QX0YY6KNK7V4', '2025-12-24 06:05:41.052+00', '2025-12-24 06:05:41.052+00', NULL, 'https://storage.danakcorp.com/images/1766556340402-744632392.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FGRC85J6JXEYNBGCPY0S4', '2025-12-24 06:07:39.4+00', '2025-12-24 06:07:39.4+00', NULL, 'https://storage.danakcorp.com/images/1766556458335-857124841.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FHCW86SPVBSC8N62YEY5R', '2025-12-24 06:08:00.393+00', '2025-12-24 06:08:00.393+00', NULL, 'https://storage.danakcorp.com/images/1766556479530-161860621.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FHYWZ73T36XKTDQ5FEN7G', '2025-12-24 06:08:18.847+00', '2025-12-24 06:08:18.847+00', NULL, 'https://storage.danakcorp.com/images/1766556498215-91175544.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FQDYPGWD05HHATWBC499Z', '2025-12-24 06:11:18.103+00', '2025-12-24 06:11:18.103+00', NULL, 'https://storage.danakcorp.com/images/1766556677355-108854669.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FS7QGJJ3YRSM10Y5YWD6C', '2025-12-24 06:12:17.264+00', '2025-12-24 06:12:17.264+00', NULL, 'https://storage.danakcorp.com/images/1766556736283-691587656.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FSY2XKBREKJ9HCNJN924C', '2025-12-24 06:12:40.157+00', '2025-12-24 06:12:40.157+00', NULL, 'https://storage.danakcorp.com/images/1766556759367-58066921.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FTFZ25MHQATRC41Y3EPQS', '2025-12-24 06:12:58.468+00', '2025-12-24 06:12:58.468+00', NULL, 'https://storage.danakcorp.com/images/1766556777353-490756125.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FXARH84SJYESXVAG0AH3Q', '2025-12-24 06:14:31.441+00', '2025-12-24 06:14:31.441+00', NULL, 'https://storage.danakcorp.com/images/1766556870740-118553581.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FXXV4DP74NGWJC0J43MDP', '2025-12-24 06:14:50.98+00', '2025-12-24 06:14:50.98+00', NULL, 'https://storage.danakcorp.com/images/1766556890334-962678132.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FYVRNQAAA0DQZ8NZ02F2R', '2025-12-24 06:15:21.621+00', '2025-12-24 06:15:21.621+00', NULL, 'https://storage.danakcorp.com/images/1766556921029-865260745.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FZMJ4QGTG6NZEDKWP7V0W', '2025-12-24 06:15:47.013+00', '2025-12-24 06:15:47.013+00', NULL, 'https://storage.danakcorp.com/images/1766556946363-320833501.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7G0F8GTY7KZ530Y9XF8YXF', '2025-12-24 06:16:14.352+00', '2025-12-24 06:16:14.352+00', NULL, 'https://storage.danakcorp.com/images/1766556973771-832565827.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7G300731YZQDT3FVAP0KHH', '2025-12-24 06:17:37.031+00', '2025-12-24 06:17:37.031+00', NULL, 'https://storage.danakcorp.com/images/1766557056358-727862983.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7G7BHM8WEXFHHKD09MG10J', '2025-12-24 06:19:59.925+00', '2025-12-24 06:19:59.925+00', NULL, 'https://storage.danakcorp.com/images/1766557199259-72955190.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7G8ZM2FPZ9H5BVGD983CTS', '2025-12-24 06:20:53.25+00', '2025-12-24 06:20:53.25+00', NULL, 'https://storage.danakcorp.com/images/1766557252452-988016525.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7G9N7J647RH94K5WQB1Q3B', '2025-12-24 06:21:15.378+00', '2025-12-24 06:21:15.378+00', NULL, 'https://storage.danakcorp.com/images/1766557274778-439028456.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GA36DDJBQJ15XT4D0E2D6', '2025-12-24 06:21:29.678+00', '2025-12-24 06:21:29.678+00', NULL, 'https://storage.danakcorp.com/images/1766557289094-415104854.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GAEZKQKRC80EP7F03K22K', '2025-12-24 06:21:41.747+00', '2025-12-24 06:21:41.747+00', NULL, 'https://storage.danakcorp.com/images/1766557300923-302277606.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GAYJAHXAZV84PN3X2N4FY', '2025-12-24 06:21:57.707+00', '2025-12-24 06:21:57.707+00', NULL, 'https://storage.danakcorp.com/images/1766557317091-602116315.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GN1W5Q0WZWCJR8V62AD88', '2025-12-24 06:27:28.774+00', '2025-12-24 06:27:28.774+00', NULL, 'https://storage.danakcorp.com/images/1766557647999-47333939.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GNPEX4CMZHRVYB4AZYSK3', '2025-12-24 06:27:49.853+00', '2025-12-24 06:27:49.853+00', NULL, 'https://storage.danakcorp.com/images/1766557669282-172512141.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GPSY27QGH5XSPXWHD8VJ5', '2025-12-24 06:28:26.179+00', '2025-12-24 06:28:26.179+00', NULL, 'https://storage.danakcorp.com/images/1766557705283-692546793.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GQGZK52YM87E283013JS4', '2025-12-24 06:28:49.779+00', '2025-12-24 06:28:49.779+00', NULL, 'https://storage.danakcorp.com/images/1766557729175-762576743.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GRBRZTFFHDS1A6MH2CMJ4', '2025-12-24 06:29:17.215+00', '2025-12-24 06:29:17.215+00', NULL, 'https://storage.danakcorp.com/images/1766557756487-665300947.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GRYXHE220H9C3VH4VAC8E', '2025-12-24 06:29:36.817+00', '2025-12-24 06:29:36.817+00', NULL, 'https://storage.danakcorp.com/images/1766557776067-804149228.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GSKMZH8WAH5VYAS4GH02C', '2025-12-24 06:29:58.048+00', '2025-12-24 06:29:58.048+00', NULL, 'https://storage.danakcorp.com/images/1766557797509-101726352.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GT6A83SX8NKPVHYXAKJ3H', '2025-12-24 06:30:17.16+00', '2025-12-24 06:30:17.16+00', NULL, 'https://storage.danakcorp.com/images/1766557816484-357699340.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GTTKT3H3EDFTRCY8P2AE9', '2025-12-24 06:30:37.946+00', '2025-12-24 06:30:37.946+00', NULL, 'https://storage.danakcorp.com/images/1766557837292-326521286.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7J0VP01FCP0TEHYG0MD191', '2025-12-24 06:51:24.224+00', '2025-12-24 06:51:24.224+00', NULL, 'https://storage.danakcorp.com/images/1766559083575-564721059.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7J1BMTMBMV009EYC7N4G7D', '2025-12-24 06:51:40.57+00', '2025-12-24 06:51:40.57+00', NULL, 'https://storage.danakcorp.com/images/1766559099561-720966516.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7J1W90CASGWZKBFMN75STB', '2025-12-24 06:51:57.601+00', '2025-12-24 06:51:57.601+00', NULL, 'https://storage.danakcorp.com/images/1766559116496-834964990.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NS1HJ2C707G3KXRRZCN7Y', '2025-12-24 07:57:02.387+00', '2025-12-24 07:57:02.387+00', NULL, 'https://storage.danakcorp.com/images/1766563021426-600579018.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NSHYZ6XESEJR7MCKS7CY0', '2025-12-24 07:57:19.199+00', '2025-12-24 07:57:19.199+00', NULL, 'https://storage.danakcorp.com/images/1766563038525-727531190.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NSYX5W066SDN0RYBKXHQ3', '2025-12-24 07:57:32.454+00', '2025-12-24 07:57:32.454+00', NULL, 'https://storage.danakcorp.com/images/1766563051485-824602001.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NTCZK4NYE3KCYDM1247GP', '2025-12-24 07:57:46.867+00', '2025-12-24 07:57:46.867+00', NULL, 'https://storage.danakcorp.com/images/1766563066017-372210647.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NW5KZEW4M2ZJX93T53G41', '2025-12-24 07:58:44.863+00', '2025-12-24 07:58:44.863+00', NULL, 'https://storage.danakcorp.com/images/1766563124296-331607800.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NWQ7YFJ7HDNWRB5F1V47Q', '2025-12-24 07:59:02.91+00', '2025-12-24 07:59:02.91+00', NULL, 'https://storage.danakcorp.com/images/1766563142133-359920423.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NXCZSEN7M2TN0DT0XVVR9', '2025-12-24 07:59:25.177+00', '2025-12-24 07:59:25.177+00', NULL, 'https://storage.danakcorp.com/images/1766563164270-350101986.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NXXMYZFMAWMRS23GRZSKP', '2025-12-24 07:59:42.238+00', '2025-12-24 07:59:42.238+00', NULL, 'https://storage.danakcorp.com/images/1766563181587-86630830.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NYP6M0HNC3A4XCXHK4W4M', '2025-12-24 08:00:07.38+00', '2025-12-24 08:00:07.38+00', NULL, 'https://storage.danakcorp.com/images/1766563204966-651499380.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NZ3PDZX4WTVV3KC5MTD8K', '2025-12-24 08:00:21.198+00', '2025-12-24 08:00:21.198+00', NULL, 'https://storage.danakcorp.com/images/1766563220596-103699525.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7P0FWPK3HTYVX14XMGCHTC', '2025-12-24 08:01:06.455+00', '2025-12-24 08:01:06.455+00', NULL, 'https://storage.danakcorp.com/images/1766563265634-373953449.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7P0SAN0PA6KAGE3ABAXSWF', '2025-12-24 08:01:16.117+00', '2025-12-24 08:01:16.117+00', NULL, 'https://storage.danakcorp.com/images/1766563275517-958045207.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7P1429PQT8NFDFRZEDNH1P', '2025-12-24 08:01:27.113+00', '2025-12-24 08:01:27.113+00', NULL, 'https://storage.danakcorp.com/images/1766563286526-622849562.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QC4M3D2SF26ZH0S0SDWBD', '2025-12-24 08:24:56.708+00', '2025-12-24 08:24:56.708+00', NULL, 'https://storage.danakcorp.com/images/1766564695339-504553420.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QCEDA0T3CGZ01NGBKWZ9S', '2025-12-24 08:25:06.73+00', '2025-12-24 08:25:06.73+00', NULL, 'https://storage.danakcorp.com/images/1766564706032-159367016.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QCWQ00TVCK7BAAHSWA09H', '2025-12-24 08:25:21.376+00', '2025-12-24 08:25:21.376+00', NULL, 'https://storage.danakcorp.com/images/1766564720683-887818252.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QDA1R9WTTV2JWQ2Y3WGST', '2025-12-24 08:25:35.032+00', '2025-12-24 08:25:35.032+00', NULL, 'https://storage.danakcorp.com/images/1766564734432-483528219.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QDSCQNW2JC4SAQCK0QBNW', '2025-12-24 08:25:50.743+00', '2025-12-24 08:25:50.743+00', NULL, 'https://storage.danakcorp.com/images/1766564750099-854146988.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QE1NXGG08JQ5F2BC35NFC', '2025-12-24 08:25:59.23+00', '2025-12-24 08:25:59.23+00', NULL, 'https://storage.danakcorp.com/images/1766564758573-696060786.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QFFXDVTSB5VAXB640HR7N', '2025-12-24 08:26:46.574+00', '2025-12-24 08:26:46.574+00', NULL, 'https://storage.danakcorp.com/images/1766564805823-829938380.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QJ37DT4NCD6QY9MNGCZXZ', '2025-12-24 08:28:11.885+00', '2025-12-24 08:28:11.885+00', NULL, 'https://storage.danakcorp.com/images/1766564891137-468827500.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QJJZX6Y7CR8R60W3TNKRS', '2025-12-24 08:28:28.029+00', '2025-12-24 08:28:28.029+00', NULL, 'https://storage.danakcorp.com/images/1766564907215-921380792.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QK69ZH6B4S602YKZCBVMB', '2025-12-24 08:28:47.807+00', '2025-12-24 08:28:47.807+00', NULL, 'https://storage.danakcorp.com/images/1766564926872-672612697.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R3EM739KW6PV431V6P19S', '2025-12-24 08:37:40.616+00', '2025-12-24 08:37:40.616+00', NULL, 'https://storage.danakcorp.com/images/1766565459604-638026702.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R3YZ6SAQWZ4CMC4GCJ1V4', '2025-12-24 08:37:57.351+00', '2025-12-24 08:37:57.351+00', NULL, 'https://storage.danakcorp.com/images/1766565476771-42527917.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R4D5BVGPWKXE6A7FNVNRA', '2025-12-24 08:38:11.883+00', '2025-12-24 08:38:11.883+00', NULL, 'https://storage.danakcorp.com/images/1766565491264-946731491.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R4RFYFFQ4GV5TQ3DH6BW6', '2025-12-24 08:38:23.486+00', '2025-12-24 08:38:23.486+00', NULL, 'https://storage.danakcorp.com/images/1766565502475-288189136.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R5KMSAGCXZCTQPTVYDTA7', '2025-12-24 08:38:51.289+00', '2025-12-24 08:38:51.289+00', NULL, 'https://storage.danakcorp.com/images/1766565530628-800474580.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R64HYQD37W8ASB2Q817MW', '2025-12-24 08:39:08.606+00', '2025-12-24 08:39:08.606+00', NULL, 'https://storage.danakcorp.com/images/1766565547983-878658938.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R6KF9YWZBZ3WPAKXQS2CH', '2025-12-24 08:39:23.881+00', '2025-12-24 08:39:23.881+00', NULL, 'https://storage.danakcorp.com/images/1766565563263-891688321.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R7RM02R8FZ4DN65WMJVE8', '2025-12-24 08:40:01.92+00', '2025-12-24 08:40:01.92+00', NULL, 'https://storage.danakcorp.com/images/1766565600547-704332792.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R8G53XBC6AG6KAVSBW94G', '2025-12-24 08:40:26.019+00', '2025-12-24 08:40:26.019+00', NULL, 'https://storage.danakcorp.com/images/1766565625437-528383713.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R901TPNAX9YYRG6W0GWHF', '2025-12-24 08:40:42.298+00', '2025-12-24 08:40:42.298+00', NULL, 'https://storage.danakcorp.com/images/1766565641620-53176909.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R9PKF9G1DD1HMS2QQQN9V', '2025-12-24 08:41:05.391+00', '2025-12-24 08:41:05.391+00', NULL, 'https://storage.danakcorp.com/images/1766565664427-433669317.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RK2G6E5JKEDK9P95W79PR', '2025-12-24 08:46:12.487+00', '2025-12-24 08:46:12.487+00', NULL, 'https://storage.danakcorp.com/images/1766565971515-159903323.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RKSM9QD6RA2H6WGE23KEC', '2025-12-24 08:46:36.169+00', '2025-12-24 08:46:36.169+00', NULL, 'https://storage.danakcorp.com/images/1766565995424-288101561.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RMR9DRA1TG2BJ9Q8QAZ15', '2025-12-24 08:47:07.565+00', '2025-12-24 08:47:07.565+00', NULL, 'https://storage.danakcorp.com/images/1766566026942-868807643.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RN6FH0TX69RZTKJKM6MEN', '2025-12-24 08:47:22.097+00', '2025-12-24 08:47:22.097+00', NULL, 'https://storage.danakcorp.com/images/1766566041525-973019175.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RNX4EE92QR2C24577BJCW', '2025-12-24 08:47:45.294+00', '2025-12-24 08:47:45.294+00', NULL, 'https://storage.danakcorp.com/images/1766566064741-271844433.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RPHHCKNXNQH29WSG84D2Z', '2025-12-24 08:48:06.189+00', '2025-12-24 08:48:06.189+00', NULL, 'https://storage.danakcorp.com/images/1766566084368-438871956.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RQ6MFA5CDQ4J55NG3N7PX', '2025-12-24 08:48:27.791+00', '2025-12-24 08:48:27.791+00', NULL, 'https://storage.danakcorp.com/images/1766566106954-47179607.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RQNXJNRXS8MXV57R50PKN', '2025-12-24 08:48:43.443+00', '2025-12-24 08:48:43.443+00', NULL, 'https://storage.danakcorp.com/images/1766566122836-486871124.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RR9W6B9BTWJ0N2H11TWM1', '2025-12-24 08:49:03.879+00', '2025-12-24 08:49:03.879+00', NULL, 'https://storage.danakcorp.com/images/1766566143171-668309076.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RRVQWYHSHN3M7R25GNGHS', '2025-12-24 08:49:22.172+00', '2025-12-24 08:49:22.172+00', NULL, 'https://storage.danakcorp.com/images/1766566161551-844376214.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RSAKAP1TJ59GN11DEAXD9', '2025-12-24 08:49:37.386+00', '2025-12-24 08:49:37.386+00', NULL, 'https://storage.danakcorp.com/images/1766566176755-30806596.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RSV35PY1A6193H4GDW1ZW', '2025-12-24 08:49:54.277+00', '2025-12-24 08:49:54.277+00', NULL, 'https://storage.danakcorp.com/images/1766566193720-339416210.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RWX3D4ZZE35W345HDK35S', '2025-12-24 08:51:34.638+00', '2025-12-24 08:51:34.638+00', NULL, 'https://storage.danakcorp.com/images/1766566293736-981836756.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RX79BVR30WGKXAZQ3D0CM', '2025-12-24 08:51:45.068+00', '2025-12-24 08:51:45.068+00', NULL, 'https://storage.danakcorp.com/images/1766566304318-193301989.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RXY99C8PV6SAR0AJ00F16', '2025-12-24 08:52:08.618+00', '2025-12-24 08:52:08.618+00', NULL, 'https://storage.danakcorp.com/images/1766566327768-2675508.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RYBPV273KCR350ESK2FN1', '2025-12-24 08:52:22.364+00', '2025-12-24 08:52:22.364+00', NULL, 'https://storage.danakcorp.com/images/1766566341717-462491426.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RYTNTE20YS29X2XJT7GY3', '2025-12-24 08:52:37.69+00', '2025-12-24 08:52:37.69+00', NULL, 'https://storage.danakcorp.com/images/1766566357028-432111036.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RZ8K86GCJSW1E0EC5T4B2', '2025-12-24 08:52:51.944+00', '2025-12-24 08:52:51.944+00', NULL, 'https://storage.danakcorp.com/images/1766566371354-279596850.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RZPNGASPYFP9XKMSY208B', '2025-12-24 08:53:06.352+00', '2025-12-24 08:53:06.352+00', NULL, 'https://storage.danakcorp.com/images/1766566385674-569493499.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S0HEQZ4AY5TTFKF36HS6F', '2025-12-24 08:53:33.783+00', '2025-12-24 08:53:33.783+00', NULL, 'https://storage.danakcorp.com/images/1766566413058-975642584.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S0ZEQ6BH163HCX0MQZHBJ', '2025-12-24 08:53:48.119+00', '2025-12-24 08:53:48.119+00', NULL, 'https://storage.danakcorp.com/images/1766566427440-982841441.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S1EF6R6J6AMMC6HP5J0AA', '2025-12-24 08:54:03.494+00', '2025-12-24 08:54:03.494+00', NULL, 'https://storage.danakcorp.com/images/1766566442808-761588425.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S27EFH2CDEK71GVQC839G', '2025-12-24 08:54:29.074+00', '2025-12-24 08:54:29.074+00', NULL, 'https://storage.danakcorp.com/images/1766566468108-519151895.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S2N705Z754YS3YFQW4AGQ', '2025-12-24 08:54:43.169+00', '2025-12-24 08:54:43.169+00', NULL, 'https://storage.danakcorp.com/images/1766566482278-661272876.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S31HNKPAAK58CX77BNJCA', '2025-12-24 08:54:55.798+00', '2025-12-24 08:54:55.798+00', NULL, 'https://storage.danakcorp.com/images/1766566494050-408184442.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S3AX9VW5B830MYJYGXQ0B', '2025-12-24 08:55:05.385+00', '2025-12-24 08:55:05.385+00', NULL, 'https://storage.danakcorp.com/images/1766566504760-709260995.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
--
|
||||
|
||||
\unrestrict 4pvnNoptyhScdLjLBpsonU6ZnOH502ap2y2am0YHcRG7WkY1NxnW43VtUD3Gq85
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
Index,Name,URL
|
||||
1,bread,https://storage.danakcorp.com/images/1766554365832-411750172.svg
|
||||
2,avocado,https://storage.danakcorp.com/images/1766554379149-104970938.svg
|
||||
3,knife,https://storage.danakcorp.com/images/1766554389090-593863597.svg
|
||||
4,baby bottle,https://storage.danakcorp.com/images/1766554404968-905984091.svg
|
||||
5,baby bottle,https://storage.danakcorp.com/images/1766554415325-873800591.svg
|
||||
6,baby pacifier,https://storage.danakcorp.com/images/1766554428814-77027818.svg
|
||||
7,banana,https://storage.danakcorp.com/images/1766554438632-45072052.svg
|
||||
8,barbecue,https://storage.danakcorp.com/images/1766554446340-463157109.svg
|
||||
9,barbeque2,https://storage.danakcorp.com/images/1766554456605-280166176.svg
|
||||
10,drink,https://storage.danakcorp.com/images/1766554471887-315640495.svg
|
||||
11,hanburger,https://storage.danakcorp.com/images/1766554485634-242317100.svg
|
||||
12,drink2,https://storage.danakcorp.com/images/1766554504553-157481346.svg
|
||||
13,bottle1,https://storage.danakcorp.com/images/1766554528198-919877647.svg
|
||||
14,bottle2,https://storage.danakcorp.com/images/1766554544140-886744157.svg
|
||||
15,broccoli,https://storage.danakcorp.com/images/1766554565614-684869010.svg
|
||||
16,cake1,https://storage.danakcorp.com/images/1766554578855-318583145.svg
|
||||
17,cake2,https://storage.danakcorp.com/images/1766554593272-362758712.svg
|
||||
18,cake piece,https://storage.danakcorp.com/images/1766554606166-877198688.svg
|
||||
19,cup cake,https://storage.danakcorp.com/images/1766554624133-21439996.svg
|
||||
20,caupcake2,https://storage.danakcorp.com/images/1766554641691-535696200.svg
|
||||
21,candle,https://storage.danakcorp.com/images/1766554658481-114730426.svg
|
||||
22,candy,https://storage.danakcorp.com/images/1766554671760-634916570.svg
|
||||
23,carrot,https://storage.danakcorp.com/images/1766554967381-660612838.svg
|
||||
24,cheese,https://storage.danakcorp.com/images/1766555106925-267924006.svg
|
||||
25,cheese2,https://storage.danakcorp.com/images/1766555231674-387319393.svg
|
||||
26,hat robe,https://storage.danakcorp.com/images/1766555290519-155637622.svg
|
||||
27,hat robe 2,https://storage.danakcorp.com/images/1766555308301-230359494.svg
|
||||
28,cherry,https://storage.danakcorp.com/images/1766555420851-186440015.svg
|
||||
29,chicken,https://storage.danakcorp.com/images/1766555590828-644527730.svg
|
||||
30,baken,https://storage.danakcorp.com/images/1766555606997-571682462.svg
|
||||
31,lollipop,https://storage.danakcorp.com/images/1766555663659-433052581.svg
|
||||
32,cookie1,https://storage.danakcorp.com/images/1766555713098-988242030.svg
|
||||
33,cookie2,https://storage.danakcorp.com/images/1766555726158-680948882.svg
|
||||
34,crosson,https://storage.danakcorp.com/images/1766555746650-426516324.svg
|
||||
35,juice,https://storage.danakcorp.com/images/1766555765798-127083646.svg
|
||||
36,juice2,https://storage.danakcorp.com/images/1766555784837-243715698.svg
|
||||
37,juice3,https://storage.danakcorp.com/images/1766555799191-594035927.svg
|
||||
38,spoon and fork 1,https://storage.danakcorp.com/images/1766555894624-524350777.svg
|
||||
39,spoon and fork 2,https://storage.danakcorp.com/images/1766555932114-485954688.svg
|
||||
40,spoon and knife,https://storage.danakcorp.com/images/1766555949650-909996041.svg
|
||||
41,fork and knife,https://storage.danakcorp.com/images/1766555966861-75517122.svg
|
||||
42,hot dog,https://storage.danakcorp.com/images/1766556006875-239689681.svg
|
||||
43,soap,https://storage.danakcorp.com/images/1766556257163-459958715.svg
|
||||
44,dish,https://storage.danakcorp.com/images/1766556293812-665835248.svg
|
||||
45,donuts,https://storage.danakcorp.com/images/1766556307082-162280793.svg
|
||||
46,egg plant,https://storage.danakcorp.com/images/1766556340402-744632392.svg
|
||||
47,egg,https://storage.danakcorp.com/images/1766556458335-857124841.svg
|
||||
48,fast-food,https://storage.danakcorp.com/images/1766556479530-161860621.svg
|
||||
49,taco,https://storage.danakcorp.com/images/1766556498215-91175544.svg
|
||||
50,fire,https://storage.danakcorp.com/images/1766556677355-108854669.svg
|
||||
51,fish,https://storage.danakcorp.com/images/1766556736283-691587656.svg
|
||||
52,fish,https://storage.danakcorp.com/images/1766556759367-58066921.svg
|
||||
53,french fries,https://storage.danakcorp.com/images/1766556777353-490756125.svg
|
||||
54,fried egg,https://storage.danakcorp.com/images/1766556870740-118553581.svg
|
||||
55,refrigerator,https://storage.danakcorp.com/images/1766556890334-962678132.svg
|
||||
56,grape,https://storage.danakcorp.com/images/1766556921029-865260745.svg
|
||||
57,hat robe,https://storage.danakcorp.com/images/1766556946363-320833501.svg
|
||||
58,hot dog,https://storage.danakcorp.com/images/1766556973771-832565827.svg
|
||||
59,ice cream,https://storage.danakcorp.com/images/1766557056358-727862983.svg
|
||||
60,ice cream,https://storage.danakcorp.com/images/1766557199259-72955190.svg
|
||||
61,ice cream 3,https://storage.danakcorp.com/images/1766557252452-988016525.svg
|
||||
62,jar,https://storage.danakcorp.com/images/1766557274778-439028456.svg
|
||||
63,jelly,https://storage.danakcorp.com/images/1766557289094-415104854.svg
|
||||
64,juice1,https://storage.danakcorp.com/images/1766557300923-302277606.svg
|
||||
65,juice 2,https://storage.danakcorp.com/images/1766557317091-602116315.svg
|
||||
66,kitchen stove,https://storage.danakcorp.com/images/1766557647999-47333939.svg
|
||||
67,orange slash,https://storage.danakcorp.com/images/1766557669282-172512141.svg
|
||||
68,loaf of bread,https://storage.danakcorp.com/images/1766557705283-692546793.svg
|
||||
69,loaf of bread 2,https://storage.danakcorp.com/images/1766557729175-762576743.svg
|
||||
70,meat,https://storage.danakcorp.com/images/1766557756487-665300947.svg
|
||||
71,meat 2,https://storage.danakcorp.com/images/1766557776067-804149228.svg
|
||||
72,mixer,https://storage.danakcorp.com/images/1766557797509-101726352.svg
|
||||
73,mortar,https://storage.danakcorp.com/images/1766557816484-357699340.svg
|
||||
74,noodle,https://storage.danakcorp.com/images/1766557837292-326521286.svg
|
||||
75,nuts,https://storage.danakcorp.com/images/1766559083575-564721059.svg
|
||||
76,olive,https://storage.danakcorp.com/images/1766559099561-720966516.svg
|
||||
77,onion,https://storage.danakcorp.com/images/1766559116496-834964990.svg
|
||||
78,pizza,https://storage.danakcorp.com/images/1766563021426-600579018.svg
|
||||
79,rolling pin,https://storage.danakcorp.com/images/1766563038525-727531190.svg
|
||||
80,solt,https://storage.danakcorp.com/images/1766563051485-824602001.svg
|
||||
81,scales,https://storage.danakcorp.com/images/1766563066017-372210647.svg
|
||||
82,basket,https://storage.danakcorp.com/images/1766563124296-331607800.svg
|
||||
83,skewer,https://storage.danakcorp.com/images/1766563142133-359920423.svg
|
||||
84,strawberry,https://storage.danakcorp.com/images/1766563164270-350101986.svg
|
||||
85,toast,https://storage.danakcorp.com/images/1766563181587-86630830.svg
|
||||
86,watermelon,https://storage.danakcorp.com/images/1766563204966-651499380.svg
|
||||
87,whisk,https://storage.danakcorp.com/images/1766563220596-103699525.svg
|
||||
88,wine glass,https://storage.danakcorp.com/images/1766563265634-373953449.svg
|
||||
89,wine glass 2,https://storage.danakcorp.com/images/1766563275517-958045207.svg
|
||||
90,wine glass 3,https://storage.danakcorp.com/images/1766563286526-622849562.svg
|
||||
91,colored bakeries,https://storage.danakcorp.com/images/1766564695339-504553420.svg
|
||||
92,colored bio,https://storage.danakcorp.com/images/1766564706032-159367016.svg
|
||||
93,colored bibimbap,https://storage.danakcorp.com/images/1766564720683-887818252.svg
|
||||
94,colored boba,https://storage.danakcorp.com/images/1766564734432-483528219.svg
|
||||
95,colored breakfast,https://storage.danakcorp.com/images/1766564750099-854146988.svg
|
||||
96,colored hamburger,https://storage.danakcorp.com/images/1766564758573-696060786.svg
|
||||
97,colored hamburger 2,https://storage.danakcorp.com/images/1766564805823-829938380.svg
|
||||
98,colored coffee,https://storage.danakcorp.com/images/1766564891137-468827500.svg
|
||||
99,colored coca,https://storage.danakcorp.com/images/1766564907215-921380792.svg
|
||||
100,colored cookies,https://storage.danakcorp.com/images/1766564926872-672612697.svg
|
||||
101,colored noodle,https://storage.danakcorp.com/images/1766565459604-638026702.svg
|
||||
102,colored crossan,https://storage.danakcorp.com/images/1766565476771-42527917.svg
|
||||
103,colored sweet,https://storage.danakcorp.com/images/1766565491264-946731491.svg
|
||||
104,colored dumpling,https://storage.danakcorp.com/images/1766565502475-288189136.svg
|
||||
105,colored fish and chips,https://storage.danakcorp.com/images/1766565530628-800474580.svg
|
||||
106,colored focaccia,https://storage.danakcorp.com/images/1766565547983-878658938.svg
|
||||
107,colored french fries,https://storage.danakcorp.com/images/1766565563263-891688321.svg
|
||||
108,colored fried chicken,https://storage.danakcorp.com/images/1766565600547-704332792.svg
|
||||
109,colored fried rice,https://storage.danakcorp.com/images/1766565625437-528383713.svg
|
||||
110,colored fries,https://storage.danakcorp.com/images/1766565641620-53176909.svg
|
||||
111,hat robe,https://storage.danakcorp.com/images/1766565664427-433669317.svg
|
||||
112,colored hot dog,https://storage.danakcorp.com/images/1766565971515-159903323.svg
|
||||
113,colored hot dog,https://storage.danakcorp.com/images/1766565995424-288101561.svg
|
||||
114,colored hot pot,https://storage.danakcorp.com/images/1766566026942-868807643.svg
|
||||
115,colored ice cream,https://storage.danakcorp.com/images/1766566041525-973019175.svg
|
||||
116,colored ice tea,https://storage.danakcorp.com/images/1766566064741-271844433.svg
|
||||
117,colored instant noodle cup,https://storage.danakcorp.com/images/1766566084368-438871956.svg
|
||||
118,lemon,https://storage.danakcorp.com/images/1766566106954-47179607.svg
|
||||
119,colored martini,https://storage.danakcorp.com/images/1766566122836-486871124.svg
|
||||
120,colored match,https://storage.danakcorp.com/images/1766566143171-668309076.svg
|
||||
121,colored mochi donuts,https://storage.danakcorp.com/images/1766566161551-844376214.svg
|
||||
122,colored mochi,https://storage.danakcorp.com/images/1766566176755-30806596.svg
|
||||
123,colored nachos,https://storage.danakcorp.com/images/1766566193720-339416210.svg
|
||||
124,colored origini,https://storage.danakcorp.com/images/1766566293736-981836756.svg
|
||||
125,colored pan cake,https://storage.danakcorp.com/images/1766566304318-193301989.svg
|
||||
126,colored pepperoni pizza,https://storage.danakcorp.com/images/1766566327768-2675508.svg
|
||||
127,colored pizza 4,https://storage.danakcorp.com/images/1766566341717-462491426.svg
|
||||
128,colored podding,https://storage.danakcorp.com/images/1766566357028-432111036.svg
|
||||
129,quesadilla,https://storage.danakcorp.com/images/1766566371354-279596850.svg
|
||||
130,salad,https://storage.danakcorp.com/images/1766566385674-569493499.svg
|
||||
131,colored sandwich,https://storage.danakcorp.com/images/1766566413058-975642584.svg
|
||||
132,colored stak,https://storage.danakcorp.com/images/1766566427440-982841441.svg
|
||||
133,colored soshi,https://storage.danakcorp.com/images/1766566442808-761588425.svg
|
||||
134,colored taco,https://storage.danakcorp.com/images/1766566468108-519151895.svg
|
||||
135,colored tempura,https://storage.danakcorp.com/images/1766566482278-661272876.svg
|
||||
136,colored tafu,https://storage.danakcorp.com/images/1766566494050-408184442.svg
|
||||
137,whisk,https://storage.danakcorp.com/images/1766566504760-709260995.svg
|
||||
|
@@ -0,0 +1,58 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Read the CSV file
|
||||
const csvPath = path.join(__dirname, 'icons.csv');
|
||||
const csvContent = fs.readFileSync(csvPath, 'utf8');
|
||||
|
||||
// Parse CSV lines
|
||||
const lines = csvContent.split('\n').filter(line => line.trim());
|
||||
const header = lines[0];
|
||||
const dataLines = lines.slice(1);
|
||||
|
||||
// Icon names - replace these with your actual icon names after reviewing them
|
||||
const iconNames = [
|
||||
// Add your icon names here, one for each row
|
||||
// Example: "hamburger", "pizza", "drink", etc.
|
||||
// For now, placeholders:
|
||||
"icon_1", "icon_2", "icon_3", "icon_4", "icon_5", "icon_6", "icon_7", "icon_8", "icon_9", "icon_10",
|
||||
"icon_11", "icon_12", "icon_13", "icon_14", "icon_15", "icon_16", "icon_17", "icon_18", "icon_19", "icon_20",
|
||||
"icon_21", "icon_22", "icon_23", "icon_24", "icon_25", "icon_26", "icon_27", "icon_28", "icon_29", "icon_30",
|
||||
"icon_31", "icon_32", "icon_33", "icon_34", "icon_35", "icon_36", "icon_37", "icon_38", "icon_39", "icon_40",
|
||||
"icon_41", "icon_42", "icon_43", "icon_44", "icon_45", "icon_46", "icon_47", "icon_48", "icon_49", "icon_50",
|
||||
"icon_51", "icon_52", "icon_53", "icon_54", "icon_55", "icon_56", "icon_57", "icon_58", "icon_59", "icon_60",
|
||||
"icon_61", "icon_62", "icon_63", "icon_64", "icon_65", "icon_66", "icon_67", "icon_68", "icon_69", "icon_70",
|
||||
"icon_71", "icon_72", "icon_73", "icon_74", "icon_75", "icon_76", "icon_77", "icon_78", "icon_79", "icon_80",
|
||||
"icon_81", "icon_82", "icon_83", "icon_84", "icon_85", "icon_86", "icon_87", "icon_88", "icon_89", "icon_90",
|
||||
"icon_91", "icon_92", "icon_93", "icon_94", "icon_95", "icon_96", "icon_97", "icon_98", "icon_99", "icon_100",
|
||||
"icon_101", "icon_102", "icon_103", "icon_104", "icon_105", "icon_106", "icon_107", "icon_108", "icon_109", "icon_110",
|
||||
"icon_111", "icon_112", "icon_113", "icon_114", "icon_115", "icon_116", "icon_117", "icon_118", "icon_119", "icon_120",
|
||||
"icon_121", "icon_122", "icon_123", "icon_124", "icon_125", "icon_126", "icon_127", "icon_128", "icon_129", "icon_130",
|
||||
"icon_131", "icon_132", "icon_133", "icon_134", "icon_135", "icon_136", "icon_137", "icon_138"
|
||||
];
|
||||
|
||||
// Validate that we have the right number of names
|
||||
if (iconNames.length !== dataLines.length) {
|
||||
console.error(`Error: You provided ${iconNames.length} icon names but there are ${dataLines.length} data rows in the CSV.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create new CSV content with icon names
|
||||
const newLines = [header];
|
||||
|
||||
dataLines.forEach((line, index) => {
|
||||
// Remove trailing comma if present and add the icon name
|
||||
const cleanLine = line.replace(/,$/, '');
|
||||
newLines.push(`${cleanLine},"${iconNames[index]}"`);
|
||||
});
|
||||
|
||||
// Write the updated CSV
|
||||
const newCsvContent = newLines.join('\n');
|
||||
fs.writeFileSync(csvPath, newCsvContent, 'utf8');
|
||||
|
||||
console.log(`✅ Successfully added ${iconNames.length} icon names to icons.csv`);
|
||||
console.log('📁 File saved:', csvPath);
|
||||
|
||||
// Show summary
|
||||
const namedCount = iconNames.filter(name => name && !name.startsWith('icon_')).length;
|
||||
console.log(`📊 ${namedCount} custom names, ${iconNames.length - namedCount} placeholder names`);
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to restore icon and icon_groups tables to PostgreSQL database
|
||||
# Usage: ./restore-icons.sh [dump_file]
|
||||
|
||||
set -e
|
||||
|
||||
# Load environment variables if .env file exists
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
# Source .env file, handling comments and empty lines
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
# Remove leading/trailing whitespace
|
||||
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
# Skip empty lines and lines starting with #
|
||||
[[ -z "$line" ]] && continue
|
||||
[[ "$line" =~ ^# ]] && continue
|
||||
# Remove inline comments (everything after # that's not in quotes)
|
||||
line=$(echo "$line" | sed 's/#.*$//' | sed 's/[[:space:]]*$//')
|
||||
# Skip if line is empty after removing comments
|
||||
[[ -z "$line" ]] && continue
|
||||
# Export the variable (only if it looks like KEY=VALUE)
|
||||
if [[ "$line" =~ ^[[:alpha:]_][[:alnum:]_]*= ]]; then
|
||||
export "$line" 2>/dev/null || true
|
||||
fi
|
||||
done < .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
# Get database connection details from environment variables
|
||||
DB_HOST="${DB_HOST:-localhost}"
|
||||
DB_PORT="${DB_PORT:-5432}"
|
||||
DB_NAME="${DB_NAME:-dmenu}"
|
||||
DB_USER="${DB_USER:-postgres}"
|
||||
|
||||
# Input file name
|
||||
if [ -z "$1" ]; then
|
||||
echo "Error: Please provide a dump file to restore"
|
||||
echo "Usage: ./restore-icons.sh <dump_file>"
|
||||
echo "Example: ./restore-icons.sh icons_dump_20240101_120000.sql"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DUMP_FILE="$1"
|
||||
|
||||
# Check if dump file exists
|
||||
if [ ! -f "${DUMP_FILE}" ]; then
|
||||
echo "Error: Dump file '${DUMP_FILE}' not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Restoring icon_groups and icons tables..."
|
||||
echo "Database: ${DB_NAME}@${DB_HOST}:${DB_PORT}"
|
||||
echo "Dump file: ${DUMP_FILE}"
|
||||
echo ""
|
||||
|
||||
# Ask for confirmation
|
||||
read -p "This will replace existing data in icon_groups and icons tables. Continue? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Restore cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Disable foreign key checks temporarily and truncate tables
|
||||
echo "Clearing existing data..."
|
||||
PGPASSWORD="${DB_PASS}" psql \
|
||||
-h "${DB_HOST}" \
|
||||
-p "${DB_PORT}" \
|
||||
-U "${DB_USER}" \
|
||||
-d "${DB_NAME}" \
|
||||
-c "TRUNCATE TABLE icons CASCADE;" \
|
||||
-c "TRUNCATE TABLE icon_groups CASCADE;"
|
||||
|
||||
# Restore data from dump file
|
||||
echo "Restoring data from ${DUMP_FILE}..."
|
||||
PGPASSWORD="${DB_PASS}" psql \
|
||||
-h "${DB_HOST}" \
|
||||
-p "${DB_PORT}" \
|
||||
-U "${DB_USER}" \
|
||||
-d "${DB_NAME}" \
|
||||
-f "${DUMP_FILE}"
|
||||
|
||||
echo ""
|
||||
echo "Restore completed successfully!"
|
||||
echo ""
|
||||
|
||||
# Verify the restore
|
||||
echo "Verifying restore..."
|
||||
ICON_GROUPS_COUNT=$(PGPASSWORD="${DB_PASS}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" -t -c "SELECT COUNT(*) FROM icon_groups;")
|
||||
ICONS_COUNT=$(PGPASSWORD="${DB_PASS}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" -t -c "SELECT COUNT(*) FROM icons;")
|
||||
|
||||
echo "icon_groups records: ${ICON_GROUPS_COUNT}"
|
||||
echo "icons records: ${ICONS_COUNT}"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['eslint.config.mjs'],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
eslintPluginPrettierRecommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
sourceType: 'commonjs',
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
Generated
+17122
File diff suppressed because it is too large
Load Diff
+124
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"name": "base-api",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"db:create": "npx mikro-orm schema:create --run --config ./src/config/mikro-orm.config.dev.ts",
|
||||
"db:update": "npx mikro-orm schema:update --run --config ./src/config/mikro-orm.config.dev.ts ",
|
||||
"db:drop": "npx mikro-orm schema:drop --run --config ./src/config/mikro-orm.config.dev.ts",
|
||||
"db:seed": "npx mikro-orm seeder:run --config ./src/config/mikro-orm.config.dev.ts",
|
||||
"db:reset": "npm run db:drop && npm run db:create && npm run db:seed",
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start dist/main",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node --max-old-space-size=2048 dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"migration:create": "npx mikro-orm migration:create --config ./src/config/mikro-orm.config.dev.ts",
|
||||
"migration:up": "npx mikro-orm migration:up --config ./src/config/mikro-orm.config.dev.ts",
|
||||
"migration:down": "npx mikro-orm migration:down --config ./src/config/mikro-orm.config.dev.ts",
|
||||
"migration:list": "npx mikro-orm migration:list --config ./src/config/mikro-orm.config.dev.ts",
|
||||
"migration:pending": "npx mikro-orm migration:pending --config ./src/config/mikro-orm.config.dev.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/server": "^5.1.0",
|
||||
"@as-integrations/fastify": "^3.1.0",
|
||||
"@aws-sdk/client-s3": "^3.922.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.922.0",
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/multipart": "^9.3.0",
|
||||
"@fastify/static": "^8.3.0",
|
||||
"@keyv/redis": "^5.1.3",
|
||||
"@mikro-orm/core": "^6.5.8",
|
||||
"@mikro-orm/nestjs": "^6.1.1",
|
||||
"@mikro-orm/postgresql": "^6.5.8",
|
||||
"@nest-lab/fastify-multer": "^1.3.0",
|
||||
"@nestjs/axios": "^4.0.1",
|
||||
"@nestjs/bullmq": "^11.0.4",
|
||||
"@nestjs/cache-manager": "^3.0.1",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/event-emitter": "^3.0.1",
|
||||
"@nestjs/jwt": "^11.0.1",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/platform-fastify": "^11.1.8",
|
||||
"@nestjs/platform-socket.io": "^11.1.9",
|
||||
"@nestjs/schedule": "^6.0.1",
|
||||
"@nestjs/swagger": "^11.2.1",
|
||||
"@nestjs/throttler": "^6.4.0",
|
||||
"@types/socket.io": "^3.0.1",
|
||||
"axios": "^1.13.1",
|
||||
"bullmq": "^5.65.1",
|
||||
"cache-manager": "^7.2.4",
|
||||
"cacheable": "^2.3.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"dayjs": "^1.11.19",
|
||||
"fastify": "^5.6.1",
|
||||
"firebase-admin": "^13.6.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"slugify": "^1.6.6",
|
||||
"socket.io": "^4.8.1",
|
||||
"ulid": "^3.0.1",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@mikro-orm/cli": "^6.5.8",
|
||||
"@mikro-orm/migrations": "^6.5.8",
|
||||
"@mikro-orm/seeder": "^6.5.8",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
"globals": "^16.0.0",
|
||||
"jest": "^30.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
Generated
+14002
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,67 @@
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Module } from '@nestjs/common';
|
||||
import dataBaseConfig from './config/mikro-orm.config';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { UserModule } from './modules/users/user.module';
|
||||
import { UtilsModule } from './modules/utils/utils.module';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { UploaderModule } from './modules/uploader/uploader.module';
|
||||
import { AdminModule } from './modules/admin/admin.module';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { RestaurantsModule } from './modules/restaurants/restaurants.module';
|
||||
import { FoodModule } from './modules/foods/food.module';
|
||||
import { CartModule } from './modules/cart/cart.module';
|
||||
import { RolesModule } from './modules/roles/roles.module';
|
||||
import { PaymentsModule } from './modules/payments/payments.module';
|
||||
import { DeliveryModule } from './modules/delivery/delivery.module';
|
||||
import { OrdersModule } from './modules/orders/orders.module';
|
||||
import { CouponModule } from './modules/coupons/coupon.module';
|
||||
import { ReviewModule } from './modules/review/review.module';
|
||||
import { NotificationsModule } from './modules/notifications/notifications.module';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { PagerModule } from './modules/pager/pager.module';
|
||||
import { ContactModule } from './modules/contact/contact.module';
|
||||
import { InventoryModule } from './modules/inventory/inventory.module';
|
||||
import { IconsModule } from './modules/icons/icons.module';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import { cacheConfig } from './config/cache.config';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true, cache: true }),
|
||||
CacheModule.registerAsync(cacheConfig()),
|
||||
MikroOrmModule.forRootAsync(dataBaseConfig),
|
||||
UserModule,
|
||||
UtilsModule,
|
||||
AuthModule,
|
||||
UploaderModule,
|
||||
AdminModule,
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 60, // time window in seconds
|
||||
limit: 5, // max requests per window
|
||||
},
|
||||
]),
|
||||
ScheduleModule.forRoot(),
|
||||
RestaurantsModule,
|
||||
FoodModule,
|
||||
CartModule,
|
||||
RolesModule,
|
||||
PaymentsModule,
|
||||
DeliveryModule,
|
||||
OrdersModule,
|
||||
CouponModule,
|
||||
ReviewModule,
|
||||
NotificationsModule,
|
||||
EventEmitterModule.forRoot(),
|
||||
PagerModule,
|
||||
ContactModule,
|
||||
InventoryModule,
|
||||
IconsModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
// exports: [CacheService],
|
||||
})
|
||||
export class AppModule {}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
// export const AUTH_THROTTLE = "AUTH_THROTTLE";
|
||||
export const AI_CONFIG = 'AI_CONFIG';
|
||||
export const MIKRO_ORM_QUERY_LOGGER = 'MIKRO_ORM_QUERY_LOGGER';
|
||||
export const NAJVA_CONFIG = 'NAJVA_CONFIG';
|
||||
export const AUTH_THROTTLE_TTL = 1 * 60 * 1000;
|
||||
export const AUTH_THROTTLE_LIMIT = 5;
|
||||
export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000;
|
||||
export const AUTH__REFRESH_THROTTLE_LIMIT = 10;
|
||||
//
|
||||
export const CONSOLE_JWT_STRATEGY_NAME = 'console_jwt_strategy';
|
||||
export const LOCAL_JWT_STRATEGY_NAME = 'local_jwt_strategy';
|
||||
|
||||
export const API_HEADER_SLUG = {
|
||||
name: 'X-Slug',
|
||||
required: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
default: 'zhivan',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
|
||||
/**
|
||||
* Decorator to extract userId from the authenticated request.
|
||||
* Must be used after AdminAuthGuard or AuthGuard that sets request.userId.
|
||||
*
|
||||
* @example
|
||||
* @Get('/profile')
|
||||
* @UseGuards(AdminAuthGuard)
|
||||
* getProfile(@UserId() userId: string) {
|
||||
* return this.userService.findById(userId);
|
||||
* }
|
||||
*/
|
||||
export const AdminId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
|
||||
const request = ctx.switchToHttp().getRequest<Request & { adminId?: string }>();
|
||||
return request.adminId || '';
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export { UserId } from './user-id.decorator';
|
||||
export { RestId } from './rest-id.decorator';
|
||||
export { RateLimit } from './rate-limit.decorator';
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const PERMISSIONS_KEY = 'permissions';
|
||||
|
||||
export const Permissions = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, permissions);
|
||||
@@ -0,0 +1,18 @@
|
||||
import { UseGuards, applyDecorators } from '@nestjs/common';
|
||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
||||
|
||||
import {
|
||||
AUTH_THROTTLE_LIMIT,
|
||||
AUTH_THROTTLE_TTL,
|
||||
AUTH__REFRESH_THROTTLE_LIMIT,
|
||||
AUTH__REFRESH_THROTTLE_TTL,
|
||||
} from '../constants';
|
||||
|
||||
export const RateLimit = (limit: number, ttl: number) =>
|
||||
applyDecorators(Throttle({ default: { limit, ttl } }), UseGuards(ThrottlerGuard));
|
||||
|
||||
// Predefined rate limits for common scenarios
|
||||
export const StrictRateLimit = () => RateLimit(AUTH_THROTTLE_LIMIT, AUTH_THROTTLE_TTL); // 5 requests per minute
|
||||
export const StandardRateLimit = () => RateLimit(30, 60000); // 30 requests per minute
|
||||
export const MailSendRateLimit = () => RateLimit(10, 60000); // 10 emails per minute
|
||||
export const RefreshTokenRateLimit = () => RateLimit(AUTH__REFRESH_THROTTLE_LIMIT, AUTH__REFRESH_THROTTLE_TTL); // 10 emails per minute
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
|
||||
/**
|
||||
* Decorator to extract restId from the authenticated request.
|
||||
* Must be used after AdminAuthGuard or AuthGuard that sets request.restId.
|
||||
*
|
||||
* @example
|
||||
* @Get('/restaurants')
|
||||
* @UseGuards(AdminAuthGuard)
|
||||
* getRestaurants(@RestId() restId: string) {
|
||||
* return this.restaurantService.findById(restId);
|
||||
* }
|
||||
*/
|
||||
export const RestId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
|
||||
const request = ctx.switchToHttp().getRequest<Request & { restId?: string }>();
|
||||
return request.restId || '';
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
|
||||
export const RestSlug = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
|
||||
const request = ctx.switchToHttp().getRequest<Request & { slug?: string }>();
|
||||
return request.slug || '';
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
|
||||
/**
|
||||
* Decorator to extract userId from the authenticated request.
|
||||
* Must be used after AdminAuthGuard or AuthGuard that sets request.userId.
|
||||
*
|
||||
* @example
|
||||
* @Get('/profile')
|
||||
* @UseGuards(AdminAuthGuard)
|
||||
* getProfile(@UserId() userId: string) {
|
||||
* return this.userService.findById(userId);
|
||||
* }
|
||||
*/
|
||||
export const UserId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
|
||||
const request = ctx.switchToHttp().getRequest<Request & { userId?: string }>();
|
||||
return request.userId || '';
|
||||
});
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
import { Index, PrimaryKey, Property, Filter, OptionalProps } from '@mikro-orm/core';
|
||||
import { ulid } from 'ulid';
|
||||
|
||||
@Filter({ name: 'notDeleted', cond: { deletedAt: null }, default: true })
|
||||
@Index({ properties: ['deletedAt'] })
|
||||
@Index({ properties: ['createdAt'] })
|
||||
export abstract class BaseEntity {
|
||||
[OptionalProps]?: 'id' | 'createdAt' | 'updatedAt' | 'deletedAt';
|
||||
|
||||
@PrimaryKey({ type: 'string', columnType: 'char(26)' })
|
||||
id: string = ulid();
|
||||
|
||||
@Property({ defaultRaw: 'now()', columnType: 'timestamptz' })
|
||||
createdAt: Date = new Date();
|
||||
|
||||
@Property({
|
||||
onUpdate: () => new Date(),
|
||||
defaultRaw: 'now()',
|
||||
columnType: 'timestamptz',
|
||||
})
|
||||
updatedAt: Date = new Date();
|
||||
|
||||
@Property({ nullable: true })
|
||||
deletedAt?: Date;
|
||||
}
|
||||
Executable
+780
@@ -0,0 +1,780 @@
|
||||
export const enum AuthMessage {
|
||||
UNAUTHORIZED_ACCESS = 'شما دسترسی لازم برای این عملیات را ندارید.',
|
||||
PHONE_REGISTERED = 'شماره ثبت شد',
|
||||
INVALID_PHONE_FORMAT = 'فرمت شماره تلفن صحیح نیست',
|
||||
OTP_SENT = 'کد یکبار مصرف به شماره شما ارسال شد.',
|
||||
INVALID_PHONE = 'شماره موبایل اشتباه می باشد',
|
||||
INVALID_CREDENTIAL = 'شماره موبایل یا رمز عبور اشتباه می باشد',
|
||||
OTP_FAILED = 'کد یا شماره موبایل اشتباه است',
|
||||
LOGIN_SUCCESS = 'ورود با موفقیت انجام شد',
|
||||
PASSWORD_LOGIN_SUCCESS = 'با موفقیت وارد شدید',
|
||||
FORGOT_PASSWORD_SUCCESS = 'درخواست فراموشی رمز عبور با موفقیت ثبت شد',
|
||||
PASSWORD_SET_SUCCESS = 'پسورد با موفقیت تنظیم شد',
|
||||
PASSWORD_UPDATE_SUCCESS = 'رمز عبور شما تغییر کرد',
|
||||
INVALID_OTP = 'کد صحیح نمیباشد یا منقضی شده است',
|
||||
PASSWORD_MISMATCH = 'رمز عبور یکسان نیست',
|
||||
INVALID_PASSWORD = 'ایمیل یا رمز عبور اشتباه است',
|
||||
INVALID_REPEAT_PASSWORD = 'تکرار رمز عبور اشتباه است',
|
||||
EMAIL_NOT_EMPTY = 'ایمیل نمیتواند خالی باشد.',
|
||||
INVALID_EMAIL_FORMAT = 'فرمت ایمیل صحیح نیست',
|
||||
PasswordNotEmpty = 'پسورد نمیتواند خالی باشد.',
|
||||
USER_NOT_FOUND = 'کاربری با این شماره وجود ندارد',
|
||||
ADMIN_NOT_FOUND = 'ادمینی با این شماره وجود ندارد',
|
||||
USER_EXISTS = 'با این شماره قبلا ثبت نام شده است',
|
||||
USER_REGISTER_SUCCESS = 'ثبت نام موفقیت آمیز بود',
|
||||
INVALID_USER = 'کاربری با این مشخصات یافت نشد',
|
||||
PHONE_NOT_FOUND = 'کاربری با این شماره یافت نشد',
|
||||
TOKEN_EXPIRED = 'توکن منقضی شده است',
|
||||
TOKEN_INVALID = 'توکن نامعتبر است',
|
||||
BANNED = 'در حال حاضر شما امکان دسترسی به این سرویس را ندارید',
|
||||
PHONE_EXISTS = 'شماره تلفن قبلا ثبت شده است',
|
||||
ADMIN_CREATED = 'ادمین با موفقیت ایجاد شد',
|
||||
PERM_NOT_FOUND = 'دسترسی یافت نشد',
|
||||
INVALID_PASS_FORMAT = 'فرمت رمز عبور صحیح نیست',
|
||||
PHONE_NOT_EMPTY = 'شماره تلفن نمیتواند خالی باشد',
|
||||
PASSWORD_FORMAT_INVALID = 'رمز عبور باید به صورت رشته باشد',
|
||||
OTP_FORMAT_INVALID = 'کد یکبار مصرف باید عددی و ۵ رقمی باشد',
|
||||
PHONE_FORMAT_INVALID = 'شماره تلفن باید معتبر و به فرمت ایران باشد',
|
||||
TOKEN_NOT_EMPTY = 'توکن نمیتواند خالی باشد',
|
||||
PASSWORD_NOT_EMPTY = 'رمز عبور نمیتواند خالی باشد',
|
||||
PASSWORD_CHANGED_SUCCESSFULLY = 'رمز عبور با موفقیت تغییر کرد',
|
||||
PASSWORD_CONFIRMATION_NOT_EMPTY = 'تایید رمز عبور نمیتواند خالی باشد',
|
||||
PASSWORD_LENGTH = 'رمز عبور باید حداقل ۸ کاراکتر باشد',
|
||||
OTP_NOT_EMPTY = 'کد یکبار مصرف نمیتواند خالی باشد',
|
||||
LAST_PASSWORD_NOT_EMPTY = 'رمز عبور قبلی نمیتواند خالی باشد',
|
||||
FIRST_NAME_NOT_EMPTY = 'نام نمیتواند خالی باشد',
|
||||
LAST_NAME_NOT_EMPTY = 'نام خانوادگی نمیتواند خالی باشد',
|
||||
FIRST_NAME_SHOULD_BE_BETWEEN_2_AND_50 = 'نام باید بین ۲ تا ۵۰ کاراکتر باشد',
|
||||
LAST_NAME_SHOULD_BE_BETWEEN_2_AND_50 = 'نام خانوادگی باید بین ۲ تا ۵۰ کاراکتر باشد',
|
||||
BIRTH_DATE_NOT_EMPTY = 'تاریخ تولد نمیتواند خالی باشد',
|
||||
NATIONAL_CODE_INCORRECT = 'کد ملی باید ۱۰ رقمی باشد',
|
||||
NATIONAL_NOT_EMPTY = 'کد ملی نمیتواند خالی باشد',
|
||||
OTP_ALREADY_SENT = 'کد یکبار مصرف قبلا ارسال شده است',
|
||||
TOO_MANY_REQUESTS = 'تعداد درخواست های شما بیش از حد مجاز است',
|
||||
NOT_ADMIN = 'شما دسترسی به بخش ادمین ندارید',
|
||||
PHONE_SHOULD_BE_11_DIGIT = 'شماره تلفن باید ۱۱ رقم باشد',
|
||||
TIMESTAMP_NOT_EMPTY = 'زمان نمیتواند خالی باشد',
|
||||
ADMIN_CAN_NOT_LOGIN = 'ادمین نمیتواند وارد شود',
|
||||
NATIONAL_CODE_INVALID = 'کد ملی نامعتبر است',
|
||||
INVALID_REFRESH_TOKEN = 'توکن رفرش نامعتبر است',
|
||||
REFRESH_TOKEN_EXPIRED = 'توکن رفرش منقضی شده است',
|
||||
LOGOUT_SUCCESS = 'خروج با موفقیت انجام شد',
|
||||
CURRENT_PASSWORD_INVALID = 'رمز عبور فعلی نامعتبر است',
|
||||
TOKEN_EXPIRED_OR_INVALID = 'توکن منقضی شده یا نامعتبر است',
|
||||
ACCESS_DENIED = 'دسترسی شما محدود شده است',
|
||||
USER_ACCOUNT_INACTIVE = 'حساب کاربری شما غیر فعال است',
|
||||
TOKEN_MISSED_OR_EXPIRED = 'توکن مفقود یا منقضی شده است',
|
||||
OLD_PASSWORD_REQUIRED = 'رمز عبور قبلی الزامی است',
|
||||
PASSWORD_CHANGE_FAILED = 'تغییر رمز عبور با مشکل روبرد شد',
|
||||
SLUG_REQUIRED = 'اسلاگ الزامی است',
|
||||
INVALID_TOKEN_PAYLOAD = 'محتویات توکن نامعتبر است',
|
||||
INVALID_SLUG = 'اسلاگ نامعتبر است',
|
||||
INVALID_OR_EXPIRED_TOKEN = 'توکن نامعتبر یا منقضی شده است',
|
||||
}
|
||||
export const enum FoodMessage {
|
||||
NOT_FOUND = 'غذایی با این مشخصات یافت نشد',
|
||||
}
|
||||
export const enum CategoryMessage {
|
||||
NOT_FOUND = 'دستهبندی مورد نظر یافت نشد',
|
||||
NOT_CREATED = 'ایجاد دستهبندی با خطا مواجه شد',
|
||||
NOT_UPDATED = 'بهروزرسانی دستهبندی با خطا مواجه شد',
|
||||
NOT_DELETED = 'حذف دستهبندی با خطا مواجه شد',
|
||||
}
|
||||
export const enum UserMessage {
|
||||
USER_NOT_FOUND = 'کاربری با این مشخصات یافت نشد',
|
||||
Rest_NOT_FOUND = 'رستورانی با این مشخصات یافت نشد',
|
||||
USER_EXISTS = 'با این شماره قبلا ثبت نام شده است',
|
||||
USER_REGISTER_SUCCESS = 'ثبت نام موفقیت آمیز بود',
|
||||
EMAIL_ADDRESS_ALREADY_EXISTS = 'ایمیل قبلا ثبت شده است',
|
||||
EMAIL_USER_NOT_FOUND = 'ایمیل یافت نشد',
|
||||
EMAIL_USER_DELETED_SUCCESSFULLY = 'ایمیل با موفقیت حذف شد',
|
||||
FAILED_TO_DELETE_EMAIL_USER = 'خطا در حذف ایمیل',
|
||||
EMAIL_USER_QUOTA_UPDATED_SUCCESSFULLY = 'ایمیل با موفقیت به روز رسانی شد',
|
||||
FAILED_TO_UPDATE_EMAIL_USER_QUOTA = 'خطا در به روز رسانی حجم ایمیل',
|
||||
USERNAME_REQUIRED = 'نام کاربری الزامی است',
|
||||
USERNAME_STRING = 'نام کاربری باید یک رشته باشد',
|
||||
USERNAME_MIN_LENGTH = 'نام کاربری باید حداقل ۲ کاراکتر باشد',
|
||||
USERNAME_MATCHES = 'نام کاربری فقط میتواند شامل حروف، اعداد، نقطه، خط زیر و خط فاصله باشد',
|
||||
PASSWORD_MATCHES = 'رمز عبور باید حداقل ۸ کاراکتر باشد و شامل حروف بزرگ، حروف کوچک، اعداد و علائم خاص باشد',
|
||||
PASSWORD_STRING = 'رمز عبور باید یک رشته باشد',
|
||||
PASSWORD_REQUIRED = 'رمز عبور الزامی است',
|
||||
PASSWORD_MIN_LENGTH = 'رمز عبور باید حداقل ۸ کاراکتر باشد',
|
||||
DOMAIN_ID_STRING = 'شناسه دامنه باید یک رشته باشد',
|
||||
DOMAIN_ID_REQUIRED = 'شناسه دامنه الزامی است',
|
||||
DOMAIN_ID_UUID = 'شناسه دامنه باید یک یو یو آی دی باشد',
|
||||
DISPLAY_NAME_STRING = 'نام نمایشی باید یک رشته باشد',
|
||||
DISPLAY_NAME_OPTIONAL = 'نام نمایشی اختیاری است',
|
||||
QUOTA_NUMBER = 'حجم ایمیل باید یک عدد باشد',
|
||||
QUOTA_OPTIONAL = 'حجم ایمیل اختیاری است',
|
||||
QUOTA_NOT_EMPTY = 'حجم ایمیل نمیتواند خالی باشد',
|
||||
ALIASES_ARRAY = 'الیاس ها باید یک آرایه باشد',
|
||||
ALIASES_STRING = 'الیاس ها باید یک رشته باشد',
|
||||
ALIASES_OPTIONAL = 'الیاس ها اختیاری است',
|
||||
TITLE_STRING = 'عنوان باید یک رشته باشد',
|
||||
TITLE_OPTIONAL = 'عنوان اختیاری است',
|
||||
TITLE_NOT_EMPTY = 'عنوان نمیتواند خالی باشد',
|
||||
EMAIL_USER_CREATED_SUCCESSFULLY = 'ایمیل با موفقیت ایجاد شد',
|
||||
EMAIL_USER_UPDATED_SUCCESSFULLY = 'ایمیل کاربر با موفقیت بهروزرسانی شد',
|
||||
STATUS_UPDATED = 'وضعیت کاربر با موفقیت تغییر کرد',
|
||||
TEMPLATE_UUID = 'شناسه قالب باید یک یو یو آی دی باشد',
|
||||
TEMPLATE_NOT_EMPTY = 'شناسه قالب نمیتواند خالی باشد',
|
||||
ALIASES_EMAIL = 'آدرس ایمیل الیاس باید معتبر باشد',
|
||||
FORWARDERS_EMAIL_VALID = 'آدرس ایمیل فوروارد باید معتبر باشد',
|
||||
FORWARDERS_ARRAY = 'آدرس های فوروارد باید یک آرایه باشد',
|
||||
FORWARDERS_STRING = 'آدرس های فوروارد باید یک رشته باشد',
|
||||
PROFILE_PICTURE_URL = 'آدرس تصویر پروفایل باید یک آدرس اینترنتی معتبر باشد',
|
||||
USER_UPDATED_SUCCESSFULLY = 'کاربر با موفقیت به روز رسانی شد',
|
||||
PROFILE_PICTURE_NOT_EMPTY = 'تصویر پروفایل نمیتواند خالی باشد',
|
||||
PUSH_TOKEN_NOT_FOUND = 'توکن پوش نامعتبر است',
|
||||
PUSH_TOKEN_ADDED_SUCCESSFULLY = 'توکن پوش با موفقیت اضافه شد',
|
||||
NO_PUSH_TOKENS_FOUND = 'توکن پوشی برای این کاربر یافت نشد',
|
||||
PUSH_TOKEN_REMOVED_SUCCESSFULLY = 'توکن پوش با موفقیت حذف شد',
|
||||
}
|
||||
|
||||
export const enum CommonMessage {
|
||||
VALIDITY_TYPE_REQUIRED = 'نوع اعتبار سنجی مورد نیاز است',
|
||||
THIS_FILED_IS_REQUIRED = 'این فیلد الزامی است',
|
||||
VALID_FOR_CHOOSE = 'معتبر برای انتخاب',
|
||||
UPDATE_SUCCESS = 'با موفقیت به روز رسانی شد',
|
||||
CREATED = 'با موفقیت ایجاد شد',
|
||||
DELETED = 'با موفقیت حذف شد',
|
||||
ID_REQUIRED = 'شناسه مورد نیاز است',
|
||||
ID_SHOULD_BE_UUID = 'شناسه باید یک یو یو آی دی باشد',
|
||||
PaymentTypeQueryNotEmpty = 'نوع پرداخت نمیتواند خالی باشد',
|
||||
SEARCH_QUERY_STRING = 'رشته جستجو باید یک رشته باشد',
|
||||
UPDATED = 'با موفقیت آپدیت شد',
|
||||
IS_ACTIVE_SHOULD_BE_1_0 = 'وضعیت باید یکی از مقادیر ۰ و ۱ باشد',
|
||||
DATE_MUST_BE_DATE = 'تاریخ باید یک تاریخ معتبر به صورت رشته باشد',
|
||||
}
|
||||
|
||||
export const enum UploaderMessage {
|
||||
UPLOAD_FILE_INVALID = 'فایل معتبر نیست',
|
||||
UPLOAD_FILE_TOO_LARGE = 'فایل بزرگتر از حد مجاز است . حداکثر حجم فایل [MAX_FILE_SIZE] مگابایت است',
|
||||
UPLOAD_FILE_TYPE_NOT_SUPPORTED = 'نوع فایل معتبر نیست . مجاز است : [MIME_TYPES]',
|
||||
UPLOAD_FILE_SUCCESS = 'فایل با موفقیت آپلود شد',
|
||||
}
|
||||
|
||||
export const enum EmailMessage {
|
||||
EMAIL_VERIFICATION = 'تایید ایمیل',
|
||||
EMAIL_SENDING_FAILED = 'ارسال ایمیل با خطا مواجه شد',
|
||||
LOGIN = 'ورود به سیستم',
|
||||
INVOICE = 'فاکتور',
|
||||
WALLET = 'اعلان کیف پول',
|
||||
TICKET = 'تیکت پشتیبانی',
|
||||
ANNOUNCEMENT = 'اعلان',
|
||||
PAYMENT_REMINDER = 'یادآوری پرداخت',
|
||||
PAYMENT_CANCELLATION = 'لغو پرداخت',
|
||||
EMAIL_SENDING_SUCCESS = 'ارسال ایمیل با موفقیت انجام شد',
|
||||
EMAIL_FORWARD_SUCCESS = 'فوروارد ایمیل با موفقیت انجام شد',
|
||||
EMAIL_STATUS_RETRIEVED_SUCCESSFULLY = 'وضعیت ایمیل با موفقیت دریافت شد',
|
||||
MESSAGES_SEARCHED_SUCCESSFULLY = 'جستجوی ایمیل با موفقیت انجام شد',
|
||||
MESSAGES_LISTED_SUCCESSFULLY = 'لیست ایمیل ها با موفقیت دریافت شد',
|
||||
MESSAGE_RETRIEVED_SUCCESSFULLY = 'ایمیل با موفقیت دریافت شد',
|
||||
MESSAGE_DELETED_SUCCESSFULLY = 'ایمیل با موفقیت حذف شد',
|
||||
USER_ID_REQUIRED = 'شناسه کاربری مورد نیاز است',
|
||||
USER_ID_MUST_BE_STRING = 'شناسه کاربری باید یک رشته باشد',
|
||||
MAILBOX_ID_REQUIRED = 'شناسه میل باکس ایمیل مورد نیاز است',
|
||||
MAILBOX_ID_MUST_BE_STRING = 'شناسه میل باکس ایمیل باید یک رشته باشد',
|
||||
QUEUE_ID_REQUIRED = 'شناسه پیشنویس ایمیل مورد نیاز است',
|
||||
QUEUE_ID_MUST_BE_STRING = 'شناسه پیشنویس ایمیل باید یک رشته باشد',
|
||||
MESSAGE_ID_MUST_BE_NUMBER = 'شناسه ایمیل باید یک عدد باشد',
|
||||
FROM_ADDRESS_REQUIRED = 'آدرس از مورد نیاز است',
|
||||
|
||||
// Validation Messages for Email DTOs
|
||||
FROM_ADDRESS_MUST_BE_EMAIL = 'آدرس فرستنده باید یک ایمیل معتبر باشد',
|
||||
|
||||
// Query validation messages
|
||||
SEARCH_QUERY_PARAMETER_MUST_BE_STRING = 'پارامتر جستجو باید یک رشته باشد',
|
||||
MESSAGE_IDS_MUST_BE_STRING = 'شناسههای پیام باید یک رشته باشد',
|
||||
THREAD_ID_MUST_BE_STRING = 'شناسه رشته گفتگو باید یک رشته باشد',
|
||||
OR_QUERY_MUST_BE_OBJECT = 'پارامتر OR باید یک شیء باشد',
|
||||
DATE_START_MUST_BE_DATE_STRING = 'تاریخ شروع باید یک رشته تاریخ معتبر باشد',
|
||||
DATE_END_MUST_BE_DATE_STRING = 'تاریخ پایان باید یک رشته تاریخ معتبر باشد',
|
||||
TO_ADDRESS_MUST_BE_STRING = 'آدرس گیرنده باید یک رشته باشد',
|
||||
MIN_SIZE_MUST_BE_NUMBER = 'حداقل اندازه باید یک عدد باشد',
|
||||
MIN_SIZE_MUST_BE_NON_NEGATIVE = 'حداقل اندازه نمیتواند منفی باشد',
|
||||
MAX_SIZE_MUST_BE_NUMBER = 'حداکثر اندازه باید یک عدد باشد',
|
||||
MAX_SIZE_MUST_BE_NON_NEGATIVE = 'حداکثر اندازه نمیتواند منفی باشد',
|
||||
ATTACHMENTS_FILTER_MUST_BE_BOOLEAN = 'فیلتر پیوستها باید بولین باشد',
|
||||
FLAGGED_FILTER_MUST_BE_BOOLEAN = 'فیلتر پیامهای علامتدار باید بولین باشد',
|
||||
UNSEEN_FILTER_MUST_BE_BOOLEAN = 'فیلتر پیامهای خواندهنشده باید بولین باشد',
|
||||
INCLUDE_HEADERS_MUST_BE_STRING = 'لیست هدرها باید یک رشته باشد',
|
||||
SEARCHABLE_FILTER_MUST_BE_BOOLEAN = 'فیلتر قابل جستجو باید بولین باشد',
|
||||
THREAD_COUNTERS_MUST_BE_BOOLEAN = 'شمارنده رشته گفتگو باید بولین باشد',
|
||||
ORDER_MUST_BE_VALID = 'ترتیب باید یکی از مقادیر asc یا desc باشد',
|
||||
NEXT_CURSOR_MUST_BE_STRING = 'نشانگر صفحه بعد باید یک رشته باشد',
|
||||
PREVIOUS_CURSOR_MUST_BE_STRING = 'نشانگر صفحه قبل باید یک رشته باشد',
|
||||
|
||||
RECIPIENT_NAME_MUST_BE_STRING = 'نام گیرنده باید یک رشته باشد',
|
||||
RECIPIENT_ADDRESS_REQUIRED = 'آدرس گیرنده مورد نیاز است',
|
||||
RECIPIENT_ADDRESS_MUST_BE_EMAIL = 'آدرس گیرنده باید یک ایمیل معتبر باشد',
|
||||
HEADER_KEY_REQUIRED = 'کلید هدر مورد نیاز است',
|
||||
HEADER_KEY_MUST_BE_STRING = 'کلید هدر باید یک رشته باشد',
|
||||
HEADER_VALUE_REQUIRED = 'مقدار هدر مورد نیاز است',
|
||||
HEADER_VALUE_MUST_BE_STRING = 'مقدار هدر باید یک رشته باشد',
|
||||
ATTACHMENT_FILENAME_MUST_BE_STRING = 'نام فایل پیوست باید یک رشته باشد',
|
||||
ATTACHMENT_CONTENT_TYPE_MUST_BE_STRING = 'نوع محتوای پیوست باید یک رشته باشد',
|
||||
ATTACHMENT_ENCODING_MUST_BE_STRING = 'کدگذاری پیوست باید یک رشته باشد',
|
||||
ATTACHMENT_CONTENT_TRANSFER_ENCODING_MUST_BE_STRING = 'کدگذاری انتقال محتوای پیوست باید یک رشته باشد',
|
||||
ATTACHMENT_CONTENT_DISPOSITION_INVALID = 'نحوه نمایش پیوست باید inline یا attachment باشد',
|
||||
ATTACHMENT_CONTENT_REQUIRED = 'محتوای پیوست مورد نیاز است',
|
||||
ATTACHMENT_CONTENT_MUST_BE_STRING = 'محتوای پیوست باید یک رشته باشد',
|
||||
ATTACHMENT_CID_MUST_BE_STRING = 'شناسه محتوای پیوست باید یک رشته باشد',
|
||||
DRAFT_MAILBOX_REQUIRED = 'شناسه صندوق پیشنویس مورد نیاز است',
|
||||
DRAFT_MAILBOX_MUST_BE_STRING = 'شناسه صندوق پیشنویس باید یک رشته باشد',
|
||||
DRAFT_ID_REQUIRED = 'شناسه پیشنویس مورد نیاز است',
|
||||
DRAFT_ID_MUST_BE_NUMBER = 'شناسه پیشنویس باید یک عدد باشد',
|
||||
ENVELOPE_FROM_REQUIRED = 'آدرس فرستنده در envelope مورد نیاز است',
|
||||
ENVELOPE_TO_REQUIRED = 'آدرس گیرنده در envelope مورد نیاز است',
|
||||
ENVELOPE_TO_MUST_BE_ARRAY = 'آدرس گیرنده در envelope باید یک آرایه باشد',
|
||||
MAILBOX_MUST_BE_STRING = 'شناسه صندوق باید یک رشته باشد',
|
||||
REPLY_TO_MUST_BE_VALID = 'آدرس پاسخ باید معتبر باشد',
|
||||
TO_ADDRESSES_REQUIRED = 'آدرسهای گیرنده مورد نیاز است',
|
||||
TO_ADDRESSES_MUST_BE_ARRAY = 'آدرسهای گیرنده باید یک آرایه باشد',
|
||||
CC_ADDRESSES_MUST_BE_ARRAY = 'آدرسهای کپی کربن باید یک آرایه باشد',
|
||||
BCC_ADDRESSES_MUST_BE_ARRAY = 'آدرسهای کپی مخفی باید یک آرایه باشد',
|
||||
HEADERS_MUST_BE_ARRAY = 'هدرهای سفارشی باید یک آرایه باشد',
|
||||
SUBJECT_MUST_BE_STRING = 'موضوع ایمیل باید یک رشته باشد',
|
||||
TEXT_CONTENT_MUST_BE_STRING = 'محتوای متنی باید یک رشته باشد',
|
||||
HTML_CONTENT_MUST_BE_STRING = 'محتوای HTML باید یک رشته باشد',
|
||||
ATTACHMENTS_MUST_BE_ARRAY = 'پیوستها باید یک آرایه باشد',
|
||||
SESSION_ID_MUST_BE_STRING = 'شناسه جلسه باید یک رشته باشد',
|
||||
IP_ADDRESS_MUST_BE_STRING = 'آدرس IP باید یک رشته باشد',
|
||||
IS_DRAFT_MUST_BE_BOOLEAN = 'وضعیت پیشنویس باید بولین باشد',
|
||||
SEND_TIME_MUST_BE_DATE = 'زمان ارسال باید یک تاریخ معتبر باشد',
|
||||
UPLOAD_ONLY_MUST_BE_BOOLEAN = 'حالت فقط آپلود باید بولین باشد',
|
||||
ENVELOPE_MUST_BE_VALID = 'envelope باید معتبر باشد',
|
||||
|
||||
// Message operations
|
||||
MESSAGE_MARKED_AS_SEEN_SUCCESSFULLY = 'پیام با موفقیت به عنوان خوانده شده علامت گذاری شد',
|
||||
ALL_MESSAGES_MARKED_AS_READ_SUCCESSFULLY = 'تمام پیام ها با موفقیت به عنوان خوانده شده علامت گذاری شدند',
|
||||
MESSAGE_MARK_AS_SEEN_FAILED = 'علامت گذاری پیام با خطا مواجه شد',
|
||||
SENT_MESSAGES_RETRIEVED_SUCCESSFULLY = 'پیام های ارسالی با موفقیت دریافت شد',
|
||||
SENT_MESSAGES_RETRIEVAL_FAILED = 'دریافت پیام های ارسالی با خطا مواجه شد',
|
||||
TRASH_MESSAGES_RETRIEVED_SUCCESSFULLY = 'پیام های حذف شده با موفقیت دریافت شد',
|
||||
TRASH_MESSAGES_RETRIEVAL_FAILED = 'دریافت پیام های حذف شده با خطا مواجه شد',
|
||||
DRAFT_UPDATED_SUCCESSFULLY = 'پیش نویس با موفقیت به روز رسانی شد',
|
||||
DRAFT_SENT_SUCCESSFULLY = 'پیش نویس با موفقیت ارسال شد',
|
||||
DRAFT_DELETED_SUCCESSFULLY = 'پیش نویس با موفقیت حذف شد',
|
||||
MESSAGE_MOVED_TO_ARCHIVE_SUCCESSFULLY = 'پیام با موفقیت به آرشیو منتقل شد',
|
||||
MESSAGE_MOVED_TO_FAVORITE_SUCCESSFULLY = 'پیام با موفقیت به علاقه مندی ها منتقل شد',
|
||||
MESSAGE_MOVED_TO_TRASH_SUCCESSFULLY = 'پیام با موفقیت به سطل زباله منتقل شد',
|
||||
MESSAGE_MOVED_TO_JUNK_SUCCESSFULLY = 'پیام با موفقیت به جانک منتقل شد',
|
||||
MESSAGE_MOVED_TO_INBOX_FROM_ARCHIVE_SUCCESSFULLY = 'پیام با موفقیت از آرشیو به صندوق ورودی منتقل شد',
|
||||
MESSAGE_MOVED_TO_INBOX_FROM_FAVORITE_SUCCESSFULLY = 'پیام با موفقیت از علاقه مندی ها به صندوق ورودی منتقل شد',
|
||||
MESSAGE_MOVED_TO_INBOX_FROM_TRASH_SUCCESSFULLY = 'پیام با موفقیت از سطل زباله به صندوق ورودی منتقل شد',
|
||||
MESSAGE_MOVED_TO_INBOX_FROM_JUNK_SUCCESSFULLY = 'پیام با موفقیت از جانک به صندوق ورودی منتقل شد',
|
||||
MESSAGE_NOT_FOUND = 'پیام یافت نشد',
|
||||
|
||||
// Bulk action messages
|
||||
BULK_ACTION_COMPLETED_SUCCESSFULLY = '[count] پیام با موفقیت [action] شد',
|
||||
BULK_ACTION_PARTIALLY_COMPLETED = '[successful] از [total] پیام [action] شد. [failed] پیام با خطا مواجه شد',
|
||||
BULK_ACTION_FAILED = 'هیچ پیامی [action] نشد. تمام [total] تلاش ناموفق بود',
|
||||
|
||||
// Bulk action display names
|
||||
BULK_ACTION_SEEN = 'خوانده شده علامتگذاری',
|
||||
BULK_ACTION_DELETE = 'حذف',
|
||||
BULK_ACTION_ARCHIVE = 'آرشیو',
|
||||
BULK_ACTION_UNARCHIVE = 'از آرشیو خارج',
|
||||
BULK_ACTION_FAVORITE = 'به علاقه مندی ها منتقل',
|
||||
BULK_ACTION_UNFAVORITE = 'از علاقه مندی ها حذف',
|
||||
BULK_ACTION_JUNK = 'به جانک منتقل',
|
||||
BULK_ACTION_NOTJUNK = 'از جانک از حذف',
|
||||
BULK_ACTION_TRASH = 'به سطل زباله منتقل',
|
||||
BULK_ACTION_RESTORE = 'از سطل زباله بازیابی',
|
||||
BULK_ACTION_PROCESSED = 'پردازش',
|
||||
USER_NOT_FOUND = 'کاربر یافت نشد',
|
||||
PRIORITY_MUST_BE_ONE_OF_HIGH_NORMAL_LOW = 'اولویت باید یکی از مقادیر high, normal, low باشد',
|
||||
SUBJECT_REQUIRED = 'موضوع ایمیل مورد نیاز است',
|
||||
ALL_MESSAGES_DELETED_SUCCESSFULLY = 'همه پیام ها با موفقیت حذف شدند',
|
||||
MAILBOX_ID_MUST_BE_MONGO_ID = 'شناسه میل باکس ایمیل باید یک آی دی معتبر باشد',
|
||||
MESSAGE_MARKED_AS_UNSEEN_SUCCESSFULLY = 'پیام با موفقیت به عنوان خوانده نشده علامت گذاری شد',
|
||||
MESSAGE_MARK_AS_UNSEEN_FAILED = 'علامت گذاری پیام به عنوان خوانده نشده با خطا مواجه شد',
|
||||
ATTACHMENT_ID_MUST_BE_STRING = 'شناسه پیوست باید یک رشته باشد',
|
||||
IS_FORWARD_MUST_BE_BOOLEAN = 'IS_FORWARD_MUST_BE_BOOLEAN',
|
||||
|
||||
// Favorite/Star messages (Gmail-like behavior)
|
||||
MESSAGE_MARKED_AS_FAVORITE_SUCCESSFULLY = 'پیام با موفقیت به عنوان مورد علاقه علامت گذاری شد',
|
||||
MESSAGE_REMOVED_FROM_FAVORITE_SUCCESSFULLY = 'پیام با موفقیت از مورد علاقه حذف شد',
|
||||
TRASH_EMPTYED_SUCCESSFULLY = 'سطل زباله با موفقیت خالی شد',
|
||||
SPAM_EMPTYED_SUCCESSFULLY = 'همه پیام های اسپم با موفقیت حذف شدند',
|
||||
SAVE_DRAFT_SUCCESS = 'پیش نویس با موفقیت ذخیره شد',
|
||||
HTML_CONTENT_REQUIRED = 'محتوای HTML مورد نیاز است',
|
||||
TEXT_CONTENT_REQUIRED = 'TEXT_CONTENT_REQUIRED',
|
||||
}
|
||||
|
||||
export const enum WebSocketMessage {
|
||||
// Connection Messages
|
||||
WS_CONNECTION_ESTABLISHED = 'اتصال وب سوکت با موفقیت برقرار شد',
|
||||
WS_CONNECTION_FAILED = 'اتصال وب سوکت ناموفق بود',
|
||||
WS_AUTHENTICATION_REQUIRED = 'احراز هویت وب سوکت مورد نیاز است',
|
||||
WS_AUTHENTICATION_FAILED = 'احراز هویت وب سوکت ناموفق بود',
|
||||
WS_AUTHENTICATION_SUCCESS = 'احراز هویت وب سوکت با موفقیت انجام شد',
|
||||
WS_INVALID_TOKEN = 'توکن وب سوکت نامعتبر است',
|
||||
WS_TOKEN_EXPIRED = 'توکن وب سوکت منقضی شده است',
|
||||
|
||||
// Room Management Messages
|
||||
JOINED_ROOM_SUCCESS = 'با موفقیت به اتاق متصل شدید',
|
||||
LEFT_ROOM_SUCCESS = 'با موفقیت از اتاق خارج شدید',
|
||||
ROOM_NOT_FOUND = 'اتاق یافت نشد',
|
||||
UNAUTHORIZED_ROOM_ACCESS = 'دسترسی غیرمجاز به اتاق',
|
||||
|
||||
// Session Management Messages
|
||||
WS_SESSION_CREATED = 'جلسه وب سوکت ایجاد شد',
|
||||
WS_SESSION_JOINED = 'به جلسه وب سوکت پیوستید',
|
||||
WS_SESSION_LEFT = 'از جلسه وب سوکت خارج شدید',
|
||||
WS_SESSION_EXPIRED = 'جلسه وب سوکت منقضی شده است',
|
||||
WS_SESSION_NOT_FOUND = 'جلسه وب سوکت یافت نشد',
|
||||
|
||||
// Error Messages
|
||||
WS_CONNECTION_ERROR = 'خطا در اتصال وب سوکت',
|
||||
INVALID_EVENT = 'رویداد نامعتبر',
|
||||
INVALID_DATA = 'داده نامعتبر',
|
||||
WS_RATE_LIMIT_EXCEEDED = 'تعداد درخواستهای وب سوکت از حد مجاز بیشتر است',
|
||||
INTERNAL_ERROR = 'خطای داخلی سرور',
|
||||
|
||||
// Notification Messages
|
||||
NEW_MESSAGE_NOTIFICATION = 'پیام جدید دریافت شد',
|
||||
MESSAGE_STATUS_UPDATED = 'وضعیت پیام بهروزرسانی شد',
|
||||
MESSAGE_MOVED_NOTIFICATION = 'پیام جابجا شد',
|
||||
MESSAGE_DELETED_NOTIFICATION = 'پیام حذف شد',
|
||||
|
||||
// General Messages
|
||||
OPERATION_SUCCESS = 'عملیات با موفقیت انجام شد',
|
||||
OPERATION_FAILED = 'عملیات ناموفق بود',
|
||||
PERMISSION_DENIED = 'مجوز دسترسی ندارید',
|
||||
RESOURCE_NOT_FOUND = 'منبع یافت نشد',
|
||||
|
||||
// Authentication errors
|
||||
AUTHENTICATION_REQUIRED = 'احراز هویت ضروری است',
|
||||
INVALID_TOKEN = 'توکن احراز هویت نامعتبر است',
|
||||
TOKEN_EXPIRED = 'توکن منقضی شده است',
|
||||
UNAUTHORIZED_ACCESS = 'دسترسی غیرمجاز',
|
||||
USER_NOT_AUTHENTICATED = 'کاربر احراز هویت نشده است',
|
||||
|
||||
// Connection errors
|
||||
CONNECTION_FAILED = 'اتصال برقرار نشد',
|
||||
WEBSOCKET_ERROR = 'خطا در ارتباط WebSocket',
|
||||
CONNECTION_TIMEOUT = 'زمان اتصال به پایان رسید',
|
||||
CONNECTION_REFUSED = 'اتصال رد شد',
|
||||
|
||||
// Session errors
|
||||
SESSION_NOT_FOUND = 'جلسه چت یافت نشد',
|
||||
SESSION_CREATION_FAILED = 'ایجاد جلسه چت با شکست مواجه شد',
|
||||
SESSION_ACCESS_DENIED = 'دسترسی به جلسه چت مجاز نیست',
|
||||
SESSION_EXPIRED = 'جلسه چت منقضی شده است',
|
||||
SESSION_ALREADY_EXISTS = 'جلسه چت قبلاً وجود دارد',
|
||||
|
||||
// Message errors
|
||||
MESSAGE_TOO_LONG = 'پیام از حداکثر طول مجاز تجاوز کرده است',
|
||||
MESSAGE_EMPTY = 'پیام نمیتواند خالی باشد',
|
||||
MESSAGE_SEND_FAILED = 'ارسال پیام با شکست مواجه شد',
|
||||
INVALID_MESSAGE_FORMAT = 'فرمت پیام نامعتبر است',
|
||||
|
||||
// Rate limiting
|
||||
RATE_LIMIT_EXCEEDED = 'تعداد پیامهای ارسالی بیش از حد مجاز است',
|
||||
TOO_MANY_CONNECTIONS = 'تعداد اتصالات بیش از حد مجاز است',
|
||||
|
||||
// Service errors
|
||||
LLM_SERVICE_ERROR = 'سرویس هوش مصنوعی موقتاً در دسترس نیست',
|
||||
SERVICE_UNAVAILABLE = 'سرویس موقتاً در دسترس نیست',
|
||||
INTERNAL_SERVER_ERROR = '.خطای داخلی سرور',
|
||||
|
||||
// Validation errors
|
||||
INVALID_SESSION_ID = 'شناسه جلسه نامعتبر است',
|
||||
INVALID_USER_ID = 'شناسه کاربر نامعتبر است',
|
||||
INVALID_MESSAGE_ID = 'شناسه پیام نامعتبر است',
|
||||
REQUIRED_FIELD_MISSING = 'فیلد ضروری موجود نیست',
|
||||
|
||||
// Success messages
|
||||
AUTHENTICATED = 'احراز هویت موفقیتآمیز',
|
||||
SESSION_CREATED = 'جلسه چت با موفقیت ایجاد شد',
|
||||
CHAT_JOINED = 'با موفقیت به چت متصل شدید',
|
||||
MESSAGE_SENT = 'پیام ارسال شد',
|
||||
CONNECTION_ESTABLISHED = 'اتصال برقرار شد',
|
||||
}
|
||||
|
||||
export const enum SmsMessage {
|
||||
SEND_SMS_ERROR = 'خطا در ارسال پیامک',
|
||||
}
|
||||
|
||||
export const enum RestMessage {
|
||||
NOT_FOUND = 'کسب و کار یافت نشد',
|
||||
BUSINESS_ID_REQUIRED = 'شناسه کسب و کار مورد نیاز است',
|
||||
SLUG_REQUIRED = 'اسلاگ مورد نیاز است',
|
||||
SLUG_MUST_BE_STRING = 'اسلاگ باید یک رشته باشد',
|
||||
DOMAIN_INVALID = 'دامنه وارد شده معتبر نیست',
|
||||
DOMAIN_UPDATED = 'دامنه با موفقیت بهروزرسانی شد',
|
||||
DOMAIN_VERIFICATION_INITIATED = 'فرآیند تایید دامنه آغاز شد',
|
||||
DOMAIN_VERIFICATION_FAILED = 'تایید دامنه با خطا مواجه شد',
|
||||
DOMAIN_VERIFICATION_SUCCESS = 'دامنه با موفقیت تایید شد',
|
||||
DOMAIN_VERIFICATION_METHOD_INVALID = 'روش تایید دامنه نامعتبر است',
|
||||
DOMAIN_ALREADY_VERIFIED = 'این دامنه قبلا تایید شده است',
|
||||
DOMAIN_REQUIRED = 'دامنه مورد نیاز است',
|
||||
DOMAIN_MUST_BE_STRING = 'دامنه باید یک رشته باشد',
|
||||
STAFF_NOT_FOUND = 'کاربر ادمین یافت نشد',
|
||||
ZARINPAL_MERCHANT_ID_REQUIRED = 'شناسه مرچنت زرین پال مورد نیاز است',
|
||||
ZARINPAL_MERCHANT_ID_STRING = 'شناسه مرچنت زرین پال باید یک رشته باشد',
|
||||
LOGO_URL_REQUIRED = 'آدرس تصویر لوگو مورد نیاز است',
|
||||
LOGO_URL_STRING = 'آدرس تصویر لوگو باید یک رشته باشد',
|
||||
LOGO_URL_INVALID = 'آدرس تصویر لوگو معتبر نیست',
|
||||
BUSINESS_SETTINGS_UPDATED = 'تنظیمات کسب و کار با موفقیت به روز رسانی شد',
|
||||
NAME_REQUIRED = 'نام کسب و کار مورد نیاز است',
|
||||
NAME_STRING = 'نام کسب و کار باید یک رشته باشد',
|
||||
DOMAIN_CONNECTED = 'دامنه با موفقیت متصل شد',
|
||||
DOMAIN_NOT_CONNECTED = 'دامنه به سرور ما متصل نشد',
|
||||
DOMAIN_NOT_FOUND = 'دامنه یافت نشد',
|
||||
DOMAIN_VERIFICATION_TOKEN_REQUIRED = 'توکن تایید دامنه مورد نیاز است',
|
||||
INSUFFICIENT_QUOTA = 'حجم شما برای ساخت ایمیل کافی نمی باشد، لطفا حجم را افزایش دهید',
|
||||
QUOTA_DOWNGRADE_NOT_ALLOWED = 'کاهش حجم ایمیل مجاز نیست، فقط میتوانید حجم را افزایش دهید',
|
||||
QUOTA_AMOUNT_REQUIRED = 'مقدار حجم مورد نیاز است',
|
||||
QUOTA_AMOUNT_MUST_BE_NUMBER = 'مقدار حجم باید یک عدد باشد',
|
||||
QUOTA_AMOUNT_MUST_BE_POSITIVE = 'مقدار حجم باید مثبت باشد',
|
||||
CALLBACK_URL_MUST_BE_STRING = 'آدرس بازگشت باید یک رشته باشد',
|
||||
CALLBACK_URL_INVALID = 'آدرس بازگشت معتبر نیست',
|
||||
PURCHASE_DESCRIPTION_MUST_BE_STRING = 'توضیحات خرید باید یک رشته باشد',
|
||||
QUOTA_PURCHASE_SUCCESS = 'درخواست خرید حجم با موفقیت ثبت شد',
|
||||
QUOTA_PURCHASE_FAILED = 'خرید حجم با خطا مواجه شد',
|
||||
PAYMENT_GATEWAY_ERROR = 'خطا در اتصال به درگاه پرداخت',
|
||||
QUOTA_UPDATED = 'حجم ایمیل با موفقیت به روز شد',
|
||||
USER_DOES_NOT_HAVE_ACCESS_TO_SERVICE = 'کاربر دسترسی به این سرویس را ندارد',
|
||||
}
|
||||
|
||||
export const enum DomainMessage {
|
||||
// Domain Ownership & Verification
|
||||
NOT_BELONG_TO_BUSINESS = 'دامنه به این کسب و کار تعلق ندارد',
|
||||
NOT_VERIFIED = 'دامنه باید تایید شده باشد',
|
||||
DOMAIN_NOT_FOUND = 'دامنه یافت نشد',
|
||||
DOMAIN_ALREADY_EXISTS = 'دامنه [name] قبلا ثبت شده است',
|
||||
DOMAIN_DELETED_SUCCESSFULLY = 'دامنه با موفقیت حذف شد',
|
||||
VERIFICATION_NOT_FOUND = 'تایید دامنه یافت نشد',
|
||||
NO_DOMAIN_FOUND = 'دامنه ای یافت نشد',
|
||||
DOMAIN_CHECKED = 'دامنه با موفقیت بررسی شد',
|
||||
|
||||
// Domain Creation & Setup
|
||||
DOMAIN_CREATED_WITH_RECORDS = 'دامنه [name] با موفقیت ایجاد شد و تمام رکوردهای مورد نیاز تولید گردید',
|
||||
ALL_REQUIRED_RECORDS_GENERATED = 'تمام رکوردهای مورد نیاز (MX, SPF, DMARC, DKIM) به طور خودکار برای شما تولید شدهاند',
|
||||
ESTIMATED_PROPAGATION_TIME = 'تغییرات DNS معمولاً ۱۵ دقیقه تا ۴۸ ساعت در سراسر جهان منتشر میشود',
|
||||
|
||||
// DNS Records Instructions
|
||||
ADD_MX_RECORD = 'یک رکورد MX با نام [name] که به [value] با اولویت [priority] اشاره کند اضافه کنید',
|
||||
ADD_TXT_RECORD = 'یک رکورد TXT با نام [name] و مقدار [value] اضافه کنید',
|
||||
ADD_DKIM_RECORD = 'یک رکورد TXT با نام [name] و مقدار [value] اضافه کنید (کلید عمومی DKIM)',
|
||||
ADD_DMARC_RECORD = 'یک رکورد TXT با نام [name] و مقدار [value] اضافه کنید (سیاست DMARC)',
|
||||
ADD_CNAME_RECORD = 'یک رکورد CNAME با نام [name] که به [value] اشاره کند اضافه کنید',
|
||||
ADD_GENERIC_RECORD = 'یک رکورد [type] با نام [name] و مقدار [value] اضافه کنید',
|
||||
|
||||
// Domain Setup Steps
|
||||
STEP_LOGIN_DNS_PANEL = 'وارد پنل مدیریت DNS ثبت کننده دامنه خود شوید',
|
||||
STEP_ADD_ALL_RECORDS = 'تمام رکوردهای DNS مورد نیاز لیست شده در بالا را به زون DNS دامنه خود اضافه کنید',
|
||||
STEP_INCLUDE_ALL_TYPES = 'مطمئن شوید که رکوردهای MX، SPF، DMARC و DKIM را برای عملکرد کامل ایمیل شامل کردهاید',
|
||||
STEP_WAIT_PROPAGATION = 'منتظر انتشار DNS بمانید (معمولاً ۱۵ دقیقه تا ۴۸ ساعت)',
|
||||
STEP_USE_CHECK_ENDPOINT = "از نقطه پایانی 'بررسی DNS' برای تایید تنظیم صحیح رکوردهای خود استفاده کنید",
|
||||
STEP_DOMAIN_READY = 'پس از تایید، دامنه شما برای سرویسهای ایمیل آماده خواهد بود!',
|
||||
|
||||
// Status Messages
|
||||
RECORDS_PENDING_VERIFICATION = '[count] رکورد DNS هنوز در انتظار تایید هستند',
|
||||
ALL_REQUIRED_RECORDS_MUST_BE_CONFIGURED = 'تمام [count] رکورد مورد نیاز باید برای کار درست ایمیل تنظیم شوند',
|
||||
DOMAIN_FULLY_VERIFIED = 'دامنه شما به طور کامل تایید شده و برای ایمیل آماده است!',
|
||||
EMAIL_ACCOUNTS_READY = 'اکنون میتوانید حسابهای ایمیل ایجاد کرده و شروع به ارسال/دریافت ایمیل کنید',
|
||||
|
||||
// Verification Status
|
||||
DOMAIN_VERIFICATION_INITIATED = 'فرآیند تایید دامنه آغاز شد',
|
||||
DOMAIN_VERIFICATION_FAILED = 'تایید دامنه با خطا مواجه شد',
|
||||
DOMAIN_VERIFICATION_SUCCESS = 'دامنه با موفقیت تایید شد',
|
||||
DOMAIN_VERIFICATION_PENDING = 'تایید دامنه در انتظار است',
|
||||
|
||||
// DNS Record Status
|
||||
DNS_RECORD_VERIFIED = 'رکورد DNS تایید شد',
|
||||
DNS_RECORD_FAILED = 'رکورد DNS تایید نشد',
|
||||
DNS_RECORD_NOT_FOUND_OR_MISMATCH = 'رکورد DNS یافت نشد یا مقدار مطابقت ندارد',
|
||||
|
||||
// DKIM Messages
|
||||
DKIM_ENABLED_AUTOMATICALLY = 'DKIM به طور خودکار برای دامنه [name] فعال شد',
|
||||
DKIM_FAILED_TO_CREATE = 'ایجاد DKIM برای دامنه [name] ناموفق بود',
|
||||
DKIM_ENABLED_SUCCESSFULLY = 'DKIM برای دامنه [name] با موفقیت فعال شد',
|
||||
|
||||
// Error Messages
|
||||
FAILED_TO_VERIFY_DOMAIN = 'تایید دامنه [name] ناموفق بود',
|
||||
FAILED_TO_SETUP_MAIL_SERVER = 'راهاندازی دامنه [name] در سرور ایمیل ناموفق بود',
|
||||
DOMAIN_CREATION_FAILED = 'ایجاد دامنه ناموفق بود',
|
||||
DNS_VERIFICATION_ATTEMPTS_EXCEEDED = 'تعداد تلاشهای تایید DNS از حد مجاز بیشتر شد',
|
||||
|
||||
// Service Messages
|
||||
DOMAIN_UPDATED_SUCCESSFULLY = 'دامنه بروزرسانی شد',
|
||||
VERIFICATION_TOKEN_PREFIX = 'توکن تایید: [token]',
|
||||
RECORD_NEEDS_FIXING = 'رکورد [type] برای [name] نیاز به اصلاح دارد: [error]',
|
||||
RECORD_PENDING_VERIFICATION_DETAIL = 'رکورد [type] برای [name] هنوز در انتظار تایید است. لطفا مطمئن شوید که انتشار DNS کامل شده است.',
|
||||
RECORD_DESCRIPTION_EMAIL_FUNCTIONALITY = 'رکورد [type] برای عملکرد ایمیل',
|
||||
BUSINESS_ALREADY_HAS_DOMAIN = 'این کسب و کار قبلا دامنه ای دارد',
|
||||
CAN_NOT_USE_THIS_DOMAIN = 'این دامنه نمیتواند ثبت شود',
|
||||
}
|
||||
|
||||
export const enum MailServerMessage {
|
||||
FAILED_TO_CREATE_ACCOUNT = 'خطا در ایجاد ایمیل',
|
||||
FAILED_TO_CREATE_DKIM_KEY = 'خطا در ایجاد کلید DKIM',
|
||||
}
|
||||
|
||||
export const enum DnsRecordMessage {
|
||||
DNS_RECORD_NOT_FOUND = 'رکورد DNS یافت نشد',
|
||||
DNS_RECORD_DELETED = 'رکورد DNS با موفقیت حذف شد',
|
||||
}
|
||||
|
||||
export const enum RoleMessage {
|
||||
NOT_FOUND = 'نقش یافت نشد',
|
||||
}
|
||||
|
||||
export const enum SignatureMessage {
|
||||
NAME_NOT_EMPTY = 'نام امضاء نمیتواند خالی باشد',
|
||||
NAME_MAX_LENGTH = 'نام امضاء نمیتواند بیشتر از حداکثر طول مجاز باشد',
|
||||
NAME_STRING = 'نام امضاء باید یک رشته باشد',
|
||||
TEXT_CONTENT_NOT_EMPTY = 'محتوای متنی امضاء نمیتواند خالی باشد',
|
||||
TEXT_CONTENT_STRING = 'محتوای متنی امضاء باید یک رشته باشد',
|
||||
TEXT_CONTENT_MAX_LENGTH = 'محتوای متنی امضاء نمیتواند بیشتر از حداکثر طول مجاز باشد',
|
||||
IS_ACTIVE_NOT_EMPTY = 'وضعیت فعال بودن نمیتواند خالی باشد',
|
||||
IS_ACTIVE_BOOLEAN = 'وضعیت فعال بودن باید بولین باشد',
|
||||
IS_DEFAULT_NOT_EMPTY = 'وضعیت پیش فرض نمیتواند خالی باشد',
|
||||
IS_DEFAULT_BOOLEAN = 'وضعیت پیش فرض باید بولین باشد',
|
||||
CREATED = 'امضاء با موفقیت ایجاد شد',
|
||||
NOT_FOUND = 'امضاء یافت نشد',
|
||||
DELETED = 'امضاء با موفقیت حذف شد',
|
||||
TOGGLE_ACTIVE = 'وضعیت امضاء با موفقیت تغییر کرد',
|
||||
}
|
||||
|
||||
export const enum TemplateMessage {
|
||||
TEMPLATE_NOT_FOUND = 'قالب یافت نشد',
|
||||
TEMPLATE_CREATED_SUCCESSFULLY = 'قالب با موفقیت ایجاد شد',
|
||||
TEMPLATE_UPDATED_SUCCESSFULLY = 'قالب با موفقیت به روزرسانی شد',
|
||||
TEMPLATE_DELETED_SUCCESSFULLY = 'قالب با موفقیت حذف شد',
|
||||
TEMPLATE_RETRIEVED_SUCCESSFULLY = 'قالب با موفقیت دریافت شد',
|
||||
TEMPLATES_LISTED_SUCCESSFULLY = 'لیست قالب ها با موفقیت دریافت شد',
|
||||
TEMPLATE_NAME_REQUIRED = 'نام قالب الزامی است',
|
||||
TEMPLATE_NAME_STRING = 'نام قالب باید یک رشته باشد',
|
||||
TEMPLATE_NAME_MAX_LENGTH = 'نام قالب نمیتواند بیشتر از ۲۵۵ کاراکتر باشد',
|
||||
TEMPLATE_STRUCTURE_REQUIRED = 'ساختار قالب الزامی است',
|
||||
TEMPLATE_STRUCTURE_VALID = 'ساختار قالب باید معتبر باشد',
|
||||
TEMPLATE_RAW_HTML_STRING = 'HTML خام باید یک رشته باشد',
|
||||
TEMPLATE_BUSINESS_ID_REQUIRED = 'شناسه کسب و کار الزامی است',
|
||||
TEMPLATE_BUSINESS_ID_UUID = 'شناسه کسب و کار باید یک UUID معتبر باشد',
|
||||
TEMPLATE_ID_REQUIRED = 'شناسه قالب الزامی است',
|
||||
TEMPLATE_ID_UUID = 'شناسه قالب باید یک UUID معتبر باشد',
|
||||
TEMPLATE_ACCESS_DENIED = 'شما دسترسی به این قالب را ندارید',
|
||||
TEMPLATE_BUSINESS_NOT_FOUND = 'کسب و کار مالک قالب یافت نشد',
|
||||
SEARCH_STRING = 'نام برای جستجو باید یک رشته باشد',
|
||||
TEMPLATE_SELECTED_SUCCESSFULLY = 'وضعیت قالب با موفقیت تغییر کرد',
|
||||
TEMPLATE_THUMBNAIL_URL_VALID = 'آدرس تصویر قالب باید یک آدرس معتبر باشد',
|
||||
TEMPLATE_RAW_HTML_REQUIRED = 'HTML خام قالب الزامی است',
|
||||
}
|
||||
|
||||
export const enum NotificationMessage {
|
||||
TITLE_IS_REQUIRED = 'عنوان اعلان الزامی است',
|
||||
TITLE_STRING = 'عنوان اعلان باید یک رشته باشد',
|
||||
MESSAGE_IS_REQUIRED = 'متن اعلان الزامی است',
|
||||
MESSAGE_STRING = 'متن اعلان باید یک رشته باشد',
|
||||
USERID_IS_REQUIRED = 'شناسه کاربر الزامی است',
|
||||
USERID_IS_STRING = 'شناسه کاربر باید یک رشته باشد',
|
||||
USERID_IS_UUID = 'شناسه کاربر باید یک UUID معتبر باشد',
|
||||
TYPE_IS_REQUIRED = 'نوع اعلان الزامی است',
|
||||
TYPE_ENUM = 'نوع اعلان باید یک enum معتبر باشد',
|
||||
NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = 'اعلان یافت نشد یا دسترسی آن را ندارید',
|
||||
LOGIN_MESSAGE = 'ورود با موفقیت انجام شد در تاریخ [loginDate]',
|
||||
LOGIN = 'ورود',
|
||||
NEW_EMAIL = 'ایمیل جدید',
|
||||
NEW_EMAIL_MESSAGE = 'یک ایمیل جدید با موضوع [subject] دریافت شد',
|
||||
CHANGE_PASSWORD_MESSAGE = 'رمز عبور شما با موفقیت تغییر کرد',
|
||||
CHANGE_PASSWORD = 'تغییر رمز عبور',
|
||||
NOT_FOUND = 'اعلان یافت نشد',
|
||||
READ_SUCCESS = 'اعلان با موفقیت خوانده شد',
|
||||
ORDER_CREATED = 'سفارش جدید ایجاد شد',
|
||||
ORDER_STATUS_CHANGED = 'وضعیت سفارش تغییر کرد',
|
||||
PAYMENT_SUCCESS = 'پرداخت با موفقیت انجام شد',
|
||||
PAGER_CREATED = 'پیج جدید ایجاد شد',
|
||||
}
|
||||
|
||||
export const enum SettingMessageEnum {
|
||||
SMS_MUST_BE_BOOLEAN = 'تنظیمات ارسال پیامک باید یک boolean باشد',
|
||||
EMAIL_MUST_BE_BOOLEAN = 'تنظیمات ارسال ایمیل باید یک boolean باشد',
|
||||
NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = 'اعلان یافت نشد یا دسترسی آن را ندارید',
|
||||
}
|
||||
|
||||
export const enum AiMessage {
|
||||
NO_RESPONSE_FROM_AI_MODEL = 'خطا در دریافت پاسخ از مدل AI',
|
||||
MESSAGE_NOT_FOUND_OR_ACCESS_DENIED = 'ایمیل یافت نشد یا دسترسی آن را ندارید',
|
||||
EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED = 'محتوای ایمیل خالی است یا قابل استخراج نیست',
|
||||
DESCRIPTION_IS_REQUIRED = 'شرح و هدف قالب الزامی است',
|
||||
DESCRIPTION_MUST_BE_A_STRING = 'شرح قالب باید یک رشته باشد',
|
||||
DESCRIPTION_MUST_BE_AT_LEAST_10_CHARACTERS = 'شرح قالب باید حداقل ۱۰ کاراکتر باشد',
|
||||
DESCRIPTION_MUST_BE_LESS_THAN_500_CHARACTERS = 'شرح قالب نمیتواند بیشتر از ۵۰۰ کاراکتر باشد',
|
||||
THEME_MUST_BE_A_VALID_VALUE = 'تم قالب باید یکی از مقادیر مجاز باشد',
|
||||
STYLE_MUST_BE_A_VALID_VALUE = 'سبک قالب باید یکی از مقادیر مجاز باشد',
|
||||
PURPOSE_MUST_BE_A_VALID_VALUE = 'هدف قالب باید یکی از مقادیر مجاز باشد',
|
||||
TEMPLATE_NAME_MUST_BE_A_STRING = 'نام قالب باید یک رشته باشد',
|
||||
TEMPLATE_NAME_MUST_BE_LESS_THAN_100_CHARACTERS = 'نام قالب نمیتواند بیشتر از ۱۰۰ کاراکتر باشد',
|
||||
PREFERRED_COLORS_MUST_BE_A_STRING = 'رنگهای ترجیحی باید یک رشته باشد',
|
||||
PREFERRED_COLORS_MUST_BE_LESS_THAN_200_CHARACTERS = 'رنگهای ترجیحی نمیتواند بیشتر از ۲۰۰ کاراکتر باشد',
|
||||
PREFERRED_COLORS_IS_REQUIRED = 'رنگهای ترجیحی الزامی است',
|
||||
}
|
||||
|
||||
export const enum TwoFactorMessage {
|
||||
TWO_FACTOR_SETUP_INITIATED = 'راهاندازی احراز هویت دو مرحلهای با موفقیت آغاز شد',
|
||||
TWO_FACTOR_ENABLED_SUCCESSFULLY = 'احراز هویت دو مرحلهای با موفقیت فعال شد',
|
||||
TWO_FACTOR_DISABLED_SUCCESSFULLY = 'احراز هویت دو مرحلهای با موفقیت غیرفعال شد',
|
||||
TWO_FACTOR_SETUP_FAILED = 'راهاندازی احراز هویت دو مرحلهای ناموفق بود',
|
||||
TWO_FACTOR_ENABLE_FAILED = 'فعالسازی احراز هویت دو مرحلهای ناموفق بود',
|
||||
TWO_FACTOR_TOKEN_VERIFIED_SUCCESSFULLY = 'کد احراز هویت دو مرحلهای با موفقیت تأیید شد',
|
||||
TWO_FACTOR_TOKEN_VERIFIED_FAILED = 'کد احراز هویت دو مرحلهای نامعتبر میباشد',
|
||||
TWO_FACTOR_NOT_ENABLED = 'احراز هویت دو مرحلهای فعال نیست',
|
||||
TWO_FACTOR_BACKUP_CODES_GENERATED_SUCCESSFULLY = 'کدهای پشتیبان با موفقیت تولید شدند',
|
||||
TWO_FACTOR_BACKUP_CODE_VERIFIED_SUCCESSFULLY = 'کد پشتیبان با موفقیت تأیید شد',
|
||||
TWO_FACTOR_BACKUP_CODE_VERIFIED_FAILED = 'کد پشتیبان نامعتبر است',
|
||||
TWO_FACTOR_TOKEN_REQUIRED = 'کد احراز هویت دو مرحلهای الزامی است',
|
||||
TWO_FACTOR_TOKEN_INVALID = 'کد احراز هویت دو مرحلهای نامعتبر است',
|
||||
}
|
||||
|
||||
export const enum CouponMessage {
|
||||
NOT_FOUND = 'کوپن یافت نشد',
|
||||
CODE_ALREADY_EXISTS = 'کد کوپن قبلاً ثبت شده است',
|
||||
END_DATE_MUST_BE_AFTER_START_DATE = 'تاریخ پایان باید بعد از تاریخ شروع باشد',
|
||||
PERCENTAGE_MUST_BE_BETWEEN_0_AND_100 = 'درصد تخفیف باید بین ۰ تا ۱۰۰ باشد',
|
||||
COUPON_INACTIVE = 'کوپن غیرفعال است',
|
||||
COUPON_NOT_STARTED = 'کوپن هنوز شروع نشده است',
|
||||
COUPON_EXPIRED = 'کوپن منقضی شده است',
|
||||
COUPON_MAX_USES_REACHED = 'حداکثر استفاده از کوپن به پایان رسیده است',
|
||||
MIN_ORDER_AMOUNT_NOT_MET = 'مبلغ سفارش کمتر از حداقل مجاز است',
|
||||
COUPON_CREATED = 'کوپن با موفقیت ایجاد شد',
|
||||
COUPON_UPDATED = 'کوپن با موفقیت بهروزرسانی شد',
|
||||
COUPON_DELETED = 'کوپن با موفقیت حذف شد',
|
||||
COUPON_VALID = 'کوپن معتبر است',
|
||||
COUPON_INVALID = 'کوپن نامعتبر است',
|
||||
COUPON_RESTRICTED_TO_USER = 'کوپن فقط برای کاربر مشخصی معتبر است',
|
||||
}
|
||||
|
||||
export const enum ReviewMessage {
|
||||
NOT_FOUND = 'نظر یافت نشد',
|
||||
ALREADY_COMMENTED = 'شما قبلاً برای این غذا نظر دادهاید',
|
||||
CAN_ONLY_UPDATE_OWN = 'شما فقط میتوانید نظرات خود را ویرایش کنید',
|
||||
CAN_ONLY_DELETE_OWN = 'شما فقط میتوانید نظرات خود را حذف کنید',
|
||||
RATING_REQUIRED = 'امتیاز الزامی است',
|
||||
RATING_INVALID = 'امتیاز باید بین ۱ تا ۵ باشد',
|
||||
COMMENT_CREATED = 'نظر با موفقیت ثبت شد',
|
||||
COMMENT_UPDATED = 'نظر با موفقیت بهروزرسانی شد',
|
||||
COMMENT_DELETED = 'نظر با موفقیت حذف شد',
|
||||
}
|
||||
|
||||
export const enum IconMessage {
|
||||
NOT_FOUND = 'آیکون یافت نشد',
|
||||
NOT_CREATED = 'ایجاد آیکون با خطا مواجه شد',
|
||||
NOT_UPDATED = 'بهروزرسانی آیکون با خطا مواجه شد',
|
||||
NOT_DELETED = 'حذف آیکون با خطا مواجه شد',
|
||||
}
|
||||
|
||||
export const enum GroupMessage {
|
||||
NOT_FOUND = 'گروه آیکون یافت نشد',
|
||||
NOT_CREATED = 'ایجاد گروه آیکون با خطا مواجه شد',
|
||||
NOT_UPDATED = 'بهروزرسانی گروه آیکون با خطا مواجه شد',
|
||||
NOT_DELETED = 'حذف گروه آیکون با خطا مواجه شد',
|
||||
}
|
||||
|
||||
export const enum OrderMessage {
|
||||
NOT_FOUND = 'سفارش یافت نشد',
|
||||
PAYMENT_METHOD_MISSING = 'روش پرداخت سفارش مشخص نشده است',
|
||||
INVALID_STATUS_TRANSITION = 'انتقال وضعیت نامعتبر است',
|
||||
CART_EMPTY = 'سبد خرید خالی است. لطفا قبل از ایجاد سفارش، آیتمهایی به سبد اضافه کنید',
|
||||
DELIVERY_METHOD_NOT_FOUND = 'روش ارسال یافت نشد',
|
||||
PAYMENT_METHOD_REQUIRED = 'روش پرداخت الزامی است. لطفا قبل از ایجاد سفارش، روش پرداخت را تنظیم کنید',
|
||||
USER_NOT_FOUND = 'کاربر یافت نشد',
|
||||
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
|
||||
DELIVERY_NOT_FOUND = 'روش ارسال یافت نشد',
|
||||
MIN_ORDER_AMOUNT_NOT_MET = 'مبلغ سفارش کمتر از حداقل مجاز برای این روش ارسال است',
|
||||
TABLE_NUMBER_REQUIRED = 'شماره میز برای سفارش در محل الزامی است',
|
||||
ADDRESS_REQUIRED = 'آدرس الزامی است. لطفا قبل از ایجاد سفارش، آدرس تحویل را تنظیم کنید',
|
||||
CAR_ADDRESS_REQUIRED = 'آدرس خودرو الزامی است. لطفا قبل از ایجاد سفارش، آدرس خودرو را تنظیم کنید',
|
||||
PAYMENT_METHOD_NOT_FOUND = 'روش پرداخت یافت نشد',
|
||||
PAYMENT_METHOD_NOT_ENABLED = 'روش پرداخت برای این رستوران فعال نیست',
|
||||
FOOD_NOT_FOUND = 'غذا یافت نشد',
|
||||
FOOD_NOT_BELONGS_TO_RESTAURANT = 'غذا به این رستوران تعلق ندارد',
|
||||
}
|
||||
|
||||
export const enum CartMessage {
|
||||
TABLE_NUMBER_REQUIRED = 'شماره میز برای سفارش در محل الزامی است',
|
||||
CAR_ADDRESS_REQUIRED = 'آدرس خودرو برای تحویل به خودرو الزامی است',
|
||||
ADDRESS_REQUIRED = 'آدرس برای تحویل پیک الزامی است',
|
||||
NOT_FOUND = 'سبد خرید یافت نشد',
|
||||
COUPON_NOT_FOUND = 'کوپن یافت نشد',
|
||||
FOOD_NOT_FOUND = 'غذا یافت نشد',
|
||||
FOOD_NOT_BELONGS_TO_RESTAURANT = 'غذا به این رستوران تعلق ندارد',
|
||||
FOOD_NO_INVENTORY = 'غذا موجودی ندارد',
|
||||
INSUFFICIENT_STOCK = 'موجودی کافی نیست',
|
||||
USER_NOT_FOUND = 'کاربر یافت نشد',
|
||||
ADDRESS_NOT_FOUND = 'آدرس یافت نشد',
|
||||
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
|
||||
ADDRESS_NOT_BELONGS_TO_USER = 'آدرس به کاربر فعلی تعلق ندارد',
|
||||
ADDRESS_NO_COORDINATES = 'آدرس مختصات جغرافیایی ندارد',
|
||||
ADDRESS_OUTSIDE_SERVICE_AREA = 'آدرس خارج از محدوده سرویس رستوران است',
|
||||
DELIVERY_METHOD_NOT_FOUND = 'روش ارسال یافت نشد',
|
||||
DELIVERY_METHOD_NOT_ENABLED = 'روش ارسال برای این رستوران فعال نیست',
|
||||
PAYMENT_METHOD_NOT_FOUND = 'روش پرداخت یافت نشد',
|
||||
PAYMENT_METHOD_NOT_ENABLED = 'روش پرداخت برای این رستوران فعال نیست',
|
||||
WALLET_NOT_FOUND = 'کیف پول کاربر یافت نشد',
|
||||
WALLET_INSUFFICIENT = 'موجودی کیف پول کافی نیست',
|
||||
COUPON_MAX_USES_REACHED = 'حداکثر استفاده از کوپن به پایان رسیده است',
|
||||
ITEM_NOT_FOUND = 'آیتم در سبد خرید یافت نشد',
|
||||
DELIVERY_METHOD_NOT_FOUND_FOR_RESTAURANT = 'روش ارسال برای این رستوران یافت نشد',
|
||||
COUPON_CANNOT_BE_APPLIED = 'این کوپن قابل اعمال بر روی آیتمهای سبد خرید شما نیست. لطفا آیتمهایی از دستهبندیها یا غذاهای مشخص شده اضافه کنید',
|
||||
FOODS_MUST_HAVE_PICKUP_SERVE_FOR_COURIER = 'تمام غذاها باید قابلیت تحویل پیک داشته باشند. غذاهای زیر این قابلیت را ندارند: [foodTitles]',
|
||||
FOOD_ONLY_AVAILABLE_DURING_MEAL_TIMES = 'غذا [foodTitle] فقط در ساعات وعدههای غذایی (صبحانه، ناهار، عصرانه یا شام) در دسترس است. زمان فعلی خارج از ساعات وعدههای غذایی است',
|
||||
FOOD_ONLY_AVAILABLE_FOR_MEAL_TYPES = 'غذا [foodTitle] فقط برای [allowedMealTypes] در دسترس است. زمان فعلی [mealType] است که با این غذا سازگار نیست',
|
||||
FOOD_ONLY_AVAILABLE_ON_DAYS = 'غذا [foodTitle] فقط در روزهای [allowedDays] در دسترس است. امروز [currentDay] است که این غذا در دسترس نیست',
|
||||
}
|
||||
|
||||
export const enum PaymentMessage {
|
||||
UNSUPPORTED_PAYMENT_METHOD = 'روش پرداخت پشتیبانی نمیشود',
|
||||
ORDER_NOT_FOUND = 'سفارش یافت نشد',
|
||||
AMOUNT_MUST_BE_GREATER_THAN_ZERO = 'مبلغ باید بیشتر از صفر باشد',
|
||||
PAYMENT_METHOD_NOT_FOUND = 'روش پرداخت یافت نشد',
|
||||
GATEWAY_REQUIRED = 'درگاه پرداخت برای پرداخت آنلاین الزامی است',
|
||||
MERCHANT_ID_REQUIRED = 'شناسه مرچنت برای پرداخت آنلاین الزامی است',
|
||||
RESTAURANT_DOMAIN_REQUIRED = 'دامنه رستوران برای پرداخت آنلاین الزامی است',
|
||||
WALLET_NOT_FOUND = 'کیف پول کاربر یافت نشد',
|
||||
INSUFFICIENT_WALLET_BALANCE = 'موجودی کیف پول کافی نیست',
|
||||
PAYMENT_NOT_FOUND = 'پرداخت یافت نشد',
|
||||
INVALID_PAYMENT_CONFIGURATION = 'پیکربندی پرداخت نامعتبر است',
|
||||
PAYMENT_METHOD_NOT_CASH = 'روش پرداخت نقدی نیست',
|
||||
PAYMENT_ALREADY_PAID = 'پرداخت قبلا انجام شده است',
|
||||
EXISTING_PENDING_PAYMENT_MISMATCH = 'پرداخت در انتظار موجود با روش پرداخت سفارش مطابقت ندارد',
|
||||
ZARINPAL_INVALID_RESPONSE = 'پاسخ نامعتبر از API زرینپال',
|
||||
ZARINPAL_PAYMENT_REQUEST_ERROR = 'خطا در درخواست پرداخت زرینپال',
|
||||
ZARINPAL_VERIFY_ERROR = 'خطا در تایید پرداخت زرینپال',
|
||||
GATEWAY_ERROR = 'خطا در اتصال به درگاه پرداخت',
|
||||
}
|
||||
|
||||
export const enum DeliveryMessage {
|
||||
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
|
||||
DELIVERY_METHOD_NOT_FOUND = 'روش ارسال یافت نشد',
|
||||
}
|
||||
|
||||
export const enum InventoryMessage {
|
||||
AVAILABLE_STOCK_EXCEEDS_TOTAL = 'موجودی موجود نمیتواند از موجودی کل بیشتر باشد',
|
||||
FOOD_NOT_FOUND = 'غذا یافت نشد',
|
||||
FOOD_NOT_BELONGS_TO_RESTAURANT = 'غذا به این رستوران تعلق ندارد',
|
||||
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
|
||||
FOODS_NOT_FOUND = 'غذاها یافت نشدند',
|
||||
}
|
||||
|
||||
|
||||
export const enum PagerMessage {
|
||||
CREATED_SUCCESS = 'درخواست پیج با موفقیت ایجاد شد',
|
||||
}
|
||||
|
||||
export const enum UserSuccessMessage {
|
||||
ADDRESS_DELETED_SUCCESS = 'آدرس با موفقیت حذف شد',
|
||||
SCORE_CONVERTED_SUCCESS = 'امتیاز با موفقیت به کیف پول تبدیل شد',
|
||||
}
|
||||
|
||||
export const enum ContactMessage {
|
||||
NOT_FOUND = 'تماس یافت نشد',
|
||||
CREATED = 'تماس با موفقیت ثبت شد',
|
||||
UPDATED = 'تماس با موفقیت بهروزرسانی شد',
|
||||
DELETED = 'تماس با موفقیت حذف شد',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Permission names enum
|
||||
* All permission names used in the system should be defined here
|
||||
*/
|
||||
export enum Permission {
|
||||
MANAGE_PAGER = 'manage_pager',
|
||||
// Food Management
|
||||
MANAGE_FOODS = 'manage_foods',
|
||||
MANAGE_CATEGORIES = 'manage_categories',
|
||||
|
||||
// Order Management
|
||||
MANAGE_ORDERS = 'manage_orders',
|
||||
|
||||
// User & Admin Management
|
||||
MANAGE_ADMINS = 'manage_admins',
|
||||
MANAGE_USERS = 'manage_users',
|
||||
|
||||
// Reports & Finances
|
||||
VIEW_REPORTS = 'view_reports',
|
||||
|
||||
|
||||
UPDATE_RESTAURANT = 'update_restaurant',
|
||||
|
||||
// Role Management
|
||||
MANAGE_ROLES = 'manage_roles',
|
||||
MANAGE_REVIEWS = 'manage_reviews',
|
||||
MANAGE_COUPONS = 'manage_coupons',
|
||||
MANAGE_PAYMENTS = 'manage_payments',
|
||||
MANAGE_DELIVERY = 'manage_delivery',
|
||||
MANAGE_SETTINGS = 'manage_settings',
|
||||
MANAGE_SCHEDULES = 'manage_schedules',
|
||||
MANAGE_REPORTS = 'manage_reports',
|
||||
MANAGE_CONTACTS = 'manage_contacts',
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission titles mapping (Farsi)
|
||||
* Maps permission names to their Farsi titles
|
||||
*/
|
||||
export const PermissionTitles: Record<Permission, string> = {
|
||||
[Permission.MANAGE_PAGER]: 'مدیریت پیجر',
|
||||
[Permission.MANAGE_FOODS]: 'مدیریت غذاها',
|
||||
[Permission.MANAGE_CATEGORIES]: 'مدیریت دستهبندیها',
|
||||
[Permission.MANAGE_ORDERS]: 'مدیریت سفارشات',
|
||||
[Permission.MANAGE_ADMINS]: 'مدیریت مدیران',
|
||||
[Permission.MANAGE_USERS]: 'مدیریت کاربران',
|
||||
[Permission.VIEW_REPORTS]: 'مشاهده گزارشات',
|
||||
[Permission.UPDATE_RESTAURANT]: 'ویرایش رستوران',
|
||||
[Permission.MANAGE_ROLES]: 'مدیریت نقشها',
|
||||
[Permission.MANAGE_REVIEWS]: 'مدیریت نظرات',
|
||||
[Permission.MANAGE_COUPONS]: 'مدیریت کوپنها',
|
||||
[Permission.MANAGE_PAYMENTS]: 'مدیریت پرداختها',
|
||||
[Permission.MANAGE_DELIVERY]: 'مدیریت تحویل',
|
||||
[Permission.MANAGE_SETTINGS]: 'مدیریت تنظیمات',
|
||||
[Permission.MANAGE_SCHEDULES]: 'مدیریت برنامهها',
|
||||
[Permission.MANAGE_REPORTS]: 'مدیریت گزارشات',
|
||||
[Permission.MANAGE_CONTACTS]: 'مدیریت تماسهای کاربران',
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { ValueProvider } from '@nestjs/common';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import { MIKRO_ORM_QUERY_LOGGER } from '../constants';
|
||||
|
||||
export const mikroOrmQueryLoggerProvider: ValueProvider = {
|
||||
provide: MIKRO_ORM_QUERY_LOGGER, //
|
||||
useValue: new Logger('mikro-orm'),
|
||||
};
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
import { Keyv } from 'keyv';
|
||||
import KeyvRedis from '@keyv/redis';
|
||||
import type { CacheModuleAsyncOptions } from '@nestjs/cache-manager';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { CacheableMemory } from 'cacheable';
|
||||
|
||||
export function cacheConfig(): CacheModuleAsyncOptions {
|
||||
return {
|
||||
isGlobal: true,
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
const redisUri = configService.get('REDIS_URI');
|
||||
const namespace = configService.get('CACHE_NAMESPACE', 'app-cache');
|
||||
const ttl = +configService.get<number>('CACHE_TTL', 3600); //1 hour
|
||||
|
||||
return {
|
||||
stores: [
|
||||
new Keyv({
|
||||
store: new CacheableMemory({ ttl: ttl * 1000, lruSize: 5000 }),
|
||||
namespace: namespace
|
||||
}),
|
||||
new KeyvRedis(redisUri, { namespace: namespace }),
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
import type { HttpModuleAsyncOptions } from '@nestjs/axios';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
export function httpConfig(): HttpModuleAsyncOptions {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
global: true,
|
||||
useFactory: (configService: ConfigService) => {
|
||||
return {
|
||||
timeout: configService.getOrThrow<number>('AXIOS_TIMEOUT'),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
global: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { defineConfig, PostgreSqlDriver } from '@mikro-orm/postgresql';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
// 1. Load environment variables from your .env file
|
||||
dotenv.config();
|
||||
|
||||
// 2. Read variables directly from process.env
|
||||
const DB_PASS = process.env.DB_PASS;
|
||||
const DB_USER = process.env.DB_USER;
|
||||
const DB_HOST = process.env.DB_HOST;
|
||||
const DB_PORT = process.env.DB_PORT ? +process.env.DB_PORT : 5432;
|
||||
const DB_NAME = process.env.DB_NAME;
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
// 3. Add checks to ensure variables are loaded
|
||||
if (!DB_PASS || !DB_USER || !DB_HOST || !DB_PORT || !DB_NAME) {
|
||||
throw new Error('One or more database environment variables are not set.');
|
||||
}
|
||||
|
||||
const encodedPassword = encodeURIComponent(DB_PASS);
|
||||
|
||||
// 4. Export the result of defineConfig as the default
|
||||
export default defineConfig({
|
||||
entities: ['dist/**/*.entity.js'],
|
||||
entitiesTs: ['src/**/*.entity.ts'],
|
||||
driver: PostgreSqlDriver,
|
||||
dbName: DB_NAME,
|
||||
debug: false,
|
||||
clientUrl: `postgres://${DB_USER}:${encodedPassword}@${DB_HOST}:${DB_PORT}`,
|
||||
ensureDatabase: isProduction
|
||||
? false
|
||||
: {
|
||||
forceCheck: true,
|
||||
create: true,
|
||||
schema: 'update',
|
||||
},
|
||||
forceUtcTimezone: true,
|
||||
pool: {
|
||||
min: isProduction ? 5 : 2,
|
||||
max: isProduction ? 20 : 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
acquireTimeoutMillis: 30000,
|
||||
reapIntervalMillis: 1000,
|
||||
createTimeoutMillis: 15000,
|
||||
destroyTimeoutMillis: 5000,
|
||||
},
|
||||
// 5. Use console for CLI logging, not the injected Nest logger
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
logger: isProduction ? console.log.bind(console) : console.debug.bind(console),
|
||||
schemaGenerator: {
|
||||
disableForeignKeys: false,
|
||||
},
|
||||
migrations: {
|
||||
path: './database/migrations',
|
||||
pathTs: './database/migrations',
|
||||
tableName: 'migrations',
|
||||
transactional: true,
|
||||
allOrNothing: true,
|
||||
dropTables: false,
|
||||
safe: true,
|
||||
emit: 'ts',
|
||||
},
|
||||
connect: true,
|
||||
allowGlobalContext: true,
|
||||
seeder: {
|
||||
path: './dist/seeders',
|
||||
pathTs: './src/seeders',
|
||||
defaultSeeder: 'DatabaseSeeder',
|
||||
},
|
||||
driverOptions: {
|
||||
connection: {
|
||||
keepAlive: true,
|
||||
keepAliveInitialDelayMillis: 10000,
|
||||
statement_timeout: 60000,
|
||||
query_timeout: 60000,
|
||||
idle_in_transaction_session_timeout: 60000,
|
||||
connectionTimeoutMillis: 10000,
|
||||
application_name: DB_NAME,
|
||||
},
|
||||
connectionRetries: 5,
|
||||
connectionRetryDelay: 3000,
|
||||
validateConnection: (connection: any) => {
|
||||
try {
|
||||
return !!(connection && !connection.closed);
|
||||
} catch (e: unknown) {
|
||||
// Use console.error directly
|
||||
console.error(`Connection validation failed: ${(e as Error).message}`);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
poolErrorHandler: (err: Error) => {
|
||||
// Use console.error directly
|
||||
console.error(`PostgreSQL pool error: ${err.message}`);
|
||||
if (err.message.includes('ECONNRESET') || err.message.includes('Connection terminated unexpectedly')) {
|
||||
console.warn('Connection reset detected, will attempt to reconnect');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { MikroOrmModuleAsyncOptions } from '@mikro-orm/nestjs';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
|
||||
// import { defineConfig } from '@mikro-orm/postgresql';
|
||||
|
||||
import type { Logger } from '@nestjs/common';
|
||||
import { MIKRO_ORM_QUERY_LOGGER } from '../common/constants';
|
||||
import { mikroOrmQueryLoggerProvider } from '../common/providers/mikro-orm-logger.provider';
|
||||
|
||||
const dataBaseConfig: MikroOrmModuleAsyncOptions = {
|
||||
// entities: ['dist/**/*.entity.js'],
|
||||
// entitiesTs: ['src/**/*.entity.ts'],
|
||||
// dbName: 'your_db_name',
|
||||
// type: 'postgresql',
|
||||
// user: 'your_user',
|
||||
// password: 'your_password',
|
||||
// host: 'localhost',
|
||||
// port: 5432,
|
||||
// debug: true,
|
||||
|
||||
inject: [ConfigService, MIKRO_ORM_QUERY_LOGGER],
|
||||
providers: [mikroOrmQueryLoggerProvider],
|
||||
useFactory: (configService: ConfigService, logger: Logger) => {
|
||||
const DB_PASS = configService.getOrThrow<string>('DB_PASS');
|
||||
const DB_USER = configService.getOrThrow<string>('DB_USER');
|
||||
const DB_HOST = configService.getOrThrow<string>('DB_HOST');
|
||||
const DB_PORT = configService.getOrThrow<number>('DB_PORT');
|
||||
const DB_NAME = configService.getOrThrow<string>('DB_NAME');
|
||||
const encodedPassword = encodeURIComponent(DB_PASS);
|
||||
const isProduction = configService.getOrThrow<string>('NODE_ENV') === 'production';
|
||||
|
||||
return {
|
||||
// entities: ['dist/**/*.entity.js'],
|
||||
entitiesTs: ['src/**/*.entity.ts'],
|
||||
driver: PostgreSqlDriver,
|
||||
autoLoadEntities: true,
|
||||
dbName: DB_NAME,
|
||||
debug: false,
|
||||
clientUrl: `postgres://${DB_USER}:${encodedPassword}@${DB_HOST}:${DB_PORT}`,
|
||||
ensureDatabase: isProduction
|
||||
? false
|
||||
: {
|
||||
forceCheck: true,
|
||||
create: true,
|
||||
schema: 'update',
|
||||
},
|
||||
forceUtcTimezone: true,
|
||||
pool: {
|
||||
min: isProduction ? 5 : 2,
|
||||
max: isProduction ? 20 : 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
acquireTimeoutMillis: 30000,
|
||||
reapIntervalMillis: 1000,
|
||||
createTimeoutMillis: 15000,
|
||||
destroyTimeoutMillis: 5000,
|
||||
},
|
||||
logger: isProduction ? message => logger.log(message) : message => logger.debug(message),
|
||||
schemaGenerator: {
|
||||
// createForeignKey: !isProduction,
|
||||
disableForeignKeys: false,
|
||||
// createIndex: true,
|
||||
},
|
||||
migrations: {
|
||||
path: './database/migrations',
|
||||
pathTs: './database/migrations',
|
||||
tableName: 'migrations',
|
||||
transactional: true,
|
||||
allOrNothing: true,
|
||||
dropTables: false,
|
||||
safe: true,
|
||||
emit: 'ts',
|
||||
},
|
||||
connect: true,
|
||||
allowGlobalContext: true,
|
||||
driverOptions: {
|
||||
connection: {
|
||||
keepAlive: true,
|
||||
keepAliveInitialDelayMillis: 10000,
|
||||
statement_timeout: 60000,
|
||||
query_timeout: 60000,
|
||||
idle_in_transaction_session_timeout: 60000,
|
||||
connectionTimeoutMillis: 10000,
|
||||
application_name: configService.getOrThrow<string>('DB_NAME'),
|
||||
},
|
||||
connectionRetries: 5,
|
||||
connectionRetryDelay: 3000,
|
||||
validateConnection: (connection: any) => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
return !!(connection && !connection.closed);
|
||||
} catch (e: unknown) {
|
||||
logger.error(`Connection validation failed: ${(e as Error).message}`);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
poolErrorHandler: (err: Error) => {
|
||||
logger.error(`PostgreSQL pool error: ${err.message}`);
|
||||
if (err.message.includes('ECONNRESET') || err.message.includes('Connection terminated unexpectedly')) {
|
||||
logger.warn('Connection reset detected, will attempt to reconnect');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default dataBaseConfig;
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
|
||||
export function getSwaggerConfig(app: INestApplication) {
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('D-menu API')
|
||||
.setDescription('API documentation for D-menu backend')
|
||||
.setVersion('1.0')
|
||||
.addBearerAuth() // optional: for JWT endpoints
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
|
||||
SwaggerModule.setup('docs', app, document, {
|
||||
swaggerOptions: {
|
||||
persistAuthorization: true,
|
||||
displayRequestDuration: true,
|
||||
filter: true,
|
||||
showExtensions: true,
|
||||
docExpansion: 'none',
|
||||
|
||||
// HIDE MODELS / SCHEMAS
|
||||
defaultModelsExpandDepth: -1,
|
||||
defaultModelExpandDepth: -1,
|
||||
},
|
||||
});
|
||||
}
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { FastifyReply } from 'fastify';
|
||||
|
||||
@Catch(HttpException)
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: HttpException, host: ArgumentsHost) {
|
||||
// Handle HTTP/REST context
|
||||
try {
|
||||
const ctx = host.switchToHttp();
|
||||
const reply = ctx.getResponse<FastifyReply>();
|
||||
|
||||
// Safety check: ensure reply has the status method
|
||||
if (!reply || typeof reply.status !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
// const request = ctx.getRequest<FastifyRequest>();
|
||||
const status = exception.getStatus() || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
const exceptionResponse = exception.getResponse();
|
||||
const message = this.extractMessage(exceptionResponse);
|
||||
|
||||
reply.status(status).send({
|
||||
statusCode: status,
|
||||
success: false,
|
||||
error: {
|
||||
message,
|
||||
// timestamp: new Date().toISOString(),
|
||||
// path: request.url,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// If we can't handle it, let it propagate
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private extractMessage(response: string | Record<string, any>): string | string[] {
|
||||
if (typeof response === 'string') {
|
||||
return [response];
|
||||
}
|
||||
if (typeof response === 'object' && 'message' in response) {
|
||||
return Array.isArray(response.message) ? response.message : [response.message];
|
||||
}
|
||||
|
||||
return 'something wrong';
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
|
||||
// import { FastifyRequest } from 'fastify';
|
||||
// import { Observable, map } from 'rxjs';
|
||||
|
||||
// import { IPageFormat } from '../../../../danak_dsc_api/src/common/interfaces/IPagination';
|
||||
|
||||
// @Injectable()
|
||||
// export class PaginationInterceptor implements NestInterceptor {
|
||||
// private readonly logger = new Logger(PaginationInterceptor.name);
|
||||
|
||||
// intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||
// const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||
// const query = request.query as { page: string; limit: string };
|
||||
|
||||
// const page = parseInt(query.page, 10) || 1;
|
||||
// const limit = parseInt(query.limit, 10) || 10;
|
||||
|
||||
// const className = context.getClass().name;
|
||||
// const handlerName = context.getHandler().name;
|
||||
|
||||
// return next.handle().pipe(
|
||||
// map(data => {
|
||||
// if (data && (data.paginate || data.count)) {
|
||||
// const { count, paginate, ...response } = data;
|
||||
// this.logger.log(`paginate response from ${className}.${handlerName}`);
|
||||
// const pager = this.formatPage(page, limit, count, request);
|
||||
|
||||
// return {
|
||||
// pager,
|
||||
// ...response,
|
||||
// };
|
||||
// }
|
||||
|
||||
// return data;
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
|
||||
// private formatPage(page: number, limit: number, totalItems: number, request: FastifyRequest): IPageFormat {
|
||||
// const totalPages = Math.ceil(totalItems / limit);
|
||||
// const prevPage = page === 1 ? false : page - 1;
|
||||
// const nextPage = page >= totalPages ? false : page + 1;
|
||||
|
||||
// const formatLink = (pageNum: number | boolean): string | boolean => {
|
||||
// if (!pageNum) return false;
|
||||
// const protocol = request.protocol;
|
||||
// const hostname = request.hostname;
|
||||
// const originalUrl = request.url;
|
||||
// return `${protocol}://${hostname}${originalUrl.split('?')[0]}?page=${pageNum}&limit=${limit}`;
|
||||
// };
|
||||
|
||||
// return {
|
||||
// page,
|
||||
// limit,
|
||||
// totalItems,
|
||||
// totalPages,
|
||||
// prevPage: formatLink(prevPage),
|
||||
// nextPage: formatLink(nextPage),
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class ResponseInterceptor implements NestInterceptor {
|
||||
constructor() { }
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler<Record<string, unknown>>): Observable<unknown> {
|
||||
// For REST endpoints, wrap the response
|
||||
try {
|
||||
const ctx = context.switchToHttp().getResponse<FastifyReply>();
|
||||
const statusCode = ctx.statusCode;
|
||||
|
||||
// If statusCode is undefined, skip wrapping
|
||||
if (statusCode === undefined) {
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
return next.handle().pipe(
|
||||
map(data => {
|
||||
// If data is already wrapped or doesn't need wrapping, return as-is
|
||||
if (data && typeof data === 'object' && 'data' in data && 'success' in data) {
|
||||
console.log('data', data)
|
||||
|
||||
return {
|
||||
data: data,
|
||||
...('nextCursor' in (data as any) && (data as any).nextCursor != null
|
||||
? { nextCursor: (data as any).nextCursor }
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a paginated result (has both data and meta properties)
|
||||
if (data && typeof data === 'object' && 'data' in data && 'meta' in data) {
|
||||
return {
|
||||
statusCode,
|
||||
success: true,
|
||||
data: data.data,
|
||||
meta: data.meta,
|
||||
};
|
||||
}
|
||||
|
||||
if (data && 'data' in data) {
|
||||
|
||||
return {
|
||||
statusCode,
|
||||
success: true,
|
||||
data: data.data,
|
||||
...('nextCursor' in data
|
||||
? { nextCursor: (data as any).nextCursor }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
statusCode,
|
||||
success: true,
|
||||
data,
|
||||
};
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
// If we can't get HTTP context, return data as-is
|
||||
return next.handle();
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
export class HTTPLogger implements NestMiddleware {
|
||||
private readonly logger = new Logger(HTTPLogger.name);
|
||||
// use(req: any, res: any, next: (error?: Error | any) => void) {}
|
||||
|
||||
use(req: FastifyRequest, rep: FastifyReply['raw'], next: () => void) {
|
||||
const { ip, method, originalUrl } = req;
|
||||
const userAgent = req.headers['user-agent'] || '';
|
||||
const startAt = process.hrtime();
|
||||
|
||||
rep.on('finish', () => {
|
||||
const { statusCode } = rep;
|
||||
const contentLength = rep.getHeader('content-length') || 0;
|
||||
const dif = process.hrtime(startAt);
|
||||
const responseTime = dif[0] * 1e3 + dif[1] * 1e-6;
|
||||
this.logger.log(
|
||||
`${method} - ${originalUrl} - ${statusCode} - ${contentLength} - ${userAgent} - ${ip} - ${responseTime.toFixed(2)}ms`,
|
||||
);
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Logger, ValidationPipe } from '@nestjs/common';
|
||||
import { getSwaggerConfig } from './config/swagger.config';
|
||||
import multipart from '@fastify/multipart';
|
||||
import fastifyCookie from '@fastify/cookie';
|
||||
|
||||
// 👈 Import the Fastify application type
|
||||
import { type NestFastifyApplication, FastifyAdapter } from '@nestjs/platform-fastify';
|
||||
import { HttpExceptionFilter } from './core/exceptions/http.exceptions';
|
||||
import { ResponseInterceptor } from './core/interceptors/response.interceptor';
|
||||
// import { PaginationInterceptor } from './core/interceptors/pagination.interceptor';
|
||||
|
||||
async function bootstrap() {
|
||||
const logger = new Logger('APP');
|
||||
|
||||
// 1. Create the application using the Fastify adapter
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
AppModule,
|
||||
new FastifyAdapter(), // Instantiate the FastifyAdapter
|
||||
// Optional: Pass the custom logger (like your 'APP' logger) to NestFactory
|
||||
{ logger: ['error', 'warn', 'log', 'debug', 'verbose'] },
|
||||
);
|
||||
|
||||
await app.register(fastifyCookie);
|
||||
|
||||
await app.register(multipart);
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
|
||||
app.useGlobalInterceptors(
|
||||
new ResponseInterceptor(),
|
||||
// , new PaginationInterceptor()
|
||||
);
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
|
||||
const configService = app.get<ConfigService>(ConfigService);
|
||||
// Ensure the port is handled correctly, use 4000 as fallback if needed
|
||||
const APP_PORT = configService.getOrThrow<number>('APP_PORT') ?? 4000;
|
||||
|
||||
// 2. Swagger Integration Note
|
||||
// Since you are using platform-fastify, you must ensure your getSwaggerConfig
|
||||
// function is configured to use the Fastify platform option if necessary.
|
||||
getSwaggerConfig(app);
|
||||
|
||||
app.enableCors({
|
||||
origin: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||
credentials: true,
|
||||
optionsSuccessStatus: 204,
|
||||
});
|
||||
|
||||
// 3. Listen on port using Fastify's listen method
|
||||
// '0.0.0.0' is recommended for Docker/containerized environments
|
||||
await app.listen(APP_PORT, '0.0.0.0', () => {
|
||||
logger.log(`Application is running on: http://localhost:${APP_PORT}`);
|
||||
logger.log(`Swagger documentation: http://localhost:${APP_PORT}/docs`);
|
||||
});
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { AdminService } from './providers/admin.service';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Admin } from './entities/admin.entity';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { AdminRepository } from './repositories/admin.repository';
|
||||
import { Role } from '../roles/entities/role.entity';
|
||||
import { Permission } from '../roles/entities/permission.entity';
|
||||
import { RolePermission } from '../roles/entities/rolePermission.entity';
|
||||
import { AdminController } from './controllers/admin.controller';
|
||||
import { UtilsModule } from '../utils/utils.module';
|
||||
import { RestaurantsModule } from '../restaurants/restaurants.module';
|
||||
import { AdminRole } from './entities/adminRole.entity';
|
||||
|
||||
@Module({
|
||||
providers: [AdminService, AdminRepository],
|
||||
controllers: [AdminController],
|
||||
imports: [
|
||||
MikroOrmModule.forFeature([Admin, Role, Permission, RolePermission, AdminRole]),
|
||||
JwtModule,
|
||||
UtilsModule,
|
||||
forwardRef(() => RestaurantsModule),
|
||||
],
|
||||
exports: [AdminService, AdminRepository],
|
||||
})
|
||||
export class AdminModule {}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Controller, Post, Body, HttpCode, HttpStatus, Get, UseGuards, Patch, Param, Delete } from '@nestjs/common';
|
||||
import { AdminService } from '../providers/admin.service';
|
||||
import { CreateAdminDto } from '../dto/create-admin.dto';
|
||||
import { UpdateAdminDto } from '../dto/update-admin.dto';
|
||||
import { ApiTags, ApiOperation, ApiBody, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
|
||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||
import { SuperAdminAuthGuard } from 'src/modules/auth/guards/superAdminAuth.guard';
|
||||
import { RestId } from 'src/common/decorators';
|
||||
import { AdminId } from 'src/common/decorators/admin-id.decorator';
|
||||
import { Admin } from '../entities/admin.entity';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { CreateMyRestaurantAdminDto } from '../dto/create-my-restaurant-admin.dto';
|
||||
|
||||
@ApiBearerAuth()
|
||||
@ApiTags('admin')
|
||||
@Controller()
|
||||
export class AdminController {
|
||||
constructor(private readonly adminService: AdminService) {}
|
||||
|
||||
@Get('admin/admins')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_ADMINS)
|
||||
@ApiOperation({ summary: 'admin' })
|
||||
async getAll(@RestId() restId: string) {
|
||||
const admin = await this.adminService.findAllByRestaurantId(restId);
|
||||
return admin;
|
||||
}
|
||||
|
||||
@Get('admin/admins/profile')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_ADMINS)
|
||||
@ApiOperation({ summary: 'admin' })
|
||||
async getMe(@AdminId() adminId: string, @RestId() restId: string) {
|
||||
const admin = await this.adminService.findById(adminId, restId);
|
||||
return admin;
|
||||
}
|
||||
|
||||
@Post('admin/admins')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_ADMINS)
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create a new admin' })
|
||||
@ApiBody({ type: CreateAdminDto })
|
||||
async create(@Body() dto: CreateAdminDto, @RestId() restId: string) {
|
||||
const admin = await this.adminService.createAdminForMyRestaurant(restId, dto);
|
||||
return admin;
|
||||
}
|
||||
|
||||
@Patch('admin/admins/:adminId')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_ADMINS)
|
||||
@ApiOperation({ summary: 'Update an admin' })
|
||||
@ApiParam({ name: 'adminId', description: 'Admin ID' })
|
||||
@ApiBody({ type: UpdateAdminDto })
|
||||
update(@Param('adminId') adminId: string, @Body() dto: UpdateAdminDto, @RestId() restId: string): Promise<Admin> {
|
||||
return this.adminService.update(adminId, restId, dto);
|
||||
}
|
||||
|
||||
@Get('admin/admins/:adminId')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_ADMINS)
|
||||
@ApiOperation({ summary: 'Get an admin by ID' })
|
||||
@ApiParam({ name: 'id', description: 'Admin ID' })
|
||||
getById(@Param('adminId') adminId: string, @RestId() restId: string): Promise<Admin | null> {
|
||||
return this.adminService.findById(adminId, restId);
|
||||
}
|
||||
|
||||
@Delete('admin/admins/:adminId')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_ADMINS)
|
||||
@ApiOperation({ summary: 'Delete an admin by ID' })
|
||||
@ApiParam({ name: 'id', description: 'Admin ID' })
|
||||
deleteById(@Param('adminId') adminId: string, @RestId() restId: string): Promise<void> {
|
||||
return this.adminService.remove(adminId, restId);
|
||||
}
|
||||
|
||||
/** Super Admin Endpoints */
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@Get('super-admin/restaurants/:restaurantId/admins')
|
||||
@ApiOperation({ summary: 'Get admins for a specific restaurant (Super Admin only)' })
|
||||
@ApiParam({ name: 'restaurantId', description: 'Restaurant ID' })
|
||||
getAdminForRestaurant(@Param('restaurantId') restaurantId: string): Promise<Admin[]> {
|
||||
return this.adminService.getAdminsForRestaurantBySuperAdmin(restaurantId);
|
||||
}
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@Post('super-admin/restaurants/:restaurantId/admins')
|
||||
@ApiOperation({ summary: 'Create admin for a specific restaurant (Super Admin only)' })
|
||||
@ApiParam({ name: 'restaurantId', description: 'Restaurant ID' })
|
||||
@ApiBody({ type: CreateMyRestaurantAdminDto })
|
||||
createAdminForRestaurant(
|
||||
@Param('restaurantId') restaurantId: string,
|
||||
@Body() dto: CreateMyRestaurantAdminDto,
|
||||
): Promise<Admin> {
|
||||
return this.adminService.createAdminForRestaurantBySuperAdmin(restaurantId, dto);
|
||||
}
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@Delete('super-admin/restaurants/:restaurantId/admins/:adminId')
|
||||
@ApiOperation({ summary: 'Revoke admin role from a restaurant (Super Admin only)' })
|
||||
@ApiParam({ name: 'restaurantId', description: 'Restaurant ID' })
|
||||
@ApiParam({ name: 'adminId', description: 'Admin ID' })
|
||||
revokeAdminFromRestaurant(
|
||||
@Param('restaurantId') restaurantId: string,
|
||||
@Param('adminId') adminId: string,
|
||||
): Promise<void> {
|
||||
return this.adminService.remove(adminId, restaurantId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateAdminDto {
|
||||
@ApiProperty({ description: 'Mobile phone number' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
phone!: string;
|
||||
|
||||
@ApiProperty({ description: 'First name', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
firstName?: string;
|
||||
|
||||
@ApiProperty({ description: 'Last name', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
lastName?: string;
|
||||
|
||||
@ApiProperty({ description: 'Role id for the admin' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
roleId!: string;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateMyRestaurantAdminDto {
|
||||
@ApiProperty({ description: 'Mobile phone number' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
phone!: string;
|
||||
|
||||
@ApiProperty({ description: 'First name', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
firstName?: string;
|
||||
|
||||
@ApiProperty({ description: 'Last name', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
lastName?: string;
|
||||
|
||||
@ApiProperty({ description: 'Role id for the admin' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
roleId!: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateAdminDto } from './create-admin.dto';
|
||||
|
||||
export class UpdateAdminDto extends PartialType(CreateAdminDto) {}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Cascade, Collection, Entity, OneToMany, Property } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { AdminRole } from './adminRole.entity';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
|
||||
@Entity({ tableName: 'admins' })
|
||||
export class Admin extends BaseEntity {
|
||||
@Property({ nullable: true })
|
||||
firstName?: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
lastName?: string;
|
||||
|
||||
private _phone!: string;
|
||||
|
||||
@Property({ unique: true })
|
||||
get phone(): string {
|
||||
return this._phone;
|
||||
}
|
||||
|
||||
set phone(value: string) {
|
||||
this._phone = normalizePhone(value);
|
||||
}
|
||||
|
||||
// Add the new role property
|
||||
@OneToMany(() => AdminRole, adminRole => adminRole.admin, {
|
||||
cascade: [Cascade.ALL],
|
||||
orphanRemoval: true,
|
||||
})
|
||||
roles = new Collection<AdminRole>(this);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Entity, Index, ManyToOne, Unique } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Role } from '../../roles/entities/role.entity';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
import { Admin } from './admin.entity';
|
||||
|
||||
@Entity({ tableName: 'admin_roles' })
|
||||
@Unique({ properties: ['admin', 'restaurant'] })
|
||||
@Index({ properties: ['admin', 'restaurant'] })
|
||||
@Index({ properties: ['admin'] })
|
||||
@Index({ properties: ['restaurant'] })
|
||||
export class AdminRole extends BaseEntity {
|
||||
@ManyToOne(() => Admin)
|
||||
admin!: Admin;
|
||||
|
||||
@ManyToOne(() => Role)
|
||||
role!: Role;
|
||||
|
||||
@ManyToOne(() => Restaurant, { nullable: true })
|
||||
restaurant?: Restaurant;
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
import { EntityRepository } from '@mikro-orm/core';
|
||||
import { Admin } from '../entities/admin.entity';
|
||||
import { Role } from '../../roles/entities/role.entity';
|
||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { CacheService } from '../../utils/cache.service';
|
||||
import { CreateMyRestaurantAdminDto } from '../dto/create-my-restaurant-admin.dto';
|
||||
import { UpdateAdminDto } from '../dto/update-admin.dto';
|
||||
import { AdminRole } from '../entities/adminRole.entity';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
constructor(
|
||||
@InjectRepository(Admin)
|
||||
private readonly adminRepository: EntityRepository<Admin>,
|
||||
@InjectRepository(AdminRole)
|
||||
private readonly adminRoleRepository: EntityRepository<AdminRole>,
|
||||
private readonly em: EntityManager,
|
||||
private readonly cacheService: CacheService,
|
||||
) {}
|
||||
|
||||
async findByPhone(phone: string): Promise<Admin | null> {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
return this.adminRepository.findOne({ phone: normalizedPhone });
|
||||
}
|
||||
|
||||
async findById(adminId: string, restId: string): Promise<Admin | null> {
|
||||
const admin = await this.adminRepository.findOne(
|
||||
{ id: adminId, roles: { restaurant: { id: restId } } },
|
||||
{ populate: ['roles', 'roles.role'] },
|
||||
);
|
||||
return admin;
|
||||
}
|
||||
|
||||
async findAllByRestaurantId(restId: string): Promise<Admin[]> {
|
||||
return this.adminRepository.find({ roles: { restaurant: { id: restId } } }, { populate: ['roles', 'roles.role'] });
|
||||
}
|
||||
|
||||
async createAdminForMyRestaurant(restId: string, dto: CreateMyRestaurantAdminDto) {
|
||||
const { phone, firstName, lastName, roleId } = dto;
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
let admin: Admin | null = null;
|
||||
admin = await this.adminRepository.findOne({
|
||||
phone: normalizedPhone,
|
||||
});
|
||||
if (!admin) {
|
||||
admin = this.adminRepository.create({
|
||||
phone: normalizedPhone,
|
||||
firstName,
|
||||
lastName,
|
||||
});
|
||||
await this.em.persistAndFlush(admin);
|
||||
}
|
||||
const role = await this.em.findOne(Role, { id: roleId, isSystem: false });
|
||||
if (!role) throw new NotFoundException('Role not found');
|
||||
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) throw new NotFoundException('Restaurant not found');
|
||||
|
||||
let adminRole = await this.adminRoleRepository.findOne({
|
||||
admin: admin,
|
||||
restaurant: restaurant,
|
||||
});
|
||||
|
||||
if (!adminRole) {
|
||||
adminRole = this.adminRoleRepository.create({
|
||||
admin: admin,
|
||||
role,
|
||||
restaurant,
|
||||
});
|
||||
} else {
|
||||
adminRole.role = role;
|
||||
}
|
||||
|
||||
await this.em.persistAndFlush(adminRole);
|
||||
return admin;
|
||||
}
|
||||
|
||||
async createAdminForRestaurantBySuperAdmin(restId: string, dto: CreateMyRestaurantAdminDto) {
|
||||
const { phone, firstName, lastName, roleId } = dto;
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
let admin: Admin | null = null;
|
||||
admin = await this.adminRepository.findOne({
|
||||
phone: normalizedPhone,
|
||||
});
|
||||
if (!admin) {
|
||||
admin = this.adminRepository.create({
|
||||
phone: normalizedPhone,
|
||||
firstName,
|
||||
lastName,
|
||||
});
|
||||
await this.em.persistAndFlush(admin);
|
||||
}
|
||||
const role = await this.em.findOne(Role, { id: roleId });
|
||||
if (!role) throw new NotFoundException('Role* not found' + roleId);
|
||||
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) throw new NotFoundException('Restaurant not found');
|
||||
|
||||
let adminRole = await this.adminRoleRepository.findOne({
|
||||
admin: admin,
|
||||
restaurant: restaurant,
|
||||
});
|
||||
|
||||
if (!adminRole) {
|
||||
adminRole = this.adminRoleRepository.create({
|
||||
admin: admin,
|
||||
role,
|
||||
restaurant,
|
||||
});
|
||||
} else {
|
||||
adminRole.role = role;
|
||||
}
|
||||
|
||||
await this.em.persistAndFlush(adminRole);
|
||||
return admin;
|
||||
}
|
||||
|
||||
async getAdminsForRestaurantBySuperAdmin(restId: string): Promise<Admin[]> {
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) throw new NotFoundException('Restaurant not found');
|
||||
|
||||
return this.adminRepository.find({ roles: { restaurant: restaurant } }, { populate: ['roles', 'roles.role'] });
|
||||
}
|
||||
|
||||
async update(adminId: string, restId: string, dto: UpdateAdminDto): Promise<Admin> {
|
||||
const admin = await this.adminRepository.findOne(
|
||||
{ id: adminId, roles: { restaurant: { id: restId } } },
|
||||
{ populate: ['roles', 'roles.role', 'roles.restaurant'] },
|
||||
);
|
||||
|
||||
if (!admin) {
|
||||
throw new NotFoundException('Admin not found');
|
||||
}
|
||||
|
||||
const { roleId, ...rest } = dto;
|
||||
|
||||
// Update scalar fields (firstName, lastName, phone)
|
||||
if (rest.firstName !== undefined) {
|
||||
admin.firstName = rest.firstName;
|
||||
}
|
||||
if (rest.lastName !== undefined) {
|
||||
admin.lastName = rest.lastName;
|
||||
}
|
||||
if (rest.phone !== undefined && rest.phone !== admin.phone) {
|
||||
const normalizedPhone = normalizePhone(rest.phone);
|
||||
const exists = await this.adminRepository.findOne({ phone: normalizedPhone });
|
||||
if (exists) {
|
||||
throw new ConflictException('This Phone Number is already taken');
|
||||
}
|
||||
admin.phone = normalizedPhone;
|
||||
}
|
||||
|
||||
// Update role if roleId is provided
|
||||
if (roleId) {
|
||||
const role = await this.em.findOne(Role, { id: roleId, isSystem: false });
|
||||
if (!role) {
|
||||
throw new NotFoundException('Role not found');
|
||||
}
|
||||
|
||||
// Find existing AdminRole for this admin and restaurant
|
||||
const existingAdminRole = await this.em.findOne(AdminRole, {
|
||||
admin: { id: adminId },
|
||||
restaurant: { id: restId },
|
||||
});
|
||||
|
||||
if (existingAdminRole) {
|
||||
// Update existing role
|
||||
existingAdminRole.role = role;
|
||||
} else {
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) throw new NotFoundException('Restaurant not found');
|
||||
// Create new AdminRole
|
||||
const newAdminRole = this.em.create(AdminRole, {
|
||||
admin,
|
||||
role,
|
||||
restaurant,
|
||||
});
|
||||
admin.roles.add(newAdminRole);
|
||||
}
|
||||
}
|
||||
|
||||
await this.em.persistAndFlush(admin);
|
||||
return admin;
|
||||
}
|
||||
|
||||
async remove(adminId: string, restId: string): Promise<void> {
|
||||
const adminRole = await this.adminRoleRepository.findOne({ admin: { id: adminId }, restaurant: { id: restId } });
|
||||
if (!adminRole) {
|
||||
throw new NotFoundException('Admin role not found');
|
||||
}
|
||||
return this.em.removeAndFlush(adminRole);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { Admin } from '../entities/admin.entity';
|
||||
import { AdminRole } from '../entities/adminRole.entity';
|
||||
import { RestRepository } from '../../restaurants/repositories/rest.repository';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
|
||||
@Injectable()
|
||||
export class AdminRepository extends EntityRepository<Admin> {
|
||||
constructor(
|
||||
readonly em: EntityManager,
|
||||
private readonly restRepository: RestRepository,
|
||||
) {
|
||||
super(em, Admin);
|
||||
}
|
||||
|
||||
async findByPhoneAndRestaurantSlug(phone: string, slug: string): Promise<Admin | null> {
|
||||
console.log('phone', phone);
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
// First, find the restaurant by slug using the same repository as auth service
|
||||
const restaurant = await this.restRepository.findOne({ slug });
|
||||
if (!restaurant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find AdminRole that matches the admin phone and restaurant
|
||||
const adminRole = await this.em.findOne(
|
||||
AdminRole,
|
||||
{
|
||||
admin: { phone: normalizedPhone },
|
||||
restaurant: { id: restaurant.id },
|
||||
},
|
||||
{ populate: ['admin', 'admin.roles', 'role', 'role.permissions', 'restaurant'] },
|
||||
);
|
||||
|
||||
if (!adminRole || !adminRole.admin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure all roles are populated
|
||||
await adminRole.admin.roles.loadItems();
|
||||
for (const role of adminRole.admin.roles.getItems()) {
|
||||
if (role.role && role.role.permissions && !role.role.permissions.isInitialized()) {
|
||||
await role.role.permissions.loadItems();
|
||||
}
|
||||
}
|
||||
|
||||
return adminRole.admin;
|
||||
}
|
||||
async findAdminsWithPermission(restaurantId: string, permission: string): Promise<Admin[]> {
|
||||
const admins = await this.em.find(
|
||||
Admin,
|
||||
{ roles: { restaurant: { id: restaurantId }, role: { permissions: { name: permission } } } },
|
||||
{ populate: ['roles', 'roles.role', 'roles.role.permissions'] },
|
||||
);
|
||||
return admins;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Admin } from '../entities/admin.entity';
|
||||
|
||||
export interface AdminDetailResponse {
|
||||
id: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
phone: string;
|
||||
role: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
permissions: string[];
|
||||
restaurant?: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class AdminDetailTransformer {
|
||||
static transform(admin: Admin): AdminDetailResponse | null {
|
||||
if (!admin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract role information
|
||||
const role = admin.roles.getItems().find(r => r.role)?.role;
|
||||
if (!role) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: admin.id,
|
||||
firstName: admin.firstName,
|
||||
lastName: admin.lastName,
|
||||
phone: admin.phone,
|
||||
role: {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
},
|
||||
permissions: role.permissions.getItems().map(p => p.name) || [],
|
||||
restaurant: {
|
||||
id: role.restaurant?.id || '',
|
||||
name: role.restaurant?.name || '',
|
||||
slug: role.restaurant?.slug || '',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Extract permissions - ensure collection is loaded if needed
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { AuthController } from './controllers/auth.controller';
|
||||
import { UtilsModule } from '../utils/utils.module';
|
||||
import { UserModule } from '../users/user.module';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { TokensService } from './services/tokens.service';
|
||||
import { RestaurantsModule } from '../restaurants/restaurants.module';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
import { AdminAuthGuard } from './guards/adminAuth.guard';
|
||||
import { RefreshToken } from './entities/refresh-token.entity';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UtilsModule,
|
||||
forwardRef(() => UserModule),
|
||||
JwtModule.registerAsync({
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const expiresIn = configService.getOrThrow<number>('JWT_EXPIRATION_TIME');
|
||||
return {
|
||||
global: true,
|
||||
secret: configService.getOrThrow<string>('JWT_SECRET'),
|
||||
signOptions: {
|
||||
// Use string format with time unit for explicit expiration (value should be in seconds)
|
||||
expiresIn: `${expiresIn}s`,
|
||||
},
|
||||
};
|
||||
},
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
forwardRef(() => AdminModule),
|
||||
forwardRef(() => RestaurantsModule),
|
||||
MikroOrmModule.forFeature([User, RefreshToken]),
|
||||
forwardRef(() => NotificationsModule),
|
||||
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, TokensService, AdminAuthGuard],
|
||||
exports: [AdminAuthGuard],
|
||||
})
|
||||
export class AuthModule {}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { RequestOtpDto } from '../dto/request-otp.dto';
|
||||
import { DirectLoginDto } from '../dto/direct-login.dto';
|
||||
import { ApiTags, ApiOperation, ApiBody } from '@nestjs/swagger';
|
||||
import { VerifyOtpDto } from '../dto/verify-otp.dto';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import { RefreshTokenRateLimit } from 'src/common/decorators/rate-limit.decorator';
|
||||
import { RefreshTokenDto } from '../dto/refresh-token.dto';
|
||||
import { SuperAdminAuthGuard } from '../guards/superAdminAuth.guard';
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller()
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) { }
|
||||
|
||||
@Throttle({ default: { limit: 3, ttl: 180_000 } })
|
||||
@Post('public/auth/otp/request')
|
||||
@ApiOperation({ summary: 'Request OTP for login or signup' })
|
||||
@ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
|
||||
otpRequest(@Body() dto: RequestOtpDto) {
|
||||
return this.authService.requestOtp(dto, false);
|
||||
}
|
||||
|
||||
@Post('public/auth/otp/verify')
|
||||
@ApiOperation({ summary: 'Verify OTP code' })
|
||||
@ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' })
|
||||
otpVerify(@Body() dto: VerifyOtpDto) {
|
||||
return this.authService.verifyOtp(dto.phone, dto.slug, dto.otp); // assuming dto has `otp` property
|
||||
}
|
||||
|
||||
@RefreshTokenRateLimit()
|
||||
@ApiOperation({ summary: 'refresh the user access token / refresh token' })
|
||||
@Post('public/auth/refresh')
|
||||
refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
|
||||
return this.authService.refreshToken(refreshTokenDto.refreshToken);
|
||||
}
|
||||
|
||||
@Throttle({ default: { limit: 3, ttl: 180_000 } })
|
||||
@Post('admin/auth/otp/request')
|
||||
@ApiOperation({ summary: 'Request OTP for login or signup' })
|
||||
@ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
|
||||
otpRequestAdmin(@Body() dto: RequestOtpDto) {
|
||||
return this.authService.requestOtp(dto, true);
|
||||
}
|
||||
|
||||
@Post('admin/auth/otp/verify')
|
||||
@ApiOperation({ summary: 'Verify OTP code' })
|
||||
@ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' })
|
||||
otpVerifyAdmin(@Body() dto: VerifyOtpDto) {
|
||||
return this.authService.verifyOtpAdmin(dto.phone, dto.slug, dto.otp);
|
||||
}
|
||||
|
||||
@RefreshTokenRateLimit()
|
||||
@ApiOperation({ summary: 'refresh the admin access token / refresh token' })
|
||||
@Post('admin/auth/refresh')
|
||||
refreshTokenAdmin(@Body() refreshTokenDto: RefreshTokenDto) {
|
||||
return this.authService.refreshToken(refreshTokenDto.refreshToken);
|
||||
}
|
||||
|
||||
//super admin routes
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@Post('super-admin/auth/direct-login')
|
||||
@ApiOperation({ summary: 'Direct login for DSC - returns login credentials' })
|
||||
@ApiBody({ type: DirectLoginDto, description: 'Phone number and restaurant slug for direct login' })
|
||||
directloginForDsc(@Body() dto: DirectLoginDto) {
|
||||
return this.authService.loginAdminForDsc(dto.phone, dto.slug);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IsNotEmpty, IsString, IsMobilePhone } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class DirectLoginDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
|
||||
@IsMobilePhone('fa-IR')
|
||||
phone: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'zhivan', description: 'Restaurant slug' })
|
||||
slug: string;
|
||||
}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class RefreshTokenDto {
|
||||
@ApiProperty({ description: 'Refresh token' })
|
||||
@IsString({ message: 'Refresh token must be a string' })
|
||||
@IsNotEmpty({ message: 'Refresh token is required' })
|
||||
refreshToken: string;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IsNotEmpty, IsString, IsMobilePhone } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class RequestOtpDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
|
||||
@IsMobilePhone('fa-IR')
|
||||
phone: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'zhivan', description: 'restaurant slug' })
|
||||
slug: string;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { IsNotEmpty, IsString, Length, IsMobilePhone } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class VerifyOtpDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
|
||||
@IsMobilePhone('fa-IR')
|
||||
phone: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '', description: 'Otp' })
|
||||
@Length(5)
|
||||
otp: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'zhivan', description: 'restaurant slug' })
|
||||
slug: string;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Entity, Enum, Index, Property } from '@mikro-orm/core';
|
||||
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
|
||||
export enum RefreshTokenType {
|
||||
USER = 'user',
|
||||
ADMIN = 'admin',
|
||||
}
|
||||
|
||||
@Entity({ tableName: 'refreshtokens' })
|
||||
@Index({ properties: ['ownerId', 'restId', 'type'] })
|
||||
@Index({ properties: ['hashedToken'] })
|
||||
@Index({ properties: ['expiresAt'] })
|
||||
export class RefreshToken extends BaseEntity {
|
||||
@Property({ type: 'varchar', length: 255 })
|
||||
hashedToken!: string;
|
||||
|
||||
@Property()
|
||||
ownerId!: string;
|
||||
|
||||
@Property()
|
||||
restId!: string;
|
||||
|
||||
@Enum(() => RefreshTokenType)
|
||||
type!: RefreshTokenType;
|
||||
|
||||
@Property({ type: 'timestamptz' })
|
||||
expiresAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
Inject,
|
||||
Logger,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Request } from 'express';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { IAdminTokenPayload } from '../interfaces/IToken-payload';
|
||||
import { PERMISSIONS_KEY } from '../../../common/decorators/permissions.decorator';
|
||||
import { PermissionsService } from 'src/modules/roles/providers/permissions.service';
|
||||
|
||||
export interface AdminAuthRequest extends Request {
|
||||
adminId: string;
|
||||
restId: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminAuthGuard implements CanActivate {
|
||||
private readonly logger = new Logger(AdminAuthGuard.name);
|
||||
|
||||
constructor(
|
||||
@Inject(JwtService)
|
||||
private readonly jwtService: JwtService,
|
||||
@Inject(ConfigService)
|
||||
private readonly configService: ConfigService,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly reflector: Reflector,
|
||||
) { }
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest<AdminAuthRequest>();
|
||||
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
if (!token) {
|
||||
this.logger.warn('No token provided', {
|
||||
hasAuthHeader: !!request.headers.authorization,
|
||||
authHeader: request.headers.authorization ? 'present' : 'missing',
|
||||
headers: Object.keys(request.headers),
|
||||
});
|
||||
throw new UnauthorizedException('No token provided');
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the JWT secret from config
|
||||
const secret = this.configService.getOrThrow<string>('JWT_SECRET');
|
||||
|
||||
// Verify token with the secret
|
||||
const payload = await this.jwtService.verifyAsync<IAdminTokenPayload>(token, {
|
||||
secret,
|
||||
});
|
||||
|
||||
if (!payload.adminId || !payload.restId) {
|
||||
this.logger.error('Invalid token payload structure', payload);
|
||||
throw new UnauthorizedException('Invalid token payload');
|
||||
}
|
||||
|
||||
request['adminId'] = payload.adminId;
|
||||
request['restId'] = payload.restId;
|
||||
|
||||
// check if the user has the required permissions
|
||||
const requiredPermissions =
|
||||
this.reflector.getAllAndOverride<string[]>(PERMISSIONS_KEY, [context.getHandler(), context.getClass()]) ?? [];
|
||||
|
||||
if (!requiredPermissions || requiredPermissions.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const adminPermission = await this.permissionsService.getAdminPermissions(payload.adminId, payload.restId);
|
||||
if (!adminPermission || !Array.isArray(adminPermission)) {
|
||||
this.logger.error('No permissions found', { adminId: payload.adminId, restId: payload.restId });
|
||||
throw new ForbiddenException('No permissions found');
|
||||
}
|
||||
|
||||
const hasPermission = requiredPermissions.every(p => adminPermission.includes(p));
|
||||
|
||||
if (!hasPermission) {
|
||||
this.logger.warn('Insufficient permissions', {
|
||||
adminId: payload.adminId,
|
||||
restId: payload.restId,
|
||||
required: requiredPermissions,
|
||||
has: adminPermission,
|
||||
});
|
||||
throw new ForbiddenException('You are not authorized to access this resource');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err instanceof ForbiddenException || err instanceof UnauthorizedException) {
|
||||
throw err;
|
||||
}
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||
const errorStack = err instanceof Error ? err.stack : undefined;
|
||||
this.logger.error('Token verification error in AdminAuthGuard', {
|
||||
error: errorMessage,
|
||||
stack: errorStack,
|
||||
});
|
||||
throw new UnauthorizedException('Invalid or expired token');
|
||||
}
|
||||
}
|
||||
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const authHeader = request.headers.authorization || request.headers['authorization'];
|
||||
if (!authHeader) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [type, token] = authHeader.split(' ');
|
||||
if (type?.toLowerCase() !== 'bearer' || !token) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, Inject, Logger } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ITokenPayload } from '../interfaces/IToken-payload';
|
||||
|
||||
export interface UserAuthRequest extends Request {
|
||||
userId: string;
|
||||
restId: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
private readonly logger = new Logger(AuthGuard.name);
|
||||
|
||||
constructor(
|
||||
@Inject(JwtService)
|
||||
private readonly jwtService: JwtService,
|
||||
@Inject(ConfigService)
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest<UserAuthRequest>();
|
||||
|
||||
try {
|
||||
const secret = this.configService.getOrThrow<string>('JWT_SECRET');
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
const slug = this.extractSlugFromHeader(request);
|
||||
if (!slug) {
|
||||
throw new UnauthorizedException('Slug is required');
|
||||
}
|
||||
if (!token) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync<ITokenPayload>(token, {
|
||||
secret,
|
||||
});
|
||||
if (!payload.userId || !payload.restId) {
|
||||
this.logger.error('Invalid token payload structure', payload);
|
||||
throw new UnauthorizedException('Invalid token payload');
|
||||
}
|
||||
if (payload.slug !== slug) {
|
||||
throw new UnauthorizedException('Invalid slug');
|
||||
}
|
||||
request['userId'] = payload.userId;
|
||||
request['restId'] = payload.restId;
|
||||
} catch {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err instanceof UnauthorizedException) {
|
||||
throw err;
|
||||
}
|
||||
this.logger.error('error in AuthGuard', err);
|
||||
throw new UnauthorizedException('Invalid or expired token');
|
||||
}
|
||||
}
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
private extractSlugFromHeader(request: Request): string | undefined {
|
||||
// Fastify normalizes headers to lowercase, so check both cases
|
||||
const slug = (request.headers['x-slug'] || request.headers['X-Slug']) as string;
|
||||
return slug;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, Inject, Logger } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ITokenPayload } from '../interfaces/IToken-payload';
|
||||
|
||||
export interface UserOptionalAuthRequest extends Request {
|
||||
userId?: string;
|
||||
restId?: string;
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class OptionalAuthGuard implements CanActivate {
|
||||
private readonly logger = new Logger(OptionalAuthGuard.name);
|
||||
|
||||
constructor(
|
||||
@Inject(JwtService)
|
||||
private readonly jwtService: JwtService,
|
||||
@Inject(ConfigService)
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<UserOptionalAuthRequest>();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
const slug = this.extractSlugFromHeader(request);
|
||||
|
||||
// If no token is provided, allow the request without authentication
|
||||
if (!slug) {
|
||||
console.log('No slug provided');
|
||||
this.logger.warn('No slug provided');
|
||||
return false;
|
||||
}
|
||||
if (!token) {
|
||||
request['slug'] = slug;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to verify the token if it exists
|
||||
try {
|
||||
const secret = this.configService.getOrThrow<string>('JWT_SECRET');
|
||||
const payload = await this.jwtService.verifyAsync<ITokenPayload>(token, {
|
||||
secret,
|
||||
});
|
||||
|
||||
// Validate payload structure
|
||||
if (!payload.userId || !payload.restId) {
|
||||
this.logger.warn('Invalid token payload structure', payload);
|
||||
// Allow request but don't set user info
|
||||
request['slug'] = slug;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validate slug if provided
|
||||
if (slug && payload.slug !== slug) {
|
||||
this.logger.warn('Token slug does not match header slug', {
|
||||
tokenSlug: payload.slug,
|
||||
headerSlug: slug,
|
||||
});
|
||||
// Allow request but don't set user info
|
||||
request['slug'] = slug;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Token is valid, set user info
|
||||
request['userId'] = payload.userId;
|
||||
request['restId'] = payload.restId;
|
||||
request['slug'] = slug;
|
||||
return true;
|
||||
} catch (err) {
|
||||
// Token is invalid or expired, but allow the request anyway
|
||||
this.logger.debug('Token verification failed in OptionalAuthGuard', {
|
||||
error: err instanceof Error ? err.message : 'Unknown error',
|
||||
});
|
||||
// Set restId from slug
|
||||
|
||||
request['slug'] = slug;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
|
||||
private extractSlugFromHeader(request: Request): string | undefined {
|
||||
// Fastify normalizes headers to lowercase, so check both cases
|
||||
const slug = (request.headers['x-slug'] || request.headers['X-Slug']) as string;
|
||||
return slug;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, Inject, Logger } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class SuperAdminAuthGuard implements CanActivate {
|
||||
private readonly logger = new Logger(SuperAdminAuthGuard.name);
|
||||
|
||||
constructor(
|
||||
@Inject(ConfigService)
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
|
||||
try {
|
||||
const credentials = this.extractBasicAuthCredentials(request);
|
||||
if (!credentials) {
|
||||
throw new UnauthorizedException('Basic authentication required');
|
||||
}
|
||||
|
||||
const { username, password } = credentials;
|
||||
const expectedUsername = this.configService.get<string>('SUPER_ADMIN_USERNAME') ?? 'danak@dsc.com';
|
||||
const expectedPassword = this.configService.get<string>('SUPER_ADMIN_PASSWORD') ?? 'DsCdAnAk?@ABC';
|
||||
if (username !== expectedUsername || password !== expectedPassword) {
|
||||
this.logger.warn(`Invalid super admin credentials attempt for username: ${username}`);
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err instanceof UnauthorizedException) {
|
||||
throw err;
|
||||
}
|
||||
this.logger.error('error in SuperAdminAuthGuard', err);
|
||||
throw new UnauthorizedException('Authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
private extractBasicAuthCredentials(request: Request): { username: string; password: string } | null {
|
||||
const authHeader = request.headers.authorization;
|
||||
if (!authHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [type, encoded] = authHeader.split(' ');
|
||||
if (type?.toLowerCase() !== 'basic' || !encoded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = Buffer.from(encoded, 'base64').toString('utf-8');
|
||||
const [username, password] = decoded.split(':');
|
||||
|
||||
if (!username || !password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { username, password };
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to decode Basic Auth credentials', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export interface ITokenPayload {
|
||||
userId: string;
|
||||
restId: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface IAdminTokenPayload {
|
||||
adminId: string;
|
||||
restId: string;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { RequestOtpDto } from '../dto/request-otp.dto';
|
||||
import { CacheService } from '../../utils/cache.service';
|
||||
import { SmsService } from '../../notifications/services/sms.service';
|
||||
import { UserService } from '../../users/providers/user.service';
|
||||
import { randomInt } from 'crypto';
|
||||
import { TokensService } from './tokens.service';
|
||||
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
|
||||
import { AuthMessage, RestMessage } from 'src/common/enums/message.enum';
|
||||
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
|
||||
import { AdminLoginTransformer } from '../transformers/admin-login.transformer';
|
||||
import { UserLoginTransformer } from '../transformers/user-login.transformer';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
readonly ADMIN_PERMISSIONS_KEY = 'admin-perms';
|
||||
private OTP_EXPIRATION_TIME: number;
|
||||
constructor(
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly smsService: SmsService,
|
||||
private readonly userService: UserService,
|
||||
private readonly tokensService: TokensService,
|
||||
private readonly restRepository: RestRepository,
|
||||
private readonly adminRepository: AdminRepository,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
this.OTP_EXPIRATION_TIME = this.configService.get<number>('OTP_EXPIRATION_TIME') ?? 240;
|
||||
}
|
||||
|
||||
private userOtpKey(restaurantSlug: string, phone: string) {
|
||||
return `otp:${restaurantSlug}:${phone}`;
|
||||
}
|
||||
|
||||
private adminOtpKey(restaurantSlug: string, phone: string) {
|
||||
return `otp-admin:${restaurantSlug}:${phone}`;
|
||||
}
|
||||
|
||||
async requestOtp(dto: RequestOtpDto, isAdmin: boolean) {
|
||||
const { phone, slug } = dto;
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
const code = this.generateOtpCode();
|
||||
const key = isAdmin ? this.adminOtpKey(slug, normalizedPhone) : this.userOtpKey(slug, normalizedPhone);
|
||||
await this.cacheService.set(key, code, this.OTP_EXPIRATION_TIME);
|
||||
|
||||
await this.smsService.sendotp(normalizedPhone, code);
|
||||
|
||||
return { code: null };
|
||||
}
|
||||
|
||||
async verifyOtp(phone: string, slug: string, code: string) {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
const key = this.userOtpKey(slug, normalizedPhone);
|
||||
const cachedCode = await this.cacheService.get(key);
|
||||
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
||||
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
||||
|
||||
await this.cacheService.del(key);
|
||||
|
||||
const user = await this.userService.findOrCreateByPhone(normalizedPhone);
|
||||
|
||||
const rest = await this.restRepository.findOne({ slug });
|
||||
|
||||
if (!rest) {
|
||||
throw new BadRequestException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
const tokens = await this.tokensService.generateAccessAndRefreshToken(user.id, rest.id, false, slug);
|
||||
|
||||
const userResponse = UserLoginTransformer.transform(user, rest);
|
||||
|
||||
return { tokens, user: userResponse };
|
||||
}
|
||||
|
||||
async verifyOtpAdmin(phone: string, slug: string, code: string) {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
const key = this.adminOtpKey(slug, normalizedPhone);
|
||||
const cachedCode = await this.cacheService.get(key);
|
||||
|
||||
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
||||
|
||||
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
||||
await this.cacheService.del(key);
|
||||
|
||||
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug);
|
||||
|
||||
if (!admin) {
|
||||
throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
|
||||
}
|
||||
|
||||
const rest = await this.restRepository.findOne({ slug });
|
||||
|
||||
if (!rest) {
|
||||
throw new BadRequestException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
const tokens = await this.tokensService.generateAccessAndRefreshToken(admin.id, rest.id, true, slug);
|
||||
|
||||
const adminResponse = await AdminLoginTransformer.transform(admin, rest);
|
||||
|
||||
return { tokens, admin: adminResponse };
|
||||
}
|
||||
/**
|
||||
*
|
||||
to use for login directly from DSC
|
||||
*/
|
||||
async loginAdminForDsc(phone: string, slug: string) {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug);
|
||||
|
||||
if (!admin) {
|
||||
throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
|
||||
}
|
||||
|
||||
const rest = await this.restRepository.findOne({ slug });
|
||||
|
||||
if (!rest) {
|
||||
throw new BadRequestException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
const tokens = await this.tokensService.generateAccessAndRefreshToken(admin.id, rest.id, true, slug);
|
||||
|
||||
const adminResponse = await AdminLoginTransformer.transform(admin, rest);
|
||||
|
||||
return { tokens, admin: adminResponse };
|
||||
}
|
||||
|
||||
private generateOtpCode(): string {
|
||||
const code = randomInt(10000, 100000);
|
||||
return code.toString();
|
||||
}
|
||||
|
||||
refreshToken(oldRefreshToken: string) {
|
||||
return this.tokensService.refreshToken(oldRefreshToken);
|
||||
}
|
||||
}
|
||||
Executable
+145
@@ -0,0 +1,145 @@
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { LockMode } from '@mikro-orm/core';
|
||||
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { AuthMessage } from '../../../common/enums/message.enum';
|
||||
import { RefreshToken, RefreshTokenType } from '../entities/refresh-token.entity';
|
||||
import { IAdminTokenPayload, ITokenPayload } from '../interfaces/IToken-payload';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TokensService {
|
||||
private readonly logger = new Logger(TokensService.name);
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
async generateAccessAndRefreshToken(
|
||||
ownerId: string,
|
||||
restaurantId: string,
|
||||
isAdmin: boolean,
|
||||
slug: string,
|
||||
em?: EntityManager,
|
||||
) {
|
||||
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
|
||||
const accessExpire = this.configService.getOrThrow<number>('JWT_EXPIRATION_TIME');
|
||||
|
||||
const payload: ITokenPayload | IAdminTokenPayload = isAdmin
|
||||
? { adminId: ownerId, restId: restaurantId }
|
||||
: { userId: ownerId, restId: restaurantId, slug };
|
||||
|
||||
const accessToken = await this.generateAccessToken(payload, accessExpire);
|
||||
const refreshToken = this.generateRefreshToken();
|
||||
const type = isAdmin ? RefreshTokenType.ADMIN : RefreshTokenType.USER;
|
||||
|
||||
// Only pass em if it's a transaction manager (not this.em), otherwise pass undefined to trigger flush
|
||||
await this.storeRefreshToken(ownerId, restaurantId, refreshToken, type, em);
|
||||
|
||||
return {
|
||||
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'seconds').valueOf() },
|
||||
refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, 'day').valueOf() },
|
||||
};
|
||||
}
|
||||
|
||||
private generateAccessToken(payload: ITokenPayload | IAdminTokenPayload, expiresIn: number) {
|
||||
// Ensure expiresIn is passed as a string with time unit for reliability
|
||||
// JWT library accepts: number (seconds) or string with unit (e.g., "3600s", "1h")
|
||||
// Using string format is more explicit and prevents unit confusion
|
||||
return this.jwtService.signAsync(payload, { expiresIn: `${expiresIn}s` });
|
||||
}
|
||||
|
||||
async storeRefreshToken(
|
||||
ownerId: string,
|
||||
restId: string,
|
||||
refreshToken: string,
|
||||
type: RefreshTokenType,
|
||||
em?: EntityManager,
|
||||
) {
|
||||
const entityManager = em || this.em;
|
||||
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
|
||||
const expiresAt = dayjs().add(refreshExpire, 'day').toDate();
|
||||
|
||||
const hashedToken = this.hashToken(refreshToken);
|
||||
|
||||
const token = entityManager.create(RefreshToken, {
|
||||
hashedToken,
|
||||
ownerId,
|
||||
restId,
|
||||
type,
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
if (em) {
|
||||
// Within transaction, just persist (flush will be called by transaction)
|
||||
entityManager.persist(token);
|
||||
} else {
|
||||
// Outside transaction, persist and flush immediately
|
||||
await entityManager.persistAndFlush(token);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshToken(oldRefreshToken: string) {
|
||||
const hashedToken = this.hashToken(oldRefreshToken);
|
||||
|
||||
// Use transaction to ensure atomicity and prevent race conditions
|
||||
return this.em.transactional(async em => {
|
||||
// Lock the token row to prevent concurrent refresh attempts
|
||||
// Using pessimistic write lock (FOR UPDATE) to prevent race conditions
|
||||
const token = await em.findOne(RefreshToken, { hashedToken }, { lockMode: LockMode.PESSIMISTIC_WRITE });
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if (dayjs(token.expiresAt).isBefore(dayjs())) {
|
||||
em.remove(token);
|
||||
await em.flush();
|
||||
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
|
||||
}
|
||||
|
||||
// Store token data before removal
|
||||
const ownerId = token.ownerId;
|
||||
const restId = token.restId;
|
||||
const isAdmin = token.type === RefreshTokenType.ADMIN;
|
||||
|
||||
// Verify restaurant still exists
|
||||
const restaurant = await em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) {
|
||||
throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate new tokens first (before removing old token)
|
||||
// Pass the transaction EntityManager to ensure operations are within the transaction
|
||||
const tokens = await this.generateAccessAndRefreshToken(ownerId, restaurant.id, isAdmin, restaurant.slug, em);
|
||||
|
||||
// Remove old token only after new token is created
|
||||
em.remove(token);
|
||||
|
||||
// Persist changes atomically
|
||||
await em.flush();
|
||||
|
||||
return tokens;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to generate new tokens after refresh', error);
|
||||
// Transaction will rollback automatically, preserving the old token
|
||||
throw new UnauthorizedException('Failed to refresh token');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private generateRefreshToken() {
|
||||
return randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
private hashToken(token: string): string {
|
||||
return createHash('sha256').update(token).digest('hex');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { Admin } from '../../admin/entities/admin.entity';
|
||||
import type { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
|
||||
export interface AdminLoginResponse {
|
||||
id: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
phone: string;
|
||||
role: string;
|
||||
permissions: string[];
|
||||
restaurant?: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class AdminLoginTransformer {
|
||||
static async transform(admin: Admin, restaurant?: Restaurant): Promise<AdminLoginResponse> {
|
||||
// Find the AdminRole that matches the restaurant (or get the first one)
|
||||
let adminRole = admin.roles.getItems().find(r => (restaurant ? r.restaurant?.id === restaurant.id : true));
|
||||
|
||||
// If no match found, get the first one
|
||||
if (!adminRole && admin.roles.getItems().length > 0) {
|
||||
adminRole = admin.roles.getItems()[0];
|
||||
}
|
||||
|
||||
if (!adminRole) {
|
||||
return {
|
||||
id: admin.id,
|
||||
firstName: admin.firstName,
|
||||
lastName: admin.lastName,
|
||||
phone: admin.phone,
|
||||
role: '',
|
||||
permissions: [],
|
||||
restaurant: restaurant
|
||||
? {
|
||||
id: restaurant.id,
|
||||
name: restaurant.name,
|
||||
slug: restaurant.slug,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Get the role from AdminRole
|
||||
const role = adminRole.role;
|
||||
if (!role) {
|
||||
return {
|
||||
id: admin.id,
|
||||
firstName: admin.firstName,
|
||||
lastName: admin.lastName,
|
||||
phone: admin.phone,
|
||||
role: '',
|
||||
permissions: [],
|
||||
restaurant: restaurant
|
||||
? {
|
||||
id: restaurant.id,
|
||||
name: restaurant.name,
|
||||
slug: restaurant.slug,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Extract permissions - ensure collection is loaded if needed
|
||||
let permissions: string[] = [];
|
||||
if (role.permissions) {
|
||||
if (role.permissions.isInitialized()) {
|
||||
permissions = role.permissions.getItems().map(p => p.name);
|
||||
} else {
|
||||
await role.permissions.loadItems();
|
||||
permissions = role.permissions.getItems().map(p => p.name);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: admin.id,
|
||||
firstName: admin.firstName,
|
||||
lastName: admin.lastName,
|
||||
phone: admin.phone,
|
||||
role: role.name,
|
||||
permissions,
|
||||
restaurant: restaurant
|
||||
? {
|
||||
id: restaurant.id,
|
||||
name: restaurant.name,
|
||||
slug: restaurant.slug,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { User } from '../../users/entities/user.entity';
|
||||
import type { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
|
||||
export interface UserLoginResponse {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
phone: string;
|
||||
|
||||
isActive?: boolean;
|
||||
restaurant: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class UserLoginTransformer {
|
||||
static transform(user: User, restaurant: Restaurant): UserLoginResponse {
|
||||
return {
|
||||
id: user.id,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
phone: user.phone,
|
||||
isActive: user.isActive,
|
||||
restaurant: {
|
||||
id: restaurant.id,
|
||||
name: restaurant.name,
|
||||
slug: restaurant.slug,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiCreatedResponse,
|
||||
ApiOkResponse,
|
||||
ApiNotFoundResponse,
|
||||
ApiParam,
|
||||
ApiBody,
|
||||
ApiBearerAuth,
|
||||
ApiHeader,
|
||||
} from '@nestjs/swagger';
|
||||
import { DeliveryService } from '../providers/delivery.service';
|
||||
import { CreateDeliveryDto } from '../dto/create-delivery.dto';
|
||||
import { UpdateDeliveryDto } from '../dto/update-delivery.dto';
|
||||
import { RestId } from 'src/common/decorators/rest-id.decorator';
|
||||
import { AuthGuard } from 'src/modules/auth/guards/auth.guard';
|
||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||
import { Delivery } from '../entities/delivery.entity';
|
||||
import { API_HEADER_SLUG } from 'src/common/constants';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
|
||||
@ApiTags('Delivery')
|
||||
@Controller()
|
||||
export class DeliveryController {
|
||||
constructor(private readonly deliveryService: DeliveryService) {}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('public/delivery-methods/restaurant')
|
||||
@ApiOperation({ summary: 'Get restaurant delivery methods' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
findAllDeliveryMethods(@RestId() restId: string) {
|
||||
return this.deliveryService.findAllForRestaurantId(restId);
|
||||
}
|
||||
|
||||
/*** Admin ***/
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_DELIVERY)
|
||||
@Post('admin/delivery-methods/restaurant')
|
||||
@ApiOperation({ summary: 'Create a delivery method for a restaurant' })
|
||||
@ApiBody({ type: CreateDeliveryDto })
|
||||
create(@Body() createDto: CreateDeliveryDto, @RestId() restId: string) {
|
||||
return this.deliveryService.create(restId, createDto);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_DELIVERY)
|
||||
@Get('admin/delivery-methods/restaurant')
|
||||
@ApiOperation({ summary: 'Get the restaurant delivery methods' })
|
||||
findRestaurantDeliveryMethods(@RestId() restId: string) {
|
||||
return this.deliveryService.findAllForRestaurantId(restId);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_DELIVERY)
|
||||
@Get('admin/delivery-methods/restaurant/:deliveryId')
|
||||
@ApiOperation({ summary: 'Get a restaurant delivery method by delivery ID' })
|
||||
@ApiParam({ name: 'deliveryId', description: 'Delivery method ID' })
|
||||
findOne(@Param('deliveryId') deliveryId: string, @RestId() restId: string) {
|
||||
return this.deliveryService.findOne(restId, deliveryId);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_DELIVERY)
|
||||
@Patch('admin/delivery-methods/restaurant/:deliveryId')
|
||||
@ApiOperation({ summary: 'Update a restaurant delivery method' })
|
||||
@ApiParam({ name: 'deliveryId', description: 'Delivery method ID' })
|
||||
@ApiBody({ type: UpdateDeliveryDto })
|
||||
update(@Param('deliveryId') deliveryId: string, @Body() updateDto: UpdateDeliveryDto, @RestId() restId: string) {
|
||||
return this.deliveryService.update(restId, deliveryId, updateDto);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_DELIVERY)
|
||||
@Delete('admin/delivery-methods/restaurant/:deliveryId')
|
||||
@ApiOperation({ summary: 'Delete a restaurant delivery method' })
|
||||
@ApiParam({ name: 'deliveryId', description: 'Delivery method ID' })
|
||||
remove(@Param('deliveryId') deliveryId: string, @RestId() restId: string) {
|
||||
return this.deliveryService.remove(restId, deliveryId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { DeliveryController } from './controllers/delivery.controller';
|
||||
import { DeliveryService } from './providers/delivery.service';
|
||||
import { RestaurantsModule } from '../restaurants/restaurants.module';
|
||||
import { Delivery } from './entities/delivery.entity';
|
||||
import { DeliveryRepository } from './repositories/delivery.repository';
|
||||
|
||||
@Module({
|
||||
controllers: [DeliveryController],
|
||||
providers: [DeliveryService, DeliveryRepository],
|
||||
exports: [DeliveryService, DeliveryRepository],
|
||||
imports: [MikroOrmModule.forFeature([Delivery]), JwtModule, RestaurantsModule],
|
||||
})
|
||||
export class DeliveryModule {}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { IsNumber, IsBoolean, IsOptional, IsString, Min, IsEnum } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { DeliveryFeeTypeEnum, DeliveryMethodEnum } from '../interface/delivery';
|
||||
|
||||
export class CreateDeliveryDto {
|
||||
@ApiProperty({ example: 'dineIn', description: 'Delivery method name', enum: DeliveryMethodEnum })
|
||||
@IsEnum(DeliveryMethodEnum)
|
||||
method!: DeliveryMethodEnum;
|
||||
|
||||
@ApiProperty({ example: 5000, description: 'Delivery fee' })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
deliveryFee!: number;
|
||||
|
||||
@ApiProperty({ example: 100000, description: 'Minimum order price for free delivery' })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
minOrderPrice!: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 'توضیحات روش ارسال', description: 'Delivery method description' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: true, description: 'Is this delivery method enabled?' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Type(() => Boolean)
|
||||
enabled?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ example: 0, description: 'Display order for sorting delivery methods' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
order?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 0, description: 'Kilometer number' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
distanceBasedMinCost?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 0, description: 'Delivery fee per kilometer' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
perKilometerFee?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: DeliveryFeeTypeEnum.FIXED,
|
||||
description: 'Delivery fee type',
|
||||
enum: DeliveryFeeTypeEnum,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(DeliveryFeeTypeEnum)
|
||||
deliveryFeeType!: DeliveryFeeTypeEnum;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateDeliveryDto } from './create-delivery.dto';
|
||||
|
||||
export class UpdateDeliveryDto extends PartialType(CreateDeliveryDto) {}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Entity, Property, Enum, ManyToOne } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
import { DeliveryFeeTypeEnum, DeliveryMethodEnum } from '../interface/delivery';
|
||||
|
||||
@Entity({ tableName: 'deliveries' })
|
||||
export class Delivery extends BaseEntity {
|
||||
@Enum(() => DeliveryMethodEnum)
|
||||
method!: DeliveryMethodEnum;
|
||||
|
||||
@ManyToOne(() => Restaurant)
|
||||
restaurant!: Restaurant;
|
||||
|
||||
@Property({ type: 'decimal', precision: 10, scale: 0, default: 0 })
|
||||
deliveryFee: number = 0;
|
||||
|
||||
@Enum(() => DeliveryFeeTypeEnum)
|
||||
deliveryFeeType: DeliveryFeeTypeEnum = DeliveryFeeTypeEnum.FIXED;
|
||||
|
||||
@Property({ type: 'decimal', precision: 10, scale: 0, default: 0 })
|
||||
perKilometerFee: number | null = null;
|
||||
|
||||
@Property({ type: 'decimal', precision: 10, scale: 0, default: 0 })
|
||||
distanceBasedMinCost: number | null = null;
|
||||
|
||||
@Property({ type: 'decimal', precision: 10, scale: 0, default: 0 })
|
||||
minOrderPrice: number = 0;
|
||||
|
||||
@Property({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Property({ default: true })
|
||||
enabled: boolean = true;
|
||||
|
||||
@Property({ type: 'integer', default: 0 })
|
||||
order: number = 0;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export enum DeliveryMethodEnum {
|
||||
DineIn = 'dineIn',
|
||||
CustomerPickup = 'customerPickup',
|
||||
DeliveryCar = 'deliveryCar',
|
||||
DeliveryCourier = 'deliveryCourier',
|
||||
}
|
||||
|
||||
export enum DeliveryFeeTypeEnum {
|
||||
FIXED = 'fixed',
|
||||
DISTANCE_BASED = 'distanceBased',
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
||||
import { EntityManager, RequiredEntityData } from '@mikro-orm/postgresql';
|
||||
import { Delivery } from '../entities/delivery.entity';
|
||||
import { DeliveryRepository } from '../repositories/delivery.repository';
|
||||
import { CreateDeliveryDto } from '../dto/create-delivery.dto';
|
||||
import { UpdateDeliveryDto } from '../dto/update-delivery.dto';
|
||||
import { RestRepository } from '../../restaurants/repositories/rest.repository';
|
||||
import { DeliveryMethodEnum } from '../interface/delivery';
|
||||
import { DeliveryMessage } from 'src/common/enums/message.enum';
|
||||
|
||||
@Injectable()
|
||||
export class DeliveryService {
|
||||
constructor(
|
||||
private readonly deliveryRepository: DeliveryRepository,
|
||||
private readonly restRepository: RestRepository,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
async create(restId: string, createDto: CreateDeliveryDto): Promise<Delivery> {
|
||||
const restaurant = await this.restRepository.findOne({ id: restId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(DeliveryMessage.RESTAURANT_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Check if the delivery method with the same name already exists for this restaurant
|
||||
const existing = await this.deliveryRepository.findOne({
|
||||
restaurant: { id: restId },
|
||||
method: createDto.method,
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictException('این روش ارسال قبلاً برای این رستوران ثبت شده است.');
|
||||
}
|
||||
|
||||
const data: RequiredEntityData<Delivery> = {
|
||||
restaurant,
|
||||
method: createDto.method,
|
||||
deliveryFee: createDto.deliveryFee,
|
||||
minOrderPrice: createDto.minOrderPrice,
|
||||
description: createDto.description,
|
||||
enabled: createDto.enabled ?? true,
|
||||
order: createDto.order ?? 0,
|
||||
deliveryFeeType: createDto.deliveryFeeType,
|
||||
perKilometerFee: createDto.perKilometerFee ?? null,
|
||||
distanceBasedMinCost: createDto.distanceBasedMinCost ?? null,
|
||||
};
|
||||
|
||||
const delivery = this.deliveryRepository.create(data);
|
||||
await this.em.persistAndFlush(delivery);
|
||||
return delivery;
|
||||
}
|
||||
|
||||
async findAllForRestaurantId(restId: string): Promise<Delivery[]> {
|
||||
return this.deliveryRepository.find({ restaurant: { id: restId } }, { orderBy: { order: 'asc' } });
|
||||
}
|
||||
|
||||
async findOne(restId: string, deliveryId: string): Promise<Delivery> {
|
||||
const delivery = await this.deliveryRepository.findOne({
|
||||
id: deliveryId,
|
||||
restaurant: { id: restId },
|
||||
});
|
||||
|
||||
if (!delivery) {
|
||||
throw new NotFoundException(DeliveryMessage.DELIVERY_METHOD_NOT_FOUND);
|
||||
}
|
||||
|
||||
return delivery;
|
||||
}
|
||||
|
||||
async update(restId: string, deliveryId: string, updateDto: UpdateDeliveryDto): Promise<Delivery> {
|
||||
const delivery = await this.deliveryRepository.findOne({
|
||||
restaurant: { id: restId },
|
||||
id: deliveryId,
|
||||
});
|
||||
|
||||
if (!delivery) {
|
||||
throw new NotFoundException(DeliveryMessage.DELIVERY_METHOD_NOT_FOUND);
|
||||
}
|
||||
|
||||
// If method is being updated, check for conflicts
|
||||
if (updateDto.method !== undefined && updateDto.method !== delivery.method) {
|
||||
const existing = await this.deliveryRepository.findOne({
|
||||
restaurant: { id: restId },
|
||||
method: updateDto.method,
|
||||
id: { $ne: deliveryId },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictException('این روش ارسال قبلاً برای این رستوران ثبت شده است.');
|
||||
}
|
||||
}
|
||||
|
||||
this.em.assign(delivery, updateDto);
|
||||
await this.em.persistAndFlush(delivery);
|
||||
|
||||
return delivery;
|
||||
}
|
||||
|
||||
async remove(restId: string, deliveryId: string): Promise<void> {
|
||||
const delivery = await this.deliveryRepository.findOne({
|
||||
restaurant: { id: restId },
|
||||
id: deliveryId,
|
||||
});
|
||||
|
||||
if (!delivery) {
|
||||
throw new NotFoundException(DeliveryMessage.DELIVERY_METHOD_NOT_FOUND);
|
||||
}
|
||||
|
||||
await this.em.removeAndFlush(delivery);
|
||||
}
|
||||
|
||||
findAll(): DeliveryMethodEnum[] {
|
||||
return Object.values(DeliveryMethodEnum);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { Delivery } from '../entities/delivery.entity';
|
||||
|
||||
@Injectable()
|
||||
export class DeliveryRepository extends EntityRepository<Delivery> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, Delivery);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
||||
import { CategoryService } from '../providers/category.service';
|
||||
import { CreateCategoryDto } from '../dto/create-category.dto';
|
||||
import { UpdateCategoryDto } from '../dto/update-category.dto';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiCreatedResponse,
|
||||
ApiOkResponse,
|
||||
ApiNotFoundResponse,
|
||||
ApiParam,
|
||||
ApiBody,
|
||||
ApiBearerAuth,
|
||||
ApiHeader,
|
||||
} from '@nestjs/swagger';
|
||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { RestId } from 'src/common/decorators';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { API_HEADER_SLUG } from 'src/common/constants';
|
||||
|
||||
@ApiTags('category')
|
||||
@Controller()
|
||||
export class CategoryController {
|
||||
constructor(private readonly categoryService: CategoryService) { }
|
||||
|
||||
@Get('public/categories/restaurant/:slug')
|
||||
@ApiOperation({ summary: 'Get all categories by restaurant slug' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiParam({ name: 'slug', required: true, description: 'Restaurant Slug' })
|
||||
@ApiOkResponse({ description: 'List of all categories for the restaurant' })
|
||||
findAllByRestaurant(@Param('slug') slug: string) {
|
||||
return this.categoryService.findAllByRestaurant(slug);
|
||||
}
|
||||
|
||||
/*** Admin ***/
|
||||
|
||||
@ApiOperation({ summary: 'Create category' })
|
||||
@ApiBody({ type: CreateCategoryDto })
|
||||
@Permissions(Permission.MANAGE_CATEGORIES)
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Post('admin/categories')
|
||||
create(@Body() dto: CreateCategoryDto, @RestId() restId: string) {
|
||||
return this.categoryService.create(restId, dto);
|
||||
}
|
||||
@Permissions(Permission.MANAGE_CATEGORIES)
|
||||
@ApiOperation({ summary: 'my restaurant categories' })
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('admin/categories')
|
||||
findAll(@RestId() restId: string) {
|
||||
return this.categoryService.findAllByRestaurantId(restId);
|
||||
}
|
||||
|
||||
@Permissions(Permission.MANAGE_CATEGORIES)
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('admin/categories/:id')
|
||||
@ApiOperation({ summary: 'Get category' })
|
||||
@ApiParam({ name: 'id', required: true, type: String })
|
||||
findOne(@Param('id') id: string, @RestId() restId: string) {
|
||||
return this.categoryService.findOne(restId, id);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_CATEGORIES)
|
||||
@Patch('admin/categories/:id')
|
||||
@ApiOperation({ summary: 'Update category' })
|
||||
@ApiParam({ name: 'id' })
|
||||
@ApiBody({ type: UpdateCategoryDto })
|
||||
update(@Param('id') id: string, @Body() dto: UpdateCategoryDto, @RestId() restId: string) {
|
||||
return this.categoryService.update(restId, id, dto);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_CATEGORIES)
|
||||
@Delete('admin/categories/:id')
|
||||
@ApiOperation({ summary: 'Delete category' })
|
||||
@ApiParam({ name: 'id' })
|
||||
@ApiOkResponse({ description: 'Deleted' })
|
||||
remove(@Param('id') id: string, @RestId() restId: string) {
|
||||
return this.categoryService.remove(restId, id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards } from '@nestjs/common';
|
||||
import { FoodService } from '../providers/food.service';
|
||||
import { CreateFoodDto } from '../dto/create-food.dto';
|
||||
import { UpdateFoodDto } from '../dto/update-food.dto';
|
||||
import { FindFoodsDto } from '../dto/find-foods.dto';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiCreatedResponse,
|
||||
ApiOkResponse,
|
||||
ApiNotFoundResponse,
|
||||
ApiQuery,
|
||||
ApiBody,
|
||||
ApiParam,
|
||||
ApiBearerAuth,
|
||||
ApiHeader,
|
||||
} from '@nestjs/swagger';
|
||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||
import { RestId, UserId } from 'src/common/decorators';
|
||||
import { AuthGuard } from 'src/modules/auth/guards/auth.guard';
|
||||
import { OptionalAuthGuard } from 'src/modules/auth/guards/optinalAuth.guard';
|
||||
import { API_HEADER_SLUG } from 'src/common/constants';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
|
||||
@ApiTags('foods')
|
||||
@Controller()
|
||||
export class FoodController {
|
||||
constructor(private readonly foodsService: FoodService) { }
|
||||
|
||||
@Get('public/foods/restaurant/:slug')
|
||||
@ApiOperation({ summary: 'Get all foods by restaurant slug' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiParam({ name: 'slug', required: true, description: 'Restaurant Slug' })
|
||||
findAllByRestaurant(@Param('slug') slug: string) {
|
||||
return this.foodsService.findByWeekDateAndMealType(slug);
|
||||
}
|
||||
|
||||
@Get('public/foods/:foodId')
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get a food by id' })
|
||||
@ApiParam({ name: 'foodId', required: true })
|
||||
findPublicFoodById(@Param('foodId') foodId: string, @UserId() userId?: string): Promise<any> {
|
||||
const a = this.foodsService.findPublicById(foodId, userId);
|
||||
return a;
|
||||
}
|
||||
|
||||
@Post('public/foods/favorite/:foodId')
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'toggle a food as favorite' })
|
||||
@ApiParam({ name: 'foodId', required: true })
|
||||
setAsFavorute(@Param('foodId') foodId: string, @UserId() userId: string) {
|
||||
return this.foodsService.toggleFavorite(userId, foodId);
|
||||
}
|
||||
|
||||
@Get('public/foods/favorite')
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'get my favorites' })
|
||||
getMyFavorites(@UserId() userId: string, @RestId() restId: string) {
|
||||
return this.foodsService.getMyFavorites(userId, restId);
|
||||
}
|
||||
|
||||
/* ---------------------------------- Admin ---------------------------------- */
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_FOODS)
|
||||
@Post('admin/foods')
|
||||
@ApiOperation({ summary: 'Create a new food' })
|
||||
@ApiBody({ type: CreateFoodDto })
|
||||
create(@Body() createFoodDto: CreateFoodDto, @RestId() restId: string) {
|
||||
return this.foodsService.create(restId, createFoodDto);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_FOODS)
|
||||
@Get('admin/foods')
|
||||
@ApiOperation({ summary: 'Get a paginated list of foods' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@ApiQuery({ name: 'search', required: false, type: String })
|
||||
@ApiQuery({ name: 'orderBy', required: false, type: String })
|
||||
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
|
||||
@ApiQuery({ name: 'categoryId', required: false, type: String })
|
||||
@ApiQuery({ name: 'isActive', required: false, type: Boolean })
|
||||
async findAll(@Query() dto: FindFoodsDto, @RestId() restId: string) {
|
||||
const result = await this.foodsService.findAll(restId, dto);
|
||||
return result;
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_FOODS)
|
||||
@Get('admin/foods/:id')
|
||||
@ApiOperation({ summary: 'Get a food by id' })
|
||||
@ApiParam({ name: 'id', required: true })
|
||||
findById(@Param('id') id: string, @RestId() restId: string) {
|
||||
return this.foodsService.findAdminById(restId, id);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_FOODS)
|
||||
@Patch('admin/foods/:id')
|
||||
@ApiOperation({ summary: 'Update a food' })
|
||||
@ApiParam({ name: 'id', required: true })
|
||||
@ApiBody({ type: UpdateFoodDto })
|
||||
update(@Param('id') id: string, @Body() updateFoodDto: UpdateFoodDto, @RestId() restId: string) {
|
||||
return this.foodsService.update(restId, id, updateFoodDto);
|
||||
}
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_FOODS)
|
||||
@Delete('admin/foods/:id')
|
||||
@ApiOperation({ summary: 'Delete (soft) a food' })
|
||||
@ApiParam({ name: 'id', required: true })
|
||||
remove(@Param('id') id: string, @RestId() restId: string) {
|
||||
return this.foodsService.remove(restId, id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { Inventory } from '../../inventory/entities/inventory.entity';
|
||||
|
||||
@Injectable()
|
||||
export class FoodStockCrone {
|
||||
private readonly logger = new Logger(FoodStockCrone.name);
|
||||
|
||||
constructor(private readonly em: EntityManager) {}
|
||||
|
||||
// run every day at 00:03
|
||||
@Cron('3 0 * * *', {
|
||||
name: 'resetAvailableStock',
|
||||
timeZone: 'UTC',
|
||||
})
|
||||
async handleCron() {
|
||||
try {
|
||||
this.logger.debug('Starting daily inventory reset (availableStock = totalStock)');
|
||||
|
||||
const inventories = await this.em.find(Inventory, {});
|
||||
if (!inventories || inventories.length === 0) {
|
||||
this.logger.debug('No inventory records found to reset');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`Resetting available stock for ${inventories.length} inventory records`);
|
||||
|
||||
await this.em.transactional(async em => {
|
||||
for (const inv of inventories) {
|
||||
// reload inside transaction to avoid concurrency issues
|
||||
const record = await em.findOne(Inventory, { id: inv.id });
|
||||
if (!record) continue;
|
||||
record.availableStock = record.totalStock;
|
||||
em.persist(record);
|
||||
}
|
||||
await em.flush();
|
||||
});
|
||||
|
||||
this.logger.log('Daily inventory reset completed');
|
||||
} catch (err) {
|
||||
this.logger.error(`FoodStockCrone failed: ${err?.message}`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { IsString, IsOptional, IsBoolean, IsInt, Min } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateCategoryDto {
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'ایرانی' })
|
||||
title!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Type(() => Boolean)
|
||||
@ApiPropertyOptional({ example: true })
|
||||
isActive?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'https://cdn.example.com/avatar.png' })
|
||||
avatarUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 1 })
|
||||
order?: number;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayUnique,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Max,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { MealType } from '../interface/food.interface';
|
||||
|
||||
export class CreateFoodDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
categoryId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'قرمه سبزی' })
|
||||
title?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'توضیحات غذا' })
|
||||
desc?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
content?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayUnique()
|
||||
@IsInt({ each: true })
|
||||
@Min(0, { each: true })
|
||||
@Max(6, { each: true })
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ type: [Number], example: [0, 1, 2, 3, 4, 5, 6] })
|
||||
weekDays?: number[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayUnique()
|
||||
@IsEnum(MealType, { each: true })
|
||||
@ApiPropertyOptional({ enum: MealType, isArray: true })
|
||||
mealTypes?: MealType[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 120000 })
|
||||
price?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 15 })
|
||||
prepareTime?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: true })
|
||||
@Type(() => Boolean)
|
||||
isActive?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
images?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@Type(() => Boolean)
|
||||
inPlaceServe?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@Type(() => Boolean)
|
||||
pickupServe?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 0 })
|
||||
discount?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
@ApiProperty({ example: 50 })
|
||||
dailyStock: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@Type(() => Boolean)
|
||||
isSpecialOffer?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 1 })
|
||||
order?: number;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { IsOptional, IsNumber, IsString, IsIn, IsBoolean } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class FindFoodsDto {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 1 })
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 10 })
|
||||
limit?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
search?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'createdAt' })
|
||||
orderBy?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['asc', 'desc'])
|
||||
@ApiPropertyOptional({ example: 'desc' })
|
||||
order?: 'asc' | 'desc';
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
categoryId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Type(() => Boolean)
|
||||
@ApiPropertyOptional({ example: true })
|
||||
isActive?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateCategoryDto } from './create-category.dto';
|
||||
|
||||
export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateFoodDto } from './create-food.dto';
|
||||
|
||||
export class UpdateFoodDto extends PartialType(CreateFoodDto) {}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Entity, Index, Property, Collection, OneToMany, ManyToOne } from '@mikro-orm/core';
|
||||
import { Food } from './food.entity';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
|
||||
@Entity({ tableName: 'categories' })
|
||||
@Index({ properties: ['restaurant', 'isActive'] })
|
||||
@Index({ properties: ['isActive'] })
|
||||
export class Category extends BaseEntity {
|
||||
@Property()
|
||||
title!: string;
|
||||
|
||||
@OneToMany(() => Food, food => food.category)
|
||||
foods = new Collection<Food>(this);
|
||||
|
||||
@Property({ default: true })
|
||||
isActive: boolean = true;
|
||||
|
||||
@ManyToOne(() => Restaurant)
|
||||
restaurant!: Restaurant;
|
||||
|
||||
@Property({ nullable: true })
|
||||
avatarUrl?: string;
|
||||
|
||||
@Property({ type: 'int', nullable: true })
|
||||
order?: number;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Entity, ManyToOne, Unique } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
import { Food } from '../../foods/entities/food.entity';
|
||||
|
||||
@Entity({ tableName: 'favorites' })
|
||||
@Unique({ properties: ['user', 'food'] })
|
||||
export class Favorite extends BaseEntity {
|
||||
@ManyToOne(() => User)
|
||||
user: User;
|
||||
|
||||
@ManyToOne(() => Food)
|
||||
food: Food;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Cascade, Collection, Entity, Index, ManyToOne, OneToMany, Property, OneToOne } from '@mikro-orm/core';
|
||||
import { Category } from './category.entity';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity';
|
||||
import { Review } from 'src/modules/review/entities/review.entity';
|
||||
import { Inventory } from 'src/modules/inventory/entities/inventory.entity';
|
||||
import { MealType } from '../interface/food.interface';
|
||||
import { Favorite } from './favorite.entity';
|
||||
|
||||
@Entity({ tableName: 'foods' })
|
||||
@Index({ properties: ['restaurant', 'isActive'] })
|
||||
@Index({ properties: ['category', 'isActive'] })
|
||||
@Index({ properties: ['isActive'] })
|
||||
export class Food extends BaseEntity {
|
||||
@ManyToOne(() => Restaurant)
|
||||
restaurant: Restaurant;
|
||||
|
||||
@ManyToOne(() => Category)
|
||||
category: Category;
|
||||
|
||||
@OneToMany(() => Review, review => review.food, { cascade: [Cascade.ALL], orphanRemoval: true })
|
||||
reviews = new Collection<Review>(this);
|
||||
|
||||
@OneToOne(() => Inventory, {
|
||||
mappedBy: 'food',
|
||||
nullable: true,
|
||||
})
|
||||
inventory?: Inventory;
|
||||
|
||||
@OneToMany(() => Favorite, favorite => favorite.food)
|
||||
favorites = new Collection<Favorite>(this);
|
||||
|
||||
@Property({ nullable: true })
|
||||
title?: string;
|
||||
|
||||
@Property({ type: 'text', nullable: true })
|
||||
desc?: string;
|
||||
|
||||
@Property({ type: 'json', nullable: true })
|
||||
content?: string[];
|
||||
|
||||
@Property({ type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||
price?: number;
|
||||
|
||||
@Property({ type: 'int', nullable: true })
|
||||
order?: number;
|
||||
|
||||
@Property({ type: 'int', nullable: true })
|
||||
prepareTime?: number; // in minutes
|
||||
|
||||
@Property({ type: 'jsonb', default: [] })
|
||||
weekDays: number[] = [0, 1, 2, 3, 4, 5, 6];
|
||||
|
||||
@Property({ type: 'jsonb', default: [] })
|
||||
mealTypes: MealType[] = [];
|
||||
|
||||
@Property({ type: 'boolean', default: true })
|
||||
isActive: boolean = true;
|
||||
|
||||
@Property({ type: 'json', nullable: true })
|
||||
images?: string[];
|
||||
|
||||
@Property({ type: 'boolean', default: false })
|
||||
inPlaceServe: boolean = false;
|
||||
|
||||
@Property({ type: 'boolean', default: false })
|
||||
pickupServe: boolean = false;
|
||||
|
||||
@Property({ type: 'float', default: null })
|
||||
score?: number | null = null;
|
||||
|
||||
@Property({ type: 'float', default: 0 })
|
||||
discount: number = 0;
|
||||
|
||||
@Property({ type: 'boolean', default: false })
|
||||
isSpecialOffer: boolean = false;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FoodService } from './providers/food.service';
|
||||
import { FoodStockCrone } from './crone/food.crone';
|
||||
import { FoodController } from './controllers/food.controller';
|
||||
import { CategoryController } from './controllers/category.controller';
|
||||
import { CategoryService } from './providers/category.service';
|
||||
import { FoodRepository } from './repositories/food.repository';
|
||||
import { CategoryRepository } from './repositories/category.repository';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Category } from './entities/category.entity';
|
||||
import { Food } from './entities/food.entity';
|
||||
import { RestaurantsModule } from '../restaurants/restaurants.module';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { UtilsModule } from '../utils/utils.module';
|
||||
import { Favorite } from './entities/favorite.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MikroOrmModule.forFeature([Food, Category, Favorite]),
|
||||
RestaurantsModule,
|
||||
AuthModule,
|
||||
JwtModule,
|
||||
UtilsModule,
|
||||
],
|
||||
controllers: [FoodController, CategoryController],
|
||||
providers: [FoodService, CategoryService, FoodRepository, CategoryRepository, FoodStockCrone],
|
||||
exports: [FoodRepository, CategoryRepository],
|
||||
})
|
||||
export class FoodModule {}
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum MealType {
|
||||
BREAKFAST = 'breakfast',
|
||||
LUNCH = 'lunch',
|
||||
DINNER = 'dinner',
|
||||
SNACK = 'snack',
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CreateCategoryDto } from '../dto/create-category.dto';
|
||||
import { UpdateCategoryDto } from '../dto/update-category.dto';
|
||||
import { CategoryRepository } from '../repositories/category.repository';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { RequiredEntityData } from '@mikro-orm/core';
|
||||
import { Category } from '../entities/category.entity';
|
||||
import { CategoryMessage, RestMessage } from 'src/common/enums/message.enum';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
|
||||
@Injectable()
|
||||
export class CategoryService {
|
||||
constructor(
|
||||
private readonly categoryRepository: CategoryRepository,
|
||||
private readonly em: EntityManager,
|
||||
) { }
|
||||
|
||||
async create(restId: string, dto: CreateCategoryDto): Promise<Category> {
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
const data: RequiredEntityData<Category> = {
|
||||
title: dto.title,
|
||||
isActive: dto.isActive ?? true,
|
||||
restaurant: restaurant,
|
||||
avatarUrl: dto.avatarUrl,
|
||||
};
|
||||
|
||||
const category = this.categoryRepository.create(data);
|
||||
await this.em.persistAndFlush(category);
|
||||
return category;
|
||||
}
|
||||
|
||||
async findAllByRestaurant(slug: string): Promise<Category[]> {
|
||||
const restaurant = await this.em.findOne(Restaurant, { slug });
|
||||
if (!restaurant || !restaurant.id) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
return this.categoryRepository.find({ restaurant: restaurant, isActive: true });
|
||||
}
|
||||
|
||||
async findAllByRestaurantId(restId: string): Promise<Category[]> {
|
||||
return this.categoryRepository.find({ restaurant: { id: restId } });
|
||||
}
|
||||
|
||||
// async findAll(opts?: { restId?: string; isActive?: boolean }): Promise<Category[]> {
|
||||
// const where: FilterQuery<Category> = {};
|
||||
// if (opts?.restId) where.restId = opts.restId;
|
||||
// if (opts && typeof opts.isActive === 'boolean') where.isActive = opts.isActive;
|
||||
// const cats = await this.categoryRepository.find(where, { populate: ['foods'] });
|
||||
|
||||
// // Return plain objects to avoid circular references during JSON serialization
|
||||
// return cats.map(cat => ({
|
||||
// id: cat.id,
|
||||
// title: cat.title,
|
||||
// isActive: cat.isActive,
|
||||
// restId: cat.restId,
|
||||
// avatarUrl: cat.avatarUrl,
|
||||
// createdAt: cat.createdAt,
|
||||
// updatedAt: cat.updatedAt,
|
||||
// foods: cat.foods.getItems().map(f => ({ id: f.id, title: f.title })),
|
||||
// })) as unknown as Category[];
|
||||
// }
|
||||
|
||||
async findOne(restId: string, id: string): Promise<Category> {
|
||||
const cat = await this.categoryRepository.findOne({ id, restaurant: { id: restId } }, { populate: ['foods'] });
|
||||
if (!cat) throw new NotFoundException(CategoryMessage.NOT_FOUND);
|
||||
|
||||
return {
|
||||
id: cat.id,
|
||||
title: cat.title,
|
||||
isActive: cat.isActive,
|
||||
restaurant: cat.restaurant,
|
||||
avatarUrl: cat.avatarUrl,
|
||||
createdAt: cat.createdAt,
|
||||
updatedAt: cat.updatedAt,
|
||||
foods: cat.foods.getItems().map(f => ({ id: f.id, title: f.title })),
|
||||
} as unknown as Category;
|
||||
}
|
||||
|
||||
async findRestaurantCategories(restId: string): Promise<Category> {
|
||||
const cat = await this.categoryRepository.findOne({ restaurant: { id: restId } }, { populate: ['foods'] });
|
||||
if (!cat) throw new NotFoundException(CategoryMessage.NOT_FOUND);
|
||||
|
||||
return {
|
||||
id: cat.id,
|
||||
title: cat.title,
|
||||
isActive: cat.isActive,
|
||||
restaurant: cat.restaurant,
|
||||
avatarUrl: cat.avatarUrl,
|
||||
createdAt: cat.createdAt,
|
||||
updatedAt: cat.updatedAt,
|
||||
} as unknown as Category;
|
||||
}
|
||||
|
||||
async update(restId: string, id: string, dto: UpdateCategoryDto): Promise<Category> {
|
||||
const cat = await this.categoryRepository.findOne({ id, restaurant: { id: restId } });
|
||||
if (!cat) throw new NotFoundException(CategoryMessage.NOT_FOUND);
|
||||
this.em.assign(cat, dto);
|
||||
await this.em.persistAndFlush(cat);
|
||||
return cat;
|
||||
}
|
||||
|
||||
async remove(restId: string, id: string): Promise<void> {
|
||||
const cat = await this.categoryRepository.findOne({ id, restaurant: { id: restId } });
|
||||
if (!cat) throw new NotFoundException(CategoryMessage.NOT_FOUND);
|
||||
cat.deletedAt = new Date();
|
||||
await this.em.persistAndFlush(cat);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CreateFoodDto } from '../dto/create-food.dto';
|
||||
import { FindFoodsDto } from '../dto/find-foods.dto';
|
||||
import { FoodRepository } from '../repositories/food.repository';
|
||||
import { CategoryRepository } from '../repositories/category.repository';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { RequiredEntityData, FilterQuery } from '@mikro-orm/core';
|
||||
import { Food } from '../entities/food.entity';
|
||||
import { CategoryMessage, FoodMessage, RestMessage } from 'src/common/enums/message.enum';
|
||||
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
|
||||
import { CacheService } from '../../utils/cache.service';
|
||||
import { Favorite } from '../entities/favorite.entity';
|
||||
import { MealType } from '../interface/food.interface';
|
||||
import { Inventory } from 'src/modules/inventory/entities/inventory.entity';
|
||||
|
||||
@Injectable()
|
||||
export class FoodService {
|
||||
private readonly FOODS_BY_RESTAURANT_CACHE_KEY_PREFIX = 'foods:restaurant:';
|
||||
private readonly FOODS_CACHE_TTL = 600; // 10 minutes in seconds
|
||||
|
||||
constructor(
|
||||
private readonly foodRepository: FoodRepository,
|
||||
private readonly categoryRepository: CategoryRepository,
|
||||
private readonly restRepository: RestRepository,
|
||||
private readonly em: EntityManager,
|
||||
private readonly cacheService: CacheService,
|
||||
) { }
|
||||
|
||||
async create(restId: string, createFoodDto: CreateFoodDto) {
|
||||
const { categoryId, dailyStock = 0, ...rest } = createFoodDto;
|
||||
const restaurant = await this.restRepository.findOne({ id: restId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
const category = await this.categoryRepository.findOne({ id: categoryId, restaurant: { id: restId } });
|
||||
if (!category) {
|
||||
throw new NotFoundException(CategoryMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
const { food, inventory } = await this.em.transactional(async em => {
|
||||
// prepare data with defaults for required fields so repository.create typing is satisfied
|
||||
const data: RequiredEntityData<Food> = {
|
||||
desc: rest.desc,
|
||||
isActive: rest.isActive ?? true,
|
||||
inPlaceServe: rest.inPlaceServe ?? false,
|
||||
pickupServe: rest.pickupServe ?? false,
|
||||
discount: rest.discount ?? 0,
|
||||
isSpecialOffer: rest.isSpecialOffer ?? false,
|
||||
order: rest.order ?? null,
|
||||
// map single-title/content DTO to localized fields
|
||||
title: rest.title,
|
||||
content: rest.content,
|
||||
// numeric/array fields
|
||||
price: rest.price ?? 0,
|
||||
prepareTime: rest.prepareTime ?? 0,
|
||||
images: rest.images ?? undefined,
|
||||
// new fields
|
||||
weekDays: rest.weekDays ?? [0, 1, 2, 3, 4, 5, 6],
|
||||
mealTypes: rest.mealTypes ?? [],
|
||||
restaurant: restaurant,
|
||||
category: category,
|
||||
};
|
||||
|
||||
const food = em.create(Food, data);
|
||||
const newInventoryRecord = em.create(Inventory, {
|
||||
food: food,
|
||||
availableStock: dailyStock,
|
||||
totalStock: dailyStock,
|
||||
});
|
||||
|
||||
await em.flush();
|
||||
return { food, inventory: newInventoryRecord };
|
||||
});
|
||||
|
||||
// Re-load created entities with the primary EM to ensure they're attached
|
||||
const savedFood = food?.id
|
||||
? await this.foodRepository.findOne({ id: food.id }, { populate: ['category', 'restaurant'] })
|
||||
: null;
|
||||
const savedInventory = inventory?.id ? await this.em.findOne(Inventory, { id: inventory.id }) : inventory;
|
||||
|
||||
return { food: savedFood ?? food, inventory: savedInventory ?? inventory };
|
||||
}
|
||||
|
||||
findAll(restId: string, dto: FindFoodsDto) {
|
||||
return this.foodRepository.findAllPaginated(restId, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public food detail (only active foods are visible).
|
||||
*/
|
||||
async findPublicById(foodId: string, userId?: string): Promise<any> {
|
||||
const food = await this.foodRepository.findOne({ id: foodId, isActive: true }, { populate: ['category', 'inventory'] });
|
||||
if (!food) throw new NotFoundException(FoodMessage.NOT_FOUND);
|
||||
let isFavorite = false;
|
||||
if (userId) {
|
||||
isFavorite = (await this.em.count(Favorite, { user: { id: userId }, food: { id: foodId } })) > 0;
|
||||
}
|
||||
return {
|
||||
...food,
|
||||
isFavorite,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin food detail (scoped to the authenticated restaurant).
|
||||
*/
|
||||
async findAdminById(restId: string, id: string): Promise<Food> {
|
||||
const food = await this.foodRepository.findOne({ id, restaurant: { id: restId } }, { populate: ['category','inventory'] });
|
||||
|
||||
if (!food) throw new NotFoundException(FoodMessage.NOT_FOUND);
|
||||
return food;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find active foods for a restaurant based on current week day and meal type in Iran timezone.
|
||||
* @param slug - Restaurant slug identifier
|
||||
* @returns Array of active foods matching current day and meal time
|
||||
*/
|
||||
async findByWeekDateAndMealType(slug: string): Promise<Food[]> {
|
||||
const restaurant = await this.restRepository.findOne({ slug });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
const { iranWeekDay, mealType } = this.getCurrentIranTimeContext();
|
||||
|
||||
const queryFilter: FilterQuery<Food> = {
|
||||
restaurant: { slug },
|
||||
isActive: true,
|
||||
// weekDays: { $contains: iranWeekDay },
|
||||
// ...(mealType ? { mealTypes: { $contains: mealType } } : {}),
|
||||
} as unknown as FilterQuery<Food>;
|
||||
|
||||
return this.foodRepository.find(queryFilter, { populate: ['category'] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current Iran timezone context (weekday and meal type).
|
||||
* @returns Object containing Iran weekday (0-6, where 0=Saturday) and current meal type
|
||||
*/
|
||||
private getCurrentIranTimeContext(): { iranWeekDay: number; mealType: MealType | null } {
|
||||
const nowInIran = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Tehran' }));
|
||||
const weekDay = nowInIran.getDay(); // 0 = Sunday, 6 = Saturday
|
||||
const currentHour = nowInIran.getHours();
|
||||
|
||||
// Convert to Iran weekday: Saturday=0, Sunday=1, ..., Friday=6
|
||||
// JavaScript: Sunday=0, Monday=1, ..., Saturday=6
|
||||
// Iran week: Saturday=0, Sunday=1, ..., Friday=6
|
||||
const iranWeekDay = (weekDay + 1) % 7;
|
||||
|
||||
const mealType = this.getMealTypeByHour(currentHour);
|
||||
|
||||
return { iranWeekDay, mealType };
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine meal type based on current hour in Iran timezone.
|
||||
* @param hour - Current hour (0-23)
|
||||
* @returns Meal type or null if outside meal hours
|
||||
*/
|
||||
private getMealTypeByHour(hour: number): MealType | null {
|
||||
const MEAL_TIME_RANGES = {
|
||||
BREAKFAST: { start: 6, end: 11 },
|
||||
LUNCH: { start: 11, end: 15 },
|
||||
SNACK: { start: 15, end: 19 },
|
||||
DINNER: { start: 19, end: 24 },
|
||||
} as const;
|
||||
|
||||
if (hour >= MEAL_TIME_RANGES.BREAKFAST.start && hour < MEAL_TIME_RANGES.BREAKFAST.end) {
|
||||
return MealType.BREAKFAST;
|
||||
}
|
||||
if (hour >= MEAL_TIME_RANGES.LUNCH.start && hour < MEAL_TIME_RANGES.LUNCH.end) {
|
||||
return MealType.LUNCH;
|
||||
}
|
||||
if (hour >= MEAL_TIME_RANGES.SNACK.start && hour < MEAL_TIME_RANGES.SNACK.end) {
|
||||
return MealType.SNACK;
|
||||
}
|
||||
if (hour >= MEAL_TIME_RANGES.DINNER.start && hour < MEAL_TIME_RANGES.DINNER.end) {
|
||||
return MealType.DINNER;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async update(restId: string, id: string, dto: Partial<CreateFoodDto>): Promise<Food> {
|
||||
const { categoryId, dailyStock, ...rest } = dto;
|
||||
const food = await this.foodRepository.findOne({ id, restaurant: { id: restId } }, { populate: ['restaurant'] });
|
||||
if (!food) {
|
||||
throw new NotFoundException(FoodMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
// attach new categories if provided (adds, does not attempt to preserve/remove existing ones)
|
||||
if (categoryId) {
|
||||
const category = await this.categoryRepository.findOne({ id: categoryId, restaurant: { id: restId } });
|
||||
|
||||
if (!category) {
|
||||
throw new NotFoundException(CategoryMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
this.em.assign(food, { category: category });
|
||||
}
|
||||
|
||||
// assign other fields from DTO (excluding dailyStock handled below)
|
||||
this.em.assign(food, rest);
|
||||
|
||||
// Persist food and update/create related inventory atomically
|
||||
await this.em.transactional(async em => {
|
||||
await em.persistAndFlush(food);
|
||||
|
||||
if (typeof dailyStock !== 'undefined') {
|
||||
const inventoryRecord = await em.findOne(Inventory, { food: food });
|
||||
if (inventoryRecord) {
|
||||
inventoryRecord.totalStock = dailyStock;
|
||||
} else {
|
||||
em.create(Inventory, {
|
||||
food: food,
|
||||
availableStock: dailyStock,
|
||||
totalStock: dailyStock,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await em.flush();
|
||||
});
|
||||
|
||||
// Re-load the food to ensure populated relations and stable instance
|
||||
const savedFood = await this.foodRepository.findOne({ id: food.id }, { populate: ['category', 'restaurant'] });
|
||||
|
||||
// Invalidate cache for the restaurant if present (optional)
|
||||
// await this.invalidateRestaurantFoodsCache(savedFood?.restaurant?.slug);
|
||||
|
||||
return savedFood ?? food;
|
||||
}
|
||||
|
||||
async remove(restId: string, id: string): Promise<void> {
|
||||
const food = await this.foodRepository.findOne({ id, restaurant: { id: restId } }, { populate: ['restaurant'] });
|
||||
if (!food) {
|
||||
throw new NotFoundException(FoodMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
// const restaurantSlug = food.restaurant.slug;
|
||||
food.deletedAt = new Date();
|
||||
await this.em.persistAndFlush(food);
|
||||
|
||||
// Invalidate cache for the restaurant
|
||||
// await this.invalidateRestaurantFoodsCache(restaurantSlug);
|
||||
}
|
||||
|
||||
|
||||
async toggleFavorite(userId: string, foodId: string) {
|
||||
|
||||
const favorite = await this.em.findOne(Favorite, { user: userId, food: foodId });
|
||||
if (favorite) {
|
||||
return this.em.removeAndFlush(favorite);
|
||||
}
|
||||
const newFavorite = this.em.create(Favorite, {
|
||||
user: userId,
|
||||
food: foodId,
|
||||
});
|
||||
return this.em.persistAndFlush(newFavorite);
|
||||
}
|
||||
|
||||
|
||||
async getMyFavorites(userId: string,restId: string) {
|
||||
return this.em.find(Favorite, { user: userId, food: { restaurant: { id: restId } } }, { populate: ['food'] });
|
||||
}
|
||||
/**
|
||||
* Invalidate cache for restaurant foods
|
||||
*/
|
||||
// private async invalidateRestaurantFoodsCache(slug: string): Promise<void> {
|
||||
// const cacheKey = `${this.FOODS_BY_RESTAURANT_CACHE_KEY_PREFIX}${slug}`;
|
||||
// await this.cacheService.del(cacheKey);
|
||||
// }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user