diff --git a/admin/package-lock.json b/admin/package-lock.json
index cdae542..4d68638 100644
--- a/admin/package-lock.json
+++ b/admin/package-lock.json
@@ -8,10 +8,12 @@
"name": "agendapro-admin",
"version": "1.0.0",
"dependencies": {
+ "@types/jsonwebtoken": "^9.0.10",
"@types/node": "^22.15.21",
"@types/pg": "^8.11.10",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
+ "jsonwebtoken": "^9.0.3",
"lucide-react": "^0.511.0",
"next": "^15.3.3",
"pg": "^8.16.0",
@@ -639,6 +641,22 @@
"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": {
"version": "22.19.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz",
@@ -677,6 +695,12 @@
"@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": {
"version": "1.0.30001793",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
@@ -719,6 +743,100 @@
"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": {
"version": "0.511.0",
"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"
}
},
+ "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": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
@@ -981,6 +1105,26 @@
"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": {
"version": "0.27.0",
"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",
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
"license": "ISC",
- "optional": true,
"bin": {
"semver": "bin/semver.js"
},
diff --git a/admin/package.json b/admin/package.json
index 20f41cd..94d9d1d 100644
--- a/admin/package.json
+++ b/admin/package.json
@@ -8,15 +8,17 @@
"start": "next start -p 3001"
},
"dependencies": {
- "next": "^15.3.3",
- "react": "^19.1.0",
- "react-dom": "^19.1.0",
- "typescript": "^5.8.3",
+ "@types/jsonwebtoken": "^9.0.10",
"@types/node": "^22.15.21",
+ "@types/pg": "^8.11.10",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
- "@types/pg": "^8.11.10",
+ "jsonwebtoken": "^9.0.3",
"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"
}
}
diff --git a/admin/src/app/api/auth/login/route.ts b/admin/src/app/api/auth/login/route.ts
new file mode 100644
index 0000000..02e5654
--- /dev/null
+++ b/admin/src/app/api/auth/login/route.ts
@@ -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 });
+ }
+}
diff --git a/admin/src/app/login/page.tsx b/admin/src/app/login/page.tsx
new file mode 100644
index 0000000..2a209c4
--- /dev/null
+++ b/admin/src/app/login/page.tsx
@@ -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 (
+
+
+
+
+
+
+
+
Admin Panel
+
Acesso restrito ao proprietário do sistema
+
+
+ {error &&
{error}
}
+
+
+
+
+ AgendaPRO SaaS © 2026. Todos os direitos reservados.
+
+
+
+ );
+}
diff --git a/admin/src/app/page.tsx b/admin/src/app/page.tsx
index 87223e4..0b6adf7 100644
--- a/admin/src/app/page.tsx
+++ b/admin/src/app/page.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState, useEffect, useRef } from '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 Tenants from '@/components/Tenants';
import Plans from '@/components/Plans';
@@ -33,23 +34,38 @@ const mockNotifications = [
];
export default function AdminPage() {
+ const router = useRouter();
const [currentPage, setCurrentPage] = useState('dashboard');
+ const [isLoading, setIsLoading] = useState(true);
const [showNotifications, setShowNotifications] = useState(false);
const [showProfile, setShowProfile] = useState(false);
const [notifications, setNotifications] = useState(mockNotifications);
+
+ const [adminName, setAdminName] = useState('Super Admin');
+ const [adminEmail, setAdminEmail] = useState('admin@agendapro.com');
const notifRef = useRef(null);
const profileRef = useRef(null);
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) => {
if (notifRef.current && !notifRef.current.contains(e.target as Node)) setShowNotifications(false);
if (profileRef.current && !profileRef.current.contains(e.target as Node)) setShowProfile(false);
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
- }, []);
+ }, [router]);
const playNotificationSound = () => {
try {
@@ -95,10 +111,18 @@ export default function AdminPage() {
};
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 Carregando painel...
;
+ }
+
const unreadCount = notifications.filter(n => !n.read).length;
+ const adminInitials = adminName.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
const renderPage = () => {
switch (currentPage) {
@@ -140,10 +164,10 @@ export default function AdminPage() {
-
SA
-
-
Super Admin
-
admin@agendapro.com
+
{adminInitials}
+
+
{adminName}
+
{adminEmail}
@@ -205,7 +229,7 @@ export default function AdminPage() {
-
SA
+
{adminInitials}
{showProfile && (
-
Super Admin
-
admin@agendapro.com
+
{adminName}
+
{adminEmail}