Merge branch 'feature-announcements'

This commit is contained in:
Dobromir Popov
2024-04-30 02:58:11 +03:00
24 changed files with 802 additions and 95 deletions

View File

@ -39,7 +39,7 @@ const LanguageSwitcher = () => {
open={Boolean(anchorEl)}
onClose={handleClose}
>
{locales.map((lng) => {
{locales?.map((lng) => {
if (lng === locale) return null;
return (
<MenuItem key={lng} onClick={() => changeLanguage(lng)}>

View File

@ -10,9 +10,10 @@ interface ProtectedRouteProps {
allowedRoles: UserRole[];
deniedMessage?: string;
bypass?: boolean;
autoRedirect?: boolean;
}
const ProtectedRoute = ({ children, allowedRoles, deniedMessage, bypass = false }: ProtectedRouteProps) => {
const ProtectedRoute = ({ children, allowedRoles, deniedMessage, bypass = false, autoRedirect = false }: ProtectedRouteProps) => {
const { data: session, status } = useSession()
const router = useRouter();
@ -20,10 +21,9 @@ const ProtectedRoute = ({ children, allowedRoles, deniedMessage, bypass = false
console.log("session.role:" + session?.user?.role);
if (!status || status === "unauthenticated") {
// Redirect to the sign-in page
if (!bypass) {
if (autoRedirect) {
signIn();
}
return null;
}
else {
console.log("session.role:" + session?.user?.role);
@ -41,14 +41,27 @@ const ProtectedRoute = ({ children, allowedRoles, deniedMessage, bypass = false
if (deniedMessage !== undefined) {
return <div>{deniedMessage}</div>;
}
return <div>Нямате достъп до тази страница. Ако мислите, че това е грешка, моля, свържете се с администраторите</div>;
return (
<>
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4 text-blue-500">{session?.user?.email},</h1>
<p className="mb-6">{`Нямате достъп до тази страница. Ако мислите, че това е грешка, моля, свържете се с администраторите`}</p>
</div>
</div>
</>);
}
if (status === "loading") {
return <div>Зареждане...</div>;
}
if (!session) return <a href="/api/auth/signin">Защитено съдържание. Впишете се.. </a>
return children;
if (!session) {
if (deniedMessage !== undefined) {
return <div>{deniedMessage}</div>;
}
return <a href="/api/auth/signin">Защитено съдържание. Впишете се.. </a>
}
};
export default ProtectedRoute;

View File

@ -105,19 +105,22 @@ export default function Sidebar({ isSidebarOpen, toggleSidebar }) {
useEffect(() => {
const fetchLocations = async () => {
try {
const response = await axiosInstance.get('/api/data/locations'); // Adjust the API endpoint as needed
const locationsData = response.data
.filter(location => location.isActive === true)
.map(location => ({
text: location.name,
url: `/cart/locations/${location.id}`,
}));
// Find the "Locations" menu item and populate its children with locationsData
const menuIndex = sidemenu.findIndex(item => item.id === "locations");
if (menuIndex !== -1) {
sidemenu[menuIndex].children = locationsData;
if (session) { // Don't fetch locations if the user is not authenticated
const response = await axiosInstance.get('/api/data/locations'); // Adjust the API endpoint as needed
const locationsData = response.data
.filter(location => location.isActive === true)
.map(location => ({
text: location.name,
url: `/cart/locations/${location.id}`,
}));
// Find the "Locations" menu item and populate its children with locationsData
const menuIndex = sidemenu.findIndex(item => item.id === "locations");
if (menuIndex !== -1) {
sidemenu[menuIndex].children = locationsData;
}
//setLocations(locationsData); // Optional, if you need to use locations elsewhere
}
//setLocations(locationsData); // Optional, if you need to use locations elsewhere
} catch (error) {
console.error("Error fetching locations:", error);
}
@ -174,16 +177,17 @@ function UserSection({ session }) {
}
function SignInButton() {
const t = useTranslations('common');
// const t = useTranslations('common');
return (
<div className="items-center py-2 font-bold" onClick={() => signIn()}>
<button>{t('login')}</button>
<button>вход</button>
{/* <button>{t('login')}</button> */}
</div>
);
}
function UserDetails({ session }) {
const t = useTranslations('common');
// const t = useTranslations('common');
return (
<>
<hr className="m-0" />
@ -194,7 +198,10 @@ function UserDetails({ session }) {
<div className="ml-3 overflow-hidden">
<p className="mx-1 mt-1 text-sm font-medium text-gray-800 dark:text-gray-200">{session.user.name}</p>
<p className="mx-1 mt-1 text-sm font-medium text-gray-600 dark:text-gray-400">{session.user.role}</p>
<a href="/api/auth/signout" className={styles.button} onClick={(e) => { e.preventDefault(); signOut(); }}>{t('logout')}</a>
<a href="/api/auth/signout" className={styles.button} onClick={(e) => { e.preventDefault(); signOut(); }}>
{/* {t('logout')} */}
изход
</a>
</div>
</div>
</>

View File

@ -4,7 +4,7 @@
"contacts": "Контакти",
"greeting": "Здравей",
"farewell": "Довиждане",
"changeTo": "-",
"changeTo": "",
"BG": "Български",
"EN": "Английски",
"RU": "Руски",

View File

@ -1,11 +1,13 @@
{
"common": {
"appNameLong": "Специално свидетелстване на обществени места в София",
"contacts": "Контакти",
"greeting": "Здравей",
"farewell": "Довиждане",
"changeTo": "",
"BG": "Български",
"EN": "Английски",
"RU": "Руски",
"BG": "български",
"EN": "английски",
"RU": "руски",
"login": "Вход",
"logout": "Изход"
},

View File

@ -8,5 +8,8 @@
"RU": "Russian",
"login": "login",
"logout": "logout"
},
"menu": {
"dashboard": "Dashboard"
}
}

View File

@ -1,15 +0,0 @@
{
"common": {
"greeting": "Hello",
"farewell": "Goodbye",
"changeTo": "Change to",
"BG": "Bulgarian",
"EN": "English",
"RU": "Russian",
"login": "login",
"logout": "logout"
},
"menu": {
"dashboard": "Dashboard"
}
}

View File

@ -6,7 +6,8 @@
"BG": "[RU] Български",
"EN": "[RU] Английски",
"RU": "[RU] Руски",
"login": "вход"
"login": "вход",
"contacts": "Контакти"
},
"menu": {
"dashboard": "Начало"

View File

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

View File

@ -56,6 +56,6 @@ module.exports = withPWA({
// using https://next-intl-docs.vercel.app/docs/getting-started/pages-router
locales: ['bg', 'en', 'ru'],
defaultLocale: 'bg',
autoDetect: false,
localeDetection: false,
},
})

276
package-lock.json generated
View File

@ -25,6 +25,8 @@
"autoprefixer": "^10.4.17",
"axios": "^1.6.7",
"axios-jwt": "^4.0.2",
"bcrypt": "^5.1.1",
"bcryptjs": "^2.4.3",
"date-fns": "^3.3.1",
"docx": "^8.5.0",
"docx-templates": "^4.11.4",
@ -2861,6 +2863,69 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/@mui/base": {
"version": "5.0.0-beta.36",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.36.tgz",
@ -4990,6 +5055,11 @@
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
},
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
},
"node_modules/archiver": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
@ -5054,6 +5124,18 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@ -5396,6 +5478,19 @@
}
]
},
"node_modules/bcrypt": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
"hasInstallScript": true,
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
"node-addon-api": "^5.0.0"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@ -5404,6 +5499,11 @@
"tweetnacl": "^0.14.3"
}
},
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
},
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
@ -5841,6 +5941,14 @@
"fsevents": "~2.3.2"
}
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"engines": {
"node": ">=10"
}
},
"node_modules/chrome-trace-event": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
@ -6068,6 +6176,14 @@
"simple-swizzle": "^0.2.2"
}
},
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/color/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -6197,6 +6313,11 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -6591,6 +6712,11 @@
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
},
"node_modules/depcheck": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/depcheck/-/depcheck-1.4.7.tgz",
@ -8020,6 +8146,28 @@
"node": ">=10"
}
},
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"dependencies": {
"minipass": "^3.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/fs-minipass/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -8112,6 +8260,25 @@
"resolved": "https://registry.npmjs.org/gapi-script/-/gapi-script-1.2.0.tgz",
"integrity": "sha512-NKTVKiIwFdkO1j1EzcrWu/Pz7gsl1GmBmgh+qhuV2Ytls04W/Eg5aiBL91SCiBM9lU0PMu7p1hTVxhh1rPT5Lw=="
},
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/gaxios": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.3.0.tgz",
@ -8590,6 +8757,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
},
"node_modules/hasha": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz",
@ -10543,6 +10715,37 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@ -10889,6 +11092,11 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
},
"node_modules/node-excel-export": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/node-excel-export/-/node-excel-export-1.4.4.tgz",
@ -11089,6 +11297,20 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/normalize-package-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@ -13784,6 +14006,17 @@
"inBundle": true,
"license": "ISC"
},
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/oauth": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
@ -15755,6 +15988,11 @@
"node": ">= 0.8.0"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
},
"node_modules/set-function-length": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
@ -15873,8 +16111,7 @@
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"optional": true
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
@ -16658,6 +16895,22 @@
"node": ">=6"
}
},
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^5.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
@ -16673,6 +16926,17 @@
"node": ">=6"
}
},
"node_modules/tar/node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/temp-dir": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
@ -17918,6 +18182,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/winston": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.13.0.tgz",

