feat: adicionar campo de senha ao criar novo tenant no Admin
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m33s
Details
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m33s
Details
This commit is contained in:
parent
2b6a645bfc
commit
73b7e171c9
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }))}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue