feat: adicionar campo de senha ao criar novo tenant no Admin
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m33s Details

This commit is contained in:
Sidney 2026-06-07 20:27:28 -03:00
parent 2b6a645bfc
commit 73b7e171c9
4 changed files with 47 additions and 4 deletions

View File

@ -8,11 +8,13 @@
"name": "agendapro-admin", "name": "agendapro-admin",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.10", "@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",
"bcryptjs": "^3.0.3",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"lucide-react": "^0.511.0", "lucide-react": "^0.511.0",
"next": "^15.3.3", "next": "^15.3.3",
@ -641,6 +643,12 @@
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
}, },
"node_modules/@types/bcryptjs": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
"license": "MIT"
},
"node_modules/@types/jsonwebtoken": { "node_modules/@types/jsonwebtoken": {
"version": "9.0.10", "version": "9.0.10",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
@ -695,6 +703,15 @@
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
}, },
"node_modules/bcryptjs": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/buffer-equal-constant-time": { "node_modules/buffer-equal-constant-time": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",

View File

@ -8,11 +8,13 @@
"start": "next start -p 3001" "start": "next start -p 3001"
}, },
"dependencies": { "dependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.10", "@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",
"bcryptjs": "^3.0.3",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"lucide-react": "^0.511.0", "lucide-react": "^0.511.0",
"next": "^15.3.3", "next": "^15.3.3",

View File

@ -28,22 +28,43 @@ export async function GET(req: NextRequest) {
} }
} }
import bcrypt from 'bcryptjs';
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
const { name, slug, email, phone, plan, status } = await req.json(); const { name, slug, email, phone, plan, status, password } = await req.json();
if (!name || !slug) return NextResponse.json({ error: 'Missing fields' }, { status: 400 }); if (!name || !slug) return NextResponse.json({ error: 'Missing fields' }, { status: 400 });
const planRes = await query('SELECT id FROM plans WHERE name = $1 LIMIT 1', [plan || 'Starter']); const planRes = await query('SELECT id FROM plans WHERE name = $1 LIMIT 1', [plan || 'Starter']);
const planId = planRes.rows.length > 0 ? planRes.rows[0].id : null; const planId = planRes.rows.length > 0 ? planRes.rows[0].id : null;
// Start transaction
await query('BEGIN');
// 1. Create Tenant
const res = await query( const res = await query(
`INSERT INTO tenants (name, slug, email, phone, active_plan_id, subscription_status, subscription_ends_at) `INSERT INTO tenants (name, slug, email, phone, active_plan_id, subscription_status, subscription_ends_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW() + INTERVAL '30 days') RETURNING *`, VALUES ($1, $2, $3, $4, $5, $6, NOW() + INTERVAL '30 days') RETURNING *`,
[name, slug, email, phone, planId, status || 'trial'] [name, slug, email, phone, planId, status || 'trial']
); );
return NextResponse.json(res.rows[0]); const tenant = res.rows[0];
// 2. Create Owner User if password provided
if (email && password) {
const salt = await bcrypt.genSalt(10);
const hash = await bcrypt.hash(password, salt);
await query(
`INSERT INTO users (tenant_id, name, email, password_hash, role, phone)
VALUES ($1, $2, $3, $4, 'owner', $5)`,
[tenant.id, name, email, hash, phone]
);
}
await query('COMMIT');
return NextResponse.json(tenant);
} catch (error: any) { } catch (error: any) {
await query('ROLLBACK');
return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ error: error.message }, { status: 500 });
} }
} }

View File

@ -3,7 +3,7 @@ import { useState, useEffect } from 'react';
import { Search, Plus, X, Edit, Trash2, AlertTriangle, Ban, CheckCircle } from 'lucide-react'; import { Search, Plus, X, Edit, Trash2, AlertTriangle, Ban, CheckCircle } from 'lucide-react';
interface Tenant { interface Tenant {
id: string; name: string; slug: string; type: string; plan: string; status: string; email: string; phone: string; mrr: number; createdAt: string; trialEnds: string; id: string; name: string; slug: string; type: string; plan: string; status: string; email: string; phone: string; mrr: number; createdAt: string; trialEnds: string; password?: string;
} }
const statusMap: Record<string, { label: string; cls: string }> = { const statusMap: Record<string, { label: string; cls: string }> = {
@ -168,9 +168,12 @@ export default function Tenants() {
</div> </div>
</div> </div>
<div className="grid-2"> <div className="grid-2">
<div className="form-group"><label className="form-label">E-mail</label><input className="form-input" value={form.email || ''} onChange={e => setForm(p => ({ ...p, email: e.target.value }))} /></div> <div className="form-group"><label className="form-label">E-mail</label><input className="form-input" type="email" value={form.email || ''} onChange={e => setForm(p => ({ ...p, email: e.target.value }))} /></div>
<div className="form-group"><label className="form-label">Telefone</label><input className="form-input" value={form.phone || ''} onChange={e => setForm(p => ({ ...p, phone: e.target.value }))} /></div> <div className="form-group"><label className="form-label">Telefone</label><input className="form-input" value={form.phone || ''} onChange={e => setForm(p => ({ ...p, phone: e.target.value }))} /></div>
</div> </div>
{!editing && (
<div className="form-group"><label className="form-label">Senha Inicial do Administrador *</label><input type="password" required className="form-input" value={form.password || ''} onChange={e => setForm(p => ({ ...p, password: e.target.value }))} placeholder="Mínimo 6 caracteres" /></div>
)}
<div className="grid-2"> <div className="grid-2">
<div className="form-group"><label className="form-label">Plano</label> <div className="form-group"><label className="form-label">Plano</label>
<select className="form-select" value={form.plan || ''} onChange={e => setForm(p => ({ ...p, plan: e.target.value }))}> <select className="form-select" value={form.plan || ''} onChange={e => setForm(p => ({ ...p, plan: e.target.value }))}>