View File

@ -42,6 +42,7 @@
"autoprefixer": "^10.4.17",
"axios": "^1.6.7",
"axios-jwt": "^4.0.2",
"bcrypt": "^5.1.1",
"date-fns": "^3.3.1",
"docx": "^8.5.0",
"docx-templates": "^4.11.4",
@ -115,4 +116,4 @@
"depcheck": "^1.4.7",
"prisma": "^5.13.0"
}
}
}

View File

@ -28,26 +28,27 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
export default function App({ Component, pageProps: { session, ...pageProps }, }: AppProps<{ session: Session }>) {
// console.log(pageProps);
const router = useRouter();
const [locale, setLocale] = useState(router.locale || 'bg');
const [messages, setMessages] = useState({});
useEffect(() => {
console.log("Current locale:", router.locale);
async function loadLocaleData() {
// Replace the static import with a fetch request
const res = await fetch(`/api/translations/${router.locale}`);
const res = await fetch(`/api/translations/${locale}`);
if (res.ok) {
const localeMessages = await res.json();
console.log("Loaded messages for locale:", router.locale, localeMessages);
console.log("Loaded messages for locale:", locale, localeMessages);
setMessages(localeMessages);
} else {
const localeMessages = await import(`../content/i18n/${router.locale}.json`); setMessages(localeMessages.default);
const localeMessages = await import(`../content/i18n/${locale}.json`); setMessages(localeMessages.default);
}
console.log("Loaded locale '", router.locale, "' ",);
console.log("Loaded locale '", locale, "' ",);
//console.log("Loaded messages for locale:", router.locale, localeMessages.default);
}
loadLocaleData();
}, [router.locale]);
}, [locale]);
// useEffect(() => {
// async function loadLocaleData() {
@ -101,7 +102,7 @@ export default function App({ Component, pageProps: { session, ...pageProps }, }
return (
<>
<NextIntlClientProvider
locale={router.locale}
locale={'bg'}
timeZone="Europe/Sofia"
messages={messages}
>
@ -110,7 +111,7 @@ export default function App({ Component, pageProps: { session, ...pageProps }, }
<Component {...pageProps} />
</LocalizationProvider>
</SessionProvider>
</NextIntlClientProvider>
</NextIntlClientProvider >
</>
)
}

View File

@ -8,6 +8,7 @@ import AppleProvider from "next-auth/providers/apple"
import EmailProvider from "next-auth/providers/email"
import CredentialsProvider from "next-auth/providers/credentials"
import { PrismaAdapter } from "@auth/prisma-adapter"
import bcrypt from "bcrypt"
//microsoft
import AzureADProvider from "next-auth/providers/azure-ad";
@ -16,6 +17,7 @@ import AzureADProvider from "next-auth/providers/azure-ad";
const common = require("../../../src/helpers/common");
import { isLoggedIn, setAuthTokens, clearAuthTokens, getAccessToken, getRefreshToken } from 'axios-jwt'
import { create } from "domain"
console.log("appleID:", process.env.APPLE_APP_ID);
@ -52,6 +54,7 @@ export const authOptions: NextAuthOptions = {
// tenantId: process.env.AZURE_AD_TENANT_ID,
// }),
CredentialsProvider({
id: 'credentials',
// The name to display on the sign in form (e.g. 'Sign in with...')
name: 'Credentials',
credentials: {
@ -80,17 +83,45 @@ export const authOptions: NextAuthOptions = {
{ id: "3", name: "popov", email: "popov@example.com", password: "popov123", role: "ADMIN" }
];
// Check if a user with the given username and password exists
const user = users.find(user =>
user.name === credentials.username && user.password === credentials.password
);
// If a matching user is found, return the user data, otherwise return null
if (user) {
return user; //{ id: user.id, name: user.name, email: user.email };
return user;
}
else {
const prisma = common.getPrismaClient();
const user = await prisma.user.findUnique({ where: { email: credentials.username } });
if (user) {
const match = await bcrypt.compare(credentials?.password, user.passwordHashLocalAccount);
if (match) {
console.log("User authenticated successfully.");
//create access token
user.accessToken = await getAccessToken();
return null;
return user;
}
else {
console.log("Password mismatch.");
throw new Error('невалидна парола');
}
}
else {
throw new Error("Не можем да намерим твоя имейл '" + credentials?.username + "' в участниците в ССОМ. Моля свържи се с нас за да те регистрираме ако искаш да ползваш този имейл.");
// console.log("Creating new user in the database...");
// 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 user created in the database.");
// return newUser;
}
}
}
})
/*
@ -132,35 +163,35 @@ export const authOptions: NextAuthOptions = {
var prisma = common.getPrismaClient();
console.log("[nextauth] signIn:", account.provider, user.email)
if (account.provider === 'google') {
try {
// Check user in your database and assign roles
const dbUser = await prisma.publisher.findUnique({
where: { email: user.email }
//if (account.provider === 'google' ) {
try {
// Check user in your database and assign roles
const dbUser = await prisma.publisher.findUnique({
where: { email: user.email }
});
if (dbUser) {
// Assign roles from your database to the session
user.role = dbUser.role;
user.id = dbUser.id;
//user.permissions = dbUser.permissions;
const session = { ...user };
await prisma.publisher.update({
where: { id: dbUser.id },
data: { lastLogin: new Date() }
});
if (dbUser) {
// Assign roles from your database to the session
user.role = dbUser.role;
user.id = dbUser.id;
//user.permissions = dbUser.permissions;
const session = { ...user };
await prisma.publisher.update({
where: { id: dbUser.id },
data: { lastLogin: new Date() }
});
return true; // Sign-in successful
} else {
// Optionally create a new user in your DB
// Or return false to deny access
//Let's customize the error message to give a better user experience
throw new Error(`Твоят имейл '${user.email}' не е регистриран в системата. Моля свържи се с нас за да те регистрираме ако искаш да ползваш този имейл.`);
}
} catch (e) {
console.log(e);
return true; // Sign-in successful
} else {
// Optionally create a new user in your DB
// Or return false to deny access
//Let's customize the error message to give a better user experience
throw new Error(`Твоят имейл '${user.email}' не е регистриран в системата. Моля свържи се с нас за да те регистрираме ако искаш да ползваш този имейл.`);
}
} catch (e) {
console.log(e);
}
//}
return true; // Allow other providers or default behavior
},
@ -207,6 +238,13 @@ export const authOptions: NextAuthOptions = {
};
},
},
pages: {
signIn: "/auth/signin",
signOut: "/auth/signout",
error: "/message", // Error code passed in query string as ?error=
verifyRequest: "/auth/verify-request", // (used for check email message)
newUser: null // If set, new users will be directed here on first sign in
},
}
export default NextAuth(authOptions)
export default NextAuth(authOptions)

View File

@ -33,6 +33,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'DELETE') {
switch (targetTable) {
case 'publishers':
case 'availabilities':
const targetId = req.query.nextcrud[1];
logger.info('[nextCrud] ' + targetTable + ': ' + targetId + ' DELETED by ' + session.user.email);
break;

View File

@ -34,7 +34,8 @@ export default async function handler(req, res) {
// Retrieve and validate the JWT token
//response is a special action that does not require a token
if (action == "email_response") {
//PUBLIC
if (action == "email_response" || action == "account") {
switch (emailaction) {
case "coverMeAccept":
//validate shiftId and assignmentId
@ -201,6 +202,83 @@ export default async function handler(req, res) {
});
break;
case "resetPassword":
// Send password reset form to the user
//parse the request body
let email = req.body.email || req.query.email;
let actualUser = await prisma.publisher.findUnique({
where: {
email: email
}
});
if (!actualUser) {
return res.status(200).json({ message: "Няма потребител с този имейл" });
}
else {
let requestGuid = req.query.guid;
if (!requestGuid) {
console.log("User: " + email + " requested a password reset");
let requestGuid = uuidv4();
//save the request in the database as EventLog
let eventLog = await prisma.eventLog.create({
data: {
date: new Date(),
publisher: { connect: { id: actualUser.id } },
type: EventLogType.PasswordResetRequested,
content: JSON.stringify({ guid: requestGuid })
}
});
logger.info("User: " + email + " requested a password reset. EventLogId: " + eventLog.id + "");
let model = {
email: email,
firstName: actualUser.firstName,
lastName: actualUser.lastName,
resetUrl: process.env.NEXTAUTH_URL + "/api/email?action=email_response&emailaction=resetPassword&guid=" + requestGuid + "&email=" + email,
sentDate: common.getDateFormated(new Date())
};
emailHelper.SendEmailHandlebars(to, "resetPassword", model);
res.status(200).json({ message: "Password reset request sent" });
}
else {
//1. validate the guid
let eventLog = await prisma.eventLog.findFirst({
where: {//can we query "{ guid: requestGuid }"?
type: EventLogType.PasswordResetRequested,
publisherId: actualUser.id,
date: {
gt: new Date(new Date().getTime() - 24 * 60 * 60 * 1000) //24 hours
}
}
});
if (!eventLog) {
return res.status(400).json({ message: "Invalid or expired password reset request" });
}
else {
let eventLog = await prisma.eventLog.update({
where: {
id: parseInt(requestGuid)
},
data: {
type: EventLogType.PasswordResetEmailConfirmed
}
});
//2. redirect to the password reset page
const messagePageUrl = `/auth/reset-password?email=${email}&resetToken=${requestGuid}`;
res.redirect(messagePageUrl);
}
//2.login the user
//3. redirect to the password reset page
}
}
break;
}
// //send email response to the user
// const emailResponse = await common.sendEmail(user.email, "Email Action Processed",
@ -220,6 +298,7 @@ export default async function handler(req, res) {
}
});
//PRIVATE ACTIONS
switch (action) {
case "sendCoverMeRequestByEmail":
// Send CoverMe request to the users

View File

@ -0,0 +1,135 @@
import { use, useEffect, useState } from 'react';
import Layout from '../../components/layout';
import axiosInstance from "../../src/axiosSecure";
import common from '../../src/helpers/common';
import { EventLogType } from '@prisma/client';
import { useRouter } from "next/router";
export default function ResetPassword(req, res) {
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const [resetToken, setResetToken] = useState(req.query?.resetToken || '');
const [isConfirmed, setIsConfirmed] = useState(false);
const router = useRouter();
useEffect(async () => {
if (resetToken) {
const prisma = common.getPrismaClient();
let eventLog = await prisma.eventLog.findUnique({
where: {
content: resetToken,
type: EventLogType.PasswordResetEmailConfirmed,
date: {
gt: new Date(new Date().getTime() - 24 * 60 * 60 * 1000) //24 hours
}
}
});
if (eventLog) {
setIsConfirmed(true);
}
}
}, [resetToken]);
const handleResetRequest = async (event) => {
event.preventDefault();
// Call your email API endpoint here
try {
const response = await axiosInstance.post('/api/email?action=account&emailaction=resetPassword', { email },
{ headers: { 'Content-Type': 'application/json' } });
if (response.data.message) {
setMessage(response.data.message);
} else {
if (response.ok) {
setMessage('Провери твоя имейл за инструкции как да промениш паролата си.');
} else {
if (response.error) {
setMessage(response.error);
}
}
}
} catch (error) {
setMessage(error.message);
}
};
const setNewPassword = async (event) => {
event.preventDefault();
try {
const prisma = common.getPrismaClient();
const user = await prisma.user.findUnique({
where: {
email
}
});
if (!user) {
throw new Error('Няма потребител с този имейл.');
}
const passHash = await crypto.hash(event.target.newPassword.value, 10);
await prisma.user.update({
where: {
email
},
data: {
passwordHashLocalAccount: passHash
}
});
setMessage('Паролата беше успешно променена.');
router.push('/auth/signin');
} catch (error) {
setMessage(error.message);
}
}
return (
<Layout>
<div className="min-h-screen flex items-center justify-center">
<div className="w-full max-w-md p-8 space-y-6 bg-white shadow-lg rounded-lg">
<h1 className="text-xl font-bold text-center">Променете паролата си</h1>
<form onSubmit={handleResetRequest} className="space-y-4">
{!isConfirmed &&
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">имейл</label>
<input
id="email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>}
{isConfirmed &&
<div>
<label htmlFor="newPassword" className="block text-sm font-medium text-gray-700">имейл</label>
<input
id="newPassword"
type="password"
required
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>}
<div>
<button type="submit" className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700">
Изпрати линк за промяна на паролата
</button>
<button type="button" className="mt-4 w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-blue-600 hover:text-blue-700 focus:outline-none"
onClick={() => window.location.href = '/auth/signin'}
>
страница за вход
</button>
</div>
{message && <div className="text-center text-sm text-gray-500">{message}</div>}
</form>
</div>
</div>
</Layout>
);
}

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

@ -0,0 +1,108 @@
// 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';
export default function SignIn({ csrfToken }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
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">
{/* SSO Providers */}
<div className="space-y-4 w-full px-4">
<button onClick={() => signIn('google', { callbackUrl: '/' })}
className="flex items-center justify-center w-full py-2 px-4 border border-gray-300 rounded shadow-sm text-sm 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>
{/* Email and Password Form */}
<form onSubmit={handleSubmit} className="mt-8 w-full max-w-xs px-4">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
<div className="mb-4">
<label htmlFor="email" className="block text-sm font-medium text-gray-700">имейл</label>
<input
id="email"
type="email"
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>
<div className="mb-6">
<label htmlFor="password" className="block text-sm font-medium text-gray-700">парола</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-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>
{error && <div className="text-red-500 text-sm">{error}</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={() => router.push('/auth/reset-password')}>
Забравена парола?
</button> */}
</form>
</div>
</div>
</div>
</Layout>
);
}
// This gets called on every request
export async function getServerSideProps(context) {
return {
props: {
csrfToken: await getCsrfToken(context),
},
};
}

View File

@ -49,7 +49,19 @@ export const getServerSideProps = async (context) => {
}
var url = process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,assignments,assignments.shift";
console.log("GET PUBLISHER FROM:" + url)
const { data: item } = await axios.get(url);
try {
const { data: item } = await axios.get(url);
} catch (error) {
console.log("error fetching publisher: " + error);
//redirect to message page with message "no account found". get user from session
const user = context.req.session.user;
return {
redirect: {
destination: '/message?message=Този имейл (' + user.email + ') не е регистриран. Моля свържете се с администратора.',
permanent: false,
},
}
}
// item.allShifts = item.assignments.map((a: Assignment[]) => a.shift);

View File

@ -22,7 +22,7 @@ const AdminTranslations = () => {
}, [locale]);
const handleSave = () => {
axiosInstance.post(`/api/translations/${locale}`, translations)
axiosInstance.post(`/api/translations/${locale}/modified`, translations)
.then(res => {
if (res.data.status === 'Updated') {
toast.success('Translations updated!');

View File

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

View File

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

View File

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

View File

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