feat: add PIN-based login with server-side cookie sessions
All checks were successful
Deploy HiHala Dashboard / deploy (push) Successful in 6s
All checks were successful
Deploy HiHala Dashboard / deploy (push) Successful in 6s
- Server: POST /auth/login (verify PIN, set httpOnly cookie) - Server: GET /auth/check, POST /auth/logout - Client: Login page shown when not authenticated - Session persists 7 days via httpOnly cookie - PIN stored server-side only (ADMIN_PIN env var) - Dashboard loads data only after successful auth Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,3 +14,7 @@ NOCODB_BASE_ID=your-base-id
|
|||||||
|
|
||||||
# ETL sync secret (for cron auth)
|
# ETL sync secret (for cron auth)
|
||||||
ETL_SECRET=your-secret-here
|
ETL_SECRET=your-secret-here
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
ADMIN_PIN=your-pin-code
|
||||||
|
SESSION_SECRET=your-random-session-secret
|
||||||
|
|||||||
31
server/package-lock.json
generated
31
server/package-lock.json
generated
@@ -9,11 +9,13 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2"
|
"express": "^4.18.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/cookie-parser": "^1.4.10",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.0",
|
||||||
@@ -483,6 +485,16 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/cookie-parser": {
|
||||||
|
"version": "1.4.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz",
|
||||||
|
"integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/express": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/cors": {
|
"node_modules/@types/cors": {
|
||||||
"version": "2.8.19",
|
"version": "2.8.19",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||||
@@ -730,6 +742,25 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie-parser": {
|
||||||
|
"version": "1.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
|
||||||
|
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "0.7.2",
|
||||||
|
"cookie-signature": "1.0.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie-parser/node_modules/cookie-signature": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cookie-signature": {
|
"node_modules/cookie-signature": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||||
|
|||||||
@@ -10,11 +10,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2"
|
"express": "^4.18.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/cookie-parser": "^1.4.10",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.0",
|
||||||
|
|||||||
@@ -33,3 +33,8 @@ export const nocodb = {
|
|||||||
export const etl = {
|
export const etl = {
|
||||||
secret: process.env.ETL_SECRET || '',
|
secret: process.env.ETL_SECRET || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const auth = {
|
||||||
|
adminPin: process.env.ADMIN_PIN || '',
|
||||||
|
sessionSecret: process.env.SESSION_SECRET || 'hihala-dev-session-secret',
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
|
import cookieParser from 'cookie-parser';
|
||||||
import { server, erp, nocodb } from './config';
|
import { server, erp, nocodb } from './config';
|
||||||
|
import authRoutes from './routes/auth';
|
||||||
import erpRoutes from './routes/erp';
|
import erpRoutes from './routes/erp';
|
||||||
import etlRoutes from './routes/etl';
|
import etlRoutes from './routes/etl';
|
||||||
import seasonsRoutes from './routes/seasons';
|
import seasonsRoutes from './routes/seasons';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(cors());
|
app.use(cors({ origin: true, credentials: true }));
|
||||||
|
app.use(cookieParser());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// Mount routes
|
// Mount routes
|
||||||
|
app.use('/auth', authRoutes);
|
||||||
app.use('/api/erp', erpRoutes);
|
app.use('/api/erp', erpRoutes);
|
||||||
app.use('/api/etl', etlRoutes);
|
app.use('/api/etl', etlRoutes);
|
||||||
app.use('/api/seasons', seasonsRoutes);
|
app.use('/api/seasons', seasonsRoutes);
|
||||||
|
|||||||
66
server/src/routes/auth.ts
Normal file
66
server/src/routes/auth.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { Router, Request, Response } from 'express';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { auth } from '../config';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// In-memory session store (simple — works for single server)
|
||||||
|
const sessions = new Map<string, { createdAt: number }>();
|
||||||
|
const SESSION_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||||
|
|
||||||
|
function generateSessionId(): string {
|
||||||
|
return crypto.randomBytes(32).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidSession(sessionId: string): boolean {
|
||||||
|
const session = sessions.get(sessionId);
|
||||||
|
if (!session) return false;
|
||||||
|
if (Date.now() - session.createdAt > SESSION_MAX_AGE) {
|
||||||
|
sessions.delete(sessionId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /auth/login — verify PIN, set session cookie
|
||||||
|
router.post('/login', (req: Request, res: Response) => {
|
||||||
|
const { pin } = req.body;
|
||||||
|
|
||||||
|
if (!auth.adminPin) {
|
||||||
|
res.status(503).json({ error: 'Auth not configured' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pin !== auth.adminPin) {
|
||||||
|
res.status(401).json({ error: 'Invalid PIN' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = generateSessionId();
|
||||||
|
sessions.set(sessionId, { createdAt: Date.now() });
|
||||||
|
|
||||||
|
res.cookie('hihala_session', sessionId, {
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge: SESSION_MAX_AGE,
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /auth/check — check if session is valid
|
||||||
|
router.get('/check', (req: Request, res: Response) => {
|
||||||
|
const sessionId = req.cookies?.hihala_session;
|
||||||
|
res.json({ authenticated: !!sessionId && isValidSession(sessionId) });
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /auth/logout — destroy session
|
||||||
|
router.post('/logout', (req: Request, res: Response) => {
|
||||||
|
const sessionId = req.cookies?.hihala_session;
|
||||||
|
if (sessionId) sessions.delete(sessionId);
|
||||||
|
res.clearCookie('hihala_session', { path: '/' });
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
88
src/App.css
88
src/App.css
@@ -851,6 +851,94 @@ table tbody tr:hover {
|
|||||||
accent-color: var(--accent);
|
accent-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Login page */
|
||||||
|
.login-page {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 48px 40px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 380px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card input {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card input:focus {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: -1px;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card button {
|
||||||
|
padding: 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card button:hover:not(:disabled) {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-error {
|
||||||
|
color: var(--danger, #dc2626);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-link {
|
.settings-link {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 32px 0 16px;
|
padding: 32px 0 16px;
|
||||||
|
|||||||
35
src/App.tsx
35
src/App.tsx
@@ -4,6 +4,7 @@ import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react
|
|||||||
const Dashboard = lazy(() => import('./components/Dashboard'));
|
const Dashboard = lazy(() => import('./components/Dashboard'));
|
||||||
const Comparison = lazy(() => import('./components/Comparison'));
|
const Comparison = lazy(() => import('./components/Comparison'));
|
||||||
const Settings = lazy(() => import('./components/Settings'));
|
const Settings = lazy(() => import('./components/Settings'));
|
||||||
|
import Login from './components/Login';
|
||||||
import LoadingSkeleton from './components/shared/LoadingSkeleton';
|
import LoadingSkeleton from './components/shared/LoadingSkeleton';
|
||||||
import { fetchData, getCacheStatus, refreshData } from './services/dataService';
|
import { fetchData, getCacheStatus, refreshData } from './services/dataService';
|
||||||
import { fetchSeasons } from './services/seasonsService';
|
import { fetchSeasons } from './services/seasonsService';
|
||||||
@@ -36,6 +37,7 @@ interface DataSource {
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { t, dir, switchLanguage } = useLanguage();
|
const { t, dir, switchLanguage } = useLanguage();
|
||||||
|
const [authenticated, setAuthenticated] = useState<boolean | null>(null);
|
||||||
const [data, setData] = useState<MuseumRecord[]>([]);
|
const [data, setData] = useState<MuseumRecord[]>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [refreshing, setRefreshing] = useState<boolean>(false);
|
const [refreshing, setRefreshing] = useState<boolean>(false);
|
||||||
@@ -105,16 +107,49 @@ function App() {
|
|||||||
setSeasons(s);
|
setSeasons(s);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Check auth on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
fetch('/auth/check', { credentials: 'include' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
setAuthenticated(d.authenticated);
|
||||||
|
if (d.authenticated) {
|
||||||
loadData();
|
loadData();
|
||||||
loadSeasons();
|
loadSeasons();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setAuthenticated(false));
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
setAuthenticated(true);
|
||||||
|
loadData();
|
||||||
|
loadSeasons();
|
||||||
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
loadData(true);
|
loadData(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Auth check loading
|
||||||
|
if (authenticated === null) {
|
||||||
|
return (
|
||||||
|
<div className="app" dir={dir}>
|
||||||
|
<LoadingSkeleton />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not authenticated — show login
|
||||||
|
if (!authenticated) {
|
||||||
|
return (
|
||||||
|
<div className="app" dir={dir}>
|
||||||
|
<Login onLogin={handleLogin} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="app" dir={dir}>
|
<div className="app" dir={dir}>
|
||||||
|
|||||||
74
src/components/Login.tsx
Normal file
74
src/components/Login.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
|
|
||||||
|
interface LoginProps {
|
||||||
|
onLogin: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Login({ onLogin }: LoginProps) {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const [pin, setPin] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({ pin }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
setError(t('login.invalid'));
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onLogin();
|
||||||
|
} catch {
|
||||||
|
setError(t('login.error'));
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="login-page">
|
||||||
|
<div className="login-card">
|
||||||
|
<div className="login-brand">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="var(--accent)" aria-hidden="true">
|
||||||
|
<rect x="3" y="3" width="7" height="7" rx="1"/>
|
||||||
|
<rect x="14" y="3" width="7" height="4" rx="1"/>
|
||||||
|
<rect x="3" y="14" width="7" height="7" rx="1"/>
|
||||||
|
<rect x="14" y="11" width="7" height="10" rx="1"/>
|
||||||
|
</svg>
|
||||||
|
<h1>HiHala Data</h1>
|
||||||
|
</div>
|
||||||
|
<p className="login-subtitle">{t('login.subtitle')}</p>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
inputMode="numeric"
|
||||||
|
value={pin}
|
||||||
|
onChange={e => setPin(e.target.value)}
|
||||||
|
placeholder={t('login.placeholder')}
|
||||||
|
autoFocus
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
{error && <p className="login-error">{error}</p>}
|
||||||
|
<button type="submit" disabled={loading || !pin}>
|
||||||
|
{loading ? '...' : t('login.submit')}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login;
|
||||||
@@ -168,6 +168,13 @@
|
|||||||
"namePlaceholder": "مثال: رمضان",
|
"namePlaceholder": "مثال: رمضان",
|
||||||
"add": "إضافة"
|
"add": "إضافة"
|
||||||
},
|
},
|
||||||
|
"login": {
|
||||||
|
"subtitle": "أدخل رمز PIN للوصول إلى لوحة التحكم",
|
||||||
|
"placeholder": "رمز PIN",
|
||||||
|
"submit": "تسجيل الدخول",
|
||||||
|
"invalid": "رمز PIN غير صحيح",
|
||||||
|
"error": "خطأ في الاتصال. يرجى المحاولة مرة أخرى."
|
||||||
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"config": "لم يتم تهيئة لوحة المعلومات. يرجى إعداد اتصال ERP API.",
|
"config": "لم يتم تهيئة لوحة المعلومات. يرجى إعداد اتصال ERP API.",
|
||||||
"network": "لا يمكن الوصول إلى خادم قاعدة البيانات. يرجى التحقق من اتصالك بالإنترنت.",
|
"network": "لا يمكن الوصول إلى خادم قاعدة البيانات. يرجى التحقق من اتصالك بالإنترنت.",
|
||||||
|
|||||||
@@ -168,6 +168,13 @@
|
|||||||
"namePlaceholder": "e.g. Ramadan",
|
"namePlaceholder": "e.g. Ramadan",
|
||||||
"add": "Add"
|
"add": "Add"
|
||||||
},
|
},
|
||||||
|
"login": {
|
||||||
|
"subtitle": "Enter your PIN to access the dashboard",
|
||||||
|
"placeholder": "PIN code",
|
||||||
|
"submit": "Login",
|
||||||
|
"invalid": "Invalid PIN code",
|
||||||
|
"error": "Connection error. Please try again."
|
||||||
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"config": "The dashboard is not configured. Please set up the ERP API connection.",
|
"config": "The dashboard is not configured. Please set up the ERP API connection.",
|
||||||
"network": "Cannot reach the database server. Please check your internet connection.",
|
"network": "Cannot reach the database server. Please check your internet connection.",
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
'/auth': {
|
||||||
|
target: 'http://localhost:3002',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
'/api/erp': {
|
'/api/erp': {
|
||||||
target: 'http://localhost:3002',
|
target: 'http://localhost:3002',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user