diff --git a/.env b/.env index 66b2fe7..7129205 100644 --- a/.env +++ b/.env @@ -7,8 +7,9 @@ NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58 NODE_ENV=development -# mysql +# mysql. ONLY THIS ENV is respected when generating/applying migrations in prisma DATABASE=mysql://cart:cartpw@localhost:3306/cart +# DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev # // owner: dobromir.popov@gmail.com | Специално Свидетелстване София # // https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716 diff --git a/.env.development.popov b/.env.development.popov new file mode 100644 index 0000000..b6855cf --- /dev/null +++ b/.env.development.popov @@ -0,0 +1,20 @@ +NODE_TLS_REJECT_UNAUTHORIZED=0 +# NODE_EXTRA_CA_CERTS=C:\\Users\\popov\\AppData\\Local\\mkcert +NODE_ENV=development + +PROTOCOL=http +PORT=3003 +HOST=cart.d-popov.com +NEXT_PUBLIC_PUBLIC_URL=https://cart.d-popov.com +DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev +# DATABASE=mysql://cart:cartpw@localhost:3306/cart + + +EMAIL_SENDER='"ССОМ [ТЕСТ] " ' +# MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io +# MAILTRAP_HOST=sandbox.smtp.mailtrap.io +# MAILTRAP_USER=8ec69527ff2104 +# MAILTRAP_PASS=c7bc05f171c96c + +# SSL_KEY=./certificates/localhost-key.pem +# SSL_CERT=./certificates/localhost.pem diff --git a/.vscode/launch.json b/.vscode/launch.json index a2cc8ff..b0e8405 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -45,7 +45,10 @@ "request": "launch", "type": "node-terminal", "cwd": "${workspaceFolder}", - "command": "conda activate node && npm run debug", + "command": "conda activate node && npm run debug-env", + "env": { + "APP_ENV": "development.popov" + } }, { "name": "Run conda npm TEST", diff --git a/.vscode/settings.json b/.vscode/settings.json index e0ac1ae..14e7153 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -138,5 +138,11 @@ }, "[handlebars]": { "editor.defaultFormatter": "vscode.html-language-features" - } + }, + "i18n-ally.localesPaths": [ + "content/i18n", + "components/x-date-pickers/locales" + ], + "i18n-ally.keystyle": "nested", + "i18n-ally.sourceLanguage": "bg" } \ No newline at end of file diff --git a/_doc/ToDo.md b/_doc/ToDo.md index b5f5b1f..d111925 100644 --- a/_doc/ToDo.md +++ b/_doc/ToDo.md @@ -232,4 +232,9 @@ in schedule admin - if a publisher is always pair & family is not in the shift - [x] OK заместник, предпочитание в миналото - да не може. [] Да иска потвърждение преди да заместиш някой [] да не се виждат непубликуваните смени в Моите смени -[] да не се виждат старите предпочитания ако не си админ в публишерс \ No newline at end of file +[] да не се виждат старите предпочитания ако не си админ в публишерс + +[] import avalabilities only if no availabilities are set for the month! +[] new page to show EventLog (substitutions) +[] fix "login as" +[] list with open shift replacements (coverMe requests) diff --git a/components/languageSwitcher.tsx b/components/languageSwitcher.tsx new file mode 100644 index 0000000..b8cce90 --- /dev/null +++ b/components/languageSwitcher.tsx @@ -0,0 +1,55 @@ +import { useState } from 'react'; +import { useRouter } from 'next/router'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import IconButton from '@mui/material/IconButton'; +import TranslateIcon from '@mui/icons-material/Translate'; +import { useTranslations } from 'next-intl'; + +// using https://next-intl-docs.vercel.app/docs/getting-started/pages-router +const LanguageSwitcher = () => { + const t = useTranslations('common'); + const router = useRouter(); + const { locale, locales, asPath } = router; + + const [anchorEl, setAnchorEl] = useState(null); + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const changeLanguage = (lng) => { + router.push(asPath, asPath, { locale: lng }); + handleClose(); + }; + + return ( +
+ + + + + {locales?.map((lng) => { + if (lng === locale) return null; + return ( + changeLanguage(lng)}> + {t('changeTo')} {t(lng.toUpperCase())} + + ); + })} + +
+ ); +}; + +export default LanguageSwitcher; diff --git a/components/layout.tsx b/components/layout.tsx index b1c29fe..7c5639b 100644 --- a/components/layout.tsx +++ b/components/layout.tsx @@ -68,4 +68,5 @@ export default function Layout({ children }) { ); -} \ No newline at end of file +} + diff --git a/components/protectedRoute.tsx b/components/protectedRoute.tsx index 4c2adc9..5b044a3 100644 --- a/components/protectedRoute.tsx +++ b/components/protectedRoute.tsx @@ -10,20 +10,20 @@ interface ProtectedRouteProps { allowedRoles: UserRole[]; deniedMessage?: string; bypass?: boolean; + autoRedirect?: boolean; } -const ProtectedRoute = ({ children, allowedRoles, deniedMessage, bypass = false }: ProtectedRouteProps) => { +const ProtectedRoute = ({ children, allowedRoles, deniedMessage, bypass = false, autoRedirect = false }: ProtectedRouteProps) => { const { data: session, status } = useSession() const router = useRouter(); useEffect(() => { - console.log("session.role:" + session?.user?.role); + //console.log("session.role:" + session?.user?.role); if (!status || status === "unauthenticated") { // Redirect to the sign-in page - if (!bypass) { + if (autoRedirect) { signIn(); } - return null; } else { console.log("session.role:" + session?.user?.role); @@ -41,14 +41,27 @@ const ProtectedRoute = ({ children, allowedRoles, deniedMessage, bypass = false if (deniedMessage !== undefined) { return
{deniedMessage}
; } - return
Нямате достъп до тази страница. Ако мислите, че това е грешка, моля, свържете се с администраторите
; + return ( + <> +
+
+

{session?.user?.email},

+

{`Нямате достъп до тази страница. Ако мислите, че това е грешка, моля, свържете се с администраторите`}

+
+
+ ); + } if (status === "loading") { return
Зареждане...
; } - if (!session) return Защитено съдържание. Впишете се.. - return children; + if (!session) { + if (deniedMessage !== undefined) { + return
{deniedMessage}
; + } + return Защитено съдържание. Впишете се.. + } }; export default ProtectedRoute; diff --git a/components/publisher/PublisherForm.js b/components/publisher/PublisherForm.js index 68c2181..1635270 100644 --- a/components/publisher/PublisherForm.js +++ b/components/publisher/PublisherForm.js @@ -1,5 +1,5 @@ // import axios from "axios"; -import React, { useEffect, useState } from "react"; +import React, { use, useEffect, useState } from "react"; import toast from "react-hot-toast"; import { useRouter } from "next/router"; import Link from "next/link"; @@ -15,6 +15,7 @@ import AvailabilityList from "../availability/AvailabilityList"; import ShiftsList from "../publisher/ShiftsList.tsx"; import ConfirmationModal from "../ConfirmationModal"; import { UserRole } from "@prisma/client"; +import { useSession } from "next-auth/react" // import { Tabs, List } from 'tw-elements' @@ -56,15 +57,14 @@ Array.prototype.groupBy = function (prop) { } export default function PublisherForm({ item, me }) { - - const router = useRouter(); - console.log("init PublisherForm: "); + const { data: session } = useSession() + const urls = { apiUrl: "/api/data/publishers/", - indexUrl: "/cart/publishers" + indexUrl: session?.user?.role == UserRole.ADMIN ? "/cart/publishers" : "/dash" } - + console.log("urls.indexUrl: " + urls.indexUrl); const [helpers, setHelper] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); @@ -202,10 +202,13 @@ export default function PublisherForm({ item, me }) {
-
-
+
- + +
+ +
+
@@ -263,10 +266,8 @@ export default function PublisherForm({ item, me }) { {/* ADMINISTRATORS ONLY */} - - -
+
-
{/* ---------------------------- Actions --------------------------------- */} diff --git a/components/sidebar.tsx b/components/sidebar.tsx index acd412c..4ae481e 100644 --- a/components/sidebar.tsx +++ b/components/sidebar.tsx @@ -1,14 +1,26 @@ import { signIn, signOut, useSession } from "next-auth/react"; import styles from "../styles/header.module.css"; -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef, use } from "react"; import { useRouter } from 'next/router'; import sidemenu, { footerMenu } from './sidemenuData.js'; // Move sidemenu data to a separate file import axiosInstance from "src/axiosSecure"; import common from "src/helpers/common"; +import LanguageSwitcher from "./languageSwitcher"; +import { useTranslations } from 'next-intl'; +import { getTranslations } from 'next-intl/server'; +import ProtectedPage from "pages/examples/protected"; +import ProtectedRoute from "./protectedRoute"; +import { UserRole } from "@prisma/client"; //get package version from package.json const packageVersion = require('../package.json').version; function SidebarMenuItem({ item, session, isSubmenu }) { + // const tMenu = useTranslations('menu'); + // const [t, locale] = useState(useTranslations('menu')); + // useEffect(() => { + // console.log("SidebarMenuItem locale: ", locale); + // locale(useTranslations('common')); + // }, [locale]); const router = useRouter(); const isActive = router.pathname.includes(item.url); @@ -91,25 +103,29 @@ export default function Sidebar({ isSidebarOpen, toggleSidebar }) { const { data: session, status } = useSession(); const sidebarWidth = 226; // Simplify by using a constant const sidebarRef = useRef(null); + const t = useTranslations('menu'); //const [locations, setLocations] = useState([]); useEffect(() => { const fetchLocations = async () => { try { - const response = await axiosInstance.get('/api/data/locations'); // Adjust the API endpoint as needed - const locationsData = response.data - .filter(location => location.isActive === true) - .map(location => ({ - text: location.name, - url: `/cart/locations/${location.id}`, - })); - // Find the "Locations" menu item and populate its children with locationsData - const menuIndex = sidemenu.findIndex(item => item.id === "locations"); - if (menuIndex !== -1) { - sidemenu[menuIndex].children = locationsData; + if (session) { // Don't fetch locations if the user is not authenticated + + const response = await axiosInstance.get('/api/data/locations'); // Adjust the API endpoint as needed + const locationsData = response.data + .filter(location => location.isActive === true) + .map(location => ({ + text: location.name, + url: `/cart/locations/${location.id}`, + })); + // Find the "Locations" menu item and populate its children with locationsData + const menuIndex = sidemenu.findIndex(item => item.id === "locations"); + if (menuIndex !== -1) { + sidemenu[menuIndex].children = locationsData; + } + //setLocations(locationsData); // Optional, if you need to use locations elsewhere } - //setLocations(locationsData); // Optional, if you need to use locations elsewhere } catch (error) { console.error("Error fetching locations:", error); } @@ -136,6 +152,7 @@ export default function Sidebar({ isSidebarOpen, toggleSidebar }) {
@@ -162,14 +182,17 @@ function UserSection({ session }) { } function SignInButton() { + // const t = useTranslations('common'); return (
signIn()}> - + + {/* */}
); } function UserDetails({ session }) { + // const t = useTranslations('common'); return ( <>
@@ -180,7 +203,10 @@ function UserDetails({ session }) {

{session.user.name}

{session.user.role}

- { e.preventDefault(); signOut(); }}>Изход + { e.preventDefault(); signOut(); }}> + {/* {t('logout')} */} + изход +
@@ -196,13 +222,24 @@ function FooterSection() { return ( footerMenu.map((item, index) => ( -
navigateTo(item.url)} - > - {item.text} -
+ item.roles ? ( + +
navigateTo(item.url)} + > + {item.text} +
+
+ ) : ( +
navigateTo(item.url)} + > + {item.text} +
+ ) )) ); } diff --git a/components/sidemenuData.js b/components/sidemenuData.js index 56e9119..e884535 100644 --- a/components/sidemenuData.js +++ b/components/sidemenuData.js @@ -1,6 +1,7 @@ import { UserRole } from "@prisma/client"; + const sidemenu = [ { id: "dashboard", @@ -112,6 +113,17 @@ const sidemenu = [ text: "Статистика", url: "/cart/publishers/stats", roles: [UserRole.ADMIN, UserRole.POWERUSER], + }, { + id: "coverMeLogs", + text: "Замествания", + url: "/cart/reports/coverMe", + roles: [UserRole.ADMIN, UserRole.POWERUSER], + }, + { + id: "translations", + text: "Преводи", + url: "/cart/translations", + roles: [UserRole.ADMIN, UserRole.POWERUSER], }, ] diff --git a/content/i18n/bg.json b/content/i18n/bg.json new file mode 100644 index 0000000..3a58787 --- /dev/null +++ b/content/i18n/bg.json @@ -0,0 +1,36 @@ +{ + "common": { + "appNameLong": "Специално свидетелстване на обществени места в София", + "contacts": "Контакти", + "greeting": "Здравей", + "farewell": "Довиждане", + "changeTo": "", + "BG": "Български", + "EN": "Английски", + "RU": "Руски", + "login": "Вход", + "logout": "Изход" + }, + "menu": { + "dashboard": "Възможности", + "shedule": "График", + "myshedule": "Моя График", + "locations": "Местоположения", + "cart-report": "Отчет", + "cart-experience": "Случки", + "guidelines": "Напътствия", + "permits": "Разрешителни", + "contactAll": "Участници", + "feedback": "Отзиви", + "contactUs": "За връзка", + "admin": "Админ", + "cart-places": "Места", + "cart-publishers": "Вестители", + "cart-events": "План", + "cart-calendar": "Календар", + "cart-reports": "Отчети", + "statistics": "Статистика", + "coverMeLogs": "Замествания", + "translations": "Преводи" + } +} \ No newline at end of file diff --git a/content/i18n/bg.modified.json b/content/i18n/bg.modified.json new file mode 100644 index 0000000..26788cf --- /dev/null +++ b/content/i18n/bg.modified.json @@ -0,0 +1,36 @@ +{ + "common": { + "appNameLong": "Специално свидетелстване на обществени места в София", + "contacts": "Контакти", + "greeting": "Здравей", + "farewell": "Довиждане", + "changeTo": "", + "BG": "български", + "EN": "английски", + "RU": "руски", + "login": "Вход", + "logout": "Изход" + }, + "menu": { + "dashboard": "Възможности", + "shedule": "График", + "myshedule": "Моя График", + "locations": "Местоположения", + "cart-report": "Отчет", + "cart-experience": "Случки", + "guidelines": "Напътствия", + "permits": "Разрешителни", + "contactAll": "Участници", + "feedback": "Отзиви", + "contactUs": "За връзка", + "admin": "Админ", + "cart-places": "Места", + "cart-publishers": "Вестители", + "cart-events": "План", + "cart-calendar": "Календар", + "cart-reports": "Отчети", + "statistics": "Статистика", + "coverMeLogs": "Замествания", + "translations": "Преводи" + } +} \ No newline at end of file diff --git a/content/i18n/en.json b/content/i18n/en.json new file mode 100644 index 0000000..9ca0d0e --- /dev/null +++ b/content/i18n/en.json @@ -0,0 +1,15 @@ +{ + "common": { + "greeting": "Hello", + "farewell": "Goodbye", + "changeTo": "Change to", + "BG": "Bulgarian", + "EN": "English", + "RU": "Russian", + "login": "login", + "logout": "logout" + }, + "menu": { + "dashboard": "Dashboard" + } +} \ No newline at end of file diff --git a/content/i18n/ru.json b/content/i18n/ru.json new file mode 100644 index 0000000..bae0120 --- /dev/null +++ b/content/i18n/ru.json @@ -0,0 +1,15 @@ +{ + "common": { + "greeting": "Здравей", + "farewell": "Довиждане", + "changeTo": "Смени на", + "BG": "[RU] Български", + "EN": "[RU] Английски", + "RU": "[RU] Руски", + "login": "вход", + "contacts": "Контакти" + }, + "menu": { + "dashboard": "Начало" + } +} \ No newline at end of file diff --git a/content/i18n/ru.modified.json b/content/i18n/ru.modified.json new file mode 100644 index 0000000..3500f51 --- /dev/null +++ b/content/i18n/ru.modified.json @@ -0,0 +1,15 @@ +{ + "common": { + "greeting": "Здравей", + "farewell": "Довиждане", + "changeTo": "Смени на", + "BG": "болгарский", + "EN": "английский", + "RU": "русский", + "login": "вход", + "contacts": "Контакти te" + }, + "menu": { + "dashboard": "Начало" + } +} \ No newline at end of file diff --git a/next.config.js b/next.config.js index 2406980..bc25166 100644 --- a/next.config.js +++ b/next.config.js @@ -50,4 +50,12 @@ module.exports = withPWA({ return config; }, + i18n: { + // next-intl + // https://next-intl-docs.vercel.app/docs/usage/messages + // using https://next-intl-docs.vercel.app/docs/getting-started/pages-router + locales: ['bg', 'en', 'ru'], + defaultLocale: 'bg', + localeDetection: false, + }, }) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 61a5430..e6292bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,8 @@ "autoprefixer": "^10.4.17", "axios": "^1.6.7", "axios-jwt": "^4.0.2", + "bcrypt": "^5.1.1", + "bcryptjs": "^2.4.3", "date-fns": "^3.3.1", "docx": "^8.5.0", "docx-templates": "^4.11.4", @@ -55,6 +57,7 @@ "next": "^14.1.0", "next-auth": "^4.24.6", "next-connect": "^1.0.0", + "next-intl": "^3.12.0", "next-pwa": "^5.6.0", "node-excel-export": "^1.4.4", "node-telegram-bot-api": "^0.64.0", @@ -2194,6 +2197,92 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz", + "integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==", + "dependencies": { + "@formatjs/intl-localematcher": "0.5.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz", + "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz", + "integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz", + "integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.11.4", + "@formatjs/icu-skeleton-parser": "1.3.6", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser/node_modules/@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "dependencies": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser/node_modules/@formatjs/intl-localematcher": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", + "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz", + "integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser/node_modules/@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "dependencies": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser/node_modules/@formatjs/intl-localematcher": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", + "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.2.32", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz", + "integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@heroicons/react": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.1.tgz", @@ -2774,6 +2863,69 @@ "@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/@mapbox/node-pre-gyp/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/@mapbox/node-pre-gyp/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@mapbox/node-pre-gyp/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/@mapbox/node-pre-gyp/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/@mui/base": { "version": "5.0.0-beta.36", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.36.tgz", @@ -4903,6 +5055,11 @@ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" }, + "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/archiver": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", @@ -4967,6 +5124,18 @@ "safe-buffer": "~5.1.0" } }, + "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/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -5309,6 +5478,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/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -5317,6 +5499,11 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -5754,6 +5941,14 @@ "fsevents": "~2.3.2" } }, + "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/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -5981,6 +6176,14 @@ "simple-swizzle": "^0.2.2" } }, + "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/color/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -6110,6 +6313,11 @@ "safe-buffer": "~5.1.0" } }, + "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/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -6504,6 +6712,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/depcheck": { "version": "1.4.7", "resolved": "https://registry.npmjs.org/depcheck/-/depcheck-1.4.7.tgz", @@ -7933,6 +8146,28 @@ "node": ">=10" } }, + "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", @@ -8025,6 +8260,25 @@ "resolved": "https://registry.npmjs.org/gapi-script/-/gapi-script-1.2.0.tgz", "integrity": "sha512-NKTVKiIwFdkO1j1EzcrWu/Pz7gsl1GmBmgh+qhuV2Ytls04W/Eg5aiBL91SCiBM9lU0PMu7p1hTVxhh1rPT5Lw==" }, + "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/gaxios": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.3.0.tgz", @@ -8503,6 +8757,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/hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -8947,6 +9206,34 @@ "node": ">= 0.4" } }, + "node_modules/intl-messageformat": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz", + "integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.11.4", + "@formatjs/fast-memoize": "1.2.1", + "@formatjs/icu-messageformat-parser": "2.1.0", + "tslib": "^2.1.0" + } + }, + "node_modules/intl-messageformat/node_modules/@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "dependencies": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, + "node_modules/intl-messageformat/node_modules/@formatjs/intl-localematcher": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", + "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -10428,6 +10715,37 @@ "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": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -10681,6 +10999,26 @@ "node": ">=16" } }, + "node_modules/next-intl": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.12.0.tgz", + "integrity": "sha512-N3DHT6ce6K4VHVA3y2p3U7wfBx4c31qEgQSTFCFJuNnE7XYzy49O576ewEz7/k2YaB/U5bfxaWWaMMkskofwoQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "dependencies": { + "@formatjs/intl-localematcher": "^0.2.32", + "negotiator": "^0.6.3", + "use-intl": "^3.12.0" + }, + "peerDependencies": { + "next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/next-pwa": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/next-pwa/-/next-pwa-5.6.0.tgz", @@ -10754,6 +11092,11 @@ "node": "^10 || ^12 || >=14" } }, + "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-excel-export": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/node-excel-export/-/node-excel-export-1.4.4.tgz", @@ -10954,6 +11297,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "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-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -13649,6 +14006,17 @@ "inBundle": true, "license": "ISC" }, + "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/oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", @@ -15620,6 +15988,11 @@ "node": ">= 0.8.0" } }, + "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.2.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", @@ -15738,8 +16111,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==", - "optional": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/simple-swizzle": { "version": "0.2.2", @@ -16523,6 +16895,22 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "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/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -16538,6 +16926,17 @@ "node": ">=6" } }, + "node_modules/tar/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/temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", @@ -17428,6 +17827,18 @@ "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" }, + "node_modules/use-intl": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.12.0.tgz", + "integrity": "sha512-tTJBSaxpVF1ZHqJ5+1JOaBsPmvBPscXHR0soMNQFWIURZspOueLaMXx1XHNdBv9KZGwepBju5aWXkJ0PM6ztXg==", + "dependencies": { + "@formatjs/ecma402-abstract": "^1.11.4", + "intl-messageformat": "^9.3.18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -17771,6 +18182,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/winston": { "version": "3.13.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.13.0.tgz", diff --git a/package.json b/package.json index 6ff1a1e..89803af 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "autoprefixer": "^10.4.17", "axios": "^1.6.7", "axios-jwt": "^4.0.2", + "bcrypt": "^5.1.1", "date-fns": "^3.3.1", "docx": "^8.5.0", "docx-templates": "^4.11.4", @@ -72,6 +73,7 @@ "next": "^14.1.0", "next-auth": "^4.24.6", "next-connect": "^1.0.0", + "next-intl": "^3.12.0", "next-pwa": "^5.6.0", "node-excel-export": "^1.4.4", "node-telegram-bot-api": "^0.64.0", @@ -114,4 +116,4 @@ "depcheck": "^1.4.7", "prisma": "^5.13.0" } -} +} \ No newline at end of file diff --git a/pages/_app.tsx b/pages/_app.tsx index 51e37c1..f6150ad 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -4,9 +4,14 @@ import "../styles/styles.css" import "../styles/global.css" import "tailwindcss/tailwind.css" -import type { AppProps } from "next/app"; +import App, { AppContext, AppProps } from 'next/app'; import type { Session } from "next-auth"; -import { useEffect } from "react" +import { useEffect, useState } from "react" +import { useRouter } from "next/router"; +import { NextIntlClientProvider } from 'next-intl'; +import { getServerSession } from "next-auth/next"; + + // for fontawesome import Head from 'next/head'; import { LocalizationProvider } from '@mui/x-date-pickers'; @@ -22,10 +27,25 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' // } -export default function App({ - Component, - pageProps: { session, ...pageProps }, -}: AppProps<{ session: Session }>) { +//function SmwsApp({ Component, pageProps: { locale, messages, session, ...pageProps }, }: AppProps<{ session: Session }>) { +function SmwsApp({ Component, pageProps, session, locale, messages }) { + // dynamic locale loading using our API endpoint + // const [locale, setLocale] = useState(router.locale || 'bg'); + // const [messages, setMessages] = useState({}); + // useEffect(() => { + // async function loadLocaleData() { + // const res = await fetch(`/api/translations/${locale}`); + // if (res.ok) { + // const localeMessages = await res.json(); + // console.log("Loaded messages for locale:", locale, localeMessages); + // setMessages(localeMessages); + // } else { + // const localeMessages = await import(`../content/i18n/${locale}.json`); setMessages(localeMessages.default); + // } + // console.log("locale set to'", locale, "' ",); + // } + // loadLocaleData(); + // }, [locale]); useEffect(() => { const use = async () => { @@ -67,11 +87,40 @@ export default function App({ return ( <> - - - - - + + + + + + + ) } + +async function loadLocaleData(locale) { + try { + const messages = await import(`../content/i18n/${locale}.json`); + return messages.default; + } catch (e) { + console.warn("Could not load locale data for:", locale); + return {}; + } +} + +SmwsApp.getInitialProps = async (appContext: AppContext) => { + const appProps = await App.getInitialProps(appContext); + const locale = appContext.router.locale || 'bg'; + const messages = await loadLocaleData(locale); + + return { + ...appProps, + locale, + messages, + }; +}; +export default SmwsApp; \ No newline at end of file diff --git a/pages/_document.tsx b/pages/_document.tsx index e498528..cdb83b8 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -13,7 +13,7 @@ class MyDocument extends Document { - + diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index 2db98e9..220d66e 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -8,6 +8,7 @@ import AppleProvider from "next-auth/providers/apple" import EmailProvider from "next-auth/providers/email" import CredentialsProvider from "next-auth/providers/credentials" import { PrismaAdapter } from "@auth/prisma-adapter" +import bcrypt from "bcrypt" //microsoft import AzureADProvider from "next-auth/providers/azure-ad"; @@ -16,9 +17,11 @@ import AzureADProvider from "next-auth/providers/azure-ad"; const common = require("../../../src/helpers/common"); import { isLoggedIn, setAuthTokens, clearAuthTokens, getAccessToken, getRefreshToken } from 'axios-jwt' +import { create } from "domain" -console.log("appleID:", process.env.APPLE_APP_ID); +//console.log("appleID:", process.env.APPLE_APP_ID); + // console.log(process.env.EMAIL_SERVER) // For more information on each option (and a full list of options) go to // https://next-auth.js.org/configuration/options @@ -52,6 +55,7 @@ export const authOptions: NextAuthOptions = { // tenantId: process.env.AZURE_AD_TENANT_ID, // }), CredentialsProvider({ + id: 'credentials', // The name to display on the sign in form (e.g. 'Sign in with...') name: 'Credentials', credentials: { @@ -80,17 +84,51 @@ export const authOptions: NextAuthOptions = { { id: "3", name: "popov", email: "popov@example.com", password: "popov123", role: "ADMIN" } ]; - // Check if a user with the given username and password exists const user = users.find(user => user.name === credentials.username && user.password === credentials.password ); - // If a matching user is found, return the user data, otherwise return null if (user) { - return user; //{ id: user.id, name: user.name, email: user.email }; + return user; } + else { + const prisma = common.getPrismaClient(); + const user = await prisma.user.findUnique({ where: { email: credentials.username } }); + if (user) { + const match = await bcrypt.compare(credentials?.password, user.passwordHashLocalAccount); + if (match) { + console.log("User authenticated successfully."); + //create access token + user.accessToken = await getAccessToken(); - return null; + return user; + } + else { + console.log("Password mismatch."); + throw new Error('невалидна парола'); + } + } + else { + const pub = await prisma.publisher.findUnique({ where: { email: credentials.username } }); + if (pub) { + const passHash = await bcrypt.hash(credentials.password, 10); + const newUser = await prisma.user.create({ + data: { + name: credentials.username, + email: credentials.username, + passwordHashLocalAccount: passHash + } + }); + console.log("New local credential user created for publisher ", pub.firstName, " ", pub.lastName, " (", pub.email, ")"); + return newUser; + } + else { + + throw new Error("Не можем да намерим твоя имейл '" + credentials?.username + "' в участниците в ССОМ. Моля свържи се с нас за да те регистрираме ако искаш да ползваш този имейл."); + } + + } + } } }) /* @@ -132,39 +170,45 @@ export const authOptions: NextAuthOptions = { var prisma = common.getPrismaClient(); console.log("[nextauth] signIn:", account.provider, user.email) - if (account.provider === 'google') { - try { - // Check user in your database and assign roles - const dbUser = await prisma.publisher.findUnique({ - where: { email: user.email } - }); + //if (account.provider === 'google' ) { - if (dbUser) { - // Assign roles from your database to the session - user.role = dbUser.role; - user.id = dbUser.id; - //user.permissions = dbUser.permissions; - const session = { ...user }; + // Check user in your database and assign roles + const dbUser = await prisma.publisher.findUnique({ + where: { email: user.email } + }); - await prisma.publisher.update({ - where: { id: dbUser.id }, - data: { lastLogin: new Date() } - }); - return true; // Sign-in successful - } else { - // Optionally create a new user in your DB - // Or return false to deny access - //Let's customize the error message to give a better user experience - throw new Error(`Твоят имейл '${user.email}' не е регистриран в системата. Моля свържи се с нас за да те регистрираме ако искаш да ползваш този имейл.`); - } - } catch (e) { - console.log(e); - } + if (dbUser) { + // Assign roles from your database to the session + user.role = dbUser.role; + user.id = dbUser.id; + //user.permissions = dbUser.permissions; + const session = { ...user }; + + await prisma.publisher.update({ + where: { id: dbUser.id }, + data: { lastLogin: new Date() } + }); + return true; + } else { + //user nor found in our database. deny access, showing error message. logout and redirect to message page + //throw new Error(`Твоят имейл '${user.email}' не е регистриран в системата. Моля свържи се с нас за да те регистрираме ако искаш да ползваш този имейл.`); + throw new Error(`UserNotFound&email=${encodeURIComponent(user?.email)}`); } - return true; // Allow other providers or default behavior }, + // async redirect({ url, baseUrl, user }) { + // // Redirect based on the user or error + // console.log("[nextauth] redirect", url, baseUrl, user) + // if (user) { + // return url; + // } else if (url.includes('error=UserNotFound')) { + // // Redirect to a custom error page or display an error + // return `${baseUrl}/error=UserNotFound&mail=${encodeURIComponent(user?.email)}`; + // } + // return baseUrl; + // }, + // Persist the OAuth access_token to the token right after signin async jwt({ token, user, account, profile, isNewUser }) { //!console.log("[nextauth] JWT", token, user) @@ -207,6 +251,13 @@ export const authOptions: NextAuthOptions = { }; }, }, + pages: { + signIn: "/auth/signin", + signOut: "/auth/signout", + error: "/message", // Error code passed in query string as ?error= + verifyRequest: "/auth/verify-request", // (used for check email message) + newUser: null // If set, new users will be directed here on first sign in + }, } -export default NextAuth(authOptions) +export default NextAuth(authOptions) \ No newline at end of file diff --git a/pages/api/data/[...nextcrud].ts b/pages/api/data/[...nextcrud].ts index c054391..c3aa01b 100644 --- a/pages/api/data/[...nextcrud].ts +++ b/pages/api/data/[...nextcrud].ts @@ -31,9 +31,15 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const targetTable = req.query.nextcrud[0]; //get target action if (req.method === 'DELETE') { - - const targetId = req.query.nextcrud[1]; - logger.info('[nextCrud] ' + targetTable + ': ' + targetId + 'DELETED by ' + session.user.email); + switch (targetTable) { + case 'publishers': + case 'availabilities': + const targetId = req.query.nextcrud[1]; + logger.info('[nextCrud] ' + targetTable + ': ' + targetId + ' DELETED by ' + session.user.email); + break; + default: + break; + } } return nextCrudHandler(req, res); } diff --git a/pages/api/data/prisma/[...model].ts b/pages/api/data/prisma/[...model].ts new file mode 100644 index 0000000..3ea950c --- /dev/null +++ b/pages/api/data/prisma/[...model].ts @@ -0,0 +1,101 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { PrismaClient } from '@prisma/client'; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "../../auth/[...nextauth]"; + + +const common = require('../../../../src/helpers/common'); +const logger = require('../../../../src/logger'); + +// Utility to parse query parameters into a Prisma query +const parseQueryParams = (query: any) => { + return { + select: query.select ? JSON.parse(query.select) : undefined, + include: query.include ? JSON.parse(query.include) : undefined, + where: query.where ? JSON.parse(query.where) : undefined, + orderBy: query.orderBy ? JSON.parse(query.orderBy) : undefined, + skip: query.skip ? parseInt(query.skip, 10) : undefined, + limit: query.limit ? parseInt(query.limit, 10) : undefined, + distinct: query.distinct ? query.distinct.split(',') : undefined, + }; +}; + + +const serializeValue = (value) => { + if (value === null || value === undefined) { + return 'NULL'; + } + if (typeof value === 'string') { + // Escape single quotes and backslashes for MySQL + return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "''")}'`; + } + if (typeof value === 'boolean') { + // MySQL uses 1 and 0 for TRUE and FALSE + return value ? '1' : '0'; + } + if (typeof value === 'number') { + return value; + } + if (value instanceof Date) { + // Format date objects to MySQL date strings + return `'${value.toISOString().slice(0, 19).replace('T', ' ')}'`; + } + if (Array.isArray(value) || typeof value === 'object') { + // Convert arrays and objects to JSON strings and escape them + return `'${JSON.stringify(value).replace(/\\/g, "\\\\").replace(/'/g, "''")}'`; + } + return value; +}; + +// Function to generate SQL INSERT statements for MySQL from data +const generateSQL = (data, tableName) => { + return data.map(item => { + const columns = Object.keys(item).join(", "); + const values = Object.values(item).map(serializeValue).join(", "); + return `INSERT INTO ${tableName} (${columns}) VALUES (${values});`; + }).join("\n"); +}; +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + + const session = await getServerSession(req, res, authOptions); + + if (!session) { + return res.status(401).json({ error: "Unauthorized" }); + } + + + const prisma: PrismaClient = common.getPrismaClient(); + const modelArray = (req.query.model || (req.body && req.body.model)) as string[]; + let queryOptions = {}; + + if (req.method === 'POST' && req.headers['content-type']?.includes('application/json')) { + // Handle POST request + queryOptions = req.body; + } else { + // Handle GET request + queryOptions = parseQueryParams(req.query); + } + + try { + if (!modelArray || modelArray.length === 0) { + throw new Error('Model is required as a part of the URL path.'); + } + const modelName = modelArray[0]; // Get the first part of the model array + if (!prisma[modelName]) { + throw new Error(`Model ${modelName} not found in Prisma client.`); + } + const result = await prisma[modelName].findMany(queryOptions); + if (req.query.format === 'sql') { + // Generate SQL if requested via query parameter + const sql = generateSQL(result, modelName); + res.setHeader('Content-Type', 'application/sql'); + res.send(sql); + } else { + // Normal JSON response + res.status(200).json(result); + } + } catch (error) { + console.error(error); + res.status(500).json({ error: error.message }); + } +} diff --git a/pages/api/email.ts b/pages/api/email.ts index 08f6547..c9914d7 100644 --- a/pages/api/email.ts +++ b/pages/api/email.ts @@ -34,7 +34,8 @@ export default async function handler(req, res) { // Retrieve and validate the JWT token //response is a special action that does not require a token - if (action == "email_response") { + //PUBLIC + if (action == "email_response" || action == "account") { switch (emailaction) { case "coverMeAccept": //validate shiftId and assignmentId @@ -94,7 +95,7 @@ export default async function handler(req, res) { res.redirect(messagePageUrl); return; } - + let originalPublisher = assignment.publisher; let to = assignment.shift.assignments.map(a => a.publisher.email); to.push(publisher.email); @@ -137,7 +138,7 @@ export default async function handler(req, res) { publisher: { connect: { id: publisher.id } }, shift: { connect: { id: assignment.shiftId } }, type: EventLogType.AssignmentReplacementAccepted, - content: "Заявка за заместване приета от " + publisher.firstName + " " + publisher.lastName + content: `Заявката за заместване на ${originalPublisher.firstName} ${originalPublisher.lastName} е приета от ${publisher.firstName} ${publisher.lastName}` } }); @@ -201,6 +202,83 @@ export default async function handler(req, res) { }); break; + + case "resetPassword": + // Send password reset form to the user + //parse the request body + + let email = req.body.email || req.query.email; + let actualUser = await prisma.publisher.findUnique({ + where: { + email: email + } + }); + if (!actualUser) { + return res.status(200).json({ message: "Няма потребител с този имейл" }); + } + else { + let requestGuid = req.query.guid; + if (!requestGuid) { + console.log("User: " + email + " requested a password reset"); + let requestGuid = uuidv4(); + //save the request in the database as EventLog + let eventLog = await prisma.eventLog.create({ + data: { + date: new Date(), + publisher: { connect: { id: actualUser.id } }, + type: EventLogType.PasswordResetRequested, + content: JSON.stringify({ guid: requestGuid }) + } + }); + logger.info("User: " + email + " requested a password reset. EventLogId: " + eventLog.id + ""); + + let model = { + email: email, + firstName: actualUser.firstName, + lastName: actualUser.lastName, + resetUrl: process.env.NEXTAUTH_URL + "/api/email?action=email_response&emailaction=resetPassword&guid=" + requestGuid + "&email=" + email, + sentDate: common.getDateFormated(new Date()) + }; + emailHelper.SendEmailHandlebars(to, "resetPassword", model); + res.status(200).json({ message: "Password reset request sent" }); + } + else { + //1. validate the guid + let eventLog = await prisma.eventLog.findFirst({ + where: {//can we query "{ guid: requestGuid }"? + type: EventLogType.PasswordResetRequested, + publisherId: actualUser.id, + date: { + gt: new Date(new Date().getTime() - 24 * 60 * 60 * 1000) //24 hours + } + } + }); + + if (!eventLog) { + return res.status(400).json({ message: "Invalid or expired password reset request" }); + } + else { + let eventLog = await prisma.eventLog.update({ + where: { + id: parseInt(requestGuid) + }, + data: { + type: EventLogType.PasswordResetEmailConfirmed + } + }); + //2. redirect to the password reset page + const messagePageUrl = `/auth/reset-password?email=${email}&resetToken=${requestGuid}`; + res.redirect(messagePageUrl); + } + + //2.login the user + + //3. redirect to the password reset page + } + + } + break; + } // //send email response to the user // const emailResponse = await common.sendEmail(user.email, "Email Action Processed", @@ -220,6 +298,7 @@ export default async function handler(req, res) { } }); + //PRIVATE ACTIONS switch (action) { case "sendCoverMeRequestByEmail": // Send CoverMe request to the users @@ -230,7 +309,6 @@ export default async function handler(req, res) { let toSubscribed = req.body.toSubscribed; let toAvailable = req.body.toAvailable; - let assignment = await prisma.assignment.findUnique({ where: { id: parseInt(assignmentId) @@ -248,7 +326,6 @@ export default async function handler(req, res) { } }); - // update the assignment. generate new publicGuid, isConfirmed to false let newPublicGuid = uuidv4(); await prisma.assignment.update({ @@ -269,25 +346,6 @@ export default async function handler(req, res) { targetEmails.availablePublishers = []; } - // let subscribedPublishers = targetEmails.subscribedPublishers, availablePublishers = []; - // if (toSubscribed) { - // //get all subscribed publisers - // subscribedPublishers = await prisma.publisher.findMany({ - // where: { - // isSubscribedToCoverMe: true - // } - // }); - // } - - - // if (toAvailable) { - // availablePublishers = await data.filterPublishersNew("id,firstName,lastName,email", new Date(assignment.shift.startTime), - // true, false); - - // } - // use - - //concat and remove duplicate emails let pubsToSend = targetEmails.subscribedPublishers.concat(targetEmails.availablePublishers). filter((item, index, self) => @@ -301,10 +359,10 @@ export default async function handler(req, res) { data: { date: new Date(), publisher: { connect: { id: publisher.id } }, - shift: { connect: { id: assignment.shiftId } }, + shift: { connect: { id: assignment.shift.id } }, type: EventLogType.AssignmentReplacementRequested, content: "Заявка за заместване от " + publisher.firstName + " " + publisher.lastName - + "до: " + pubsToSend.map(p => p.firstName + " " + p.lastName + "<" + p.email + ">").join(", "), + + " до: " + pubsToSend.map(p => p.firstName + " " + p.lastName + "<" + p.email + ">").join(", "), } }); logger.info("User: " + publisher.email + " sent a 'CoverMe' request for his assignment " + assignmentId + " - " + assignment.shift.cartEvent.location.name + " " + assignment.shift.startTime.toISOString() + " to " + pubsToSend.map(p => p.firstName + " " + p.lastName + "<" + p.email + ">").join(", ") + ". EventLogId: " + eventLog.id + ""); @@ -313,12 +371,12 @@ export default async function handler(req, res) { for (let i = 0; i < pubsToSend.length; i++) { //send email to subscribed publisher - let acceptUrl = process.env.NEXTAUTH_URL + "/api/email?action=email_response&emailaction=coverMeAccept&userId=" + pubsToSend[i].id + "&shiftId=" + assignment.shiftId + "&assignmentPID=" + newPublicGuid; + let acceptUrl = process.env.NEXTAUTH_URL + "/api/email?action=email_response&emailaction=coverMeAccept&userId=" + pubsToSend[i].id + "&shiftId=" + assignment.shift.id + "&assignmentPID=" + newPublicGuid; publisher.prefix = publisher.isMale ? "Брат" : "Сестра"; let model = { user: publisher, - shiftId: assignment.shiftId, + shiftId: assignment.shift.id, acceptUrl: acceptUrl, firstName: pubsToSend[i].firstName, lastName: pubsToSend[i].lastName, @@ -350,7 +408,6 @@ export default async function handler(req, res) { return res.status(400).json({ message: "Invalid action" }); } - return res.status(200).json({ message: "User action processed" }); } } diff --git a/pages/api/translations/[...locale].ts b/pages/api/translations/[...locale].ts new file mode 100644 index 0000000..4deebec --- /dev/null +++ b/pages/api/translations/[...locale].ts @@ -0,0 +1,84 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import fs from 'fs'; +import path from 'path'; +import common from "../../../src/helpers/common"; + +function flattenTranslations(data) { + const result = {}; + + function recurse(cur, prop) { + if (Object(cur) !== cur) { + result[prop] = cur; + } else if (Array.isArray(cur)) { + for (let i = 0, l = cur.length; i < l; i++) + recurse(cur[i], prop ? prop + "." + i : "" + i); + if (l == 0) + result[prop] = []; + } else { + let isEmpty = true; + for (let p in cur) { + isEmpty = false; + recurse(cur[p], prop ? prop + "." + p : p); + } + if (isEmpty) + result[prop] = {}; + } + } + recurse(data, ""); + return result; +} + +function unflattenTranslations(data) { + const result = {}; + + for (let i in data) { + const keys = i.split('.'); + keys.reduce((r, e, j) => { + return r[e] || (r[e] = isNaN(Number(keys[j + 1])) ? (keys.length - 1 === j ? data[i] : {}) : []); + }, result); + } + return result; +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { locale } = req.query; + const filePath = path.join(process.cwd(), `content/i18n/${locale.join(".")}.json`); + const modifiedFilePath = path.join(process.cwd(), `content/i18n/${locale}.modified.json`); + + switch (req.method) { + case 'GET': + let flat = common.parseBool(req.query.flat); + try { + const fileContents = fs.readFileSync(filePath, 'utf8'); + let translations = JSON.parse(fileContents); + if (fs.existsSync(modifiedFilePath)) { + const modifiedTranslations = JSON.parse(fs.readFileSync(modifiedFilePath, 'utf8')); + translations = { ...translations, ...modifiedTranslations }; + } + if (flat) { + translations = flattenTranslations(translations); + } + res.status(200).json(translations); + } catch (error) { + console.error('Error reading translation file:', error); + res.status(500).json({ error: 'Failed to read translation file' }); + } + break; + + case 'POST': + try { + const newTranslations = req.body; + const reconstructedTranslations = unflattenTranslations(newTranslations); + fs.writeFileSync(filePath, JSON.stringify(reconstructedTranslations, null, 2), 'utf8'); + res.status(200).json({ status: 'Updated' }); + } catch (error) { + console.error('Error writing translation file:', error); + res.status(500).json({ error: 'Failed to update translation file' }); + } + break; + + default: + res.setHeader('Allow', ['GET', 'POST']); + res.status(405).end(`Method ${req.method} Not Allowed`); + } +} diff --git a/pages/auth/reset-password.tsx b/pages/auth/reset-password.tsx new file mode 100644 index 0000000..82f8a4a --- /dev/null +++ b/pages/auth/reset-password.tsx @@ -0,0 +1,135 @@ +import { use, useEffect, useState } from 'react'; +import Layout from '../../components/layout'; +import axiosInstance from "../../src/axiosSecure"; +import common from '../../src/helpers/common'; +import { EventLogType } from '@prisma/client'; +import { useRouter } from "next/router"; + +export default function ResetPassword(req, res) { + const [email, setEmail] = useState(''); + const [message, setMessage] = useState(''); + const [resetToken, setResetToken] = useState(req.query?.resetToken || ''); + const [isConfirmed, setIsConfirmed] = useState(false); + const router = useRouter(); + + + useEffect(async () => { + if (resetToken) { + const prisma = common.getPrismaClient(); + let eventLog = await prisma.eventLog.findUnique({ + where: { + content: resetToken, + type: EventLogType.PasswordResetEmailConfirmed, + date: { + gt: new Date(new Date().getTime() - 24 * 60 * 60 * 1000) //24 hours + } + } + }); + if (eventLog) { + setIsConfirmed(true); + } + } + }, [resetToken]); + + const handleResetRequest = async (event) => { + event.preventDefault(); + // Call your email API endpoint here + try { + const response = await axiosInstance.post('/api/email?action=account&emailaction=resetPassword', { email }, + { headers: { 'Content-Type': 'application/json' } }); + if (response.data.message) { + setMessage(response.data.message); + } else { + if (response.ok) { + setMessage('Провери твоя имейл за инструкции как да промениш паролата си.'); + } else { + if (response.error) { + setMessage(response.error); + } + } + } + } catch (error) { + setMessage(error.message); + } + }; + + const setNewPassword = async (event) => { + event.preventDefault(); + + try { + const prisma = common.getPrismaClient(); + const user = await prisma.user.findUnique({ + where: { + email + } + }); + if (!user) { + throw new Error('Няма потребител с този имейл.'); + } + + const passHash = await crypto.hash(event.target.newPassword.value, 10); + await prisma.user.update({ + where: { + email + }, + data: { + passwordHashLocalAccount: passHash + } + }); + setMessage('Паролата беше успешно променена.'); + router.push('/auth/signin'); + + } catch (error) { + setMessage(error.message); + } + } + + + return ( + +
+
+

Променете паролата си

+
+ {!isConfirmed && +
+ + setEmail(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" + /> +
} + + {isConfirmed && +
+ + setNewPassword(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" + /> +
} +
+ + +
+ {message &&
{message}
} +
+
+
+
+ ); +} diff --git a/pages/auth/signin.tsx b/pages/auth/signin.tsx new file mode 100644 index 0000000..d8e9a62 --- /dev/null +++ b/pages/auth/signin.tsx @@ -0,0 +1,132 @@ +// pages/auth/signin.js +import { getCsrfToken, signIn } from 'next-auth/react'; +import React, { useState } from 'react'; +import { useRouter } from 'next/router'; +import Layout from '../../components/layout'; +import { useSession } from "next-auth/react" + +export default function SignIn({ csrfToken }) { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const router = useRouter(); + + const { data: session } = useSession() + //handle callbackUrl + const { callbackUrl } = router.query; + if (callbackUrl) { + if (session) { + router.push(callbackUrl); + } + } + + + + const handleSubmit = async (e) => { + e.preventDefault(); + + // Perform client-side validation if needed + if (!email || !password) { + setError('Всички полета са задължителни'); + return; + } + + // Clear any existing errors + setError(''); + + // Attempt to sign in + const result = await signIn('credentials', { + redirect: false, + username: email, + password, + callbackUrl: '/', + }); + + // Check if there was an error + if (result.error) { + setError(result.error); + } + + // Redirect to the home page or callbackUrl on success + if (result.ok && result.url) { + router.push(result.url); + } + }; + + return ( + +
+
+
+ {/* Page Title */} +

Вход

+ + {/* Section for Social Sign-On Providers */} +
+ {/*

Sign in with a Social Media Account

*/} + + {/* Add more buttons for other SSO providers here in similar style */} +
+ + {/* Divider (Optional) */} +
+
+
+ + {/* Local Account Email and Password Form */} +
+

Влез с локален акаунт

+
+ +
+ + setEmail(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + /> +
+
+ + setPassword(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + /> +
+ {error &&
{error}
} + + {/* */} +
+
+
+
+
+
+ ); +} + +// This gets called on every request +export async function getServerSideProps(context) { + return { + props: { + csrfToken: await getCsrfToken(context), + }, + }; +} diff --git a/pages/cart/publishers/edit/[id].tsx b/pages/cart/publishers/edit/[id].tsx index c4605f9..df994ba 100644 --- a/pages/cart/publishers/edit/[id].tsx +++ b/pages/cart/publishers/edit/[id].tsx @@ -1,5 +1,6 @@ import axiosServer from '../../../../src/axiosServer'; import NewPubPage from "../new"; +const common = require('../../../../src/helpers/common'); export default NewPubPage; import { Assignment, Shift, UserRole, AvailabilityType } from "prisma/prisma-client"; @@ -40,6 +41,8 @@ function getShiftGroups(shifts: [Shift]) { export const getServerSideProps = async (context) => { const axios = await axiosServer(context); + const prisma = common.getPrismaClient(); + // const isAdmin = await ProtectedRoute.IsInRole(UserRole.ADMIN); does not work on server side context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate"); if (!context.query || !context.query.id) { @@ -47,10 +50,28 @@ export const getServerSideProps = async (context) => { props: {} }; } - var url = process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,assignments,assignments.shift"; - console.log("GET PUBLISHER FROM:" + url) - const { data: item } = await axios.get(url); - + const item = await prisma.publisher.findUnique({ + where: { + id: context.query.id + }, + include: { + availabilities: true, + assignments: { + include: { + shift: true + } + } + } + }); + if (!item) { + const user = context.req.session.user; + return { + redirect: { + destination: '/message?message=Този имейл (' + user.email + ') не е регистриран. Моля свържете се с администратора.', + permanent: false, + }, + } + } // item.allShifts = item.assignments.map((a: Assignment[]) => a.shift); // ============================================================ @@ -101,7 +122,7 @@ export const getServerSideProps = async (context) => { // console.dir(item, { depth: null }); return { props: { - item: item + item: common.convertDatesToISOStrings(item), }, }; }; diff --git a/pages/cart/publishers/index.tsx b/pages/cart/publishers/index.tsx index e0b0c57..896ec40 100644 --- a/pages/cart/publishers/index.tsx +++ b/pages/cart/publishers/index.tsx @@ -30,7 +30,8 @@ function PublishersPage({ publishers = [] }: IProps) { const [showZeroShiftsOnly, setShowZeroShiftsOnly] = useState(false); const [isDeleting, setIsDeleting] = useState(false); - const [isModalOpen, setIsModalOpen] = useState(false); + const [isModalOpenDeleteAllVisible, setIsModalOpenDeleteAllVisible] = useState(false); + const [isModalOpenDeleteAllAvaillabilities, setIsModalOpenDeleteAllAvaillabilities] = useState(false); const handleDeleteAllVisible = async () => { setIsDeleting(true); @@ -45,7 +46,7 @@ function PublishersPage({ publishers = [] }: IProps) { } setIsDeleting(false); - setIsModalOpen(false); + setIsModalOpenDeleteAllVisible(false); }; const handleDeleteAllAvailabilities = async () => { @@ -60,7 +61,7 @@ function PublishersPage({ publishers = [] }: IProps) { } setIsDeleting(false); - setIsModalOpen(false); + setIsModalOpenDeleteAllAvaillabilities(false); }; useEffect(() => { @@ -108,20 +109,20 @@ function PublishersPage({ publishers = [] }: IProps) { if (shownPubs.length === 0) { return (
- { - setFilter(""); - handleFilterChange({ target: { value: "" } }); + setFilter(""); // Assuming setFilter directly updates the filter state + if (typeof handleFilterChange === 'function') { + handleFilterChange({ target: { value: "" } }); // If needed for additional logic + } }} > Clear filters - +
); - } - else { + } else { return shownPubs.map((publisher) => ( )); @@ -166,38 +167,40 @@ function PublishersPage({ publishers = [] }: IProps) {
- Добави вестител + Добави вестител
- setIsModalOpen(false)} + isOpen={isModalOpenDeleteAllVisible} + onClose={() => setIsModalOpenDeleteAllVisible(false)} onConfirm={handleDeleteAllVisible} message="Сигурни ли сте, че искате да изтриете всички показани в момента вестители?" /> - setIsModalOpen(false)} + isOpen={isModalOpenDeleteAllAvaillabilities} + onClose={() => setIsModalOpenDeleteAllAvaillabilities(false)} onConfirm={handleDeleteAllAvailabilities} message="Сигурни ли сте, че искате да изтриете предпочитанията на ВСИЧКИ вестители?" /> +
+
- {publishers.length} от {publishers.length} вестителя -
+
{renderPublishers()}
diff --git a/pages/cart/publishers/myschedule.tsx b/pages/cart/publishers/myschedule.tsx index d994f9a..10b953f 100644 --- a/pages/cart/publishers/myschedule.tsx +++ b/pages/cart/publishers/myschedule.tsx @@ -161,7 +161,7 @@ export const getServerSideProps = async (context) => { if (!session) { return { redirect: { - destination: '/auth/login', // Adjust the login path as needed + destination: '/auth/signin', // Adjust the login path as needed permanent: false, }, }; diff --git a/pages/cart/reports/coverMe.tsx b/pages/cart/reports/coverMe.tsx new file mode 100644 index 0000000..e076646 --- /dev/null +++ b/pages/cart/reports/coverMe.tsx @@ -0,0 +1,126 @@ +//page to show all repots in the database with a link to the report page +import axiosInstance from '../../../src/axiosSecure'; +import { useEffect, useState } from "react"; +import toast from "react-hot-toast"; +import { useRouter } from "next/router"; +import Link from "next/link"; +import { useSession } from "next-auth/react" +//const common = require('src/helpers/common'); +import common from '../../../src/helpers/common'; +import Layout from "../../../components/layout"; +import ProtectedRoute from '../../../components/protectedRoute'; +import { Location, Shift, UserRole, EventLog, EventType, EventLogType } from "@prisma/client"; +import { set } from 'date-fns'; + + +export default function EventLogList() { + const [eventLogs, setEventLog] = useState([]); + const [requestedAssignments, setRequestedAssignments] = useState([]); + const router = useRouter(); + const { data: session } = useSession(); + + const [locations, setLocations] = useState([]); + const [showOpenRequests, setShowOpenRequests] = useState(false); + + useEffect(() => { + const fetchLocations = async () => { + try { + const { data: eventLogsData } = await axiosInstance.get(`/api/data/prisma/eventLog?where={"type":"${EventLogType.AssignmentReplacementAccepted}"}&include={"publisher":{"select":{"firstName":true,"lastName":true}},"shift":{"include":{"assignments":{"include":{"publisher":{"select":{"firstName":true,"lastName":true}}}}}}}`); + + setEventLog(eventLogsData); + + const { data: shiftsData } = await axiosInstance.get(`/api/data/prisma/assignment?where={"publicGuid":{"not":"null"}}&include={"shift":{"include":{"assignments":{"include":{"publisher":{"select":{"firstName":true,"lastName":true}}}}}},"publisher":{"select":{"firstName":true,"lastName":true}}}`); + setRequestedAssignments(shiftsData); + + } catch (error) { + console.error(error); + } + }; + if (!locations.length) { + fetchLocations(); + } + }, []); + return ( + + + +
+
+

Заявки за заместване

+ {/* + + */} +
+ + + +
+
+ + + + + + + + + + + {!showOpenRequests && (eventLogs.map((event) => ( + + + + + + + + )) + )} + {showOpenRequests && (requestedAssignments.map((assignment) => ( + + + + + + + )) + )} + +
ОтДатаСмянаДействия
{event.publisher.firstName + " " + event.publisher.lastName}{new Date(event.shift?.startTime).toLocaleString('bg')} + {event.shift?.assignments.map((ass) => ( +
{ass.publisher.firstName + " " + ass.publisher.lastName}
+ ))} +
+ {event.content} + + +
{assignment.publisher.firstName + " " + assignment.publisher.lastName}{new Date(assignment.shift.startTime).toLocaleString('bg')} + {assignment.shift.assignments.map((ass) => ( +
{ass.publisher.firstName + " " + ass.publisher.lastName}
+ ))} +
+ +
+
+
+
+
+
+ ); +} + + diff --git a/pages/cart/translations/index.tsx b/pages/cart/translations/index.tsx new file mode 100644 index 0000000..05282a6 --- /dev/null +++ b/pages/cart/translations/index.tsx @@ -0,0 +1,110 @@ +import axiosInstance from '../../../src/axiosSecure'; +import { useState, useEffect } from 'react'; +import ProtectedRoute from "../../../components/protectedRoute"; +import { UserRole } from "@prisma/client"; +import Layout from 'components/layout'; +import { useRouter } from "next/router"; +import { ToastContainer, toast } from 'react-toastify'; + +const locales = ['bg', 'en', 'ru']; + +const AdminTranslations = () => { + const [translations, setTranslations] = useState({}); + // set locale to the current locale by default. get it from the useRouter + let router = useRouter(); + + const [locale, setLocale] = useState(router.locale); + const [baseTranslations, setBaseTranslations] = useState(locales[0]); + + useEffect(() => { + axiosInstance.get(`/api/translations/${locale}?flat=true`).then(res => setTranslations(res.data)); + axiosInstance.get(`/api/translations/${locales[0]}?flat=true`).then(res => setBaseTranslations(res.data)); + }, [locale]); + + const handleSave = () => { + axiosInstance.post(`/api/translations/${locale}/modified`, translations) + .then(res => { + if (res.data.status === 'Updated') { + toast.success('Translations updated!'); + } else { + toast.error('Something went wrong!'); + } + }) + .catch(err => toast.error('Failed to update translations: ' + err.message)); + }; + + const handleChange = (key, value) => { + setTranslations(prev => ({ ...prev, [key]: value })); + }; + + return ( + + +
+
+
+

Edit Translations

+
+ +
+ +
+
+
+
+
+ + + + {/* Adjusted width */} + {/* Adjusted width */} + {/* Adjusted width */} + + + + {Object.entries(baseTranslations).map(([key, baseValue]) => ( + + + +
KeyBase TranslationTranslation
{key}{baseValue} +