Merge commit 'f2fc5492651b3e104b0dd67dccc36499562d1d25' into production

This commit is contained in:
Dobromir Popov
2024-05-01 15:04:22 +03:00
43 changed files with 1866 additions and 185 deletions

3
.env
View File

@ -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

20
.env.development.popov Normal file
View File

@ -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='"ССОМ [ТЕСТ] " <mwitnessing@gmail.com>'
# 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

5
.vscode/launch.json vendored
View File

@ -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",

View File

@ -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"
}

View File

@ -232,4 +232,9 @@ in schedule admin - if a publisher is always pair & family is not in the shift -
[x] OK заместник, предпочитание в миналото - да не може.
[] Да иска потвърждение преди да заместиш някой
[] да не се виждат непубликуваните смени в Моите смени
[] да не се виждат старите предпочитания ако не си админ в публишерс
[] да не се виждат старите предпочитания ако не си админ в публишерс
[] 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)

View File

@ -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 (
<div>
<IconButton onClick={handleClick} color="inherit">
<TranslateIcon />
</IconButton>
<Menu
id="language-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
{locales?.map((lng) => {
if (lng === locale) return null;
return (
<MenuItem key={lng} onClick={() => changeLanguage(lng)}>
{t('changeTo')} {t(lng.toUpperCase())}
</MenuItem>
);
})}
</Menu>
</div>
);
};
export default LanguageSwitcher;

View File

@ -68,4 +68,5 @@ export default function Layout({ children }) {
</div>
</div>
);
}
}

View File

@ -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 <div>{deniedMessage}</div>;
}
return <div>Нямате достъп до тази страница. Ако мислите, че това е грешка, моля, свържете се с администраторите</div>;
return (
<>
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4 text-blue-500">{session?.user?.email},</h1>
<p className="mb-6">{`Нямате достъп до тази страница. Ако мислите, че това е грешка, моля, свържете се с администраторите`}</p>
</div>
</div>
</>);
}
if (status === "loading") {
return <div>Зареждане...</div>;
}
if (!session) return <a href="/api/auth/signin">Защитено съдържание. Впишете се.. </a>
return children;
if (!session) {
if (deniedMessage !== undefined) {
return <div>{deniedMessage}</div>;
}
return <a href="/api/auth/signin">Защитено съдържание. Впишете се.. </a>
}
};
export default ProtectedRoute;

View File

