diff --git a/package-lock.json b/package-lock.json index 59f5615..0705718 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", "@nestjs/platform-socket.io": "^11.1.11", + "@nestjs/serve-static": "^5.0.4", "@nestjs/swagger": "^11.2.4", "@nestjs/terminus": "^11.0.0", "@nestjs/throttler": "^6.5.0", @@ -53,6 +54,7 @@ "@types/bcrypt": "^6.0.0", "@types/express": "^5.0.0", "@types/jest": "^30.0.0", + "@types/multer": "^2.1.0", "@types/node": "^22.10.7", "@types/nodemailer": "^7.0.4", "@types/passport-jwt": "^4.0.1", @@ -1137,7 +1139,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -3075,7 +3076,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3242,7 +3242,6 @@ "version": "11.1.11", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.11.tgz", "integrity": "sha512-R/+A8XFqLgN8zNs2twhrOaE7dJbRQhdPX3g46am4RT/x8xGLqDphrXkUIno4cGUZHxbczChBAaAPTdPv73wDZA==", - "peer": true, "dependencies": { "file-type": "21.2.0", "iterare": "1.2.1", @@ -3288,7 +3287,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.11.tgz", "integrity": "sha512-H9i+zT3RvHi7tDc+lCmWHJ3ustXveABCr+Vcpl96dNOxgmrx4elQSTC4W93Mlav2opfLV+p0UTHY6L+bpUA4zA==", "hasInstallScript": true, - "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -3368,7 +3366,6 @@ "version": "11.1.11", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.11.tgz", "integrity": "sha512-kyABSskdMRIAMWL0SlbwtDy4yn59RL4HDdwHDz/fxWuv7/53YP8Y2DtV3/sHqY5Er0msMVTZrM38MjqXhYL7gw==", - "peer": true, "dependencies": { "cors": "2.8.5", "express": "5.2.1", @@ -3389,7 +3386,6 @@ "version": "11.1.11", "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.11.tgz", "integrity": "sha512-0z6pLg9CuTXtz7q2lRZoPOU94DN28OTa39f4cQrlZysKA6QrKM7w7z6xqb4g32qjF+LQHFNRmMJtE/pLrxBaig==", - "peer": true, "dependencies": { "socket.io": "4.8.3", "tslib": "2.8.1" @@ -3496,6 +3492,33 @@ "tslib": "^2.1.0" } }, + "node_modules/@nestjs/serve-static": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@nestjs/serve-static/-/serve-static-5.0.4.tgz", + "integrity": "sha512-3kO1M9D3vsPyWPFardxIjUYeuolS58PnhCoBTkS7t3BrdZFZCKHnBZ15js+UOzOR2Q6HmD7ssGjLd0DVYVdvOw==", + "license": "MIT", + "dependencies": { + "path-to-regexp": "8.3.0" + }, + "peerDependencies": { + "@fastify/static": "^8.0.4", + "@nestjs/common": "^11.0.2", + "@nestjs/core": "^11.0.2", + "express": "^5.0.1", + "fastify": "^5.2.1" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "express": { + "optional": true + }, + "fastify": { + "optional": true + } + } + }, "node_modules/@nestjs/swagger": { "version": "11.2.4", "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.4.tgz", @@ -3724,7 +3747,6 @@ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", "hasInstallScript": true, - "peer": true, "engines": { "node": ">=16.13" }, @@ -3789,7 +3811,6 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -4695,7 +4716,6 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -4806,11 +4826,20 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" }, + "node_modules/@types/multer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz", + "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "22.19.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4975,7 +5004,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.52.0.tgz", "integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", @@ -5613,7 +5641,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5667,7 +5694,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6157,7 +6183,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6231,7 +6256,6 @@ "version": "5.66.4", "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.66.4.tgz", "integrity": "sha512-y2VRk2z7d1YNI2JQDD7iThoD0X/0iZZ3VEp8lqT5s5U0XDl9CIjXp1LQgmE9EKy6ReHtzmYXS1f328PnUbZGtQ==", - "peer": true, "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.8.2", @@ -6305,7 +6329,6 @@ "version": "7.2.7", "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-7.2.7.tgz", "integrity": "sha512-TKeeb9nSybk1e9E5yAiPVJ6YKdX9FYhwqqy8fBfVKAFVTJYZUNmeIvwjURW6+UikNsO6l2ta27thYgo/oumDsw==", - "peer": true, "dependencies": { "@cacheable/utils": "^2.3.2", "keyv": "^5.5.4" @@ -6457,7 +6480,6 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, - "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -6501,14 +6523,12 @@ "node_modules/class-transformer": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", - "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "peer": true + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" }, "node_modules/class-validator": { "version": "0.14.3", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", - "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -7194,7 +7214,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -7253,7 +7274,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7313,7 +7333,6 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -8674,7 +8693,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -9518,7 +9536,6 @@ "version": "5.5.5", "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.5.tgz", "integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==", - "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -10263,6 +10280,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "peer": true, "engines": { "node": ">= 6" } @@ -10446,7 +10464,6 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", - "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -10573,7 +10590,6 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", - "peer": true, "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", @@ -10603,7 +10619,6 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-11.0.0.tgz", "integrity": "sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==", - "peer": true, "dependencies": { "get-caller-file": "^2.0.5", "pino": "^10.0.0", @@ -10757,7 +10772,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10811,7 +10825,6 @@ "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", "hasInstallScript": true, - "peer": true, "dependencies": { "@prisma/engines": "5.22.0" }, @@ -11876,7 +11889,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -12191,7 +12203,6 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -12329,7 +12340,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12668,6 +12678,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -12685,6 +12696,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -12697,6 +12709,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -12710,6 +12723,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, + "peer": true, "engines": { "node": ">=4.0" } @@ -12718,13 +12732,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "dev": true, + "peer": true }, "node_modules/webpack/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -12734,6 +12750,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -12746,6 +12763,7 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/package.json b/package.json index 39d2d66..1babb95 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", "@nestjs/platform-socket.io": "^11.1.11", + "@nestjs/serve-static": "^5.0.4", "@nestjs/swagger": "^11.2.4", "@nestjs/terminus": "^11.0.0", "@nestjs/throttler": "^6.5.0", @@ -63,6 +64,7 @@ "@types/bcrypt": "^6.0.0", "@types/express": "^5.0.0", "@types/jest": "^30.0.0", + "@types/multer": "^2.1.0", "@types/node": "^22.10.7", "@types/nodemailer": "^7.0.4", "@types/passport-jwt": "^4.0.1", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0c0b2b0..a49ab0e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -6,7 +6,7 @@ generator client { } datasource db { - provider = "postgresql" + provider = "sqlite" url = env("DATABASE_URL") } @@ -160,3 +160,79 @@ model Translation { @@index([locale]) @@index([namespace]) } + +// ============================================ +// CMS - Site Content Management +// ============================================ + +model Project { + id String @id @default(uuid()) + title String + image String + roles String @default("[]") // JSON array stored as string for SQLite + color String @default("#FF5733") + sortOrder Int @default(0) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? // Soft delete support + + @@index([sortOrder]) +} + +// ============================================ +// Audit Log — CMS İşlem Kayıtları +// ============================================ + +model AuditLog { + id String @id @default(uuid()) + entity String // "project", "client", "content", "media" + entityId String // İlgili kaydın ID'si + action String // "CREATE", "UPDATE", "DELETE", "RESTORE" + before String? // JSON — önceki durum + after String? // JSON — sonraki durum + userId String? // İşlemi yapan kullanıcı + ip String? // İstek IP'si + createdAt DateTime @default(now()) + + @@index([entity, entityId]) + @@index([action]) + @@index([createdAt]) +} + +model SiteContent { + id String @id @default(uuid()) + section String // "hero", "services", "process", "about", "contact", "footer", "navbar" + locale String @default("tr") + content String @default("{}") // JSON stored as string for SQLite + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([section, locale]) + @@index([section]) + @@index([locale]) +} + +model MediaFile { + id String @id @default(uuid()) + filename String + originalName String + mimetype String + path String + url String + size Int + createdAt DateTime @default(now()) +} + +model Client { + id String @id @default(uuid()) + name String + logo String // URL to logo image + website String? // Optional website URL + sortOrder Int @default(0) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([sortOrder]) +} diff --git a/src/app.module.ts b/src/app.module.ts index 7db004a..4acb805 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,7 @@ import { QueryResolver, } from 'nestjs-i18n'; import * as path from 'path'; +import { ServeStaticModule } from '@nestjs/serve-static'; // Config import { @@ -39,6 +40,7 @@ import { UsersModule } from './modules/users/users.module'; import { AdminModule } from './modules/admin/admin.module'; import { HealthModule } from './modules/health/health.module'; import { GeminiModule } from './modules/gemini/gemini.module'; +import { CmsModule } from './modules/cms/cms.module'; // Guards import { @@ -75,11 +77,11 @@ import { level: configService.get('app.isDevelopment') ? 'debug' : 'info', transport: configService.get('app.isDevelopment') ? { - target: 'pino-pretty', - options: { - singleLine: true, - }, - } + target: 'pino-pretty', + options: { + singleLine: true, + }, + } : undefined, }, }; @@ -160,6 +162,15 @@ import { // Optional Modules (controlled by env variables) GeminiModule, HealthModule, + + // CMS Module + CmsModule, + + // Serve uploaded files + ServeStaticModule.forRoot({ + rootPath: path.join(__dirname, '..', 'uploads'), + serveRoot: '/uploads', + }), ], providers: [ // Global Exception Filter @@ -199,4 +210,4 @@ import { }, ], }) -export class AppModule {} +export class AppModule { } diff --git a/src/main.ts b/src/main.ts index 5b6eddc..74a7844 100644 --- a/src/main.ts +++ b/src/main.ts @@ -75,8 +75,13 @@ async function bootstrap() { }); logger.log('Swagger initialized'); - logger.log(`Attempting to listen on port ${port}...`); - await app.listen(port, '0.0.0.0'); + try { + logger.log(`Attempting to listen on port ${port}...`); + await app.listen(port, '0.0.0.0'); + } catch (err: any) { + logger.error('Failed to bind to port:', err.message, err.stack); + process.exit(1); + } logger.log('═══════════════════════════════════════════════════════════'); logger.log(`🚀 Server is running on: http://localhost:${port}/api`); diff --git a/src/modules/cms/cms.controller.ts b/src/modules/cms/cms.controller.ts new file mode 100644 index 0000000..19474f4 --- /dev/null +++ b/src/modules/cms/cms.controller.ts @@ -0,0 +1,232 @@ +import { + Controller, + Get, + Post, + Put, + Patch, + Delete, + Body, + Param, + Query, + UseInterceptors, + UploadedFile, + ParseFilePipe, + MaxFileSizeValidator, + FileTypeValidator, + OnModuleInit, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiTags, ApiBearerAuth, ApiConsumes, ApiBody } from '@nestjs/swagger'; +import { diskStorage } from 'multer'; +import { extname } from 'path'; +import { CmsService } from './cms.service'; +import { + CreateProjectDto, + UpdateProjectDto, + ReorderProjectsDto, + UpdateSiteContentDto, + CreateClientDto, + UpdateClientDto, +} from './dto'; +import { Public, Roles } from '../../common/decorators'; + +// Generate unique filename +const storage = diskStorage({ + destination: './uploads', + filename: (req, file, cb) => { + const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}${extname(file.originalname)}`; + cb(null, uniqueName); + }, +}); + +@ApiTags('CMS') +@Controller('cms') +export class CmsController implements OnModuleInit { + constructor(private readonly cmsService: CmsService) { } + + async onModuleInit() { + await this.cmsService.seedDefaultProjects(); + await this.cmsService.seedAdminUser(); + } + + // ── Public Endpoints ──────────────────────────── + + @Public() + @Get('projects') + findAllProjects() { + return this.cmsService.findAllProjects(); + } + + @Public() + @Get('content') + findAllContent(@Query('locale') locale?: string) { + return this.cmsService.findAllContent(locale); + } + + @Public() + @Get('content/:section') + findContentBySection( + @Param('section') section: string, + @Query('locale') locale?: string, + ) { + return this.cmsService.findContentBySection(section, locale ?? 'tr'); + } + + // ── Admin: Projects ───────────────────────────── + + @ApiBearerAuth() + @Roles('admin') + @Post('projects') + createProject(@Body() dto: CreateProjectDto) { + return this.cmsService.createProject(dto); + } + + @ApiBearerAuth() + @Roles('admin') + @Put('projects/:id') + updateProject(@Param('id') id: string, @Body() dto: UpdateProjectDto) { + return this.cmsService.updateProject(id, dto); + } + + @ApiBearerAuth() + @Roles('admin') + @Delete('projects/:id') + deleteProject(@Param('id') id: string) { + return this.cmsService.deleteProject(id); + } + + @ApiBearerAuth() + @Roles('admin') + @Post('projects/:id/restore') + restoreProject(@Param('id') id: string) { + return this.cmsService.restoreProject(id); + } + + @ApiBearerAuth() + @Roles('admin') + @Patch('projects/reorder') + reorderProjects(@Body() dto: ReorderProjectsDto) { + return this.cmsService.reorderProjects(dto); + } + + // ── Admin: Site Content ───────────────────────── + + @ApiBearerAuth() + @Roles('admin') + @Put('content/:section') + upsertContent( + @Param('section') section: string, + @Query('locale') locale: string = 'tr', + @Body() dto: UpdateSiteContentDto, + ) { + return this.cmsService.upsertContent(section, locale, dto); + } + + // ── Admin: Media Upload ───────────────────────── + + @ApiBearerAuth() + @Roles('admin') + @Post('upload') + @UseInterceptors(FileInterceptor('file', { storage })) + @ApiConsumes('multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + properties: { file: { type: 'string', format: 'binary' } }, + }, + }) + async uploadFile( + @UploadedFile( + new ParseFilePipe({ + validators: [ + new FileTypeValidator({ fileType: /image\/(jpeg|jpg|png|webp|gif|svg\+xml)/ }), + ], + }), + ) + file: Express.Multer.File, + ) { + const url = `/uploads/${file.filename}`; + const media = await this.cmsService.createMediaFile({ + filename: file.filename, + originalName: file.originalname, + mimetype: file.mimetype, + path: file.path, + url, + size: file.size, + }); + return media; + } + + @Public() + @Get('media') + findAllMedia() { + return this.cmsService.findAllMedia(); + } + + @ApiBearerAuth() + @Roles('admin') + @Delete('media/:id') + deleteMedia(@Param('id') id: string) { + return this.cmsService.deleteMedia(id); + } + + // ── Public: Clients ───────────────────────── + + @Public() + @Get('clients') + findAllClients() { + return this.cmsService.findAllClients(); + } + + // ── Admin: Clients ───────────────────────── + + @ApiBearerAuth() + @Roles('admin') + @Post('clients') + createClient(@Body() dto: CreateClientDto) { + return this.cmsService.createClient(dto); + } + + @ApiBearerAuth() + @Roles('admin') + @Put('clients/:id') + updateClient(@Param('id') id: string, @Body() dto: UpdateClientDto) { + return this.cmsService.updateClient(id, dto); + } + + @ApiBearerAuth() + @Roles('admin') + @Delete('clients/:id') + deleteClient(@Param('id') id: string) { + return this.cmsService.deleteClient(id); + } + + // ── Admin: Audit Logs ─────────────────────── + + @ApiBearerAuth() + @Roles('admin') + @Get('audit-logs') + findAuditLogs( + @Query('entity') entity?: string, + @Query('action') action?: string, + @Query('limit') limit?: string, + @Query('offset') offset?: string, + ) { + return this.cmsService.findAuditLogs({ + entity, + action, + limit: limit ? parseInt(limit, 10) : 50, + offset: offset ? parseInt(offset, 10) : 0, + }); + } + + @ApiBearerAuth() + @Roles('admin') + @Get('audit-logs/:entity/:entityId') + findEntityAuditLogs( + @Param('entity') entity: string, + @Param('entityId') entityId: string, + ) { + return this.cmsService.findAuditLogsByEntity(entity, entityId); + } +} diff --git a/src/modules/cms/cms.module.ts b/src/modules/cms/cms.module.ts new file mode 100644 index 0000000..da72a76 --- /dev/null +++ b/src/modules/cms/cms.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { CmsController } from './cms.controller'; +import { CmsService } from './cms.service'; +import { DatabaseModule } from '../../database/database.module'; + +@Module({ + imports: [DatabaseModule], + controllers: [CmsController], + providers: [CmsService], + exports: [CmsService], +}) +export class CmsModule { } diff --git a/src/modules/cms/cms.service.ts b/src/modules/cms/cms.service.ts new file mode 100644 index 0000000..789fa3b --- /dev/null +++ b/src/modules/cms/cms.service.ts @@ -0,0 +1,456 @@ +import { Injectable, Logger, NotFoundException, InternalServerErrorException } from '@nestjs/common'; +import * as bcrypt from 'bcrypt'; +import { PrismaService } from '../../database/prisma.service'; +import { + CreateProjectDto, + UpdateProjectDto, + ReorderProjectsDto, + UpdateSiteContentDto, + CreateClientDto, + UpdateClientDto, +} from './dto'; + +@Injectable() +export class CmsService { + private readonly logger = new Logger(CmsService.name); + + constructor(private readonly prisma: PrismaService) { } + + // ── Audit Logging ────────────────────────────── + + private async audit( + entity: string, + entityId: string, + action: string, + before?: any, + after?: any, + ) { + try { + await this.prisma.auditLog.create({ + data: { + entity, + entityId, + action, + before: before ? JSON.stringify(before) : null, + after: after ? JSON.stringify(after) : null, + }, + }); + this.logger.log(`[AUDIT] ${action} ${entity}:${entityId}`); + } catch (err) { + // Audit failure should never break the main operation + this.logger.error(`[AUDIT] Failed to log ${action} ${entity}:${entityId}`, err.stack); + } + } + + // ── Write Verification ───────────────────────── + + private async verifyWrite( + model: string, + id: string, + operation: string, + ): Promise { + const record = await (this.prisma as any)[model].findUnique({ where: { id } }); + if (!record) { + this.logger.error(`[VERIFY] ${operation} failed — record ${model}:${id} not found after write`); + throw new InternalServerErrorException( + `Veritabanı yazma doğrulaması başarısız: ${model}:${id} yazıldıktan sonra bulunamadı`, + ); + } + this.logger.debug(`[VERIFY] ${operation} ${model}:${id} — OK`); + return record as T; + } + + // ── Projects ────────────────────────────────── + + async findAllProjects() { + return this.prisma.project.findMany({ + where: { isActive: true, deletedAt: null }, + orderBy: { sortOrder: 'asc' }, + }); + } + + async findProjectById(id: string) { + const project = await this.prisma.project.findUnique({ where: { id } }); + if (!project || project.deletedAt) throw new NotFoundException('Project not found'); + return project; + } + + async createProject(dto: CreateProjectDto) { + // Auto-set sortOrder to last position + const lastProject = await this.prisma.project.findFirst({ + where: { deletedAt: null }, + orderBy: { sortOrder: 'desc' }, + }); + const nextOrder = (lastProject?.sortOrder ?? -1) + 1; + + const created = await this.prisma.project.create({ + data: { + title: dto.title, + image: dto.image, + roles: JSON.stringify(dto.roles), + color: dto.color ?? '#FF5733', + sortOrder: dto.sortOrder ?? nextOrder, + }, + }); + + // Write verification + const verified = await this.verifyWrite('project', created.id, 'CREATE Project'); + + // Audit log + await this.audit('project', created.id, 'CREATE', null, verified); + + return verified; + } + + async updateProject(id: string, dto: UpdateProjectDto) { + const before = await this.findProjectById(id); // throws if not found + + const updated = await this.prisma.project.update({ + where: { id }, + data: { + ...dto, + roles: dto.roles ? JSON.stringify(dto.roles) : undefined, + }, + }); + + // Write verification + const verified = await this.verifyWrite('project', id, 'UPDATE Project'); + + // Audit log + await this.audit('project', id, 'UPDATE', before, verified); + + return verified; + } + + async deleteProject(id: string) { + const before = await this.findProjectById(id); + + // Soft delete — projeyi silmek yerine deletedAt işaretle + const deleted = await this.prisma.project.update({ + where: { id }, + data: { deletedAt: new Date() }, + }); + + // Write verification — deletedAt'ın set edildiğini doğrula + const verified = await (this.prisma as any).project.findUnique({ where: { id } }); + if (!verified || !verified.deletedAt) { + this.logger.error(`[VERIFY] Soft DELETE failed — project:${id} deletedAt is still null`); + throw new InternalServerErrorException('Silme doğrulaması başarısız'); + } + + // Audit log + await this.audit('project', id, 'DELETE', before, null); + + return deleted; + } + + async restoreProject(id: string) { + const project = await this.prisma.project.findUnique({ where: { id } }); + if (!project) throw new NotFoundException('Project not found'); + if (!project.deletedAt) throw new NotFoundException('Project is not deleted'); + + const restored = await this.prisma.project.update({ + where: { id }, + data: { deletedAt: null }, + }); + + // Write verification + const verified = await this.verifyWrite('project', id, 'RESTORE Project'); + + // Audit log + await this.audit('project', id, 'RESTORE', null, verified); + + return verified; + } + + async reorderProjects(dto: ReorderProjectsDto) { + const updates = dto.items.map((item) => + this.prisma.project.update({ + where: { id: item.id }, + data: { sortOrder: item.sortOrder }, + }), + ); + await this.prisma.$transaction(updates); + return this.findAllProjects(); + } + + // ── Site Content ────────────────────────────── + + async findAllContent(locale?: string) { + const where = locale ? { locale } : {}; + const contents = await this.prisma.siteContent.findMany({ where }); + + // Transform into a section-keyed object + const result: Record = {}; + for (const c of contents) { + if (!result[c.locale]) result[c.locale] = {}; + try { + result[c.locale][c.section] = JSON.parse(c.content); + } catch { + result[c.locale][c.section] = c.content; + } + } + return result; + } + + async findContentBySection(section: string, locale: string = 'tr') { + const content = await this.prisma.siteContent.findUnique({ + where: { section_locale: { section, locale } }, + }); + if (!content) return null; + try { + return JSON.parse(content.content); + } catch { + return content.content; + } + } + + async upsertContent( + section: string, + locale: string, + dto: UpdateSiteContentDto, + ) { + const contentStr = JSON.stringify(dto.content); + + // Get before state for audit + const before = await this.prisma.siteContent.findUnique({ + where: { section_locale: { section, locale } }, + }); + + const result = await this.prisma.siteContent.upsert({ + where: { section_locale: { section, locale } }, + create: { + section, + locale, + content: contentStr, + }, + update: { + content: contentStr, + }, + }); + + // Audit log + await this.audit( + 'content', + result.id, + before ? 'UPDATE' : 'CREATE', + before, + result, + ); + + return result; + } + + // ── Media Files ────────────────────────────── + + async createMediaFile(file: { + filename: string; + originalName: string; + mimetype: string; + path: string; + url: string; + size: number; + }) { + const created = await this.prisma.mediaFile.create({ data: file }); + + // Audit log + await this.audit('media', created.id, 'CREATE', null, created); + + return created; + } + + async findAllMedia() { + return this.prisma.mediaFile.findMany({ + orderBy: { createdAt: 'desc' }, + }); + } + + async deleteMedia(id: string) { + const media = await this.prisma.mediaFile.findUnique({ where: { id } }); + if (!media) throw new NotFoundException('Media not found'); + + const result = await this.prisma.mediaFile.delete({ where: { id } }); + + // Audit log + await this.audit('media', id, 'DELETE', media, null); + + return result; + } + + // ── Seed ────────────────────────────────────── + + async seedDefaultProjects() { + const count = await this.prisma.project.count({ + where: { deletedAt: null }, + }); + if (count > 0) { + this.logger.log('Projects already seeded, skipping'); + return; + } + + const defaults = [ + { + title: 'Deadpool', + image: 'https://picsum.photos/seed/deadpool/1920/1080?blur=2', + roles: JSON.stringify(['Official Turkish Voice', 'Character Voice Acting']), + color: '#FF5733', + sortOrder: 0, + }, + { + title: 'Spider-Man', + image: 'https://picsum.photos/seed/spiderman/1920/1080?blur=2', + roles: JSON.stringify(['Official Turkish Voice', 'Character Voice Acting']), + color: '#C70039', + sortOrder: 1, + }, + { + title: 'The Lion King', + image: 'https://picsum.photos/seed/lionking/1920/1080?blur=2', + roles: JSON.stringify(['Simba', 'Musical Performance', 'Vocals']), + color: '#900C3F', + sortOrder: 2, + }, + { + title: 'Monster Notebook', + image: 'https://picsum.photos/seed/monster/1920/1080?blur=2', + roles: JSON.stringify(['Brand Face', 'Corporate Voice']), + color: '#511845', + sortOrder: 3, + }, + ]; + + for (const p of defaults) { + const created = await this.prisma.project.create({ data: p }); + await this.audit('project', created.id, 'CREATE', null, created); + } + this.logger.log(`Seeded ${defaults.length} default projects`); + } + + async seedAdminUser() { + const adminExists = await this.prisma.user.findUnique({ + where: { email: 'admin@haruncan.com' }, + }); + if (adminExists) { + this.logger.log('Admin user already exists, skipping'); + return; + } + + const hashedPassword = await bcrypt.hash('admin123', 10); + + await this.prisma.user.create({ + data: { + email: 'admin@haruncan.com', + password: hashedPassword, + firstName: 'Harun', + lastName: 'CAN', + roles: { + create: { + role: { + connectOrCreate: { + where: { name: 'admin' }, + create: { name: 'admin', description: 'Administrator' }, + }, + }, + }, + }, + }, + }); + + this.logger.log('✅ Admin user created: admin@haruncan.com / admin123'); + } + + // ── Clients ────────────────────────────────── + + async findAllClients() { + return this.prisma.client.findMany({ + where: { isActive: true }, + orderBy: { sortOrder: 'asc' }, + }); + } + + async createClient(dto: CreateClientDto) { + const lastClient = await this.prisma.client.findFirst({ + orderBy: { sortOrder: 'desc' }, + }); + const nextOrder = (lastClient?.sortOrder ?? -1) + 1; + + const created = await this.prisma.client.create({ + data: { + name: dto.name, + logo: dto.logo, + website: dto.website, + sortOrder: dto.sortOrder ?? nextOrder, + }, + }); + + // Write verification + const verified = await this.verifyWrite('client', created.id, 'CREATE Client'); + + // Audit log + await this.audit('client', created.id, 'CREATE', null, verified); + + return verified; + } + + async updateClient(id: string, dto: UpdateClientDto) { + const before = await this.prisma.client.findUniqueOrThrow({ where: { id } }).catch(() => { + throw new NotFoundException('Client not found'); + }); + + const updated = await this.prisma.client.update({ where: { id }, data: dto }); + + // Write verification + const verified = await this.verifyWrite('client', id, 'UPDATE Client'); + + // Audit log + await this.audit('client', id, 'UPDATE', before, verified); + + return verified; + } + + async deleteClient(id: string) { + const client = await this.prisma.client.findUnique({ where: { id } }); + if (!client) throw new NotFoundException('Client not found'); + + const result = await this.prisma.client.delete({ where: { id } }); + + // Audit log + await this.audit('client', id, 'DELETE', client, null); + + return result; + } + + // ── Audit Logs (Admin Queries) ─────────────── + + async findAuditLogs(options?: { + entity?: string; + entityId?: string; + action?: string; + limit?: number; + offset?: number; + }) { + const where: any = {}; + if (options?.entity) where.entity = options.entity; + if (options?.entityId) where.entityId = options.entityId; + if (options?.action) where.action = options.action; + + const [logs, total] = await Promise.all([ + this.prisma.auditLog.findMany({ + where, + orderBy: { createdAt: 'desc' }, + take: options?.limit ?? 50, + skip: options?.offset ?? 0, + }), + this.prisma.auditLog.count({ where }), + ]); + + return { logs, total }; + } + + async findAuditLogsByEntity(entity: string, entityId: string) { + return this.prisma.auditLog.findMany({ + where: { entity, entityId }, + orderBy: { createdAt: 'desc' }, + }); + } +} diff --git a/src/modules/cms/dto/cms.dto.ts b/src/modules/cms/dto/cms.dto.ts new file mode 100644 index 0000000..a38e2ea --- /dev/null +++ b/src/modules/cms/dto/cms.dto.ts @@ -0,0 +1,142 @@ +import { + IsString, + IsOptional, + IsArray, + IsInt, + IsBoolean, + IsObject, + Min, +} from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +// ── Projects ────────────────────────────────────── + +export class CreateProjectDto { + @ApiProperty({ example: 'Deadpool' }) + @IsString() + title: string; + + @ApiProperty({ example: 'https://example.com/deadpool.jpg' }) + @IsString() + image: string; + + @ApiProperty({ example: ['Official Turkish Voice', 'Character Voice Acting'] }) + @IsArray() + @IsString({ each: true }) + roles: string[]; + + @ApiPropertyOptional({ example: '#FF5733' }) + @IsOptional() + @IsString() + color?: string; + + @ApiPropertyOptional({ example: 0 }) + @IsOptional() + @IsInt() + @Min(0) + sortOrder?: number; +} + +export class UpdateProjectDto { + @ApiPropertyOptional({ example: 'Deadpool 3' }) + @IsOptional() + @IsString() + title?: string; + + @ApiPropertyOptional({ example: 'https://example.com/deadpool3.jpg' }) + @IsOptional() + @IsString() + image?: string; + + @ApiPropertyOptional({ example: ['Official Turkish Voice'] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + roles?: string[]; + + @ApiPropertyOptional({ example: '#C70039' }) + @IsOptional() + @IsString() + color?: string; + + @ApiPropertyOptional({ example: 1 }) + @IsOptional() + @IsInt() + @Min(0) + sortOrder?: number; + + @ApiPropertyOptional({ example: true }) + @IsOptional() + @IsBoolean() + isActive?: boolean; +} + +export class ReorderProjectsDto { + @ApiProperty({ + example: [ + { id: 'uuid-1', sortOrder: 0 }, + { id: 'uuid-2', sortOrder: 1 }, + ], + }) + @IsArray() + items: { id: string; sortOrder: number }[]; +} + +// ── Site Content ────────────────────────────────── + +export class UpdateSiteContentDto { + @ApiProperty({ example: { title: 'Hello', desc: 'World' } }) + @IsObject() + content: Record; +} + +// ── Clients ────────────────────────────────────── + +export class CreateClientDto { + @ApiProperty({ example: 'Netflix' }) + @IsString() + name: string; + + @ApiProperty({ example: 'https://example.com/netflix-logo.png' }) + @IsString() + logo: string; + + @ApiPropertyOptional({ example: 'https://netflix.com' }) + @IsOptional() + @IsString() + website?: string; + + @ApiPropertyOptional({ example: 0 }) + @IsOptional() + @IsInt() + @Min(0) + sortOrder?: number; +} + +export class UpdateClientDto { + @ApiPropertyOptional({ example: 'Netflix' }) + @IsOptional() + @IsString() + name?: string; + + @ApiPropertyOptional({ example: 'https://example.com/netflix-logo.png' }) + @IsOptional() + @IsString() + logo?: string; + + @ApiPropertyOptional({ example: 'https://netflix.com' }) + @IsOptional() + @IsString() + website?: string; + + @ApiPropertyOptional({ example: 0 }) + @IsOptional() + @IsInt() + @Min(0) + sortOrder?: number; + + @ApiPropertyOptional({ example: true }) + @IsOptional() + @IsBoolean() + isActive?: boolean; +} diff --git a/src/modules/cms/dto/index.ts b/src/modules/cms/dto/index.ts new file mode 100644 index 0000000..25db02b --- /dev/null +++ b/src/modules/cms/dto/index.ts @@ -0,0 +1 @@ +export * from './cms.dto'; diff --git a/uploads/1772836705449-386698498.webp b/uploads/1772836705449-386698498.webp new file mode 100644 index 0000000..6dd7416 Binary files /dev/null and b/uploads/1772836705449-386698498.webp differ diff --git a/uploads/1772836714460-810290801.webp b/uploads/1772836714460-810290801.webp new file mode 100644 index 0000000..6dd7416 Binary files /dev/null and b/uploads/1772836714460-810290801.webp differ diff --git a/uploads/1772836777451-720750819.webp b/uploads/1772836777451-720750819.webp new file mode 100644 index 0000000..6dd7416 Binary files /dev/null and b/uploads/1772836777451-720750819.webp differ diff --git a/uploads/1772836785630-554541704.webp b/uploads/1772836785630-554541704.webp new file mode 100644 index 0000000..6dd7416 Binary files /dev/null and b/uploads/1772836785630-554541704.webp differ diff --git a/uploads/1772836812306-24774814.webp b/uploads/1772836812306-24774814.webp new file mode 100644 index 0000000..6dd7416 Binary files /dev/null and b/uploads/1772836812306-24774814.webp differ diff --git a/uploads/1772836820013-985207017.webp b/uploads/1772836820013-985207017.webp new file mode 100644 index 0000000..6dd7416 Binary files /dev/null and b/uploads/1772836820013-985207017.webp differ diff --git a/uploads/1772836836133-281285727.webp b/uploads/1772836836133-281285727.webp new file mode 100644 index 0000000..6dd7416 Binary files /dev/null and b/uploads/1772836836133-281285727.webp differ diff --git a/uploads/1772836867451-316497790.webp b/uploads/1772836867451-316497790.webp new file mode 100644 index 0000000..6dd7416 Binary files /dev/null and b/uploads/1772836867451-316497790.webp differ diff --git a/uploads/1772982194781-173304442.webp b/uploads/1772982194781-173304442.webp new file mode 100644 index 0000000..0511cbb Binary files /dev/null and b/uploads/1772982194781-173304442.webp differ diff --git a/uploads/1772983229310-723555621.webp b/uploads/1772983229310-723555621.webp new file mode 100644 index 0000000..6dd7416 Binary files /dev/null and b/uploads/1772983229310-723555621.webp differ diff --git a/uploads/1772983501530-2875755.webp b/uploads/1772983501530-2875755.webp new file mode 100644 index 0000000..6dd7416 Binary files /dev/null and b/uploads/1772983501530-2875755.webp differ diff --git a/uploads/1772983860203-545700454.webp b/uploads/1772983860203-545700454.webp new file mode 100644 index 0000000..6dd7416 Binary files /dev/null and b/uploads/1772983860203-545700454.webp differ diff --git a/uploads/1772984524655-837143941.png b/uploads/1772984524655-837143941.png new file mode 100644 index 0000000..39d583e Binary files /dev/null and b/uploads/1772984524655-837143941.png differ diff --git a/uploads/1772985263057-412119294.png b/uploads/1772985263057-412119294.png new file mode 100644 index 0000000..f37764b Binary files /dev/null and b/uploads/1772985263057-412119294.png differ diff --git a/uploads/1772985271261-647774470.webp b/uploads/1772985271261-647774470.webp new file mode 100644 index 0000000..8c2d26a Binary files /dev/null and b/uploads/1772985271261-647774470.webp differ diff --git a/uploads/1772985293317-376544079.jpg b/uploads/1772985293317-376544079.jpg new file mode 100644 index 0000000..2995a4d --- /dev/null +++ b/uploads/1772985293317-376544079.jpg @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/uploads/1772985457669-609882992.webp b/uploads/1772985457669-609882992.webp new file mode 100644 index 0000000..6dd7416 Binary files /dev/null and b/uploads/1772985457669-609882992.webp differ diff --git a/uploads/1772985607638-95283599.png b/uploads/1772985607638-95283599.png new file mode 100644 index 0000000..39d583e Binary files /dev/null and b/uploads/1772985607638-95283599.png differ