diff --git a/app/admin.tsx b/app/admin.tsx
new file mode 100644
index 0000000..8d1a1de
--- /dev/null
+++ b/app/admin.tsx
@@ -0,0 +1,64 @@
+import { useState, useEffect } from 'react';
+import axios from 'axios';
+import { requireAuthentication } from './middleware';
+
+export async function getServerSideProps(context) {
+ await requireAuthentication(context.req, context.res, () => {});
+
+ return { props: {} };
+}
+
+export default function AdminPage() {
+ const [users, setUsers] = useState([]);
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+
+ useEffect(() => {
+ async function fetchUsers() {
+ const response = await axios.get('/api/users');
+ setUsers(response.data);
+ }
+ fetchUsers();
+ }, []);
+
+ const handleCreateUser = async (e: React.FormEvent) => {
+ e.preventDefault();
+ try {
+ await axios.post('/api/auth/signup', { username, password });
+ setUsername('');
+ setPassword('');
+ setError('');
+ } catch (error) {
+ setError('User already exists');
+ }
+ };
+
+ return (
+
+
Admin Page
+
+ {error &&
{error}
}
+
Existing Users
+
+ {users.map((user) => (
+ - {user.username}
+ ))}
+
+
+ );
+}
diff --git a/app/api/auth/check.ts b/app/api/auth/check.ts
new file mode 100644
index 0000000..5e84fd4
--- /dev/null
+++ b/app/api/auth/check.ts
@@ -0,0 +1,10 @@
+import { NextApiRequest, NextApiResponse } from 'next';
+import { getLoginSession } from '../../auth';
+
+export default async function handle(req: NextApiRequest, res: NextApiResponse) {
+ const user = await getLoginSession(req);
+ if (!user) {
+ return res.status(401).json({ loggedIn: false });
+ }
+ return res.status(200).json({ loggedIn: true });
+}
diff --git a/app/api/auth/login.ts b/app/api/auth/login.ts
new file mode 100644
index 0000000..c6777ce
--- /dev/null
+++ b/app/api/auth/login.ts
@@ -0,0 +1,30 @@
+import { NextApiRequest, NextApiResponse } from 'next';
+import bcrypt from 'bcrypt';
+import { prisma } from '../../prisma';
+import { setLoginSession } from '../../auth';
+
+export default async function handle(req: NextApiRequest, res: NextApiResponse) {
+ if (req.method !== 'POST') {
+ return res.status(405).end();
+ }
+
+ const { username, password } = req.body;
+
+ const user = await prisma.user.findUnique({
+ where: { username },
+ });
+
+ if (!user) {
+ return res.status(401).json({ error: 'Invalid username or password' });
+ }
+
+ const isValid = await bcrypt.compare(password, user.password);
+
+ if (!isValid) {
+ return res.status(401).json({ error: 'Invalid username or password' });
+ }
+
+ await setLoginSession(res, user);
+
+ res.status(200).json({ message: 'Login successful' });
+}
diff --git a/app/api/auth/signup.ts b/app/api/auth/signup.ts
new file mode 100644
index 0000000..6e5e21e
--- /dev/null
+++ b/app/api/auth/signup.ts
@@ -0,0 +1,26 @@
+import { NextApiRequest, NextApiResponse } from 'next';
+import bcrypt from 'bcrypt';
+import { prisma } from '../../prisma';
+
+export default async function handle(req: NextApiRequest, res: NextApiResponse) {
+ if (req.method !== 'POST') {
+ return res.status(405).end();
+ }
+
+ const { username, password } = req.body;
+
+ const hashedPassword = await bcrypt.hash(password, 10);
+
+ try {
+ const user = await prisma.user.create({
+ data: {
+ username,
+ password: hashedPassword,
+ role: 'admin',
+ },
+ });
+ res.status(200).json({ message: 'User created successfully' });
+ } catch (error) {
+ res.status(500).json({ error: 'User already exists' });
+ }
+}
diff --git a/app/auth.ts b/app/auth.ts
new file mode 100644
index 0000000..f724d5e
--- /dev/null
+++ b/app/auth.ts
@@ -0,0 +1,46 @@
+import { serialize, parse } from 'cookie';
+import { NextApiResponse } from 'next';
+import { v4 as uuidv4 } from 'uuid';
+import { prisma } from './prisma';
+
+const TOKEN_NAME = 'token';
+
+export async function setLoginSession(res: NextApiResponse, user: any) {
+ const token = uuidv4();
+
+ await prisma.session.create({
+ data: {
+ token,
+ userId: user.id,
+ expiresAt: new Date(Date.now() + 60 * 60 * 24 * 7 * 1000), // 1 week
+ },
+ });
+
+ const cookie = serialize(TOKEN_NAME, token, {
+ maxAge: 60 * 60 * 24 * 7, // 1 week
+ httpOnly: true,
+ secure: process.env.NODE_ENV === 'production',
+ path: '/',
+ sameSite: 'lax',
+ });
+
+ res.setHeader('Set-Cookie', cookie);
+}
+
+export async function getLoginSession(req: any) {
+ const cookies = parse(req.headers.cookie || '');
+ const token = cookies[TOKEN_NAME];
+
+ if (!token) return null;
+
+ const session = await prisma.session.findUnique({
+ where: { token },
+ include: { user: true },
+ });
+
+ if (!session || session.expiresAt < new Date()) {
+ return null;
+ }
+
+ return session.user;
+}
diff --git a/app/context/AuthContext.tsx b/app/context/AuthContext.tsx
new file mode 100644
index 0000000..6b2b7e9
--- /dev/null
+++ b/app/context/AuthContext.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import React, { createContext, useState, useEffect, useContext } from 'react';
+
+interface AuthContextType {
+ isAuthenticated: boolean;
+ login: () => void;
+ logout: () => void;
+}
+
+const AuthContext = createContext(undefined);
+
+export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
+
+ useEffect(() => {
+ // Add your authentication logic here
+ const checkAuth = async () => {
+ // For example, call an API to check if the user is authenticated
+ setIsAuthenticated(true); // or false, depending on the result
+ };
+ checkAuth();
+ }, []);
+
+ const login = () => {
+ setIsAuthenticated(true);
+ };
+
+ const logout = () => {
+ setIsAuthenticated(false);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (context === undefined) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+};
diff --git a/app/layout.tsx b/app/layout.tsx
index 3b3a953..d964a48 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,15 +1,16 @@
-import "./globals.css";
-import type { Metadata } from "next";
+"use client";
-export const metadata: Metadata = {
- title: "Metabase Manager",
- description: "Manage your Metabase instance",
-};
+import "./globals.css";
+import { AuthProvider } from '@/app/context/AuthContext'; // Import AuthProvider
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
- {children}
+
+
+ {children}
+
+
);
}
diff --git a/app/login.tsx b/app/login.tsx
new file mode 100644
index 0000000..14a32cb
--- /dev/null
+++ b/app/login.tsx
@@ -0,0 +1,42 @@
+import { useState } from 'react';
+import { useRouter } from 'next/router';
+import axios from 'axios';
+
+export default function LoginPage() {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const router = useRouter();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ try {
+ await axios.post('/api/auth/login', { username, password });
+ router.push('/');
+ } catch (error) {
+ setError('Invalid username or password');
+ }
+ };
+
+ return (
+
+
Login
+ {error &&
{error}
}
+
+
+ );
+}
diff --git a/app/metadata.ts b/app/metadata.ts
new file mode 100644
index 0000000..a5b08e2
--- /dev/null
+++ b/app/metadata.ts
@@ -0,0 +1,7 @@
+// app/metadata.ts
+import type { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "Metabase Manager",
+ description: "Manage your Metabase instance",
+};
diff --git a/app/middleware.ts b/app/middleware.ts
new file mode 100644
index 0000000..fa20e6c
--- /dev/null
+++ b/app/middleware.ts
@@ -0,0 +1,22 @@
+import { NextApiRequest, NextApiResponse, NextPageContext } from 'next';
+import { getLoginSession } from './auth';
+
+export async function requireAuthentication(
+ req: NextApiRequest | NextPageContext['req'],
+ res: NextApiResponse | NextPageContext['res'],
+ next: () => void
+) {
+ const user = await getLoginSession(req);
+
+ if (!user) {
+ if (res) {
+ res.statusCode = 302;
+ res.setHeader('Location', '/login');
+ res.end();
+ }
+ return;
+ }
+
+ req.user = user;
+ next();
+}
diff --git a/app/page.tsx b/app/page.tsx
index 9247219..bfd44ed 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -21,8 +21,24 @@ import {
} from "./api";
import React, { Fragment } from "react"; // needed for collapsible
+import { useAuth } from '@/app/context/AuthContext'; // Update this line
+import { useRouter } from 'next/router';
export default function Home() {
+ //AUTH LOGIN ROUTING
+ const { isAuthenticated } = useAuth();
+ const router = useRouter();
+
+ useEffect(() => {
+ if (!isAuthenticated) {
+ router.push('/login'); // Redirect to login page if not authenticated
+ }
+ }, [isAuthenticated, router]);
+
+ if (!isAuthenticated) {
+ return Loading...
;
+ }
+
const [sourceServers, setSourceServers] = useState([]);
const [proceedLoading, setProceedLoading] = useState(false);
const [destinationServers, setDestinationServers] = useState([]);
diff --git a/app/prisma.ts b/app/prisma.ts
new file mode 100644
index 0000000..63e06ac
--- /dev/null
+++ b/app/prisma.ts
@@ -0,0 +1,5 @@
+import { PrismaClient } from '@prisma/client';
+
+const prisma = new PrismaClient();
+
+export { prisma };
diff --git a/bun.lockb b/bun.lockb
index 8eeb41f..54d0090 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package-lock.json b/package-lock.json
index eab32c9..9283c8b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@types/react": "18.2.45",
"@types/react-dom": "18.2.18",
"autoprefixer": "10.4.15",
+ "bcrypt": "^5.1.1",
"cross-env": "^7.0.3",
"cross-fetch": "^4.0.0",
"dotenv": "^16.3.1",
@@ -189,6 +190,25 @@
"@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/@next/env": {
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.4.tgz",
@@ -882,6 +902,11 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+ },
"node_modules/acorn": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
@@ -966,6 +991,24 @@
"node": ">= 8"
}
},
+ "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/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==",
+ "deprecated": "This package is no longer supported.",
+ "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",
@@ -1187,6 +1230,19 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
+ "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/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -1356,6 +1412,14 @@
"node": ">= 6"
}
},
+ "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/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -1377,6 +1441,14 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
+ "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/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -1403,6 +1475,11 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
+ "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/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
@@ -1498,6 +1575,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "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/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -1506,6 +1588,14 @@
"node": ">=6"
}
},
+ "node_modules/detect-libc": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
+ "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -2222,6 +2312,28 @@
"url": "https://github.com/sponsors/rawify"
}
},
+ "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",
@@ -2270,6 +2382,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/gauge": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
+ "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
+ "deprecated": "This package is no longer supported.",
+ "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/get-intrinsic": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
@@ -2514,6 +2646,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/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -2724,6 +2861,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-generator-function": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
@@ -3196,6 +3341,28 @@
"node": ">=12"
}
},
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -3235,6 +3402,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",
@@ -3356,6 +3554,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-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -3380,6 +3583,20 @@
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
},
+ "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-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -3396,6 +3613,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/npmlog": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
+ "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
+ "deprecated": "This package is no longer supported.",
+ "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/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -4089,6 +4318,19 @@
"pify": "^2.3.0"
}
},
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -4253,6 +4495,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
"node_modules/safe-regex-test": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
@@ -4288,6 +4549,11 @@
"node": ">=10"
}
},
+ "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/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -4320,6 +4586,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "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=="
+ },
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -4363,6 +4634,32 @@
"node": ">=10.0.0"
}
},
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
"node_modules/string.prototype.matchall": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.9.tgz",
@@ -4581,6 +4878,33 @@
"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/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/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -4971,6 +5295,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/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -5001,4 +5333,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/package.json b/package.json
index e37564b..ee7cd40 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@types/react": "18.2.45",
"@types/react-dom": "18.2.18",
"autoprefixer": "10.4.15",
+ "bcrypt": "^5.1.1",
"cross-env": "^7.0.3",
"cross-fetch": "^4.0.0",
"dotenv": "^16.3.1",
@@ -35,4 +36,4 @@
"devDependencies": {
"prisma": "^5.2.0"
}
-}
\ No newline at end of file
+}
diff --git a/prisma/migrations/20240618125202_add_username_to_user/migration.sql b/prisma/migrations/20240618125202_add_username_to_user/migration.sql
new file mode 100644
index 0000000..aeaf1db
--- /dev/null
+++ b/prisma/migrations/20240618125202_add_username_to_user/migration.sql
@@ -0,0 +1,14 @@
+-- CreateTable
+CREATE TABLE "User" (
+ "id" SERIAL NOT NULL,
+ "username" TEXT NOT NULL,
+ "password" TEXT NOT NULL,
+ "role" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 4230133..4865a97 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -31,3 +31,11 @@ model SyncLog {
@@unique([timestamp, sourceHost, destinationHost])
}
+model User {
+ id Int @id @default(autoincrement())
+ username String @unique
+ password String
+ role String // "admin" or "user"
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
diff --git a/scripts/createAdmin.ts b/scripts/createAdmin.ts
new file mode 100644
index 0000000..6ec3b0c
--- /dev/null
+++ b/scripts/createAdmin.ts
@@ -0,0 +1,28 @@
+const { PrismaClient } = require('@prisma/client');
+const bcrypt = require('bcrypt');
+
+const prisma = new PrismaClient();
+
+async function createAdminUser() {
+ const username = 'metamanageradmin';
+ const password = 'securepassword123'; // Replace with a strong password
+
+ const hashedPassword = await bcrypt.hash(password, 10);
+
+ const existingUser = await prisma.user.findUnique({ where: { username } });
+
+ if (!existingUser) {
+ await prisma.user.create({
+ data: {
+ username,
+ password: hashedPassword,
+ role: 'admin',
+ },
+ });
+ }
+}
+
+createAdminUser()
+ .then(() => console.log('Admin user created'))
+ .catch((error) => console.error('Failed to create admin user', error))
+ .finally(() => prisma.$disconnect());