@ -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 }) {
<div className="mb-4">
<label className="label" htmlFor="desiredShiftsPerMonth">Желани смeни на месец</label>
<input type="number" id="desiredShiftsPerMonth" name="desiredShiftsPerMonth" value={publisher.desiredShiftsPerMonth} onChange={handleChange} className="textbox" placeholder="desiredShiftsPerMonth" autoFocus />
</div>
<div className="mb-4">
</div> <div className="mb-4">
<label className="label" htmlFor="email">Имейл</label>
<input type="text" id="email" name="email" value={publisher.email} onChange={handleChange} className="textbox" placeholder="Email" autoFocus />
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage={publisher.email} className="">
<div className="border border-blue-500 border-solid p-2">
<input type="text" id="email" name="email" value={publisher.email} onChange={handleChange} className="textbox" placeholder="Email" autoFocus />
</div>
</ProtectedRoute>
</div>
<div className="mb-4">
<label className="label" htmlFor="phone">Телефон</label>
@ -263,10 +266,8 @@ export default function PublisherForm({ item, me }) {
{/* ADMINISTRATORS ONLY */}
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" " className="">
<PwaManager />
<div className="border border-blue-500 border-solid p-2">
<PwaManager />
<div className="mb-4">
<label className="label" htmlFor="type">Тип</label>
<select id="type" name="type" value={publisher.type} onChange={handleChange} className="textbox" placeholder="Type" autoFocus >
@ -307,7 +308,6 @@ export default function PublisherForm({ item, me }) {
{/* Add other roles as needed */}
</select>
</div>
</div>
</ProtectedRoute>
{/* ---------------------------- Actions --------------------------------- */}

View File

@ -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 }) {
<div className="flex flex-col justify-between pb-4">
<nav>
{sidemenu.map((item, index) => (
item.text = t(item.id) || item.text, // Use the translation if available
<SidebarMenuItem key={index} item={item} session={session} />
))}
<hr className="my-3 border-gray-200 dark:border-gray-600" />
@ -145,6 +162,9 @@ export default function Sidebar({ isSidebarOpen, toggleSidebar }) {
<div className="mt-auto">
<hr className="border-gray-200 dark:border-gray-600 text-align-bottom" />
<FooterSection />
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage="">
<LanguageSwitcher />
</ProtectedRoute >
</div>
</nav>
</div>
@ -162,14 +182,17 @@ function UserSection({ session }) {
}
function SignInButton() {
// const t = useTranslations('common');
return (
<div className="items-center py-2 font-bold" onClick={() => signIn()}>
<button>Впишете се</button>
<button>вход</button>
{/* <button>{t('login')}</button> */}
</div>
);
}
function UserDetails({ session }) {
// const t = useTranslations('common');
return (
<>
<hr className="m-0" />
@ -180,7 +203,10 @@ function UserDetails({ session }) {
<div className="ml-3 overflow-hidden">
<p className="mx-1 mt-1 text-sm font-medium text-gray-800 dark:text-gray-200">{session.user.name}</p>
<p className="mx-1 mt-1 text-sm font-medium text-gray-600 dark:text-gray-400">{session.user.role}</p>
<a href="/api/auth/signout" className={styles.button} onClick={(e) => { e.preventDefault(); signOut(); }}>Изход</a>
<a href="/api/auth/signout" className={styles.button} onClick={(e) => { e.preventDefault(); signOut(); }}>
{/* {t('logout')} */}
изход
</a>
</div>
</div>
</>
@ -196,13 +222,24 @@ function FooterSection() {
return (
footerMenu.map((item, index) => (
<div
key={index}
className="px-4 py-1 mt-2 cursor-pointer hover:underline hover:text-blue-600 dark:hover:text-blue-400 "
onClick={() => navigateTo(item.url)}
>
<span className="text-gray-700 dark:text-gray-300 font-medium">{item.text}</span>
</div>
item.roles ? (
<ProtectedRoute key={index} allowedRoles={item.roles} deniedMessage="">
<div
className="px-4 py-1 mt-2 cursor-pointer hover:underline hover:text-blue-600 dark:hover:text-blue-400"
onClick={() => navigateTo(item.url)}
>
<span className="text-gray-700 dark:text-gray-300 font-medium">{item.text}</span>
</div>
</ProtectedRoute>
) : (
<div
key={index}
className="px-4 py-1 mt-2 cursor-pointer hover:underline hover:text-blue-600 dark:hover:text-blue-400"
onClick={() => navigateTo(item.url)}
>
<span className="text-gray-700 dark:text-gray-300 font-medium">{item.text}</span>
</div>
)
))
);
}

View File

@ -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],
},
]

36
content/i18n/bg.json Normal file
View File

@ -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": "Преводи"
}
}

View File

@ -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": "Преводи"
}
}

15
content/i18n/en.json Normal file
View File

@ -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"
}
}

15
content/i18n/ru.json Normal file
View File

@ -0,0 +1,15 @@
{
"common": {
"greeting": "Здравей",
"farewell": "Довиждане",
"changeTo": "Смени на",
"BG": "[RU] Български",
"EN": "[RU] Английски",
"RU": "[RU] Руски",
"login": "вход",
"contacts": "Контакти"
},
"menu": {
"dashboard": "Начало"
}
}

View File

@ -0,0 +1,15 @@
{
"common": {
"greeting": "Здравей",
"farewell": "Довиждане",
"changeTo": "Смени на",
"BG": "болгарский",
"EN": "английский",
"RU": "русский",
"login": "вход",
"contacts": "Контакти te"
},
"menu": {
"dashboard": "Начало"
}
}

View File

@ -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,
},
})

