diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..68ecdba
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "typescript.tsdk": "node_modules\\typescript\\lib"
+}
\ No newline at end of file
diff --git a/next.config.mjs b/next.config.mjs
index 63d904b..ce0e518 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -17,10 +17,6 @@ const nextConfig = {
permanent: false,
basePath: false
}] : []), {
- source: '/',
- destination: '/dashboard',
- permanent: false
- }, {
source: '/chuni',
destination: '/chuni/dashboard',
permanent: false
@@ -36,7 +32,11 @@ const nextConfig = {
experimental: {
instrumentationHook: true
},
- productionBrowserSourceMaps: true
+ productionBrowserSourceMaps: true,
+ webpack: config => {
+ config.externals = [...config.externals, 'bcrypt'];
+ return config;
+ }
};
export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
index a844ade..d946c4d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,7 @@
"mysql2": "^3.9.2",
"next": "14.1.4",
"next-auth": "^5.0.0-beta.15",
+ "next-client-cookies": "^1.1.0",
"next-themes": "^0.3.0",
"react": "^18",
"react-day-picker": "^8.10.0",
@@ -227,7 +228,6 @@
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
"optional": true,
- "peer": true,
"dependencies": {
"@emotion/memoize": "0.7.4"
}
@@ -236,8 +236,7 @@
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
- "optional": true,
- "peer": true
+ "optional": true
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
@@ -7754,6 +7753,14 @@
"url": "https://github.com/sponsors/panva"
}
},
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -8523,6 +8530,18 @@
}
}
},
+ "node_modules/next-client-cookies": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/next-client-cookies/-/next-client-cookies-1.1.0.tgz",
+ "integrity": "sha512-e/rqQTXHSFuvUJELMeCDgM7dWW6IUNOGr7noWyRSgE/5l033UaqseDrjShfRZYG45JnrYKoX653OdXTJ8cn+NA==",
+ "dependencies": {
+ "js-cookie": "^3.0.5"
+ },
+ "peerDependencies": {
+ "next": ">= 13.0.0",
+ "react": ">= 16.8.0"
+ }
+ },
"node_modules/next-themes": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz",
diff --git a/package.json b/package.json
index 9da6869..5772da8 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"mysql2": "^3.9.2",
"next": "14.1.4",
"next-auth": "^5.0.0-beta.15",
+ "next-client-cookies": "^1.1.0",
"next-themes": "^0.3.0",
"react": "^18",
"react-day-picker": "^8.10.0",
diff --git a/src/auth.ts b/src/auth.ts
index 67c2332..32435a0 100644
--- a/src/auth.ts
+++ b/src/auth.ts
@@ -1,7 +1,6 @@
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { db, GeneratedDB } from '@/db';
-import bcrypt from 'bcrypt';
import { DBUserPayload } from '@/types/user';
import { cache } from 'react';
import { SelectQueryBuilder, sql } from 'kysely';
@@ -42,12 +41,15 @@ const nextAuth = NextAuth({
callbacks: {
async jwt({ token, user }) {
token.user ??= user;
- const dbUser = await selectUserProps(db.selectFrom('aime_user as u')
- .where('u.id', '=', (token.user as any).id));
- if (dbUser) {
- const { password, ...payload } = dbUser;
- token.user = { ...(token.user as any), ...payload };
+ if (db) {
+ const dbUser = await selectUserProps(db.selectFrom('aime_user as u')
+ .where('u.id', '=', (token.user as any).id));
+
+ if (dbUser) {
+ const { password, ...payload } = dbUser;
+ token.user = { ...(token.user as any), ...payload };
+ }
}
return token;
@@ -70,6 +72,13 @@ const nextAuth = NextAuth({
(user as any).visibility = 0;
}
+ const now = new Date();
+ (user as any).last_login_date = now;
+ await db.updateTable('aime_user')
+ .set({ last_login_date: now })
+ .where('id', '=', (user as any).id)
+ .executeTakeFirst();
+
return true;
}
},
@@ -80,6 +89,8 @@ const nextAuth = NextAuth({
password: { label: 'Password', type: 'password' }
},
async authorize({ username, password }, req) {
+ const bcrypt = await import('bcrypt');
+
if (typeof username !== 'string' || typeof password !== 'string')
return null;
@@ -97,10 +108,11 @@ const nextAuth = NextAuth({
})]
});
-export const auth = cache(nextAuth.auth);
+export const auth = process.env.NEXT_RUNTIME !== 'edge' ? cache(nextAuth.auth) : (null as never);
export const {
handlers: { GET, POST },
signIn,
- signOut
+ signOut,
+ auth: uncachedAuth
} = nextAuth;
diff --git a/src/components/header-sidebar.tsx b/src/components/header-sidebar.tsx
index 68b744e..63be5cf 100644
--- a/src/components/header-sidebar.tsx
+++ b/src/components/header-sidebar.tsx
@@ -1,40 +1,89 @@
'use client';
-import { Button, Divider, Navbar } from '@nextui-org/react';
+import { Button, Divider, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Navbar } from '@nextui-org/react';
import { Bars3Icon, ChevronLeftIcon, XMarkIcon } from '@heroicons/react/20/solid';
import { Fragment, useState } from 'react';
import Link from 'next/link';
import { ThemeSwitcherDropdown, ThemeSwitcherSwitch } from '@/components/theme-switcher';
import { AdjustmentsHorizontalIcon } from '@heroicons/react/24/solid';
import { login, logout } from '@/actions/auth';
-import { usePathname, useSearchParams } from 'next/navigation';
+import { usePathname, useRouter } from 'next/navigation';
import { UserPayload } from '@/types/user';
-import { MAIN_ROUTES, ROUTES, UserOnly } from '@/routes';
+import { MAIN_ROUTES, ROUTES, Subroute, UserOnly } from '@/routes';
import { useUser } from '@/helpers/use-user';
import { useBreakpoint } from '@/helpers/use-breakpoint';
+import { useCookies } from 'next-client-cookies';
+import { UserPermissions } from '@/types/permissions';
+import { hasPermission } from '@/helpers/permissions';
+import { ChevronDownIcon } from '@heroicons/react/24/outline';
export type HeaderSidebarProps = {
- children?: React.ReactNode
+ children?: React.ReactNode,
};
-const filterUserOnly = (user: UserPayload | null | undefined, { userOnly }: { userOnly?: UserOnly }) => {
- if (!userOnly) return true;
- if (typeof userOnly === 'string') return user?.[userOnly];
- return user;
+const filterRoute = (user: UserPayload | null | undefined, { userOnly, permissions }: { userOnly?: UserOnly, permissions?: (UserPermissions | UserPermissions[])[] }) => {
+ if (typeof userOnly === 'string' && !user?.[userOnly])
+ return false;
+ if (typeof userOnly === 'boolean' && !user)
+ return false;
+ if (permissions?.length && !hasPermission(user?.permissions, ...permissions))
+ return false;
+ return true;
};
export const HeaderSidebar = ({ children }: HeaderSidebarProps) => {
const user = useUser();
- const path = usePathname();
- const params = useSearchParams();
+ const pathname = usePathname();
const [isMenuOpen, setMenuOpen] = useState(false);
const breakpoint = useBreakpoint();
+ const cookies = useCookies();
+ const router = useRouter();
- const from = params?.get('from');
- const filter = filterUserOnly.bind(null, user);
+ const path = pathname === '/' ? (user?.homepage ?? '/dashboard') : pathname;
+
+ const from = cookies.get('actaeon-navigated-from');
+ const filter = filterRoute.bind(null, user);
const routeGroup = ROUTES.find(route => route.title === from || path?.startsWith(route.url))!;
+ const renderHeaderLink = (route: Subroute) => {
+ const linkStyle = path?.startsWith(route.url) ?
+ 'font-semibold text-primary' : 'hover:text-secondary';
+
+ if (route.routes?.length)
+ return (