Merge commit 'f2fc5492651b3e104b0dd67dccc36499562d1d25' into production
This commit is contained in:
3
.env
3
.env
@ -7,8 +7,9 @@
|
|||||||
NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58
|
NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58
|
||||||
|
|
||||||
NODE_ENV=development
|
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@localhost:3306/cart
|
||||||
|
# DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev
|
||||||
|
|
||||||
# // owner: dobromir.popov@gmail.com | Специално Свидетелстване София
|
# // owner: dobromir.popov@gmail.com | Специално Свидетелстване София
|
||||||
# // https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716
|
# // https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716
|
||||||
|
20
.env.development.popov
Normal file
20
.env.development.popov
Normal 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
5
.vscode/launch.json
vendored
@ -45,7 +45,10 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"cwd": "${workspaceFolder}",
|
"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",
|
"name": "Run conda npm TEST",
|
||||||
|
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@ -138,5 +138,11 @@
|
|||||||
},
|
},
|
||||||
"[handlebars]": {
|
"[handlebars]": {
|
||||||
"editor.defaultFormatter": "vscode.html-language-features"
|
"editor.defaultFormatter": "vscode.html-language-features"
|
||||||
}
|
},
|
||||||
|
"i18n-ally.localesPaths": [
|
||||||
|
"content/i18n",
|
||||||
|
"components/x-date-pickers/locales"
|
||||||
|
],
|
||||||
|
"i18n-ally.keystyle": "nested",
|
||||||
|
"i18n-ally.sourceLanguage": "bg"
|
||||||
}
|
}
|
@ -232,4 +232,9 @@ in schedule admin - if a publisher is always pair & family is not in the shift -
|
|||||||
[x] OK заместник, предпочитание в миналото - да не може.
|
[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)
|
||||||
|
55
components/languageSwitcher.tsx
Normal file
55
components/languageSwitcher.tsx
Normal 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;
|
@ -68,4 +68,5 @@ export default function Layout({ children }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,20 +10,20 @@ interface ProtectedRouteProps {
|
|||||||
allowedRoles: UserRole[];
|
allowedRoles: UserRole[];
|
||||||
deniedMessage?: string;
|
deniedMessage?: string;
|
||||||
bypass?: boolean;
|
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 { data: session, status } = useSession()
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("session.role:" + session?.user?.role);
|
//console.log("session.role:" + session?.user?.role);
|
||||||
if (!status || status === "unauthenticated") {
|
if (!status || status === "unauthenticated") {
|
||||||
// Redirect to the sign-in page
|
// Redirect to the sign-in page
|
||||||
if (!bypass) {
|
if (autoRedirect) {
|
||||||
signIn();
|
signIn();
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log("session.role:" + session?.user?.role);
|
console.log("session.role:" + session?.user?.role);
|
||||||
@ -41,14 +41,27 @@ const ProtectedRoute = ({ children, allowedRoles, deniedMessage, bypass = false
|
|||||||
if (deniedMessage !== undefined) {
|
if (deniedMessage !== undefined) {
|
||||||
return <div>{deniedMessage}</div>;
|
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") {
|
if (status === "loading") {
|
||||||
return <div>Зареждане...</div>;
|
return <div>Зареждане...</div>;
|
||||||
}
|
}
|
||||||
if (!session) return <a href="/api/auth/signin">Защитено съдържание. Впишете се.. </a>
|
if (!session) {
|
||||||
return children;
|
if (deniedMessage !== undefined) {
|
||||||
|
return <div>{deniedMessage}</div>;
|
||||||
|
}
|
||||||
|
return <a href="/api/auth/signin">Защитено съдържание. Впишете се.. </a>
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProtectedRoute;
|
export default ProtectedRoute;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// import axios from "axios";
|
// import axios from "axios";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { use, useEffect, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@ -15,6 +15,7 @@ import AvailabilityList from "../availability/AvailabilityList";
|
|||||||
import ShiftsList from "../publisher/ShiftsList.tsx";
|
import ShiftsList from "../publisher/ShiftsList.tsx";
|
||||||
import ConfirmationModal from "../ConfirmationModal";
|
import ConfirmationModal from "../ConfirmationModal";
|
||||||
import { UserRole } from "@prisma/client";
|
import { UserRole } from "@prisma/client";
|
||||||
|
import { useSession } from "next-auth/react"
|
||||||
|
|
||||||
// import { Tabs, List } from 'tw-elements'
|
// import { Tabs, List } from 'tw-elements'
|
||||||
|
|
||||||
@ -56,15 +57,14 @@ Array.prototype.groupBy = function (prop) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function PublisherForm({ item, me }) {
|
export default function PublisherForm({ item, me }) {
|
||||||
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
console.log("init PublisherForm: ");
|
const { data: session } = useSession()
|
||||||
|
|
||||||
const urls = {
|
const urls = {
|
||||||
apiUrl: "/api/data/publishers/",
|
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 [helpers, setHelper] = useState(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
@ -202,10 +202,13 @@ export default function PublisherForm({ item, me }) {
|
|||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="label" htmlFor="desiredShiftsPerMonth">Желани смeни на месец</label>
|
<label className="label" htmlFor="desiredShiftsPerMonth">Желани смeни на месец</label>
|
||||||
<input type="number" id="desiredShiftsPerMonth" name="desiredShiftsPerMonth" value={publisher.desiredShiftsPerMonth} onChange={handleChange} className="textbox" placeholder="desiredShiftsPerMonth" autoFocus />
|
<input type="number" id="desiredShiftsPerMonth" name="desiredShiftsPerMonth" value={publisher.desiredShiftsPerMonth} onChange={handleChange} className="textbox" placeholder="desiredShiftsPerMonth" autoFocus />
|
||||||
</div>
|
</div> <div className="mb-4">
|
||||||
<div className="mb-4">
|
|
||||||
<label className="label" htmlFor="email">Имейл</label>
|
<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>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="label" htmlFor="phone">Телефон</label>
|
<label className="label" htmlFor="phone">Телефон</label>
|
||||||
@ -263,10 +266,8 @@ export default function PublisherForm({ item, me }) {
|
|||||||
|
|
||||||
{/* ADMINISTRATORS ONLY */}
|
{/* ADMINISTRATORS ONLY */}
|
||||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" " className="">
|
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" " className="">
|
||||||
|
|
||||||
<PwaManager />
|
|
||||||
|
|
||||||
<div className="border border-blue-500 border-solid p-2">
|
<div className="border border-blue-500 border-solid p-2">
|
||||||
|
<PwaManager />
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="label" htmlFor="type">Тип</label>
|
<label className="label" htmlFor="type">Тип</label>
|
||||||
<select id="type" name="type" value={publisher.type} onChange={handleChange} className="textbox" placeholder="Type" autoFocus >
|
<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 */}
|
{/* Add other roles as needed */}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
{/* ---------------------------- Actions --------------------------------- */}
|
{/* ---------------------------- Actions --------------------------------- */}
|
||||||
|
@ -1,14 +1,26 @@
|
|||||||
import { signIn, signOut, useSession } from "next-auth/react";
|
import { signIn, signOut, useSession } from "next-auth/react";
|
||||||
import styles from "../styles/header.module.css";
|
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 { useRouter } from 'next/router';
|
||||||
import sidemenu, { footerMenu } from './sidemenuData.js'; // Move sidemenu data to a separate file
|
import sidemenu, { footerMenu } from './sidemenuData.js'; // Move sidemenu data to a separate file
|
||||||
import axiosInstance from "src/axiosSecure";
|
import axiosInstance from "src/axiosSecure";
|
||||||
import common from "src/helpers/common";
|
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
|
//get package version from package.json
|
||||||
const packageVersion = require('../package.json').version;
|
const packageVersion = require('../package.json').version;
|
||||||
|
|
||||||
function SidebarMenuItem({ item, session, isSubmenu }) {
|
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 router = useRouter();
|
||||||
const isActive = router.pathname.includes(item.url);
|
const isActive = router.pathname.includes(item.url);
|
||||||
|
|
||||||
@ -91,25 +103,29 @@ export default function Sidebar({ isSidebarOpen, toggleSidebar }) {
|
|||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const sidebarWidth = 226; // Simplify by using a constant
|
const sidebarWidth = 226; // Simplify by using a constant
|
||||||
const sidebarRef = useRef(null);
|
const sidebarRef = useRef(null);
|
||||||
|
const t = useTranslations('menu');
|
||||||
//const [locations, setLocations] = useState([]);
|
//const [locations, setLocations] = useState([]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchLocations = async () => {
|
const fetchLocations = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get('/api/data/locations'); // Adjust the API endpoint as needed
|
if (session) { // Don't fetch locations if the user is not authenticated
|
||||||
const locationsData = response.data
|
|
||||||
.filter(location => location.isActive === true)
|
const response = await axiosInstance.get('/api/data/locations'); // Adjust the API endpoint as needed
|
||||||
.map(location => ({
|
const locationsData = response.data
|
||||||
text: location.name,
|
.filter(location => location.isActive === true)
|
||||||
url: `/cart/locations/${location.id}`,
|
.map(location => ({
|
||||||
}));
|
text: location.name,
|
||||||
// Find the "Locations" menu item and populate its children with locationsData
|
url: `/cart/locations/${location.id}`,
|
||||||
const menuIndex = sidemenu.findIndex(item => item.id === "locations");
|
}));
|
||||||
if (menuIndex !== -1) {
|
// Find the "Locations" menu item and populate its children with locationsData
|
||||||
sidemenu[menuIndex].children = 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) {
|
} catch (error) {
|
||||||
console.error("Error fetching locations:", 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">
|
<div className="flex flex-col justify-between pb-4">
|
||||||
<nav>
|
<nav>
|
||||||
{sidemenu.map((item, index) => (
|
{sidemenu.map((item, index) => (
|
||||||
|
item.text = t(item.id) || item.text, // Use the translation if available
|
||||||
<SidebarMenuItem key={index} item={item} session={session} />
|
<SidebarMenuItem key={index} item={item} session={session} />
|
||||||
))}
|
))}
|
||||||
<hr className="my-3 border-gray-200 dark:border-gray-600" />
|
<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">
|
<div className="mt-auto">
|
||||||
<hr className="border-gray-200 dark:border-gray-600 text-align-bottom" />
|
<hr className="border-gray-200 dark:border-gray-600 text-align-bottom" />
|
||||||
<FooterSection />
|
<FooterSection />
|
||||||
|
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage="">
|
||||||
|
<LanguageSwitcher />
|
||||||
|
</ProtectedRoute >
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@ -162,14 +182,17 @@ function UserSection({ session }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SignInButton() {
|
function SignInButton() {
|
||||||
|
// const t = useTranslations('common');
|
||||||
return (
|
return (
|
||||||
<div className="items-center py-2 font-bold" onClick={() => signIn()}>
|
<div className="items-center py-2 font-bold" onClick={() => signIn()}>
|
||||||
<button>Впишете се</button>
|
<button>вход</button>
|
||||||
|
{/* <button>{t('login')}</button> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserDetails({ session }) {
|
function UserDetails({ session }) {
|
||||||
|
// const t = useTranslations('common');
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<hr className="m-0" />
|
<hr className="m-0" />
|
||||||
@ -180,7 +203,10 @@ function UserDetails({ session }) {
|
|||||||
<div className="ml-3 overflow-hidden">
|
<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-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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -196,13 +222,24 @@ function FooterSection() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
footerMenu.map((item, index) => (
|
footerMenu.map((item, index) => (
|
||||||
<div
|
item.roles ? (
|
||||||
key={index}
|
<ProtectedRoute key={index} allowedRoles={item.roles} deniedMessage="">
|
||||||
className="px-4 py-1 mt-2 cursor-pointer hover:underline hover:text-blue-600 dark:hover:text-blue-400 "
|
<div
|
||||||
onClick={() => navigateTo(item.url)}
|
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>
|
<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>
|
||||||
|
)
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
import { UserRole } from "@prisma/client";
|
import { UserRole } from "@prisma/client";
|
||||||
|
|
||||||
|
|
||||||
const sidemenu = [
|
const sidemenu = [
|
||||||
{
|
{
|
||||||
id: "dashboard",
|
id: "dashboard",
|
||||||
@ -112,6 +113,17 @@ const sidemenu = [
|
|||||||
text: "Статистика",
|
text: "Статистика",
|
||||||
url: "/cart/publishers/stats",
|
url: "/cart/publishers/stats",
|
||||||
roles: [UserRole.ADMIN, UserRole.POWERUSER],
|
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
36
content/i18n/bg.json
Normal 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": "Преводи"
|
||||||
|
}
|
||||||
|
}
|
36
content/i18n/bg.modified.json
Normal file
36
content/i18n/bg.modified.json
Normal 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
15
content/i18n/en.json
Normal 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
15
content/i18n/ru.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"greeting": "Здравей",
|
||||||
|
"farewell": "Довиждане",
|
||||||
|
"changeTo": "Смени на",
|
||||||
|
"BG": "[RU] Български",
|
||||||
|
"EN": "[RU] Английски",
|
||||||
|
"RU": "[RU] Руски",
|
||||||
|
"login": "вход",
|
||||||
|
"contacts": "Контакти"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"dashboard": "Начало"
|
||||||
|
}
|
||||||
|
}
|
15
content/i18n/ru.modified.json
Normal file
15
content/i18n/ru.modified.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"greeting": "Здравей",
|
||||||
|
"farewell": "Довиждане",
|
||||||
|
"changeTo": "Смени на",
|
||||||
|
"BG": "болгарский",
|
||||||
|
"EN": "английский",
|
||||||
|
"RU": "русский",
|
||||||
|
"login": "вход",
|
||||||
|
"contacts": "Контакти te"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"dashboard": "Начало"
|
||||||
|
}
|
||||||
|
}
|
@ -50,4 +50,12 @@ module.exports = withPWA({
|
|||||||
|
|
||||||
return config;
|
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
423
package-lock.json
generated
@ -25,6 +25,8 @@
|
|||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"axios-jwt": "^4.0.2",
|
"axios-jwt": "^4.0.2",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
"docx": "^8.5.0",
|
"docx": "^8.5.0",
|
||||||
"docx-templates": "^4.11.4",
|
"docx-templates": "^4.11.4",
|
||||||
@ -55,6 +57,7 @@
|
|||||||
"next": "^14.1.0",
|
"next": "^14.1.0",
|
||||||
"next-auth": "^4.24.6",
|
"next-auth": "^4.24.6",
|
||||||
"next-connect": "^1.0.0",
|
"next-connect": "^1.0.0",
|
||||||
|
"next-intl": "^3.12.0",
|
||||||
"next-pwa": "^5.6.0",
|
"next-pwa": "^5.6.0",
|
||||||
"node-excel-export": "^1.4.4",
|
"node-excel-export": "^1.4.4",
|
||||||
"node-telegram-bot-api": "^0.64.0",
|
"node-telegram-bot-api": "^0.64.0",
|
||||||
@ -2194,6 +2197,92 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
|
||||||
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
|
"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": {
|
"node_modules/@heroicons/react": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.1.tgz",
|
||||||
@ -2774,6 +2863,69 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@mui/base": {
|
||||||
"version": "5.0.0-beta.36",
|
"version": "5.0.0-beta.36",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.36.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||||
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
|
"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": {
|
"node_modules/archiver": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
|
||||||
@ -4967,6 +5124,18 @@
|
|||||||
"safe-buffer": "~5.1.0"
|
"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": {
|
"node_modules/arg": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
"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": {
|
"node_modules/bcrypt-pbkdf": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||||
@ -5317,6 +5499,11 @@
|
|||||||
"tweetnacl": "^0.14.3"
|
"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": {
|
"node_modules/bidi-js": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||||
@ -5754,6 +5941,14 @@
|
|||||||
"fsevents": "~2.3.2"
|
"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": {
|
"node_modules/chrome-trace-event": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
|
||||||
@ -5981,6 +6176,14 @@
|
|||||||
"simple-swizzle": "^0.2.2"
|
"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": {
|
"node_modules/color/node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@ -6110,6 +6313,11 @@
|
|||||||
"safe-buffer": "~5.1.0"
|
"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": {
|
"node_modules/content-disposition": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
@ -6504,6 +6712,11 @@
|
|||||||
"node": ">=0.4.0"
|
"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": {
|
"node_modules/depcheck": {
|
||||||
"version": "1.4.7",
|
"version": "1.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/depcheck/-/depcheck-1.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/depcheck/-/depcheck-1.4.7.tgz",
|
||||||
@ -7933,6 +8146,28 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/gapi-script/-/gapi-script-1.2.0.tgz",
|
||||||
"integrity": "sha512-NKTVKiIwFdkO1j1EzcrWu/Pz7gsl1GmBmgh+qhuV2Ytls04W/Eg5aiBL91SCiBM9lU0PMu7p1hTVxhh1rPT5Lw=="
|
"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": {
|
"node_modules/gaxios": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.3.0.tgz",
|
||||||
@ -8503,6 +8757,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/hasha": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz",
|
||||||
@ -8947,6 +9206,34 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/invariant": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||||
@ -10428,6 +10715,37 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/mkdirp": {
|
||||||
"version": "0.5.6",
|
"version": "0.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||||
@ -10681,6 +10999,26 @@
|
|||||||
"node": ">=16"
|
"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": {
|
"node_modules/next-pwa": {
|
||||||
"version": "5.6.0",
|
"version": "5.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/next-pwa/-/next-pwa-5.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/next-pwa/-/next-pwa-5.6.0.tgz",
|
||||||
@ -10754,6 +11092,11 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"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": {
|
"node_modules/node-excel-export": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/node-excel-export/-/node-excel-export-1.4.4.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"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": {
|
"node_modules/normalize-package-data": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||||
@ -13649,6 +14006,17 @@
|
|||||||
"inBundle": true,
|
"inBundle": true,
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/oauth": {
|
||||||
"version": "0.9.15",
|
"version": "0.9.15",
|
||||||
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
||||||
@ -15620,6 +15988,11 @@
|
|||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/set-function-length": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
|
||||||
@ -15738,8 +16111,7 @@
|
|||||||
"node_modules/signal-exit": {
|
"node_modules/signal-exit": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/simple-swizzle": {
|
"node_modules/simple-swizzle": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
@ -16523,6 +16895,22 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/tar-stream": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||||
@ -16538,6 +16926,17 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/temp-dir": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
|
||||||
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw=="
|
"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": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@ -17771,6 +18182,14 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/winston": {
|
||||||
"version": "3.13.0",
|
"version": "3.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/winston/-/winston-3.13.0.tgz",
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"axios-jwt": "^4.0.2",
|
"axios-jwt": "^4.0.2",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
"docx": "^8.5.0",
|
"docx": "^8.5.0",
|
||||||
"docx-templates": "^4.11.4",
|
"docx-templates": "^4.11.4",
|
||||||
@ -72,6 +73,7 @@
|
|||||||
"next": "^14.1.0",
|
"next": "^14.1.0",
|
||||||
"next-auth": "^4.24.6",
|
"next-auth": "^4.24.6",
|
||||||
"next-connect": "^1.0.0",
|
"next-connect": "^1.0.0",
|
||||||
|
"next-intl": "^3.12.0",
|
||||||
"next-pwa": "^5.6.0",
|
"next-pwa": "^5.6.0",
|
||||||
"node-excel-export": "^1.4.4",
|
"node-excel-export": "^1.4.4",
|
||||||
"node-telegram-bot-api": "^0.64.0",
|
"node-telegram-bot-api": "^0.64.0",
|
||||||
@ -114,4 +116,4 @@
|
|||||||
"depcheck": "^1.4.7",
|
"depcheck": "^1.4.7",
|
||||||
"prisma": "^5.13.0"
|
"prisma": "^5.13.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,9 +4,14 @@ import "../styles/styles.css"
|
|||||||
import "../styles/global.css"
|
import "../styles/global.css"
|
||||||
import "tailwindcss/tailwind.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 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
|
// for fontawesome
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { LocalizationProvider } from '@mui/x-date-pickers';
|
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||||
@ -22,10 +27,25 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
export default function App({
|
//function SmwsApp({ Component, pageProps: { locale, messages, session, ...pageProps }, }: AppProps<{ session: Session }>) {
|
||||||
Component,
|
function SmwsApp({ Component, pageProps, session, locale, messages }) {
|
||||||
pageProps: { session, ...pageProps },
|
// dynamic locale loading using our API endpoint
|
||||||
}: AppProps<{ session: Session }>) {
|
// 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(() => {
|
useEffect(() => {
|
||||||
const use = async () => {
|
const use = async () => {
|
||||||
@ -67,11 +87,40 @@ export default function App({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SessionProvider session={session} >
|
<NextIntlClientProvider
|
||||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
locale={locale}
|
||||||
<Component {...pageProps} />
|
timeZone="Europe/Sofia"
|
||||||
</LocalizationProvider>
|
messages={messages}
|
||||||
</SessionProvider>
|
>
|
||||||
|
<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;
|
@ -13,7 +13,7 @@ class MyDocument extends Document {
|
|||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<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-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>
|
<link rel="apple-touch-icon" href="/old-192x192.png"></link>
|
||||||
</Head>
|
</Head>
|
||||||
|
@ -8,6 +8,7 @@ import AppleProvider from "next-auth/providers/apple"
|
|||||||
import EmailProvider from "next-auth/providers/email"
|
import EmailProvider from "next-auth/providers/email"
|
||||||
import CredentialsProvider from "next-auth/providers/credentials"
|
import CredentialsProvider from "next-auth/providers/credentials"
|
||||||
import { PrismaAdapter } from "@auth/prisma-adapter"
|
import { PrismaAdapter } from "@auth/prisma-adapter"
|
||||||
|
import bcrypt from "bcrypt"
|
||||||
|
|
||||||
//microsoft
|
//microsoft
|
||||||
import AzureADProvider from "next-auth/providers/azure-ad";
|
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");
|
const common = require("../../../src/helpers/common");
|
||||||
import { isLoggedIn, setAuthTokens, clearAuthTokens, getAccessToken, getRefreshToken } from 'axios-jwt'
|
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)
|
// console.log(process.env.EMAIL_SERVER)
|
||||||
// For more information on each option (and a full list of options) go to
|
// For more information on each option (and a full list of options) go to
|
||||||
// https://next-auth.js.org/configuration/options
|
// https://next-auth.js.org/configuration/options
|
||||||
@ -52,6 +55,7 @@ export const authOptions: NextAuthOptions = {
|
|||||||
// tenantId: process.env.AZURE_AD_TENANT_ID,
|
// tenantId: process.env.AZURE_AD_TENANT_ID,
|
||||||
// }),
|
// }),
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
|
id: 'credentials',
|
||||||
// The name to display on the sign in form (e.g. 'Sign in with...')
|
// The name to display on the sign in form (e.g. 'Sign in with...')
|
||||||
name: 'Credentials',
|
name: 'Credentials',
|
||||||
credentials: {
|
credentials: {
|
||||||
@ -80,17 +84,51 @@ export const authOptions: NextAuthOptions = {
|
|||||||
{ id: "3", name: "popov", email: "popov@example.com", password: "popov123", role: "ADMIN" }
|
{ 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 =>
|
const user = users.find(user =>
|
||||||
user.name === credentials.username && user.password === credentials.password
|
user.name === credentials.username && user.password === credentials.password
|
||||||
);
|
);
|
||||||
|
|
||||||
// If a matching user is found, return the user data, otherwise return null
|
|
||||||
if (user) {
|
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();
|
var prisma = common.getPrismaClient();
|
||||||
|
|
||||||
console.log("[nextauth] signIn:", account.provider, user.email)
|
console.log("[nextauth] signIn:", account.provider, user.email)
|
||||||
if (account.provider === 'google') {
|
//if (account.provider === 'google' ) {
|
||||||
try {
|
|
||||||
// Check user in your database and assign roles
|
|
||||||
const dbUser = await prisma.publisher.findUnique({
|
|
||||||
where: { email: user.email }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (dbUser) {
|
// Check user in your database and assign roles
|
||||||
// Assign roles from your database to the session
|
const dbUser = await prisma.publisher.findUnique({
|
||||||
user.role = dbUser.role;
|
where: { email: user.email }
|
||||||
user.id = dbUser.id;
|
});
|
||||||
//user.permissions = dbUser.permissions;
|
|
||||||
const session = { ...user };
|
|
||||||
|
|
||||||
await prisma.publisher.update({
|
if (dbUser) {
|
||||||
where: { id: dbUser.id },
|
// Assign roles from your database to the session
|
||||||
data: { lastLogin: new Date() }
|
user.role = dbUser.role;
|
||||||
});
|
user.id = dbUser.id;
|
||||||
return true; // Sign-in successful
|
//user.permissions = dbUser.permissions;
|
||||||
} else {
|
const session = { ...user };
|
||||||
// Optionally create a new user in your DB
|
|
||||||
// Or return false to deny access
|
await prisma.publisher.update({
|
||||||
//Let's customize the error message to give a better user experience
|
where: { id: dbUser.id },
|
||||||
throw new Error(`Твоят имейл '${user.email}' не е регистриран в системата. Моля свържи се с нас за да те регистрираме ако искаш да ползваш този имейл.`);
|
data: { lastLogin: new Date() }
|
||||||
}
|
});
|
||||||
} catch (e) {
|
return true;
|
||||||
console.log(e);
|
} 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
|
// Persist the OAuth access_token to the token right after signin
|
||||||
async jwt({ token, user, account, profile, isNewUser }) {
|
async jwt({ token, user, account, profile, isNewUser }) {
|
||||||
//!console.log("[nextauth] JWT", token, user)
|
//!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)
|
@ -31,9 +31,15 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
const targetTable = req.query.nextcrud[0];
|
const targetTable = req.query.nextcrud[0];
|
||||||
//get target action
|
//get target action
|
||||||
if (req.method === 'DELETE') {
|
if (req.method === 'DELETE') {
|
||||||
|
switch (targetTable) {
|
||||||
const targetId = req.query.nextcrud[1];
|
case 'publishers':
|
||||||
logger.info('[nextCrud] ' + targetTable + ': ' + targetId + 'DELETED by ' + session.user.email);
|
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);
|
return nextCrudHandler(req, res);
|
||||||
}
|
}
|
||||||
|
101
pages/api/data/prisma/[...model].ts
Normal file
101
pages/api/data/prisma/[...model].ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
@ -34,7 +34,8 @@ export default async function handler(req, res) {
|
|||||||
// Retrieve and validate the JWT token
|
// Retrieve and validate the JWT token
|
||||||
|
|
||||||
//response is a special action that does not require a 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) {
|
switch (emailaction) {
|
||||||
case "coverMeAccept":
|
case "coverMeAccept":
|
||||||
//validate shiftId and assignmentId
|
//validate shiftId and assignmentId
|
||||||
@ -94,7 +95,7 @@ export default async function handler(req, res) {
|
|||||||
res.redirect(messagePageUrl);
|
res.redirect(messagePageUrl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let originalPublisher = assignment.publisher;
|
||||||
let to = assignment.shift.assignments.map(a => a.publisher.email);
|
let to = assignment.shift.assignments.map(a => a.publisher.email);
|
||||||
to.push(publisher.email);
|
to.push(publisher.email);
|
||||||
|
|
||||||
@ -137,7 +138,7 @@ export default async function handler(req, res) {
|
|||||||
publisher: { connect: { id: publisher.id } },
|
publisher: { connect: { id: publisher.id } },
|
||||||
shift: { connect: { id: assignment.shiftId } },
|
shift: { connect: { id: assignment.shiftId } },
|
||||||
type: EventLogType.AssignmentReplacementAccepted,
|
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;
|
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
|
// //send email response to the user
|
||||||
// const emailResponse = await common.sendEmail(user.email, "Email Action Processed",
|
// 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) {
|
switch (action) {
|
||||||
case "sendCoverMeRequestByEmail":
|
case "sendCoverMeRequestByEmail":
|
||||||
// Send CoverMe request to the users
|
// Send CoverMe request to the users
|
||||||
@ -230,7 +309,6 @@ export default async function handler(req, res) {
|
|||||||
let toSubscribed = req.body.toSubscribed;
|
let toSubscribed = req.body.toSubscribed;
|
||||||
let toAvailable = req.body.toAvailable;
|
let toAvailable = req.body.toAvailable;
|
||||||
|
|
||||||
|
|
||||||
let assignment = await prisma.assignment.findUnique({
|
let assignment = await prisma.assignment.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: parseInt(assignmentId)
|
id: parseInt(assignmentId)
|
||||||
@ -248,7 +326,6 @@ export default async function handler(req, res) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// update the assignment. generate new publicGuid, isConfirmed to false
|
// update the assignment. generate new publicGuid, isConfirmed to false
|
||||||
let newPublicGuid = uuidv4();
|
let newPublicGuid = uuidv4();
|
||||||
await prisma.assignment.update({
|
await prisma.assignment.update({
|
||||||
@ -269,25 +346,6 @@ export default async function handler(req, res) {
|
|||||||
targetEmails.availablePublishers = [];
|
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
|
//concat and remove duplicate emails
|
||||||
let pubsToSend = targetEmails.subscribedPublishers.concat(targetEmails.availablePublishers).
|
let pubsToSend = targetEmails.subscribedPublishers.concat(targetEmails.availablePublishers).
|
||||||
filter((item, index, self) =>
|
filter((item, index, self) =>
|
||||||
@ -301,10 +359,10 @@ export default async function handler(req, res) {
|
|||||||
data: {
|
data: {
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
publisher: { connect: { id: publisher.id } },
|
publisher: { connect: { id: publisher.id } },
|
||||||
shift: { connect: { id: assignment.shiftId } },
|
shift: { connect: { id: assignment.shift.id } },
|
||||||
type: EventLogType.AssignmentReplacementRequested,
|
type: EventLogType.AssignmentReplacementRequested,
|
||||||
content: "Заявка за заместване от " + publisher.firstName + " " + publisher.lastName
|
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 + "");
|
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++) {
|
for (let i = 0; i < pubsToSend.length; i++) {
|
||||||
|
|
||||||
//send email to subscribed publisher
|
//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 ? "Брат" : "Сестра";
|
publisher.prefix = publisher.isMale ? "Брат" : "Сестра";
|
||||||
|
|
||||||
let model = {
|
let model = {
|
||||||
user: publisher,
|
user: publisher,
|
||||||
shiftId: assignment.shiftId,
|
shiftId: assignment.shift.id,
|
||||||
acceptUrl: acceptUrl,
|
acceptUrl: acceptUrl,
|
||||||
firstName: pubsToSend[i].firstName,
|
firstName: pubsToSend[i].firstName,
|
||||||
lastName: pubsToSend[i].lastName,
|
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(400).json({ message: "Invalid action" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json({ message: "User action processed" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
84
pages/api/translations/[...locale].ts
Normal file
84
pages/api/translations/[...locale].ts
Normal 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`);
|
||||||
|
}
|
||||||
|
}
|
135
pages/auth/reset-password.tsx
Normal file
135
pages/auth/reset-password.tsx
Normal 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
132
pages/auth/signin.tsx
Normal 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),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import axiosServer from '../../../../src/axiosServer';
|
import axiosServer from '../../../../src/axiosServer';
|
||||||
import NewPubPage from "../new";
|
import NewPubPage from "../new";
|
||||||
|
const common = require('../../../../src/helpers/common');
|
||||||
export default NewPubPage;
|
export default NewPubPage;
|
||||||
|
|
||||||
import { Assignment, Shift, UserRole, AvailabilityType } from "prisma/prisma-client";
|
import { Assignment, Shift, UserRole, AvailabilityType } from "prisma/prisma-client";
|
||||||
@ -40,6 +41,8 @@ function getShiftGroups(shifts: [Shift]) {
|
|||||||
export const getServerSideProps = async (context) => {
|
export const getServerSideProps = async (context) => {
|
||||||
const axios = await axiosServer(context);
|
const axios = await axiosServer(context);
|
||||||
|
|
||||||
|
const prisma = common.getPrismaClient();
|
||||||
|
|
||||||
// const isAdmin = await ProtectedRoute.IsInRole(UserRole.ADMIN); does not work on server side
|
// const isAdmin = await ProtectedRoute.IsInRole(UserRole.ADMIN); does not work on server side
|
||||||
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
||||||
if (!context.query || !context.query.id) {
|
if (!context.query || !context.query.id) {
|
||||||
@ -47,10 +50,28 @@ export const getServerSideProps = async (context) => {
|
|||||||
props: {}
|
props: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
var url = process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,assignments,assignments.shift";
|
const item = await prisma.publisher.findUnique({
|
||||||
console.log("GET PUBLISHER FROM:" + url)
|
where: {
|
||||||
const { data: item } = await axios.get(url);
|
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);
|
// item.allShifts = item.assignments.map((a: Assignment[]) => a.shift);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@ -101,7 +122,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
// console.dir(item, { depth: null });
|
// console.dir(item, { depth: null });
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
item: item
|
item: common.convertDatesToISOStrings(item),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -30,7 +30,8 @@ function PublishersPage({ publishers = [] }: IProps) {
|
|||||||
const [showZeroShiftsOnly, setShowZeroShiftsOnly] = useState(false);
|
const [showZeroShiftsOnly, setShowZeroShiftsOnly] = useState(false);
|
||||||
const [isDeleting, setIsDeleting] = 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 () => {
|
const handleDeleteAllVisible = async () => {
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
@ -45,7 +46,7 @@ function PublishersPage({ publishers = [] }: IProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
setIsModalOpen(false);
|
setIsModalOpenDeleteAllVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteAllAvailabilities = async () => {
|
const handleDeleteAllAvailabilities = async () => {
|
||||||
@ -60,7 +61,7 @@ function PublishersPage({ publishers = [] }: IProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
setIsModalOpen(false);
|
setIsModalOpenDeleteAllAvaillabilities(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -108,20 +109,20 @@ function PublishersPage({ publishers = [] }: IProps) {
|
|||||||
if (shownPubs.length === 0) {
|
if (shownPubs.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<a
|
<button
|
||||||
className="btn"
|
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"
|
||||||
href="javascript:void(0);"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFilter("");
|
setFilter(""); // Assuming setFilter directly updates the filter state
|
||||||
handleFilterChange({ target: { value: "" } });
|
if (typeof handleFilterChange === 'function') {
|
||||||
|
handleFilterChange({ target: { value: "" } }); // If needed for additional logic
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Clear filters
|
Clear filters
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return shownPubs.map((publisher) => (
|
return shownPubs.map((publisher) => (
|
||||||
<PublisherCard key={publisher.id} publisher={publisher} />
|
<PublisherCard key={publisher.id} publisher={publisher} />
|
||||||
));
|
));
|
||||||
@ -166,38 +167,40 @@ function PublishersPage({ publishers = [] }: IProps) {
|
|||||||
<div className="">
|
<div className="">
|
||||||
<div className="flex items-center justify-center space-x-4 m-4">
|
<div className="flex items-center justify-center space-x-4 m-4">
|
||||||
<div className="flex justify-center 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>
|
</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 ? "Изтриване..." : "Изтрий показаните вестители"}
|
{isDeleting ? "Изтриване..." : "Изтрий показаните вестители"}
|
||||||
</button>
|
</button>
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpenDeleteAllVisible}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={() => setIsModalOpenDeleteAllVisible(false)}
|
||||||
onConfirm={handleDeleteAllVisible}
|
onConfirm={handleDeleteAllVisible}
|
||||||
message="Сигурни ли сте, че искате да изтриете всички показани в момента вестители?"
|
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 ? "Изтриване..." : "Изтрий ВСИЧКИ предпочитания"}
|
{isDeleting ? "Изтриване..." : "Изтрий ВСИЧКИ предпочитания"}
|
||||||
</button>
|
</button>
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpenDeleteAllAvaillabilities}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={() => setIsModalOpenDeleteAllAvaillabilities(false)}
|
||||||
onConfirm={handleDeleteAllAvailabilities}
|
onConfirm={handleDeleteAllAvailabilities}
|
||||||
message="Сигурни ли сте, че искате да изтриете предпочитанията на ВСИЧКИ вестители?"
|
message="Сигурни ли сте, че искате да изтриете предпочитанията на ВСИЧКИ вестители?"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-center m-4">
|
<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>
|
</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">
|
<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>
|
<label htmlFor="filter">Filter:</label>
|
||||||
<input type="text" id="filter" name="filter" value={filter} onChange={handleFilterChange}
|
<input type="text" id="filter" name="filter" value={filter} onChange={handleFilterChange}
|
||||||
className="border border-gray-300 rounded-md px-2 py-1"
|
className="border border-gray-300 rounded-md px-2 py-1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<label htmlFor="zeroShiftsOnly" className="ml-4 inline-flex items-center">
|
<label htmlFor="zeroShiftsOnly" className="ml-4 inline-flex items-center">
|
||||||
<input type="checkbox" id="zeroShiftsOnly" checked={showZeroShiftsOnly}
|
<input type="checkbox" id="zeroShiftsOnly" checked={showZeroShiftsOnly}
|
||||||
onChange={e => setShowZeroShiftsOnly(e.target.checked)}
|
onChange={e => setShowZeroShiftsOnly(e.target.checked)}
|
||||||
@ -207,8 +210,8 @@ function PublishersPage({ publishers = [] }: IProps) {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<span id="filter-info" className="ml-4">{publishers.length} от {publishers.length} вестителя</span>
|
<span id="filter-info" className="ml-4">{publishers.length} от {publishers.length} вестителя</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 grid-cols-1 md:grid-cols-4 z-0">
|
<div className="grid gap-4 grid-cols-1 md:grid-cols-4 z-0">
|
||||||
{renderPublishers()}
|
{renderPublishers()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -161,7 +161,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
if (!session) {
|
if (!session) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: '/auth/login', // Adjust the login path as needed
|
destination: '/auth/signin', // Adjust the login path as needed
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
126
pages/cart/reports/coverMe.tsx
Normal file
126
pages/cart/reports/coverMe.tsx
Normal 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 >
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
110
pages/cart/translations/index.tsx
Normal file
110
pages/cart/translations/index.tsx
Normal 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;
|
@ -1,12 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Layout from "../components/layout";
|
import Layout from "../components/layout";
|
||||||
import FeedbackForm from "../components/reports/FeedbackForm";
|
import FeedbackForm from "../components/reports/FeedbackForm";
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const ContactsPage = () => {
|
const ContactsPage = () => {
|
||||||
|
const t = useTranslations('common');
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="mx-auto my-8 p-6 max-w-4xl bg-white rounded-lg shadow-md">
|
<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">
|
<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 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>
|
<li className="text-gray-700">Крейг Смит - <a href="tel:+359878994573" className="text-blue-500 hover:text-blue-600">+359 878 994 573</a></li>
|
||||||
|
@ -204,6 +204,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
props: {
|
props: {
|
||||||
initialItems: items,
|
initialItems: items,
|
||||||
userId: session?.user.id,
|
userId: session?.user.id,
|
||||||
|
// messages: (await import(`../content/i18n/${context.locale}.json`)).default
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -10,14 +10,30 @@ export default function MessagePage() {
|
|||||||
warning: "text-yellow-500",
|
warning: "text-yellow-500",
|
||||||
info: "text-blue-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 (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className={`text-2xl font-bold mb-4 ${messageStyles[type]}`}>{caption || 'Информация'}</h1>
|
<h1 className={`text-4xl font-bold mb-4 ${messageStyles[type]}`}>{caption || 'Информация'}</h1>
|
||||||
<p className="mb-6">
|
<p className="text-xl mb-6">
|
||||||
{message || 'Така ще получавате различни съобщения.'}
|
{message || 'Така ще получавате различни съобщения.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
BIN
prisma/backups/jwpwsofia-20240430-bak.gz
Normal file
BIN
prisma/backups/jwpwsofia-20240430-bak.gz
Normal file
Binary file not shown.
3
prisma/migrations/20240429133812_/migration.sql
Normal file
3
prisma/migrations/20240429133812_/migration.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `User`
|
||||||
|
ADD COLUMN `passwordHashLocalAccount` VARCHAR(191) NULL;
|
@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `EventLog`
|
||||||
|
MODIFY `type` ENUM(
|
||||||
|
'AssignmentReplacementRequested', 'AssignmentReplacementAccepted', 'SentEmail', 'PasswordResetRequested', 'PasswordResetEmailConfirmed', 'PasswordResetCompleted'
|
||||||
|
) NOT NULL;
|
@ -263,6 +263,9 @@ enum EventLogType {
|
|||||||
AssignmentReplacementRequested
|
AssignmentReplacementRequested
|
||||||
AssignmentReplacementAccepted
|
AssignmentReplacementAccepted
|
||||||
SentEmail
|
SentEmail
|
||||||
|
PasswordResetRequested
|
||||||
|
PasswordResetEmailConfirmed
|
||||||
|
PasswordResetCompleted
|
||||||
}
|
}
|
||||||
|
|
||||||
model EventLog {
|
model EventLog {
|
||||||
@ -278,13 +281,14 @@ model EventLog {
|
|||||||
|
|
||||||
//user auth and session management
|
//user auth and session management
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String?
|
name String?
|
||||||
email String? @unique
|
email String? @unique
|
||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
image String?
|
image String?
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
|
passwordHashLocalAccount String? // New field to store the hashed password
|
||||||
|
|
||||||
// Optional relation to Publisher
|
// Optional relation to Publisher
|
||||||
publisherId String? @unique
|
publisherId String? @unique
|
||||||
|
@ -697,30 +697,30 @@ exports.copyToClipboard = function (event, text) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
exports.getUser = async function (req) {
|
// exports.getUser = async function (req) {
|
||||||
// Use req if provided (server-side), otherwise call getSession without args (client-side)
|
// // Use req if provided (server-side), otherwise call getSession without args (client-side)
|
||||||
const session = req ? await getSession({ req }) : await getSession();
|
// const session = req ? await getSession({ req }) : await getSession();
|
||||||
return session?.user;
|
// return session?.user;
|
||||||
}
|
// }
|
||||||
|
|
||||||
exports.isUserInRole = async function (req, allowedRoles = []) {
|
// exports.isUserInRole = function (req, allowedRoles = []) {
|
||||||
const user = await exports.getUser(req);
|
// const user = exports.getUser(req);
|
||||||
|
|
||||||
// Check if the user is authenticated
|
// // Check if the user is authenticated
|
||||||
if (!user) {
|
// if (!user) {
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// If no specific roles are required, return true as the user is authenticated
|
// // If no specific roles are required, return true as the user is authenticated
|
||||||
if (allowedRoles.length === 0) {
|
// if (allowedRoles.length === 0) {
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Check if the user's role is among the allowed roles
|
// // Check if the user's role is among the allowed roles
|
||||||
// Ensure role exists and is a valid UserRole
|
// // Ensure role exists and is a valid UserRole
|
||||||
const userRole = user.role;
|
// const userRole = user.role;
|
||||||
return allowedRoles.includes(userRole);
|
// return allowedRoles.includes(userRole);
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -762,6 +762,37 @@ exports.getInitials = function (names) {
|
|||||||
exports.addMinutes = function (date, minutes) {
|
exports.addMinutes = function (date, minutes) {
|
||||||
return new Date(date.getTime() + minutes * 60000); // 60000 milliseconds in a minute
|
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) {
|
// exports.getInitials = function (names) {
|
||||||
// const parts = names.split(' '); // Split the full name into parts
|
// const parts = names.split(' '); // Split the full name into parts
|
||||||
// if (parts.length === 0) {
|
// if (parts.length === 0) {
|
||||||
|
23
src/templates/emails/emailConfirm.hbs
Normal file
23
src/templates/emails/emailConfirm.hbs
Normal 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>
|
22
src/templates/emails/resetPass.hbs
Normal file
22
src/templates/emails/resetPass.hbs
Normal 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>
|
Reference in New Issue
Block a user