423
package-lock.json generated
View File

@ -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",

View File

@ -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"
}
}
}

View File

@ -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 (
<>
<SessionProvider session={session} >
<LocalizationProvider dateAdapter={AdapterDayjs}>
<Component {...pageProps} />
</LocalizationProvider>
</SessionProvider>
<NextIntlClientProvider
locale={locale}
timeZone="Europe/Sofia"
messages={messages}
>
<SessionProvider session={session} >
<LocalizationProvider dateAdapter={AdapterDayjs}>
<Component {...pageProps} />
</LocalizationProvider>
</SessionProvider>
</NextIntlClientProvider >
</>
)
}
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;

View File

@ -13,7 +13,7 @@ class MyDocument extends Document {
<link rel="manifest" href="/manifest.json" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Your PWA Name" />
<meta name="apple-mobile-web-app-title" content="CCOM" />
<link rel="apple-touch-icon" href="/old-192x192.png"></link>
</Head>

View File

@ -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)

View File

@ -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);
}

View File

@ -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 });
}
}

View File

@ -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" });
}
}

View File

@ -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`);
}
}

View File

@ -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 (
<Layout>
<div className="min-h-screen flex items-center justify-center">
<div className="w-full max-w-md p-8 space-y-6 bg-white shadow-lg rounded-lg">
<h1 className="text-xl font-bold text-center">Променете паролата си</h1>
<form onSubmit={handleResetRequest} className="space-y-4">
{!isConfirmed &&
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">имейл</label>
<input
id="email"
type="email"
required
value={email}
onChange={(e) => 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"
/>
</div>}
{isConfirmed &&
<div>
<label htmlFor="newPassword" className="block text-sm font-medium text-gray-700">имейл</label>
<input
id="newPassword"
type="password"
required
value={newPassword}
onChange={(e) => 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"
/>
</div>}
<div>
<button type="submit" className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700">
Изпрати линк за промяна на паролата
</button>
<button type="button" className="mt-4 w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-blue-600 hover:text-blue-700 focus:outline-none"
onClick={() => window.location.href = '/auth/signin'}
>
страница за вход
</button>
</div>
{message && <div className="text-center text-sm text-gray-500">{message}</div>}
</form>
</div>
</div>
</Layout>
);
}

132
pages/auth/signin.tsx Normal file
View File

@ -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 (
<Layout>
<div className="page">
<div className="signin">
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-100">
{/* Page Title */}
<h1 className="text-2xl font-bold text-gray-900 mt-6">Вход</h1>
{/* Section for Social Sign-On Providers */}
<div className="mt-8 w-full max-w-md px-4 py-8 bg-white shadow rounded-lg">
{/* <h2 className="text-center text-lg font-semibold text-gray-900 mb-4">Sign in with a Social Media Account</h2> */}
<button onClick={() => signIn('google', { callbackUrl: '/' })}
className="flex items-center justify-center w-full py-3 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
<img loading="lazy" height="24" width="24" alt="Google logo"
src="https://authjs.dev/img/providers/google.svg" className="mr-2" />
Влез чрез Google
</button>
{/* Add more buttons for other SSO providers here in similar style */}
</div>
{/* Divider (Optional) */}
<div className="w-full max-w-xs mt-8 mb-8">
<hr className="border-t border-gray-300" />
</div>
{/* Local Account Email and Password Form */}
<div className="w-full max-w-md mt-8 mb-8 px-4 py-8 bg-white shadow rounded-lg">
<h2 className="text-center text-lg font-semibold text-gray-900 mb-4">Влез с локален акаунт</h2>
<form onSubmit={handleSubmit}>
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
<div className="mb-4">
<label htmlFor="email" className="block text-sm font-medium text-gray-900">имейл</label>
<input
id="email"
type="text" // allow non-email addresses for username (admins)
value={email}
onChange={(e) => 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"
/>
</div>
<div className="mb-6">
<label htmlFor="password" className="block text-sm font-medium text-gray-900">парола</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => 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"
/>
</div>
{error && <div className="text-red-500 text-sm text-center">{error}</div>}
<button type="submit" className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700">
Влез
</button>
{/* <button
type="button"
className="mt-4 w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-blue-600 hover:text-blue-700 focus:outline-none"
onClick={() => router.push('/auth/reset-password')}>
Забравена парола?
</button> */}
</form>
</div>
</div>
</div>
</div>
</Layout>
);
}
// This gets called on every request
export async function getServerSideProps(context) {
return {
props: {
csrfToken: await getCsrfToken(context),
},
};
}

View File

@ -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),
},
};
};

View File

@ -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 (
<div className="flex justify-center">
<a
className="btn"
href="javascript:void(0);"
<button
className="px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
onClick={() => {
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
</a>
</button>
</div>
);
}
else {
} else {
return shownPubs.map((publisher) => (
<PublisherCard key={publisher.id} publisher={publisher} />
));
@ -166,38 +167,40 @@ function PublishersPage({ publishers = [] }: IProps) {
<div className="">
<div className="flex items-center justify-center space-x-4 m-4">
<div className="flex justify-center m-4">
<a href="/cart/publishers/new" className="btn"> Добави вестител </a>
<a href="/cart/publishers/new" className="btn">Добави вестител</a>
</div>
<button className="button m-2 btn btn-danger" onClick={() => setIsModalOpen(true)} disabled={isDeleting} >
<button className="button m-2 btn btn-danger" onClick={() => setIsModalOpenDeleteAllVisible(true)} disabled={isDeleting}>
{isDeleting ? "Изтриване..." : "Изтрий показаните вестители"}
</button>
<ConfirmationModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
isOpen={isModalOpenDeleteAllVisible}
onClose={() => setIsModalOpenDeleteAllVisible(false)}
onConfirm={handleDeleteAllVisible}
message="Сигурни ли сте, че искате да изтриете всички показани в момента вестители?"
/>
<button className="button m-2 btn btn-danger" onClick={() => setIsModalOpen(true)} disabled={isDeleting} >
<button className="button m-2 btn btn-danger" onClick={() => setIsModalOpenDeleteAllAvaillabilities(true)} disabled={isDeleting}>
{isDeleting ? "Изтриване..." : "Изтрий ВСИЧКИ предпочитания"}
</button>
<ConfirmationModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
isOpen={isModalOpenDeleteAllAvaillabilities}
onClose={() => setIsModalOpenDeleteAllAvaillabilities(false)}
onConfirm={handleDeleteAllAvailabilities}
message="Сигурни ли сте, че искате да изтриете предпочитанията на ВСИЧКИ вестители?"
/>
<div className="flex justify-center m-4">
<a href="/cart/publishers/import" className="btn"> Import publishers </a>
<a href="/cart/publishers/import" className="btn">Import publishers</a>
</div>
</div>
<div name="filters" className="flex items-center justify-center space-x-4 m-4 sticky top-4 z-10 bg-gray-100 p-2">
<label htmlFor="filter">Filter:</label>
<input type="text" id="filter" name="filter" value={filter} onChange={handleFilterChange}
className="border border-gray-300 rounded-md px-2 py-1"
/>
<label htmlFor="zeroShiftsOnly" className="ml-4 inline-flex items-center">
<input type="checkbox" id="zeroShiftsOnly" checked={showZeroShiftsOnly}
onChange={e => setShowZeroShiftsOnly(e.target.checked)}
@ -207,8 +210,8 @@ function PublishersPage({ publishers = [] }: IProps) {
</label>
<span id="filter-info" className="ml-4">{publishers.length} от {publishers.length} вестителя</span>
</div>
<div className="grid gap-4 grid-cols-1 md:grid-cols-4 z-0">
{renderPublishers()}
</div>

View File

@ -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,
},
};

View File

@ -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 (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
<div className="h-5/6 grid place-items-start px-4 pt-8">
<div className="flex flex-col w-full px-4">
<h1 className="text-2xl font-bold text-center">Заявки за заместване</h1>
{/* <Link href="/cart/reports/report">
<button className="mt-4 bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">
Добави нов отчет
</button>
</Link> */}
<div className="flex gap-2 mb-4">
<label className={`cursor-pointer px-4 py-2 rounded-full ${!showOpenRequests ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800'}`}>
<input type="radio" name="reportType" value="ServiceReport" onChange={() => setShowOpenRequests(false)} checked={!showOpenRequests} className="sr-only" />
Приети заявки
</label>
<label className={`cursor-pointer px-4 py-2 rounded-full ${showOpenRequests ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800'}`}>
<input type="radio" name="reportType" value="Experience" onChange={() => setShowOpenRequests(true)} checked={showOpenRequests} className="sr-only" />
Отворени заявки
</label>
</div>
<div className="mt-4 w-full overflow-x-auto">
<table className="w-full table-auto">
<thead>
<tr>
<th className="px-4 py-2 text-left">От</th>
<th className="px-4 py-2 text-left">Дата</th>
<th className="px-4 py-2 text-left">Смяна</th>
<th className="px-4 py-2 text-left">Действия</th>
</tr>
</thead>
<tbody>
{!showOpenRequests && (eventLogs.map((event) => (
<tr key={event.id}>
<td className="border px-2 py-2">{event.publisher.firstName + " " + event.publisher.lastName}</td>
<td className="border px-2 py-2">{new Date(event.shift?.startTime).toLocaleString('bg')}</td>
<td className="border px-2 py-2">
{event.shift?.assignments.map((ass) => (
<div key={ass.id}>{ass.publisher.firstName + " " + ass.publisher.lastName}</div>
))}
</td>
<td className="border px-2 py-2">
{event.content}
</td>
<td className="border px-4 py-2">
<button
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">
Изтрий
</button>
</td>
</tr>
))
)}
{showOpenRequests && (requestedAssignments.map((assignment) => (
<tr key={assignment.id}>
<td className="border px-2 py-2">{assignment.publisher.firstName + " " + assignment.publisher.lastName}</td>
<td className="border px-2 py-2">{new Date(assignment.shift.startTime).toLocaleString('bg')}</td>
<td className="border px-2 py-2">
{assignment.shift.assignments.map((ass) => (
<div key={ass.id}>{ass.publisher.firstName + " " + ass.publisher.lastName}</div>
))}
</td>
<td className="border px-4 py-2">
<button
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">
Изтрий
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div >
</div >
</ProtectedRoute >
</Layout >
);
}

View File

@ -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 (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
<div className="mx-auto px-4 sm:px-6 lg:px-8">
<div className="sticky top-0 z-10 bg-white shadow-md py-4 px-4">
<div className="flex justify-between items-center">
<h1 className="text-xl font-semibold leading-tight text-gray-800">Edit Translations</h1>
<div className="flex items-center space-x-4">
<button
onClick={handleSave}
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Save Changes
</button>
<div>
<select
id="locale-select"
onChange={e => setLocale(e.target.value)}
value={locale}
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
>
{locales.map(l => (
<option key={l} value={l}>{l.toUpperCase()}</option>
))}
</select>
</div>
</div>
</div>
</div>
<div className="overflow-x-auto relative shadow-md sm:rounded-lg">
<table className="w-full text-sm text-left text-gray-500">
<thead className="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" className="py-3 px-6 w-2/12">Key</th> {/* Adjusted width */}
<th scope="col" className="py-3 px-6 w-3/12">Base Translation</th> {/* Adjusted width */}
<th scope="col" className="py-3 px-6 w-7/12">Translation</th> {/* Adjusted width */}
</tr>
</thead>
<tbody>
{Object.entries(baseTranslations).map(([key, baseValue]) => (
<tr key={key} className="bg-white border-b hover:bg-gray-50">
<td className="py-4 px-6 font-medium text-gray-900 whitespace-nowrap text-ellipsis">{key}</td>
<td className="py-4 px-6">{baseValue}</td>
<td className="py-4 px-6">
<textarea
value={translations[key] || ''}
placeholder='Въведи превод...'
onChange={e => handleChange(key, e.target.value)}
className="block w-60hv text-base px-2 py-1 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 rounded placeholder-gray-400"
style={{ width: '100%', resize: 'both', transition: 'box-shadow .3s', boxShadow: translations[key] ? '0 0 0px 1px rgba(59, 130, 246, 0.5)' : 'none' }}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* <button
onClick={handleSave}
className="mt-4 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Save Changes
</button> */}
</div>
</ProtectedRoute>
</Layout>
);
};
export default AdminTranslations;

View File

@ -1,12 +1,14 @@
import React from 'react';
import Layout from "../components/layout";
import FeedbackForm from "../components/reports/FeedbackForm";
import { useTranslations } from 'next-intl';
const ContactsPage = () => {
const t = useTranslations('common');
return (
<Layout>
<div className="mx-auto my-8 p-6 max-w-4xl bg-white rounded-lg shadow-md">
<h1 className="text-2xl font-bold text-gray-800 mb-4">Специално свидетелстване на обществени места в София - Контакти</h1>
<h1 className="text-2xl font-bold text-gray-800 mb-4">{t('appNameLong') - t('contacts')}</h1>
<ul className="list-disc pl-5">
<li className="text-gray-700 mb-2">Янко Ванчев - <a href="tel:+359878224467" className="text-blue-500 hover:text-blue-600">+359 878 22 44 67</a></li>
<li className="text-gray-700">Крейг Смит - <a href="tel:+359878994573" className="text-blue-500 hover:text-blue-600">+359 878 994 573</a></li>

View File

@ -204,6 +204,7 @@ export const getServerSideProps = async (context) => {
props: {
initialItems: items,
userId: session?.user.id,
// messages: (await import(`../content/i18n/${context.locale}.json`)).default
},
};
}

View File

@ -10,14 +10,30 @@ export default function MessagePage() {
warning: "text-yellow-500",
info: "text-blue-500",
};
const { message, type = messageStyles.info, caption } = router.query;
let { message, type = messageStyles.info, caption } = router.query;
if (router.query.error) {
switch (router.query.error) {
case 'UserNotFound':
message = `Твоят имейл '${router.query.email}' не е регистриран в системата. Моля свържи се с нас за да те регистрираме ако искаш да ползваш този имейл.`;
caption = 'Грешка';
type = messageStyles.error;
break;
default:
message = 'Възникна грешка.';
caption = 'Грешка';
type = messageStyles.error;
break;
}
}
return (
<Layout>
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className={`text-2xl font-bold mb-4 ${messageStyles[type]}`}>{caption || 'Информация'}</h1>
<p className="mb-6">
<h1 className={`text-4xl font-bold mb-4 ${messageStyles[type]}`}>{caption || 'Информация'}</h1>
<p className="text-xl mb-6">
{message || 'Така ще получавате различни съобщения.'}
</p>
</div>

Binary file not shown.

View File

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE `User`
ADD COLUMN `passwordHashLocalAccount` VARCHAR(191) NULL;

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE `EventLog`
MODIFY `type` ENUM(
'AssignmentReplacementRequested', 'AssignmentReplacementAccepted', 'SentEmail', 'PasswordResetRequested', 'PasswordResetEmailConfirmed', 'PasswordResetCompleted'
) NOT NULL;

View File

@ -263,6 +263,9 @@ enum EventLogType {
AssignmentReplacementRequested
AssignmentReplacementAccepted
SentEmail
PasswordResetRequested
PasswordResetEmailConfirmed
PasswordResetCompleted
}
model EventLog {
@ -278,13 +281,14 @@ model EventLog {
//user auth and session management
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
passwordHashLocalAccount String? // New field to store the hashed password
// Optional relation to Publisher
publisherId String? @unique

View File

@ -697,30 +697,30 @@ exports.copyToClipboard = function (event, text) {
exports.getUser = async function (req) {
// Use req if provided (server-side), otherwise call getSession without args (client-side)
const session = req ? await getSession({ req }) : await getSession();
return session?.user;
}
// exports.getUser = async function (req) {
// // Use req if provided (server-side), otherwise call getSession without args (client-side)
// const session = req ? await getSession({ req }) : await getSession();
// return session?.user;
// }
exports.isUserInRole = async function (req, allowedRoles = []) {
const user = await exports.getUser(req);
// exports.isUserInRole = function (req, allowedRoles = []) {
// const user = exports.getUser(req);
// Check if the user is authenticated
if (!user) {
return false;
}
// // Check if the user is authenticated
// if (!user) {
// return false;
// }
// If no specific roles are required, return true as the user is authenticated
if (allowedRoles.length === 0) {
return true;
}
// // If no specific roles are required, return true as the user is authenticated
// if (allowedRoles.length === 0) {
// return true;
// }
// Check if the user's role is among the allowed roles
// Ensure role exists and is a valid UserRole
const userRole = user.role;
return allowedRoles.includes(userRole);
}
// // Check if the user's role is among the allowed roles
// // Ensure role exists and is a valid UserRole
// const userRole = user.role;
// return allowedRoles.includes(userRole);
// }
@ -762,6 +762,37 @@ exports.getInitials = function (names) {
exports.addMinutes = function (date, minutes) {
return new Date(date.getTime() + minutes * 60000); // 60000 milliseconds in a minute
}
/**
* Recursively converts all Date objects in an object to ISO strings.
* @param {Object} obj - The object to convert.
* @returns {Object} - The new object with all Date objects converted to ISO strings.
*/
exports.convertDatesToISOStrings = function (obj) {
if (obj === null || obj === undefined) {
return obj;
}
if (obj instanceof Date) {
return obj.toISOString();
}
if (Array.isArray(obj)) {
return obj.map(exports.convertDatesToISOStrings);
}
if (typeof obj === 'object') {
const keys = Object.keys(obj);
return keys.reduce((acc, key) => {
acc[key] = exports.convertDatesToISOStrings(obj[key]);
return acc;
}, {});
}
return obj;
}
// exports.getInitials = function (names) {
// const parts = names.split(' '); // Split the full name into parts
// if (parts.length === 0) {

View File

@ -0,0 +1,23 @@
{{!-- Subject: ССОМ: Нужен е заместник--}}
{{!-- Text: Plain text version of your email. If not provided, HTML tags will be stripped from the HTML version for the
text version. --}}
<section>
<h3>Добре дошъл</h3>
<p>Здравей, {{firstName}} {{lastName}}</p>
<p>
Моля, потвърди своя имейл адрес, като кликнеш на бутона по-долу.
</p>
<p style="text-align: center;">
<a href="{{resetUrl}}"
target="_blank"
style="background-color: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; display: inline-block; border-radius: 5px;">
ОК
</a>
</p>
{{!-- <p>Thank you very much for considering my request.</p>
<p>Best regards,<br>{{name}}</p> --}}
</section>
<footer style="margin-top: 20px; text-align: center;">
<p>Изпратено на: {{sentDate}}</p>
</footer>

View File

@ -0,0 +1,22 @@
{{!-- Subject: ССОМ: Нужен е заместник--}}
{{!-- Text: Plain text version of your email. If not provided, HTML tags will be stripped from the HTML version for the
text version. --}}
<section>
<h3>Промяна на парола</h3>
<p>Здравей, {{firstName}} {{lastName}}</p>
<p>
Получихме заявка за промяна на паролата на твоя акаунт. Ако това не си ти, моля игнорирай този имейл.
</p>
<p style="text-align: center;">
<a href="{{resetUrl}}"
target="_blank"
style="background-color: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; display: inline-block; border-radius: 5px;">Смени
паролата си</a>
</p>
{{!-- <p>Thank you very much for considering my request.</p>
<p>Best regards,<br>{{name}}</p> --}}
</section>
<footer style="margin-top: 20px; text-align: center;">
<p>Изпратено на: {{sentDate}}</p>
</footer>