feat: implementar autenticacao real com JWT para App e Admin
Build and Deploy (Gitea Actions) / build-and-deploy (push) Failing after 40s
Details
Build and Deploy (Gitea Actions) / build-and-deploy (push) Failing after 40s
Details
This commit is contained in:
parent
2bf14b0669
commit
6960d7d400
|
|
@ -8,10 +8,12 @@
|
||||||
"name": "agendapro-admin",
|
"name": "agendapro-admin",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^22.15.21",
|
"@types/node": "^22.15.21",
|
||||||
"@types/pg": "^8.11.10",
|
"@types/pg": "^8.11.10",
|
||||||
"@types/react": "^19.1.4",
|
"@types/react": "^19.1.4",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.5",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
"lucide-react": "^0.511.0",
|
"lucide-react": "^0.511.0",
|
||||||
"next": "^15.3.3",
|
"next": "^15.3.3",
|
||||||
"pg": "^8.16.0",
|
"pg": "^8.16.0",
|
||||||
|
|
@ -639,6 +641,22 @@
|
||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/jsonwebtoken": {
|
||||||
|
"version": "9.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
|
||||||
|
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/ms": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/ms": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.19.19",
|
"version": "22.19.19",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz",
|
||||||
|
|
@ -677,6 +695,12 @@
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001793",
|
"version": "1.0.30001793",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
|
||||||
|
|
@ -719,6 +743,100 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||||
|
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^4.0.1",
|
||||||
|
"lodash.includes": "^4.3.0",
|
||||||
|
"lodash.isboolean": "^3.0.3",
|
||||||
|
"lodash.isinteger": "^4.0.4",
|
||||||
|
"lodash.isnumber": "^3.0.3",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.isstring": "^4.0.1",
|
||||||
|
"lodash.once": "^4.0.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"semver": "^7.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jwa": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jws": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^2.0.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lodash.includes": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isboolean": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isinteger": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isnumber": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isstring": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.once": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lucide-react": {
|
"node_modules/lucide-react": {
|
||||||
"version": "0.511.0",
|
"version": "0.511.0",
|
||||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.511.0.tgz",
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.511.0.tgz",
|
||||||
|
|
@ -728,6 +846,12 @@
|
||||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.12",
|
"version": "3.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||||
|
|
@ -981,6 +1105,26 @@
|
||||||
"react": "^19.2.6"
|
"react": "^19.2.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.27.0",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||||
|
|
@ -992,7 +1136,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
|
||||||
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
|
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"optional": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,17 @@
|
||||||
"start": "next start -p 3001"
|
"start": "next start -p 3001"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "^15.3.3",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"react": "^19.1.0",
|
|
||||||
"react-dom": "^19.1.0",
|
|
||||||
"typescript": "^5.8.3",
|
|
||||||
"@types/node": "^22.15.21",
|
"@types/node": "^22.15.21",
|
||||||
|
"@types/pg": "^8.11.10",
|
||||||
"@types/react": "^19.1.4",
|
"@types/react": "^19.1.4",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.5",
|
||||||
"@types/pg": "^8.11.10",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"lucide-react": "^0.511.0",
|
"lucide-react": "^0.511.0",
|
||||||
"pg": "^8.16.0"
|
"next": "^15.3.3",
|
||||||
|
"pg": "^8.16.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const { email, password } = await req.json();
|
||||||
|
if (!email || !password) return NextResponse.json({ error: 'Email e senha são obrigatórios' }, { status: 400 });
|
||||||
|
|
||||||
|
// Hardcoded Admin Credentials for SaaS owner
|
||||||
|
// In production, you'd use ENV variables like process.env.ADMIN_EMAIL
|
||||||
|
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@agendapro.com';
|
||||||
|
const ADMIN_PASS = process.env.ADMIN_PASSWORD || 'admin123';
|
||||||
|
|
||||||
|
if (email !== ADMIN_EMAIL || password !== ADMIN_PASS) {
|
||||||
|
return NextResponse.json({ error: 'Credenciais inválidas' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = process.env.JWT_SECRET || 'fallback_secret_for_development';
|
||||||
|
const token = jwt.sign({
|
||||||
|
role: 'superadmin',
|
||||||
|
email: ADMIN_EMAIL
|
||||||
|
}, secret, { expiresIn: '7d' });
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
name: 'Super Admin',
|
||||||
|
email: ADMIN_EMAIL,
|
||||||
|
role: 'superadmin'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Admin Login error:', error);
|
||||||
|
return NextResponse.json({ error: 'Erro interno do servidor' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Mail, Lock, Shield, ArrowRight } from 'lucide-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
export default function AdminLoginPage() {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
setError(data.error || 'Erro ao realizar login');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('agendapro_admin_token', data.token);
|
||||||
|
localStorage.setItem('agendapro_admin_email', data.user.email);
|
||||||
|
localStorage.setItem('agendapro_admin_name', data.user.name);
|
||||||
|
|
||||||
|
router.push('/');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError('Falha de conexão com o servidor');
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--bg-primary)' }}>
|
||||||
|
<div style={{ width: '100%', maxWidth: '420px', padding: '40px', background: 'var(--bg-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-xl)', border: '1px solid var(--border-color)', animation: 'slideUp 0.5s ease' }}>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '32px' }}>
|
||||||
|
<div style={{ width: 64, height: 64, background: 'var(--primary)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '16px', boxShadow: 'var(--shadow-md)' }}>
|
||||||
|
<Shield size={32} color="white" />
|
||||||
|
</div>
|
||||||
|
<h1 style={{ fontSize: '24px', fontWeight: 800 }}>Admin Panel</h1>
|
||||||
|
<p style={{ color: 'var(--text-secondary)', fontSize: '14px', marginTop: '4px' }}>Acesso restrito ao proprietário do sistema</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div style={{ background: 'var(--danger)', color: 'white', padding: '12px', borderRadius: 'var(--radius-md)', marginBottom: '20px', fontSize: '14px', textAlign: 'center' }}>{error}</div>}
|
||||||
|
|
||||||
|
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||||
|
<div className="form-group" style={{ margin: 0 }}>
|
||||||
|
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Mail size={14} /> E-mail Administrativo</label>
|
||||||
|
<input type="email" required className="form-input" placeholder="admin@agendapro.com"
|
||||||
|
value={email} onChange={e => setEmail(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="form-group" style={{ margin: 0 }}>
|
||||||
|
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Lock size={14} /> Senha Mestra</label>
|
||||||
|
<input type="password" required className="form-input" placeholder="••••••••"
|
||||||
|
value={password} onChange={e => setPassword(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" className="btn btn-primary" disabled={loading}
|
||||||
|
style={{ width: '100%', padding: '12px', fontSize: '16px', justifyContent: 'center', marginTop: '12px' }}>
|
||||||
|
{loading ? 'Autenticando...' : <><ArrowRight size={18} /> Entrar no Painel</>}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p style={{ textAlign: 'center', marginTop: '32px', fontSize: '12px', color: 'var(--text-muted)' }}>
|
||||||
|
AgendaPRO SaaS © 2026. Todos os direitos reservados.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { LayoutDashboard, Users, CreditCard, Package, Settings, Shield, TrendingUp, Bell, LogOut, User, CheckCircle, AlertCircle } from 'lucide-react';
|
import { LayoutDashboard, Users, CreditCard, Package, Settings, Shield, TrendingUp, Bell, LogOut, User, CheckCircle, AlertCircle } from 'lucide-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import AdminDashboard from '@/components/AdminDashboard';
|
import AdminDashboard from '@/components/AdminDashboard';
|
||||||
import Tenants from '@/components/Tenants';
|
import Tenants from '@/components/Tenants';
|
||||||
import Plans from '@/components/Plans';
|
import Plans from '@/components/Plans';
|
||||||
|
|
@ -33,23 +34,38 @@ const mockNotifications = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
|
const router = useRouter();
|
||||||
const [currentPage, setCurrentPage] = useState('dashboard');
|
const [currentPage, setCurrentPage] = useState('dashboard');
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const [showNotifications, setShowNotifications] = useState(false);
|
const [showNotifications, setShowNotifications] = useState(false);
|
||||||
const [showProfile, setShowProfile] = useState(false);
|
const [showProfile, setShowProfile] = useState(false);
|
||||||
const [notifications, setNotifications] = useState(mockNotifications);
|
const [notifications, setNotifications] = useState(mockNotifications);
|
||||||
|
|
||||||
|
const [adminName, setAdminName] = useState('Super Admin');
|
||||||
|
const [adminEmail, setAdminEmail] = useState('admin@agendapro.com');
|
||||||
|
|
||||||
const notifRef = useRef<HTMLDivElement>(null);
|
const notifRef = useRef<HTMLDivElement>(null);
|
||||||
const profileRef = useRef<HTMLDivElement>(null);
|
const profileRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Auth Check
|
||||||
|
const token = localStorage.getItem('agendapro_admin_token');
|
||||||
|
if (!token) {
|
||||||
|
router.push('/login');
|
||||||
|
} else {
|
||||||
|
setAdminName(localStorage.getItem('agendapro_admin_name') || 'Super Admin');
|
||||||
|
setAdminEmail(localStorage.getItem('agendapro_admin_email') || 'admin@agendapro.com');
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
if (notifRef.current && !notifRef.current.contains(e.target as Node)) setShowNotifications(false);
|
if (notifRef.current && !notifRef.current.contains(e.target as Node)) setShowNotifications(false);
|
||||||
if (profileRef.current && !profileRef.current.contains(e.target as Node)) setShowProfile(false);
|
if (profileRef.current && !profileRef.current.contains(e.target as Node)) setShowProfile(false);
|
||||||
};
|
};
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
}, []);
|
}, [router]);
|
||||||
|
|
||||||
const playNotificationSound = () => {
|
const playNotificationSound = () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -95,10 +111,18 @@ export default function AdminPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
window.location.href = '/';
|
localStorage.removeItem('agendapro_admin_token');
|
||||||
|
localStorage.removeItem('agendapro_admin_email');
|
||||||
|
localStorage.removeItem('agendapro_admin_name');
|
||||||
|
router.push('/login');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>Carregando painel...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
const unreadCount = notifications.filter(n => !n.read).length;
|
const unreadCount = notifications.filter(n => !n.read).length;
|
||||||
|
const adminInitials = adminName.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
|
||||||
|
|
||||||
const renderPage = () => {
|
const renderPage = () => {
|
||||||
switch (currentPage) {
|
switch (currentPage) {
|
||||||
|
|
@ -140,10 +164,10 @@ export default function AdminPage() {
|
||||||
</nav>
|
</nav>
|
||||||
<div style={{ padding: '16px', borderTop: '1px solid var(--border-color)' }}>
|
<div style={{ padding: '16px', borderTop: '1px solid var(--border-color)' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '10px', borderRadius: 'var(--radius-sm)', background: 'var(--bg-card)' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '10px', borderRadius: 'var(--radius-sm)', background: 'var(--bg-card)' }}>
|
||||||
<div className="avatar" style={{ width: 32, height: 32, fontSize: 12 }}>SA</div>
|
<div className="avatar" style={{ width: 32, height: 32, fontSize: 12 }}>{adminInitials}</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1, overflow: 'hidden' }}>
|
||||||
<div style={{ fontSize: '13px', fontWeight: 600 }}>Super Admin</div>
|
<div style={{ fontSize: '13px', fontWeight: 600, whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }}>{adminName}</div>
|
||||||
<div style={{ fontSize: '11px', color: 'var(--text-muted)' }}>admin@agendapro.com</div>
|
<div style={{ fontSize: '11px', color: 'var(--text-muted)', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }}>{adminEmail}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -205,7 +229,7 @@ export default function AdminPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ position: 'relative' }} ref={profileRef}>
|
<div style={{ position: 'relative' }} ref={profileRef}>
|
||||||
<div className="avatar" style={{ width: 36, height: 36, fontSize: 13, cursor: 'pointer', transition: 'transform 0.2s' }} onClick={handleProfileClick}>SA</div>
|
<div className="avatar" style={{ width: 36, height: 36, fontSize: 13, cursor: 'pointer', transition: 'transform 0.2s' }} onClick={handleProfileClick}>{adminInitials}</div>
|
||||||
|
|
||||||
{showProfile && (
|
{showProfile && (
|
||||||
<div className="card" style={{
|
<div className="card" style={{
|
||||||
|
|
@ -214,8 +238,8 @@ export default function AdminPage() {
|
||||||
animation: 'slideUp 0.2s ease-out forwards'
|
animation: 'slideUp 0.2s ease-out forwards'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ padding: '12px', borderBottom: '1px solid var(--border-color)', marginBottom: '8px' }}>
|
<div style={{ padding: '12px', borderBottom: '1px solid var(--border-color)', marginBottom: '8px' }}>
|
||||||
<div style={{ fontWeight: 600, fontSize: '14px' }}>Super Admin</div>
|
<div style={{ fontWeight: 600, fontSize: '14px', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }}>{adminName}</div>
|
||||||
<div style={{ color: 'var(--text-muted)', fontSize: '12px', marginTop: '2px' }}>admin@agendapro.com</div>
|
<div style={{ color: 'var(--text-muted)', fontSize: '12px', marginTop: '2px', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }}>{adminEmail}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button className="btn-ghost" style={{ width: '100%', justifyContent: 'flex-start', padding: '10px 12px', fontSize: '13px' }}>
|
<button className="btn-ghost" style={{ width: '100%', justifyContent: 'flex-start', padding: '10px 12px', fontSize: '13px' }}>
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { query } from '@/lib/db';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const { email, password } = await req.json();
|
||||||
|
if (!email || !password) return NextResponse.json({ error: 'Email e senha são obrigatórios' }, { status: 400 });
|
||||||
|
|
||||||
|
const result = await query(`
|
||||||
|
SELECT u.id, u.tenant_id, u.name, u.role, u.password_hash, t.slug as tenant_slug, t.name as tenant_name
|
||||||
|
FROM users u
|
||||||
|
JOIN tenants t ON u.tenant_id = t.id
|
||||||
|
WHERE u.email = $1 AND u.is_active = true
|
||||||
|
`, [email]);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) return NextResponse.json({ error: 'Credenciais inválidas' }, { status: 401 });
|
||||||
|
|
||||||
|
const user = result.rows[0];
|
||||||
|
const isValid = await bcrypt.compare(password, user.password_hash);
|
||||||
|
if (!isValid) return NextResponse.json({ error: 'Credenciais inválidas' }, { status: 401 });
|
||||||
|
|
||||||
|
// Generate JWT
|
||||||
|
const secret = process.env.JWT_SECRET || 'fallback_secret_for_development';
|
||||||
|
const token = jwt.sign({
|
||||||
|
userId: user.id,
|
||||||
|
tenantId: user.tenant_id,
|
||||||
|
role: user.role
|
||||||
|
}, secret, { expiresIn: '7d' });
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: email,
|
||||||
|
role: user.role,
|
||||||
|
tenant_id: user.tenant_id,
|
||||||
|
tenant_slug: user.tenant_slug,
|
||||||
|
tenant_name: user.tenant_name
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
return NextResponse.json({ error: 'Erro interno do servidor' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,17 +7,41 @@ export default function LoginPage() {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const handleLogin = (e: React.FormEvent) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// Simulação de login (em produção faria requisição à API)
|
setError('');
|
||||||
setTimeout(() => {
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
setError(data.error || 'Erro ao realizar login');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
localStorage.setItem('agendapro_token', 'demo_token');
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('agendapro_token', data.token);
|
||||||
|
localStorage.setItem('agendapro_tenant_slug', data.user.tenant_slug);
|
||||||
|
localStorage.setItem('agendapro_tenant_name', data.user.tenant_name);
|
||||||
|
localStorage.setItem('agendapro_email', data.user.email);
|
||||||
|
localStorage.setItem('agendapro_role', data.user.role);
|
||||||
|
|
||||||
router.push('/');
|
router.push('/');
|
||||||
}, 1000);
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError('Falha de conexão com o servidor');
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -41,7 +65,9 @@ export default function LoginPage() {
|
||||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '40px' }}>
|
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '40px' }}>
|
||||||
<div style={{ width: '100%', maxWidth: '400px', animation: 'slideUp 0.5s ease' }}>
|
<div style={{ width: '100%', maxWidth: '400px', animation: 'slideUp 0.5s ease' }}>
|
||||||
<h2 style={{ fontSize: '28px', fontWeight: 800, marginBottom: '8px' }}>Bem-vindo de volta</h2>
|
<h2 style={{ fontSize: '28px', fontWeight: 800, marginBottom: '8px' }}>Bem-vindo de volta</h2>
|
||||||
<p style={{ color: 'var(--text-secondary)', marginBottom: '32px' }}>Faça login para acessar o seu painel</p>
|
<p style={{ color: 'var(--text-secondary)', marginBottom: '24px' }}>Faça login para acessar o seu painel</p>
|
||||||
|
|
||||||
|
{error && <div style={{ background: 'var(--danger)', color: 'white', padding: '12px', borderRadius: 'var(--radius-md)', marginBottom: '20px', fontSize: '14px', textAlign: 'center' }}>{error}</div>}
|
||||||
|
|
||||||
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||||
<div className="form-group" style={{ margin: 0 }}>
|
<div className="form-group" style={{ margin: 0 }}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue