diff --git a/config.js b/config.js index 8993606..91d6964 100644 --- a/config.js +++ b/config.js @@ -1,17 +1,24 @@ // Private -const TELEGRAM_API_ID = Number(process.env.TELEGRAM_API_ID); -const TELEGRAM_API_HASH = String(process.env.TELEGRAM_API_HASH || ``); +const JWT_ACCESS_KEY = String(process.env.JWT_ACCESS_KEY || ``); +const POSTGRES_CONNECTION_URL = String(process.env.POSTGRES_CONNECTION_URL || ``); const PUSHER_APP_ID = String(process.env.PUSHER_APP_ID); -const PUSHER_SECRET = String(process.env.PUSHER_SECRET || ``); const PUSHER_CLUSTER = String(process.env.NEXT_PUBLIC_PUSHER_CLUSTER || ``); +const PUSHER_SECRET = String(process.env.PUSHER_SECRET || ``); +const SMTP_HOST = String(process.env.SMTP_HOST || ``); +const SMTP_LOGIN = String(process.env.SMTP_LOGIN || ``); +const SMTP_PASSWORD = String(process.env.SMTP_PASSWORD || ``); +const SMTP_PORT = Number(process.env.SMTP_PORT || ``); +const TELEGRAM_API_HASH = String(process.env.TELEGRAM_API_HASH || ``); +const TELEGRAM_API_ID = Number(process.env.TELEGRAM_API_ID); const TELEGRAM_AUTH_STRING = String(process.env.TELEGRAM_AUTH_STRING); -const POSTGRES_CONNECTION_URL = String(process.env.POSTGRES_CONNECTION_URL || ``); // Public const SITE_URL = String(process.env.NEXT_PUBLIC_SITE_URL || ``); const PUSHER_KEY = String(process.env.NEXT_PUBLIC_PUSHER_KEY || ``); module.exports = { + JWT_ACCESS_KEY, + POSTGRES_CONNECTION_URL, PUSHER_APP_ID, PUSHER_CLUSTER, PUSHER_KEY, @@ -20,5 +27,8 @@ module.exports = { TELEGRAM_API_HASH, TELEGRAM_API_ID, TELEGRAM_AUTH_STRING, - POSTGRES_CONNECTION_URL, + SMTP_HOST, + SMTP_PORT, + SMTP_LOGIN, + SMTP_PASSWORD, }; diff --git a/package-lock.json b/package-lock.json index 54b77d5..89736ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,14 @@ "version": "0.1.0", "dependencies": { "@prisma/client": "5.5.2", + "bcrypt": "5.1.1", "clsx": "2.0.0", + "cookies-next": "4.0.0", "joi": "17.11.0", + "jsonwebtoken": "9.0.2", "next": "14.0.1", "next-connect": "1.0.0", + "nodemailer": "6.9.7", "pusher": "5.1.3", "pusher-js": "8.3.0", "react": "18.2.0", @@ -22,12 +26,17 @@ "remark-parse": "11.0.0", "telegram": "2.19.8", "typescript": "4.9.3", - "unified": "11.0.4" + "unified": "11.0.4", + "uuid": "9.0.1" }, "devDependencies": { + "@types/bcrypt": "5.0.2", "@types/joi": "17.2.3", + "@types/jsonwebtoken": "9.0.5", + "@types/nodemailer": "6.4.14", "@types/react": "18.2.34", "@types/react-dom": "18.2.14", + "@types/uuid": "9.0.7", "@typescript-eslint/eslint-plugin": "5.62.0", "eslint": "8.28.0", "eslint-config-htmlacademy": "8.0.0", @@ -1223,6 +1232,25 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, "node_modules/@next/env": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.1.tgz", @@ -1581,6 +1609,20 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1652,6 +1694,15 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mdast": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz", @@ -1679,6 +1730,15 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.14", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.14.tgz", + "integrity": "sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/prettier": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", @@ -1734,6 +1794,12 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" }, + "node_modules/@types/uuid": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", + "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.14", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.14.tgz", @@ -2038,6 +2104,11 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -2070,6 +2141,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2117,7 +2199,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2150,6 +2231,23 @@ "node": ">= 8" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2456,8 +2554,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -2478,6 +2575,19 @@ } ] }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -2490,7 +2600,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2572,6 +2681,11 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2713,6 +2827,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/ci-info": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.0.tgz", @@ -2789,6 +2911,14 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2812,8 +2942,12 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, "node_modules/convert-source-map": { "version": "2.0.0", @@ -2821,6 +2955,29 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookies-next": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-4.0.0.tgz", + "integrity": "sha512-3TyzeltFCGgdOlVOVTPClSq+YV9ZCdOyA3aHRZv9f5aSgg7EyI4NSvXFOCgzT/xIxeHR4Rz8/z5Tdo9oPqaVpA==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/node": "^16.10.2", + "cookie": "^0.4.0" + } + }, + "node_modules/cookies-next/node_modules/@types/node": { + "version": "16.18.61", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.61.tgz", + "integrity": "sha512-k0N7BqGhJoJzdh6MuQg1V1ragJiXTh8VUBAZTWjJ9cUq23SG0F0xavOwZbhiP4J3y20xd6jxKx+xNUhkMAi76Q==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2953,6 +3110,11 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2961,6 +3123,14 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -3066,6 +3236,14 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.570", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.570.tgz", @@ -4228,11 +4406,32 @@ "node": ">= 6" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.2", @@ -4284,6 +4483,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4367,7 +4585,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -4572,6 +4789,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", @@ -4755,6 +4977,18 @@ "entities": "^2.0.0" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -4839,7 +5073,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -4848,8 +5081,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { "version": "1.0.6", @@ -5021,7 +5253,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -6065,6 +6296,27 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -6080,6 +6332,25 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -6135,12 +6406,47 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -6165,7 +6471,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -6177,7 +6482,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, "dependencies": { "semver": "^6.0.0" }, @@ -6192,7 +6496,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -7028,7 +7331,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -7045,6 +7347,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7141,6 +7485,11 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -7203,6 +7552,28 @@ "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", "dev": true }, + "node_modules/nodemailer": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.7.tgz", + "integrity": "sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7224,11 +7595,21 @@ "node": ">=8" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7346,7 +7727,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -7466,7 +7846,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7802,6 +8181,19 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/real-cancellable-promise": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/real-cancellable-promise/-/real-cancellable-promise-1.2.0.tgz", @@ -8011,7 +8403,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -8063,6 +8454,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", @@ -8089,7 +8499,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -8100,6 +8509,11 @@ "node": ">=10" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "node_modules/set-function-length": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", @@ -8167,8 +8581,7 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/sisteransi": { "version": "1.0.5", @@ -8291,6 +8704,14 @@ "node": ">=10.0.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -8308,7 +8729,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -8321,8 +8741,7 @@ "node_modules/string-width/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/string.prototype.matchall": { "version": "4.0.8", @@ -8405,7 +8824,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -8514,6 +8932,22 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/telegram": { "version": "2.19.8", "resolved": "https://registry.npmjs.org/telegram/-/telegram-2.19.8.tgz", @@ -8951,6 +9385,23 @@ "node": ">=6.14.2" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", @@ -9175,6 +9626,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -9204,8 +9663,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "4.0.2", @@ -9240,8 +9698,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { "version": "17.6.2", diff --git a/package.json b/package.json index a999fd4..7f696dc 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,14 @@ }, "dependencies": { "@prisma/client": "5.5.2", + "bcrypt": "5.1.1", "clsx": "2.0.0", + "cookies-next": "4.0.0", "joi": "17.11.0", + "jsonwebtoken": "9.0.2", "next": "14.0.1", "next-connect": "1.0.0", + "nodemailer": "6.9.7", "pusher": "5.1.3", "pusher-js": "8.3.0", "react": "18.2.0", @@ -28,12 +32,17 @@ "remark-parse": "11.0.0", "telegram": "2.19.8", "typescript": "4.9.3", - "unified": "11.0.4" + "unified": "11.0.4", + "uuid": "9.0.1" }, "devDependencies": { + "@types/bcrypt": "5.0.2", "@types/joi": "17.2.3", + "@types/jsonwebtoken": "9.0.5", + "@types/nodemailer": "6.4.14", "@types/react": "18.2.34", "@types/react-dom": "18.2.14", + "@types/uuid": "9.0.7", "@typescript-eslint/eslint-plugin": "5.62.0", "eslint": "8.28.0", "eslint-config-htmlacademy": "8.0.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 61d25c6..a5b8d86 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -6,13 +6,20 @@ generator client { datasource db { provider = "postgresql" url = env("POSTGRES_CONNECTION_URL") - extensions = [pgcrypto] + extensions = [pgcrypto, citext] } model User { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid name String + username String @unique @db.Citext + password String email String @unique + role Role @default(USER) + activationLink String @unique + isActivated Boolean @default(false) + session Session? + createdAt DateTime @default(now()) @map(name: "created_at") updatedAt DateTime @updatedAt @map(name: "updated_at") @@map("users") @@ -22,7 +29,24 @@ model Post { id Int @id @default(autoincrement()) title String content String + createdAt DateTime @default(now()) @map(name: "created_at") updatedAt DateTime @updatedAt @map(name: "updated_at") @@map("posts") +} + +model Session { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + user User @relation(fields: [userId], references: [id]) + userId String @unique @db.Uuid + refreshToken String? + + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + @@map("sessions") +} + +enum Role { + USER + ADMIN } \ No newline at end of file diff --git a/public/fonts/inter-bold.woff2 b/public/fonts/inter-bold.woff2 new file mode 100644 index 0000000..934017d Binary files /dev/null and b/public/fonts/inter-bold.woff2 differ diff --git a/public/fonts/inter-medium.woff2 b/public/fonts/inter-medium.woff2 new file mode 100644 index 0000000..3b2507a Binary files /dev/null and b/public/fonts/inter-medium.woff2 differ diff --git a/public/fonts/inter-regular.woff b/public/fonts/inter-regular.woff deleted file mode 100644 index f8662ba..0000000 Binary files a/public/fonts/inter-regular.woff and /dev/null differ diff --git a/public/fonts/inter-regular.woff2 b/public/fonts/inter-regular.woff2 index 14aa335..f7a761d 100644 Binary files a/public/fonts/inter-regular.woff2 and b/public/fonts/inter-regular.woff2 differ diff --git a/public/fonts/inter-semibold.woff2 b/public/fonts/inter-semibold.woff2 new file mode 100644 index 0000000..6664455 Binary files /dev/null and b/public/fonts/inter-semibold.woff2 differ diff --git a/src/components/custom-button/custom-button.module.css b/src/components/custom-button/custom-button.module.css new file mode 100644 index 0000000..32a8b87 --- /dev/null +++ b/src/components/custom-button/custom-button.module.css @@ -0,0 +1,24 @@ +.button { + max-width: max-content; + padding: 5px 15px; + display: inline-block; + background-color: var(--color-special); + border: 2px solid var(--color-bg-gray-darken); + border-radius: var(--border-radius-low); + font-size: 1.2rem; + cursor: pointer; + transition: background-color, transform var(--duration-fast); + color: var(--color-text-dark); +} + +.button:hover { + background-color: var(--color-special-darken); +} + +.button:focus { + outline: 2px solid var(--color-special-darkest); +} + +.button:active { + transform: scale(0.98); +} \ No newline at end of file diff --git a/src/components/custom-button/custom-button.tsx b/src/components/custom-button/custom-button.tsx new file mode 100644 index 0000000..2671948 --- /dev/null +++ b/src/components/custom-button/custom-button.tsx @@ -0,0 +1,18 @@ +import {clsx} from "clsx"; +import React, {ButtonHTMLAttributes} from "react"; +import customButtonStyle from "./custom-button.module.css"; + +type TProps = ButtonHTMLAttributes & { + className?: string, + children?: React.ReactNode, +} + +const CustomButton: React.FC = ({className = ``, children, ...props}) => { + return ( + + ); +}; + +export default CustomButton; diff --git a/src/components/custom-form/custom-form.module.css b/src/components/custom-form/custom-form.module.css new file mode 100644 index 0000000..3634364 --- /dev/null +++ b/src/components/custom-form/custom-form.module.css @@ -0,0 +1,10 @@ +.form { + position: relative; + z-index: inherit; +} + +.fieldset { + width: 100%; + height: 100%; + border: none; +} \ No newline at end of file diff --git a/src/components/custom-form/custom-form.tsx b/src/components/custom-form/custom-form.tsx new file mode 100644 index 0000000..6b2604b --- /dev/null +++ b/src/components/custom-form/custom-form.tsx @@ -0,0 +1,26 @@ +import {clsx} from "clsx"; +import React, {FormHTMLAttributes} from "react"; +import customFormStyle from "./custom-form.module.css"; + +type TProps = FormHTMLAttributes & { + className?: string, + children: React.ReactNode, + disabled?: boolean +} + +const CustomForm: React.ForwardRefRenderFunction = ({ + className = ``, + children, + disabled = false, + ...nativeProps +}, ref) => { + return ( +
+
+ {children} +
+
+ ); +}; + +export default React.forwardRef(CustomForm); diff --git a/src/components/custom-input/custom-input.module.css b/src/components/custom-input/custom-input.module.css new file mode 100644 index 0000000..1c7d9ab --- /dev/null +++ b/src/components/custom-input/custom-input.module.css @@ -0,0 +1,46 @@ +.label { + width: 100%; +} + +.label:hover > .labelText, +.label:focus-within > .labelText { + opacity: 1; +} + +.labelText { + display: inline-block; + margin-left: 1px; + margin-bottom: 3px; + font-size: 1rem; + font-weight: 600; + color: var(--color-text-dark); + opacity: 0.7; + transition: all var(--duration-fast) linear; +} + +.input { + width: 100%; + padding: 7px 15px; + border-radius: var(--border-radius-low); + border: none; + font-size: 1.2rem; + outline: 2px solid var(--color-special); +} + +.input::placeholder { + color: var(--color-text-dark); + opacity: 0.4; +} + +.input:hover::placeholder { + opacity: 0.5; +} + +.input:hover { + outline-color: var(--color-special-darken); +} + +.input:focus { + outline-width: 3px; + outline-color: var(--color-special-darkest); +} \ No newline at end of file diff --git a/src/components/custom-input/custom-input.tsx b/src/components/custom-input/custom-input.tsx new file mode 100644 index 0000000..dd762ea --- /dev/null +++ b/src/components/custom-input/custom-input.tsx @@ -0,0 +1,19 @@ +import {clsx} from "clsx"; +import React, {InputHTMLAttributes} from "react"; +import customInputStyle from "./custom-input.module.css"; + +type TProps = InputHTMLAttributes & { + className?: string, + label?: string, +} + +const CustomInput: React.FC = ({className = ``, label, ...props}) => { + return ( + + ); +}; + +export default CustomInput; diff --git a/src/components/header/header.module.css b/src/components/header/header.module.css index 56c5baf..994b012 100644 --- a/src/components/header/header.module.css +++ b/src/components/header/header.module.css @@ -1,3 +1,15 @@ .header { + display: flex; + flex-direction: row; background: var(--gradient-gray); +} + +.userNav { + margin: auto 0 auto auto; + height: 100%; +} + +.userNavRegLink { + display: inline-block; + color: var(--color-special); } \ No newline at end of file diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx index 7a2fc49..ae7b21e 100644 --- a/src/components/header/header.tsx +++ b/src/components/header/header.tsx @@ -1,5 +1,7 @@ import {clsx} from "clsx"; +import Link from "next/link"; import React from "react"; +import {PageRoute} from "../../const/const"; import {TLink} from "../../const/const.types"; import SiteNav from "../site-nav/site-nav"; import headerStyle from "./header.module.css"; @@ -19,6 +21,12 @@ const Header: React.FC = ({className, content}) => { return (
+ +
+ + Регистрация + +
); }; diff --git a/src/const/const.ts b/src/const/const.ts index 00a7c19..53f85d0 100644 --- a/src/const/const.ts +++ b/src/const/const.ts @@ -6,12 +6,17 @@ export enum PageRoute { TELEGRAM_FEATURES = `/telegram/features`, BLOG = `/blog`, POST = `/post`, + REGISTRATION = `/registration`, } export enum ApiRoute { + USER = `api/user`, + USER_ACTIVATION = `/api/user/activation`, + USER_REGISTRATION = `/api/user/registration`, PUSHER_AUTH = `api/pusher-auth`, TELEGRAM_AUTH = `/api/telegram/auth`, TELEGRAM_SEND_MESSAGE = `/api/telegram/send-message`, + REGISTRATION = `/api/registration`, } export const navLinks: TLink[] = [{ diff --git a/src/const/const.types.ts b/src/const/const.types.ts index 7734797..62e0a73 100644 --- a/src/const/const.types.ts +++ b/src/const/const.types.ts @@ -2,3 +2,5 @@ export type TLink = { name: string, url: string, } + +export type RC = [TError, null] | [null, TResult]; diff --git a/src/controllers/session/session.service.ts b/src/controllers/session/session.service.ts new file mode 100644 index 0000000..882287b --- /dev/null +++ b/src/controllers/session/session.service.ts @@ -0,0 +1,45 @@ +import {ApiError} from "../../service/api/error"; +import {prisma} from "../../service/prisma-client"; +import {generateToken} from "../../service/token"; +import {handlePromise, log} from "../../utils/utils"; + +type TTokens = { + accessToken: string, + refreshToken: string, +} + +const ACCESS_TOKEN_AGE_IN_SEC = 30 * 60; +const REFRESH_TOKEN_AGE_IN_SEC = 30 * 24 * 60 * 60; + +const sessionService = { + generateTokens(data: string | object | Buffer): TTokens { + const accessToken = generateToken(data, ACCESS_TOKEN_AGE_IN_SEC); + const refreshToken = generateToken(data, REFRESH_TOKEN_AGE_IN_SEC); + + return {accessToken, refreshToken}; + }, + + async saveToken(userId: string, refreshToken: string) { + const [sessionError, session] = await handlePromise(prisma.session.upsert({ + where: {userId}, + update: { + refreshToken, + }, + create: { + userId, + refreshToken, + } + })); + + if (sessionError) { + log(`Session Service saveTokens:`, sessionError); + throw ApiError.internalServerError(); + } + + return session; + } +}; + +export { + sessionService, +}; diff --git a/src/controllers/user/user.api.ts b/src/controllers/user/user.api.ts new file mode 100644 index 0000000..c0d1ef9 --- /dev/null +++ b/src/controllers/user/user.api.ts @@ -0,0 +1,25 @@ +import {ApiRoute} from "../../const/const"; +import {RC} from "../../const/const.types"; +import {Api} from "../../service/api/api"; +import {ApiError} from "../../service/api/error"; +import {TApiSuccessData} from "../../service/api/types"; +import {handlePromise} from "../../utils/utils"; +import {TRegistrationData, TUserJSON} from "./user.types"; + +const api = new Api(); + +const userApi = { + async registration(data: TRegistrationData): Promise> { + const [error, result] = await handlePromise, ApiError>(api.post(ApiRoute.USER_REGISTRATION, data)); + + if (error) { + return [error, null]; + } + + return [null, result.data]; + } +}; + +export { + userApi, +}; diff --git a/src/controllers/user/user.controller.ts b/src/controllers/user/user.controller.ts new file mode 100644 index 0000000..2b4da34 --- /dev/null +++ b/src/controllers/user/user.controller.ts @@ -0,0 +1,37 @@ +import {NextApiRequest, NextApiResponse} from "next"; +import {SITE_URL} from "../../../config"; +import {ApiError} from "../../service/api/error"; +import {UserDto} from "./user.dto"; +import {userService} from "./user.service"; +import {userValidation} from "./user.validation"; + +const userController = { + async registration(req: NextApiRequest, res: NextApiResponse) { + const {error, value} = userValidation.registration(JSON.parse(req.body)); + + if (error) { + throw ApiError.badRequest(error.message); + } + + const user = await userService.registration(value); + + res.status(200).json(UserDto.toJSON(user)); + }, + + async activateUser(req: NextApiRequest, res: NextApiResponse) { + const url = `${SITE_URL}${req.url}`; + const {error, value: activationUrl} = userValidation.activationLink(url); + + if (error) { + throw ApiError.badRequest(`Некорректный адрес`, error); + } + + const result = await userService.activateUser(activationUrl); + + res.status(200).json(result); + } +}; + +export { + userController, +}; diff --git a/src/controllers/user/user.dto.ts b/src/controllers/user/user.dto.ts new file mode 100644 index 0000000..adf0583 --- /dev/null +++ b/src/controllers/user/user.dto.ts @@ -0,0 +1,39 @@ +import {Prisma, Role} from "@prisma/client"; +import {TUserJSON} from "./user.types"; + +class UserDto { + id: string; + name: string; + email: string; + role: Role; + createdAt: Date; + updatedAt: Date; + activationLink: string; + isActivated: boolean; + + constructor(data: Prisma.UserGetPayload) { + this.id = data.id; + this.name = data.name; + this.role = data.role; + this.email = data.email; + this.createdAt = data.createdAt; + this.updatedAt = data.updatedAt; + this.activationLink = data.activationLink; + this.isActivated = data.isActivated; + } + + static toJSON(data: Prisma.UserGetPayload): TUserJSON { + return { + id: data.id, + name: data.name, + role: data.role, + email: data.email, + updatedAt: data.updatedAt.toJSON(), + createdAt: data.createdAt.toJSON(), + }; + } +} + +export { + UserDto, +}; diff --git a/src/controllers/user/user.service.ts b/src/controllers/user/user.service.ts new file mode 100644 index 0000000..62eda62 --- /dev/null +++ b/src/controllers/user/user.service.ts @@ -0,0 +1,86 @@ +import bcrypt from "bcrypt"; +import {ApiError} from "../../service/api/error"; +import {prisma} from "../../service/prisma-client"; +import {handlePromise, log} from "../../utils/utils"; +import {TRegistrationData} from "./user.types"; +import {createActivationLink} from "./user.utils"; + +const userService = { + async registration(data: TRegistrationData) { + const [findingError, user] = await handlePromise(prisma.user.findFirst({ + where: { + OR: [ + {email: data.email.toLowerCase()}, + {username: data.username}, + ], + } + })); + + if (findingError) { + log(`User Service Registration`, findingError); + throw ApiError.internalServerError(); + } + + if (user) { + const errorMessage = user.email === data.email.toLowerCase() + ? `Пользователь с таким емейлом уже существует` + : `Пользователь с таким ником уже существует`; + throw ApiError.badRequest(errorMessage); + } + + const {name, email, password, username} = data; + const hashingPassword = await bcrypt.hash(password, 5); + const activationLink = createActivationLink(); + + const [creatingError, newUser] = await handlePromise(prisma.user.create({ + data: { + name, + username, + email: email.toLowerCase(), + password: hashingPassword, + isActivated: false, + activationLink, + } + })); + + if (creatingError) { + log(`User Service Registration`, creatingError); + throw ApiError.internalServerError(); + } + + return newUser; + }, + + async activateUser(activationLink: string) { + const [findingError, user] = await handlePromise(prisma.user.findUnique({ + where: {activationLink}, + })); + + if (findingError) { + throw ApiError.internalServerError(); + } + + if (!user) { + throw ApiError.badRequest(`Ссылка недействительна`); + } + + if (user.isActivated) { + throw ApiError.badRequest(`Аккаунт уже активирован`); + } + + const [updatingError] = await handlePromise(prisma.user.update({ + where: {id: user.id}, + data: {isActivated: true} + })); + + if (!updatingError) { + throw ApiError.internalServerError(); + } + + return true; + } +}; + +export { + userService, +}; diff --git a/src/controllers/user/user.types.ts b/src/controllers/user/user.types.ts new file mode 100644 index 0000000..8c64da0 --- /dev/null +++ b/src/controllers/user/user.types.ts @@ -0,0 +1,17 @@ +import {Role} from "@prisma/client"; + +export type TRegistrationData = { + name: string, + email: string, + password: string, + username: string, +} + +export type TUserJSON = { + id: string, + name: string, + email: string, + role: Role, + createdAt: string, + updatedAt: string, +} diff --git a/src/controllers/user/user.utils.ts b/src/controllers/user/user.utils.ts new file mode 100644 index 0000000..f4f2ac8 --- /dev/null +++ b/src/controllers/user/user.utils.ts @@ -0,0 +1,7 @@ +import * as uuid from "uuid"; +import {SITE_URL} from "../../../config"; +import {ApiRoute} from "../../const/const"; + +export const createActivationLink = () => { + return `${SITE_URL}${ApiRoute.USER_ACTIVATION}?id=${uuid.v4()}`; +}; diff --git a/src/controllers/user/user.validation.ts b/src/controllers/user/user.validation.ts new file mode 100644 index 0000000..4934994 --- /dev/null +++ b/src/controllers/user/user.validation.ts @@ -0,0 +1,31 @@ +import {ValidationOptions} from "joi"; +import joi from "types-joi"; +import {validate} from "../../service/api/validation"; + +const NAME_REGEXP = /^[a-zA-Zа-яА-Я ]+$/; +const USERNAME_REGEXP = /^[a-zA-Z0-9-]+$/; + +const name = joi.string().regex(NAME_REGEXP).min(2).required(); +const password = joi.string().alphanum().min(8).required(); +const email = joi.string().email().required(); +const username = joi.string().min(3).max(24).regex(USERNAME_REGEXP).required(); +const activationLink = joi.string().uri().required(); + +const userValidation = { + registration(data: any, options?: ValidationOptions) { + return validate(joi.object({ + name, + password, + email, + username, + }).required(), data, options); + }, + + activationLink(url: string, options?: ValidationOptions) { + return validate(activationLink, url, options); + } +}; + +export { + userValidation, +}; diff --git a/src/pages/api/activation.api.ts b/src/pages/api/activation.api.ts new file mode 100644 index 0000000..ca4fa52 --- /dev/null +++ b/src/pages/api/activation.api.ts @@ -0,0 +1,10 @@ +import {NextApiRequest, NextApiResponse} from "next"; +import {createRouter} from "next-connect"; +import {userController} from "../../controllers/user/user.controller"; +import {onError} from "../../service/api/middlewares/on-error"; + +const router = createRouter(); + +router.get(userController.activateUser); + +export default router.handler({onError}); diff --git a/src/pages/api/user/registration.api.ts b/src/pages/api/user/registration.api.ts new file mode 100644 index 0000000..702cb11 --- /dev/null +++ b/src/pages/api/user/registration.api.ts @@ -0,0 +1,10 @@ +import {NextApiRequest, NextApiResponse} from "next"; +import {createRouter} from "next-connect"; +import {userController} from "../../../controllers/user/user.controller"; +import {onError} from "../../../service/api/middlewares/on-error"; + +const router = createRouter(); + +router.post(userController.registration); + +export default router.handler({onError}); diff --git a/src/pages/registration.page.tsx b/src/pages/registration.page.tsx new file mode 100644 index 0000000..baf34bb --- /dev/null +++ b/src/pages/registration.page.tsx @@ -0,0 +1,83 @@ +import {clsx} from "clsx"; +import {GetStaticProps} from "next"; +import React, {useCallback} from "react"; +import {SITE_URL} from "../../config"; +import CustomButton from "../components/custom-button/custom-button"; +import CustomForm from "../components/custom-form/custom-form"; +import CustomInput from "../components/custom-input/custom-input"; +import Layout, {TLayoutContent} from "../components/layout/layout"; +import {PageRoute} from "../const/const"; +import {createContent} from "../content/base-content"; +import {userApi} from "../controllers/user/user.api"; +import {TRegistrationData} from "../controllers/user/user.types"; +import registrationPageStyle from "../styles/registration-page.module.css"; + +const TITLE = `Главная`; +const DESCRIPTION = `Главная страница`; +const CANONICAL = `${SITE_URL}${PageRoute.MAIN}`; + +type TProps = { + content: { + layout: TLayoutContent, + }, +} + +const RegistrationForm: React.FC = () => { + const onSubmit = useCallback(async (evt: React.FormEvent) => { + evt.preventDefault(); + const form = evt.target as HTMLFormElement; + + const formData: TRegistrationData = { + name: (form.elements.namedItem(`name`) as HTMLInputElement)?.value || ``, + email: form.email.value, + password: form.password.value, + username: form.username.value, + }; + + const [error, user] = await userApi.registration(formData); + + let resultMessage; + if (error) { + resultMessage = error.message; + } else { + resultMessage = JSON.stringify(user, null, 2); + } + + // eslint-disable-next-line no-alert + alert(resultMessage); + }, []); + + return ( + + + + + + + Регистрация + + ); +}; + +const RegistrationPage: React.FC = ({content}) => { + const {layout} = content; + + return ( + +
+

Страница регистрации

+ +
+
+ ); +}; + +export const getStaticProps: GetStaticProps = async () => { + return { + props: { + content: createContent(true), + } + }; +}; + +export default RegistrationPage; diff --git a/src/service/api/api.ts b/src/service/api/api.ts index c5452af..5b47cb5 100644 --- a/src/service/api/api.ts +++ b/src/service/api/api.ts @@ -6,7 +6,7 @@ import {TApiErrorData, TApiSuccessData} from "./types"; enum ApiMethod { GET = `GET`, - // POST = `POST`, + POST = `POST`, // PATCH = `PATCH`, // PUT = `PUT`, // DELETE = `DELETE`, @@ -90,6 +90,15 @@ class Api { parser: ApiParser.JSON, }); } + + async post, TResult = unknown>(url: string | null = null, body?: TBody, options?: Omit, `body` | `method` | `parser`>) { + return await request(`${this._baseUrl}${url || ``}`, { + ...options, + ...(body ? {body: JSON.stringify(body)} : {}), + method: ApiMethod.POST, + parser: ApiParser.JSON, + }); + } } export { diff --git a/src/service/api/validation.locale.ts b/src/service/api/validation.locale.ts index 5b45877..8b655f9 100644 --- a/src/service/api/validation.locale.ts +++ b/src/service/api/validation.locale.ts @@ -2,114 +2,114 @@ // Original english example https://gist.github.com/arifmahmudrana/b991f1c15162654b3a481780884c0d4d const russianLocaleObject = { "any": { - "unknown": `не разрешен`, - "invalid": `содержит недопустимое значение`, - "empty": `не должен быть пустым`, - "required": `является обязательным`, - "allowOnly": `должен быть одним из {{#valids}}`, - "default": `вызвал ошибку при выполнении метода default` + "unknown": `{{#label}} не разрешен`, + "invalid": `{{#label}} содержит недопустимое значение`, + "empty": `{{#label}} не должен быть пустым`, + "required": `{{#label}} является обязательным`, + "allowOnly": `{{#label}} должен быть одним из {{#valids}}`, + "default": `{{#label}} вызвал ошибку при выполнении метода default` }, "alternatives": { - "base": `не соответствует ни одной из допустимых альтернатив` + "base": `{{#label}} не соответствует ни одной из допустимых альтернатив` }, "array": { - "base": `должен быть массивом`, - "includes": `в позиции {{#pos}} не соответствует ни одному из допустимых типов`, + "base": `{{#label}} должен быть массивом`, + "includes": `{{#label}} в позиции {{#pos}} не соответствует ни одному из допустимых типов`, "includesSingle": `единственное значение "{{#label}}" не соответствует ни одному из допустимых типов`, - "includesOne": `в позиции {{#pos}} не проходит проверку, потому что {{#reason}}`, + "includesOne": `{{#label}} в позиции {{#pos}} не проходит проверку, потому что {{#reason}}`, "includesOneSingle": `единственное значение "{{#label}}" не проходит проверку, потому что {{#reason}}`, - "includesRequiredUnknowns": `не содержит {{#unknownMisses}} требуемых значений`, - "includesRequiredKnowns": `не содержит {{#knownMisses}}`, - "includesRequiredBoth": `не содержит {{#knownMisses}} и {{#unknownMisses}} других требуемых значений`, - "excludes": `в позиции {{#pos}} содержит исключенное значение`, + "includesRequiredUnknowns": `{{#label}} не содержит {{#unknownMisses}} требуемых значений`, + "includesRequiredKnowns": `{{#label}} не содержит {{#knownMisses}}`, + "includesRequiredBoth": `{{#label}} не содержит {{#knownMisses}} и {{#unknownMisses}} других требуемых значений`, + "excludes": `{{#label}} в позиции {{#pos}} содержит исключенное значение`, "excludesSingle": `единственное значение "{{#label}}" содержит исключенное значение`, - "min": `должен содержать не меньше {{#limit}} элементов`, - "max": `должен содержать не больше {{#limit}} элементов`, - "length": `должен содержать {{#limit}} элементов`, - "ordered": `в позиции {{#pos}} не проходит проверку, потому что {{#reason}}`, - "orderedLength": `в позиции {{#pos}} не проходит проверку, потому что массив должен содержать не больше {{#limit}} элементов`, - "sparse": `не должен быть разреженным массивом`, - "unique": `позиция {{#pos}} содержит дублирующееся значение` + "min": `{{#label}} должен содержать не меньше {{#limit}} элементов`, + "max": `{{#label}} должен содержать не больше {{#limit}} элементов`, + "length": `{{#label}} должен содержать {{#limit}} элементов`, + "ordered": `{{#label}} в позиции {{#pos}} не проходит проверку, потому что {{#reason}}`, + "orderedLength": `{{#label}} в позиции {{#pos}} не проходит проверку, потому что массив должен содержать не больше {{#limit}} элементов`, + "sparse": `{{#label}} не должен быть разреженным массивом`, + "unique": `{{#label}} позиция {{#pos}} содержит дублирующееся значение` }, "boolean": { - "base": `должен быть булевым типом` + "base": `{{#label}} должен быть булевым типом` }, "binary": { - "base": `должен быть буфером или строкой`, - "min": `должен быть не меньше {{#limit}} байт`, - "max": `должен быть не больше {{#limit}} байт`, - "length": `должен быть равным {{#limit}} байт` + "base": `{{#label}} должен быть буфером или строкой`, + "min": `{{#label}} должен быть не меньше {{#limit}} байт`, + "max": `{{#label}} должен быть не больше {{#limit}} байт`, + "length": `{{#label}} должен быть равным {{#limit}} байт` }, "date": { - "base": `должен быть числом миллисекунд или валидной датой в строковом формате`, - "min": `должен быть не меньше "{{#limit}}"`, - "max": `должен быть не больше "{{#limit}}"`, - "isoDate": `должен быть валидной датой, соответствующей стандарту ISO 8601`, - "ref": `ссылается на "{{#ref}}", не являющегося датой` + "base": `{{#label}} должен быть числом миллисекунд или валидной датой в строковом формате`, + "min": `{{#label}} должен быть не меньше "{{#limit}}"`, + "max": `{{#label}} должен быть не больше "{{#limit}}"`, + "isoDate": `{{#label}} должен быть валидной датой, соответствующей стандарту ISO 8601`, + "ref": `{{#label}} ссылается на "{{#ref}}", не являющегося датой` }, "function": { - "base": `должен быть функцией` + "base": `{{#label}} должен быть функцией` }, "object": { - "base": `должен быть объектом`, + "base": `{{#label}} должен быть объектом`, "child": `дочерний элемент "{{#label}}" не проходит проверку, потому что {{#reason}}`, - "min": `должен иметь не меньше {{#limit}} дочерних элементов`, - "max": `должен иметь не больше {{#limit}} дочерних элементов`, - "length": `должен иметь {{#limit}} дочерних элементов`, - "allowUnknown": `не разрешен`, - "with": `не найден обязательный сопуствующий ключ "{{#peer}}"`, - "without": `конфликт с запрещённым сопутствующим ключём "{{#peer}}"`, - "missing": `должен содержать не меньше одного из {{#peers}}`, - "xor": `содержит конфликт между взаимоисключающими ключами {{#peers}}`, - "or": `должен содержать не меньше одного из {{#peers}}`, - "and": `содержит {{#present}} без требуемых сопуствующих ключей {{#missing}}`, + "min": `{{#label}} должен иметь не меньше {{#limit}} дочерних элементов`, + "max": `{{#label}} должен иметь не больше {{#limit}} дочерних элементов`, + "length": `{{#label}} должен иметь {{#limit}} дочерних элементов`, + "allowUnknown": `{{#label}} не разрешен`, + "with": `{{#label}} не найден обязательный сопуствующий ключ "{{#peer}}"`, + "without": `{{#label}} конфликт с запрещённым сопутствующим ключём "{{#peer}}"`, + "missing": `{{#label}} должен содержать не меньше одного из {{#peers}}`, + "xor": `{{#label}} содержит конфликт между взаимоисключающими ключами {{#peers}}`, + "or": `{{#label}} должен содержать не меньше одного из {{#peers}}`, + "and": `{{#label}} содержит {{#present}} без требуемых сопуствующих ключей {{#missing}}`, "nand": `"{{#main}}" не должен существовать одновременно с {{#peers}}`, "assert": `"{{#ref}}" не прошел проверку, потому что "{{#ref}}" не {{#message}}`, "rename": { - "multiple": `не может переименовать дочерний элемент "{{#from}}", потому что множественные переименования запрещены и другой ключ уже был переименован в "{{#to}}"`, - "override": `не может переименовать дочерний элемент "{{#from}}", потому что замещение запрещено и цель "{{#to}}" существует` + "multiple": `{{#label}} не может переименовать дочерний элемент "{{#from}}", потому что множественные переименования запрещены и другой ключ уже был переименован в "{{#to}}"`, + "override": `{{#label}} не может переименовать дочерний элемент "{{#from}}", потому что замещение запрещено и цель "{{#to}}" существует` }, "type": `должен быть экземпляром "{{#type}}"` }, "number": { - "base": `должен быть числом`, - "min": `должен быть не меньше {{#limit}}`, - "max": `должен быть не больше {{#limit}}`, - "less": `должен быть меньше {{#limit}}`, - "greater": `должен быть больше {{#limit}}`, - "float": `должен быть числом с плавающей точкой`, - "integer": `должен быть целым числом`, - "negative": `должен быть отрицательным числом`, - "positive": `должен быть положительным числом`, - "precision": `должен иметь не больше {{#limit}} цифр после запятой`, - "ref": `ссылается на "{{#ref}}", не являющегося числом`, - "multiple": `должен быть кратным {{#multiple}}` + "base": `{{#label}} должен быть числом`, + "min": `{{#label}} должен быть не меньше {{#limit}}`, + "max": `{{#label}} должен быть не больше {{#limit}}`, + "less": `{{#label}} должен быть меньше {{#limit}}`, + "greater": `{{#label}} должен быть больше {{#limit}}`, + "float": `{{#label}} должен быть числом с плавающей точкой`, + "integer": `{{#label}} должен быть целым числом`, + "negative": `{{#label}} должен быть отрицательным числом`, + "positive": `{{#label}} должен быть положительным числом`, + "precision": `{{#label}} должен иметь не больше {{#limit}} цифр после запятой`, + "ref": `{{#label}} ссылается на "{{#ref}}", не являющегося числом`, + "multiple": `{{#label}} должен быть кратным {{#multiple}}` }, "string": { - "base": `должен быть строкой`, - "min": `длина должна быть не меньше {{#limit}} символов`, - "max": `длина должна быть не больше {{#limit}} символов`, - "length": `длина должна быть равной {{#limit}}`, - "alphanum": `должен содержать только буквенно-цифровые символы`, - "token": `должен содержать только буквенно-цифровые символы и нижнее подчеркивание`, + "base": `{{#label}} должен быть строкой`, + "min": `{{#label}} длина должна быть не меньше {{#limit}} символов`, + "max": `{{#label}}длина должна быть не больше {{#limit}} символов`, + "length": `{{#label}} длина должна быть равной {{#limit}}`, + "alphanum": `{{#label}} должен содержать только буквенно-цифровые символы`, + "token": `{{#label}} должен содержать только буквенно-цифровые символы и нижнее подчеркивание`, "regex": { - "base": `со значением "{{#value}}" не соответствует требуемому паттерну: {{#pattern}}`, - "name": `со значением "{{#value}}" не соответствует паттерну {{#name}}` + "base": `{{#label}} со значением "{{#value}}" не соответствует требуемому паттерну: {{#pattern}}`, + "name": `{{#label}} со значением "{{#value}}" не соответствует паттерну {{#name}}` }, - "email": `должен быть валидным email-адресом`, - "uri": `должен быть валидным uri`, - "uriCustomScheme": `должен быть валидным uri со схемой, соответствующей паттерну {{#scheme}}`, - "isoDate": `должен соответствовать формату даты ISO 8601`, - "guid": `должен быть валидным GUID`, - "hex": `должен содержать только шестнадцатеричные символы`, - "hostname": `должен быть валидным именем хоста (hostname)`, - "lowercase": `должен содержать только строчные символы`, - "uppercase": `должен содержать только заглавные символы`, - "trim": `не должен содержать пробельные символы в начале или конце строки`, - "creditCard": `должен быть кредитной картой`, - "ref": `ссылается на "{{#ref}}", не являющегося числом`, - "ip": `должен быть валидным ip-адресом с {{#cidr}} CIDR`, - "ipVersion": `должен быть валидным ip-адресом одной из следующих версий {{#version}} с {{#cidr}} CIDR` + "email": `{{#label}} должен быть валидным email-адресом`, + "uri": `{{#label}} должен быть валидным uri`, + "uriCustomScheme": `{{#label}} должен быть валидным uri со схемой, соответствующей паттерну {{#scheme}}`, + "isoDate": `{{#label}} должен соответствовать формату даты ISO 8601`, + "guid": `{{#label}} должен быть валидным GUID`, + "hex": `{{#label}} должен содержать только шестнадцатеричные символы`, + "hostname": `{{#label}} должен быть валидным именем хоста (hostname)`, + "lowercase": `{{#label}} должен содержать только строчные символы`, + "uppercase": `{{#label}} должен содержать только заглавные символы`, + "trim": `{{#label}} не должен содержать пробельные символы в начале или конце строки`, + "creditCard": `{{#label}} должен быть кредитной картой`, + "ref": `{{#label}} ссылается на "{{#ref}}", не являющегося числом`, + "ip": `{{#label}} должен быть валидным ip-адресом с {{#cidr}} CIDR`, + "ipVersion": `{{#label}} должен быть валидным ip-адресом одной из следующих версий {{#version}} с {{#cidr}} CIDR` } }; diff --git a/src/service/cookie.ts b/src/service/cookie.ts new file mode 100644 index 0000000..35c488b --- /dev/null +++ b/src/service/cookie.ts @@ -0,0 +1,41 @@ +import {IncomingMessage, ServerResponse} from "http"; +import {deleteCookie, getCookie, setCookie} from "cookies-next"; +import {OptionsType} from "cookies-next/lib/types"; + +type FrontOptions = Omit +type ServerOptionsGet = OptionsType & { + req: IncomingMessage & { + cookies?: {[key: string]: string} | Partial<{[key: string]: string}> + }}; +type ServerOptionsSet = ServerOptionsGet & { + res: ServerResponse; +} + +const CookieFront = { + get: (key: string, options?: FrontOptions) => { + return getCookie(key, options); + }, + set: (key: string, data: any, options?: FrontOptions) => { + return setCookie(key, data, options); + }, + delete: (key: string, options?: FrontOptions) => { + return deleteCookie(key, options); + }, +}; + +const CookieServer = { + get: (key: string, options: ServerOptionsGet) => { + return getCookie(key, options); + }, + set: (key: string, data: any, options: ServerOptionsSet) => { + return setCookie(key, data, options); + }, + delete: (key: string, options: ServerOptionsSet) => { + return deleteCookie(key, options); + } +}; + +export { + CookieFront, + CookieServer, +}; diff --git a/src/service/mail.ts b/src/service/mail.ts new file mode 100644 index 0000000..2fc4a35 --- /dev/null +++ b/src/service/mail.ts @@ -0,0 +1,42 @@ +import nodemailer from "nodemailer"; +import {SMTP_HOST, SMTP_PORT, SMTP_LOGIN, SMTP_PASSWORD} from "../../config"; + +const FROM = `Pet Project <${SMTP_LOGIN}>`; + +type TSendMailOptions = { + to: string, + title: string, + text?: string, + html?: string, +} + +const createTransport = () => { + return nodemailer.createTransport({ + host: SMTP_HOST, + port: SMTP_PORT, + secure: true, + auth: { + user: SMTP_LOGIN, + pass: SMTP_PASSWORD, + } + }); +}; + +const mailService = { + async sendMail({to, html, title, text}: TSendMailOptions) { + const transport = createTransport(); + + return await transport.sendMail({ + from: FROM, + to, + subject: title, + text: text || ``, + html: html || ``, + attachDataUrls: true, + }); + } +}; + +export { + mailService, +}; diff --git a/src/service/token.ts b/src/service/token.ts new file mode 100644 index 0000000..f8418e3 --- /dev/null +++ b/src/service/token.ts @@ -0,0 +1,23 @@ +import jwt, {JwtPayload} from "jsonwebtoken"; +import {JWT_ACCESS_KEY} from "../../config"; + +if (!JWT_ACCESS_KEY) { + throw new Error(`Token service: missed JWT_ACCESS_KEY`); +} + +const generateToken = (data: string | object | Buffer, expiresInSeconds: number): string => { + return jwt.sign(data, JWT_ACCESS_KEY as string, {expiresIn: expiresInSeconds}); +}; + +const validateToken = (token: string): string | JwtPayload | null => { + try { + return jwt.verify(token, JWT_ACCESS_KEY as string); + } catch (err) { + return null; + } +}; + +export { + generateToken, + validateToken, +}; diff --git a/src/styles/fonts.css b/src/styles/fonts.css index 571a048..2f4de8f 100644 --- a/src/styles/fonts.css +++ b/src/styles/fonts.css @@ -1,8 +1,31 @@ @font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url("/fonts/inter-regular.woff2") format("woff2"), - url("/fonts/inter-regular.woff") format("woff"); + font-family: "Inter"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/fonts/inter-regular.woff2") format("woff2"); +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("/fonts/inter-medium.woff2") format("woff2"); +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url("/fonts/inter-semibold.woff2") format("woff2"); +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url("/fonts/inter-bold.woff2") format("woff2"); } \ No newline at end of file diff --git a/src/styles/globals.css b/src/styles/globals.css index 34330f3..3643661 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -45,6 +45,10 @@ a { margin-bottom: var(--margin-block-middle); } +.mrgb-low { + margin-bottom: var(--margin-block-low); +} + h1 { font-size: var(--font-size-h1); } diff --git a/src/styles/registration-page.module.css b/src/styles/registration-page.module.css new file mode 100644 index 0000000..95ab178 --- /dev/null +++ b/src/styles/registration-page.module.css @@ -0,0 +1,7 @@ +.registerForm > fieldset { + padding: 20px; + border: 2px solid var(--color-special); + border-radius: var(--border-radius-low); + display: grid; + grid-gap: 10px; +} \ No newline at end of file diff --git a/src/styles/variables.css b/src/styles/variables.css index beb9e3d..8f8b675 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -6,8 +6,8 @@ --color-bg-gray-darken: rgba(166, 154, 154, 0.81); --color-bg-main: #f3e0e0; --color-special: #ff9927; - --color-special-darken: #ef9025; - --color-special-darkest: #e58821; + --color-special-darken: #fd891b; + --color-special-darkest: #f18009; --color-text-light: #ffffff; --color-text-dark: #000000; --color-text-gray-dark: #3b3535; @@ -17,6 +17,7 @@ --box-shadow-dark-blue: 0px 0px 10px 0px rgba(11, 45, 71, 0.35); /* border-radius */ + --border-radius-low: 5px; --border-radius-middle: 10px; /* gradients */ @@ -27,6 +28,7 @@ --padding-container-tablet: 0 30px; --padding-container-mobile: 0 20px; --margin-block-middle: 40px; + --margin-block-low: 20px; /* page size */ --page-size-max: 1920px; @@ -35,7 +37,7 @@ --font-main: "Inter", Arial, Helvetica, sans-serif; /* font size */ - --font-size-main: 16px; + --font-size-main: 14px; --font-size-h1: 2rem; --font-size-h2: 1.5rem; --font-size-h3: 1.17rem;