Add auth server, login page, users table, create user script, etc
This commit is contained in:
22
ecosystem.config.js
Normal file
22
ecosystem.config.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
...commonSettings,
|
||||||
|
name: 'new-auth-server',
|
||||||
|
script: './inventory-server/auth/server.js',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
AUTH_PORT: 3011,
|
||||||
|
JWT_SECRET: process.env.JWT_SECRET
|
||||||
|
},
|
||||||
|
error_file: 'inventory-server/auth/logs/pm2/err.log',
|
||||||
|
out_file: 'inventory-server/auth/logs/pm2/out.log',
|
||||||
|
log_file: 'inventory-server/auth/logs/pm2/combined.log',
|
||||||
|
env_production: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
AUTH_PORT: 3011,
|
||||||
|
JWT_SECRET: process.env.JWT_SECRET
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
41
inventory-server/auth/add_user.js
Normal file
41
inventory-server/auth/add_user.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
const bcrypt = require('bcrypt');
|
||||||
|
const mysql = require('mysql2/promise');
|
||||||
|
const readline = require('readline').createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
require('dotenv').config({ path: '../.env' });
|
||||||
|
|
||||||
|
const dbConfig = {
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function addUser() {
|
||||||
|
const username = await askQuestion('Enter username: ');
|
||||||
|
const password = await askQuestion('Enter password: ');
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
|
const connection = await mysql.createConnection(dbConfig);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.query('INSERT INTO users (username, password) VALUES (?, ?)', [username, hashedPassword]);
|
||||||
|
console.log(`User ${username} added successfully.`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding user:', error);
|
||||||
|
} finally {
|
||||||
|
connection.end();
|
||||||
|
readline.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function askQuestion(query) {
|
||||||
|
return new Promise(resolve => readline.question(query, ans => {
|
||||||
|
resolve(ans);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
addUser();
|
||||||
1915
inventory-server/auth/package-lock.json
generated
Normal file
1915
inventory-server/auth/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
inventory-server/auth/package.json
Normal file
21
inventory-server/auth/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "auth-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Authentication server for inventory management",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon server.js",
|
||||||
|
"add_user": "node add_user.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"jsonwebtoken": "^9.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
inventory-server/auth/schema.sql
Normal file
6
inventory-server/auth/schema.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`username` VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
`password` VARCHAR(255) NOT NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
135
inventory-server/auth/server.js
Normal file
135
inventory-server/auth/server.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const cors = require('cors');
|
||||||
|
const mysql = require('mysql2/promise');
|
||||||
|
require('dotenv').config({ path: '../.env' });
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.AUTH_PORT || 3011;
|
||||||
|
|
||||||
|
// Database configuration
|
||||||
|
const dbConfig = {
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a connection pool
|
||||||
|
const pool = mysql.createPool(dbConfig);
|
||||||
|
|
||||||
|
app.use(cors({
|
||||||
|
origin: [
|
||||||
|
'https://inventory.kent.pw',
|
||||||
|
'http://localhost:5173',
|
||||||
|
'http://127.0.0.1:5173',
|
||||||
|
/^http:\/\/192\.168\.\d+\.\d+(:\d+)?$/,
|
||||||
|
/^http:\/\/10\.\d+\.\d+\.\d+(:\d+)?$/
|
||||||
|
],
|
||||||
|
methods: ['GET', 'POST', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
|
||||||
|
credentials: true,
|
||||||
|
exposedHeaders: ['set-cookie']
|
||||||
|
}));
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Debug middleware to log request details
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
console.log('Request details:', {
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
origin: req.get('Origin'),
|
||||||
|
headers: req.headers,
|
||||||
|
body: req.body,
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Registration endpoint
|
||||||
|
app.post('/register', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
|
const connection = await pool.getConnection();
|
||||||
|
await connection.query('INSERT INTO users (username, password) VALUES (?, ?)', [username, hashedPassword]);
|
||||||
|
connection.release();
|
||||||
|
|
||||||
|
res.status(201).json({ message: 'User registered successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Registration error:', error);
|
||||||
|
res.status(500).json({ error: 'Registration failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Login endpoint
|
||||||
|
app.post('/login', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
console.log(`Login attempt for user: ${username}`);
|
||||||
|
|
||||||
|
const connection = await pool.getConnection();
|
||||||
|
const [rows] = await connection.query(
|
||||||
|
'SELECT * FROM users WHERE username = ?',
|
||||||
|
[username],
|
||||||
|
);
|
||||||
|
connection.release();
|
||||||
|
|
||||||
|
if (rows.length === 1) {
|
||||||
|
const user = rows[0];
|
||||||
|
const passwordMatch = await bcrypt.compare(password, user.password);
|
||||||
|
|
||||||
|
if (passwordMatch) {
|
||||||
|
console.log(`User ${username} authenticated successfully`);
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ username: user.username },
|
||||||
|
process.env.JWT_SECRET,
|
||||||
|
{ expiresIn: '1h' },
|
||||||
|
);
|
||||||
|
res.json({ token });
|
||||||
|
} else {
|
||||||
|
console.error(`Invalid password for user: ${username}`);
|
||||||
|
res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`User not found: ${username}`);
|
||||||
|
res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
res.status(500).json({ error: 'Login failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Protected endpoint example
|
||||||
|
app.get('/protected', async (req, res) => {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.split(' ')[1];
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
|
|
||||||
|
// Optionally, you can fetch the user from the database here
|
||||||
|
// to verify that the user still exists or to get more user information
|
||||||
|
const connection = await pool.getConnection();
|
||||||
|
const [rows] = await connection.query('SELECT * FROM users WHERE username = ?', [decoded.username]);
|
||||||
|
connection.release();
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return res.status(401).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ message: 'Protected resource accessed', user: decoded });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Protected endpoint error:', error);
|
||||||
|
res.status(403).json({ error: 'Invalid token' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, "0.0.0.0", () => {
|
||||||
|
console.log(`Auth server running on port ${PORT}`);
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
import { Routes, Route, useNavigate, Navigate } from 'react-router-dom';
|
||||||
import { MainLayout } from './components/layout/MainLayout';
|
import { MainLayout } from './components/layout/MainLayout';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { Products } from './pages/Products';
|
import { Products } from './pages/Products';
|
||||||
@@ -9,14 +9,62 @@ import { Settings } from './pages/Settings';
|
|||||||
import { Analytics } from './pages/Analytics';
|
import { Analytics } from './pages/Analytics';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
import PurchaseOrders from './pages/PurchaseOrders';
|
import PurchaseOrders from './pages/PurchaseOrders';
|
||||||
|
import { Login } from './pages/Login';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import config from './config';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const isLoggedIn = sessionStorage.getItem('isLoggedIn') === 'true';
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAuth = async () => {
|
||||||
|
const token = sessionStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${config.authUrl}/protected`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
sessionStorage.removeItem('token');
|
||||||
|
sessionStorage.removeItem('isLoggedIn');
|
||||||
|
navigate('/login');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Token verification failed:', error);
|
||||||
|
sessionStorage.removeItem('token');
|
||||||
|
sessionStorage.removeItem('isLoggedIn');
|
||||||
|
navigate('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAuth();
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Router>
|
|
||||||
<Toaster richColors position="top-center" />
|
<Toaster richColors position="top-center" />
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
isLoggedIn ? (
|
||||||
|
<Navigate to="/dashboard" replace />
|
||||||
|
) : (
|
||||||
|
<Navigate to="/login" replace />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={
|
||||||
|
isLoggedIn ? (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Dashboard />} />
|
<Route path="/" element={<Dashboard />} />
|
||||||
@@ -28,9 +76,15 @@ function App() {
|
|||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
</Router>
|
) : (
|
||||||
|
<Navigate to="/login" replace />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,118 @@
|
|||||||
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
import { useState } from 'react';
|
||||||
import { AppSidebar } from "./AppSidebar";
|
import { NavLink, useLocation } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
LayoutDashboard,
|
||||||
|
Settings,
|
||||||
|
Package,
|
||||||
|
ShoppingBag,
|
||||||
|
PackagePlus,
|
||||||
|
BarChart4,
|
||||||
|
LogOut,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
interface MainLayoutProps {
|
const links = [
|
||||||
children: React.ReactNode;
|
{ title: 'Dashboard', path: '/', icon: <LayoutDashboard /> },
|
||||||
}
|
{ title: 'Products', path: '/products', icon: <Package /> },
|
||||||
|
{ title: 'Import', path: '/import', icon: <PackagePlus /> },
|
||||||
|
{ title: 'Orders', path: '/orders', icon: <ShoppingBag /> },
|
||||||
|
{ title: 'Purchase Orders', path: '/purchase-orders', icon: <ShoppingBag /> },
|
||||||
|
{ title: 'Analytics', path: '/analytics', icon: <BarChart4 /> },
|
||||||
|
{ title: 'Settings', path: '/settings', icon: <Settings /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function MainLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
sessionStorage.removeItem('isLoggedIn');
|
||||||
|
sessionStorage.removeItem('token');
|
||||||
|
navigate('/login');
|
||||||
|
};
|
||||||
|
|
||||||
export function MainLayout({ children }: MainLayoutProps) {
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider defaultOpen>
|
<div className="flex h-screen">
|
||||||
<div className="flex min-h-screen w-full pr-2">
|
<aside
|
||||||
<AppSidebar />
|
className={cn(
|
||||||
<main className="flex-1 overflow-hidden">
|
'border-r h-full transition-all duration-300 ease-in-out',
|
||||||
<div className="flex h-14 w-full items-center border-b px-4 gap-4">
|
open ? 'w-64' : 'w-20',
|
||||||
<SidebarTrigger />
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between p-4">
|
||||||
|
<h1
|
||||||
|
className={cn(
|
||||||
|
'text-xl font-bold transition-all duration-300 ease-in-out',
|
||||||
|
open ? 'block' : 'hidden',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Inventory
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
className="p-1.5 rounded-md hover:bg-gray-200"
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-5 h-5"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M4 6l16 0"></path>
|
||||||
|
<path d="M4 12l16 0"></path>
|
||||||
|
<path d="M4 18l12 0"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-auto h-[calc(100vh-3.5rem)] max-w-[1500px]">
|
<nav className="p-4 space-y-2">
|
||||||
{children}
|
{links.map((link) => (
|
||||||
|
<NavLink
|
||||||
|
key={link.path}
|
||||||
|
to={link.path}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 p-2 rounded-md transition-all duration-300 ease-in-out',
|
||||||
|
location.pathname === link.path
|
||||||
|
? 'bg-gray-200'
|
||||||
|
: 'hover:bg-gray-100',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{link.icon}
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'transition-all duration-300 ease-in-out',
|
||||||
|
open ? 'block' : 'hidden',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{link.title}
|
||||||
|
</span>
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full flex items-center gap-2"
|
||||||
|
onClick={handleLogout}
|
||||||
|
>
|
||||||
|
<LogOut />
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'transition-all duration-300 ease-in-out',
|
||||||
|
open ? 'block' : 'hidden',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<main className="flex-1 p-8 overflow-y-auto">{children}</main>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</SidebarProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,8 @@ const isDev = process.env.NODE_ENV === 'development';
|
|||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
apiUrl: isDev ? '/api' : 'https://inventory.kent.pw/api',
|
apiUrl: isDev ? '/api' : 'https://inventory.kent.pw/api',
|
||||||
baseUrl: isDev ? '' : 'https://inventory.kent.pw'
|
baseUrl: isDev ? '' : 'https://inventory.kent.pw',
|
||||||
|
authUrl: isDev ? '/auth-inv' : 'https://inventory.kent.pw/auth-inv'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
@@ -2,9 +2,12 @@ import { StrictMode } from 'react'
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
import { BrowserRouter as Router } from 'react-router-dom'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<Router>
|
||||||
<App />
|
<App />
|
||||||
|
</Router>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
108
inventory/src/pages/Login.tsx
Normal file
108
inventory/src/pages/Login.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
|
export function Login() {
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
const url = isDev ? "/auth-inv/login" : `${config.authUrl}/login`;
|
||||||
|
console.log('Making login request:', {
|
||||||
|
url,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: { username, password },
|
||||||
|
config
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Login response status:', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json().catch(e => ({ error: 'Failed to parse error response' }));
|
||||||
|
console.error('Login failed:', data);
|
||||||
|
throw new Error(data.error || 'Login failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Login successful:', data);
|
||||||
|
|
||||||
|
sessionStorage.setItem('token', data.token);
|
||||||
|
sessionStorage.setItem('isLoggedIn', 'true');
|
||||||
|
navigate('/');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'An unexpected error occurred',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-screen">
|
||||||
|
<Card className="w-[350px]">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Login</CardTitle>
|
||||||
|
<CardDescription>Enter your credentials to access the inventory.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form>
|
||||||
|
<div className="grid w-full items-center gap-4">
|
||||||
|
<div className="flex flex-col space-y-1.5">
|
||||||
|
<Label htmlFor="username">Username</Label>
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
placeholder="Enter your username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-1.5">
|
||||||
|
<Label htmlFor="password">Password</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex justify-between">
|
||||||
|
<Button onClick={handleLogin}>Login</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/analytics/categoryperformance.tsx","./src/components/analytics/priceanalysis.tsx","./src/components/analytics/profitanalysis.tsx","./src/components/analytics/stockanalysis.tsx","./src/components/analytics/vendorperformance.tsx","./src/components/dashboard/inventorystats.tsx","./src/components/dashboard/overview.tsx","./src/components/dashboard/recentsales.tsx","./src/components/dashboard/salesbycategory.tsx","./src/components/dashboard/trendingproducts.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/products/producteditdialog.tsx","./src/components/products/productfilters.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/hooks/use-mobile.tsx","./src/lib/utils.ts","./src/pages/analytics.tsx","./src/pages/dashboard.tsx","./src/pages/import.tsx","./src/pages/orders.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/settings.tsx"],"version":"5.6.3"}
|
{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/analytics/categoryperformance.tsx","./src/components/analytics/priceanalysis.tsx","./src/components/analytics/profitanalysis.tsx","./src/components/analytics/stockanalysis.tsx","./src/components/analytics/vendorperformance.tsx","./src/components/dashboard/inventorystats.tsx","./src/components/dashboard/overview.tsx","./src/components/dashboard/recentsales.tsx","./src/components/dashboard/salesbycategory.tsx","./src/components/dashboard/trendingproducts.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/products/producteditdialog.tsx","./src/components/products/productfilters.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/settings/calculationsettings.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/performancemetrics.tsx","./src/components/settings/stockmanagement.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/hooks/use-mobile.tsx","./src/lib/utils.ts","./src/pages/analytics.tsx","./src/pages/dashboard.tsx","./src/pages/import.tsx","./src/pages/login.tsx","./src/pages/orders.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/settings.tsx"],"version":"5.6.3"}
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
||||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
||||||
return new (P || (P = Promise))(function (resolve, reject) {
|
|
||||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
||||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
||||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
||||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
||||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
||||||
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
||||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
||||||
function step(op) {
|
|
||||||
if (f) throw new TypeError("Generator is already executing.");
|
|
||||||
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
||||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
||||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
||||||
switch (op[0]) {
|
|
||||||
case 0: case 1: t = op; break;
|
|
||||||
case 4: _.label++; return { value: op[1], done: false };
|
|
||||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
||||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
||||||
default:
|
|
||||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
||||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
||||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
||||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
||||||
if (t[2]) _.ops.pop();
|
|
||||||
_.trys.pop(); continue;
|
|
||||||
}
|
|
||||||
op = body.call(thisArg, _);
|
|
||||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
||||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
import path from "path";
|
|
||||||
import { defineConfig } from 'vite';
|
|
||||||
import react from '@vitejs/plugin-react';
|
|
||||||
import { loadEnv } from "vite";
|
|
||||||
import fs from 'fs-extra';
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig(function (_a) {
|
|
||||||
var mode = _a.mode;
|
|
||||||
var env = loadEnv(mode, process.cwd(), "");
|
|
||||||
var isDev = mode === 'development';
|
|
||||||
return {
|
|
||||||
plugins: [
|
|
||||||
react(),
|
|
||||||
{
|
|
||||||
name: 'copy-build',
|
|
||||||
closeBundle: function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
||||||
var sourcePath, targetPath, error_1;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
switch (_a.label) {
|
|
||||||
case 0:
|
|
||||||
if (!!isDev) return [3 /*break*/, 6];
|
|
||||||
sourcePath = path.resolve(__dirname, 'build');
|
|
||||||
targetPath = path.resolve(__dirname, '../inventory-server/frontend/build');
|
|
||||||
_a.label = 1;
|
|
||||||
case 1:
|
|
||||||
_a.trys.push([1, 5, , 6]);
|
|
||||||
return [4 /*yield*/, fs.ensureDir(path.dirname(targetPath))];
|
|
||||||
case 2:
|
|
||||||
_a.sent();
|
|
||||||
return [4 /*yield*/, fs.remove(targetPath)];
|
|
||||||
case 3:
|
|
||||||
_a.sent();
|
|
||||||
return [4 /*yield*/, fs.copy(sourcePath, targetPath)];
|
|
||||||
case 4:
|
|
||||||
_a.sent();
|
|
||||||
console.log('Build files copied successfully to server directory!');
|
|
||||||
return [3 /*break*/, 6];
|
|
||||||
case 5:
|
|
||||||
error_1 = _a.sent();
|
|
||||||
console.error('Error copying build files:', error_1);
|
|
||||||
process.exit(1);
|
|
||||||
return [3 /*break*/, 6];
|
|
||||||
case 6: return [2 /*return*/];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}); }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
"@": path.resolve(__dirname, "./src"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
host: "0.0.0.0",
|
|
||||||
port: 5173,
|
|
||||||
proxy: isDev ? {
|
|
||||||
"/api": {
|
|
||||||
target: "https://inventory.kent.pw",
|
|
||||||
changeOrigin: true,
|
|
||||||
secure: false,
|
|
||||||
ws: true,
|
|
||||||
xfwd: true,
|
|
||||||
cookieDomainRewrite: "",
|
|
||||||
withCredentials: true,
|
|
||||||
rewrite: function (path) { return path.replace(/^\/api/, "/api"); },
|
|
||||||
configure: function (proxy, _options) {
|
|
||||||
proxy.on("error", function (err, req, res) {
|
|
||||||
console.log("API proxy error:", err);
|
|
||||||
res.writeHead(500, {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
});
|
|
||||||
res.end(JSON.stringify({ error: "Proxy Error", message: err.message }));
|
|
||||||
});
|
|
||||||
proxy.on("proxyReq", function (proxyReq, req, _res) {
|
|
||||||
console.log("Outgoing request:", {
|
|
||||||
method: req.method,
|
|
||||||
url: req.url,
|
|
||||||
headers: proxyReq.getHeaders(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
proxy.on("proxyRes", function (proxyRes, req, _res) {
|
|
||||||
console.log("Proxy response:", {
|
|
||||||
statusCode: proxyRes.statusCode,
|
|
||||||
url: req.url,
|
|
||||||
headers: proxyRes.headers,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} : {},
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
outDir: "build",
|
|
||||||
sourcemap: true,
|
|
||||||
rollupOptions: {
|
|
||||||
output: {
|
|
||||||
manualChunks: {
|
|
||||||
vendor: ["react", "react-dom", "react-router-dom"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -32,6 +32,9 @@ export default defineConfig(({ mode }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
define: {
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
@@ -40,7 +43,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: isDev ? {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "https://inventory.kent.pw",
|
target: "https://inventory.kent.pw",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
@@ -61,14 +64,14 @@ export default defineConfig(({ mode }) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
proxy.on("proxyReq", (proxyReq, req, _res) => {
|
proxy.on("proxyReq", (proxyReq, req, _res) => {
|
||||||
console.log("Outgoing request:", {
|
console.log("Outgoing request to API:", {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
url: req.url,
|
url: req.url,
|
||||||
headers: proxyReq.getHeaders(),
|
headers: proxyReq.getHeaders(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
proxy.on("proxyRes", (proxyRes, req, _res) => {
|
proxy.on("proxyRes", (proxyRes, req, _res) => {
|
||||||
console.log("Proxy response:", {
|
console.log("API Proxy response:", {
|
||||||
statusCode: proxyRes.statusCode,
|
statusCode: proxyRes.statusCode,
|
||||||
url: req.url,
|
url: req.url,
|
||||||
headers: proxyRes.headers,
|
headers: proxyRes.headers,
|
||||||
@@ -76,7 +79,48 @@ export default defineConfig(({ mode }) => {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} : {},
|
"/auth-inv": {
|
||||||
|
target: "https://inventory.kent.pw",
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
ws: true,
|
||||||
|
xfwd: true,
|
||||||
|
cookieDomainRewrite: {
|
||||||
|
"inventory.kent.pw": "localhost"
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
onProxyReq: (proxyReq, req) => {
|
||||||
|
// Add origin header to match CORS policy
|
||||||
|
proxyReq.setHeader('Origin', 'http://localhost:5173');
|
||||||
|
},
|
||||||
|
rewrite: (path) => path.replace(/^\/auth-inv/, "/auth-inv"),
|
||||||
|
configure: (proxy, _options) => {
|
||||||
|
proxy.on("error", (err, req, res) => {
|
||||||
|
console.log("Auth proxy error:", err)
|
||||||
|
res.writeHead(500, {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
})
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({ error: "Proxy Error", message: err.message })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
proxy.on("proxyReq", (proxyReq, req, _res) => {
|
||||||
|
console.log("Outgoing request to Auth:", {
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
headers: proxyReq.getHeaders(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
proxy.on("proxyRes", (proxyRes, req, _res) => {
|
||||||
|
console.log("Auth Proxy response:", {
|
||||||
|
statusCode: proxyRes.statusCode,
|
||||||
|
url: req.url,
|
||||||
|
headers: proxyRes.headers,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: "build",
|
outDir: "build",
|
||||||
|
|||||||
Reference in New Issue
Block a user