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

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