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

+
+ setUsername(e.target.value)} + /> + setPassword(e.target.value)} + /> + +
+ {error &&

{error}

} +

Existing Users

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

} +
+ setUsername(e.target.value)} + /> + setPassword(e.target.value)} + /> + +
+
+ ); +} 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());