Merge pull request #87 from rede5/feature/reset-password

Feature/reset password
This commit is contained in:
Tiago Yamamoto 2026-02-26 18:50:25 -03:00 committed by GitHub
commit c1c1dd83a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 92 additions and 25 deletions

View file

@ -0,0 +1,56 @@
package main
import (
"context"
"log"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/jmoiron/sqlx"
"golang.org/x/crypto/bcrypt"
"github.com/saveinmed/backend-go/internal/config"
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatalf("failed to load config: %v", err)
}
db, err := sqlx.Open("pgx", cfg.DatabaseURL)
if err != nil {
log.Fatalf("Failed to connect to DB: %v", err)
}
defer db.Close()
if err := db.Ping(); err != nil {
log.Fatalf("Failed to ping DB: %v", err)
}
ctx := context.Background()
// Nova senha: senha123
newPassword := "senha123"
// Hash da senha com bcrypt
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword+cfg.PasswordPepper), bcrypt.DefaultCost)
if err != nil {
log.Fatalf("Failed to hash password: %v", err)
}
// Atualizar senha do usuário
query := `UPDATE users SET password_hash = $1 WHERE email = $2`
result, err := db.ExecContext(ctx, query, string(hashedPassword), "usuario@saveinmed.com")
if err != nil {
log.Fatalf("Failed to update password: %v", err)
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
log.Fatalf("User not found: usuario@saveinmed.com")
}
log.Println("✅ Password reset successful!")
log.Println("📧 Email: usuario@saveinmed.com")
log.Println("🔑 New Password: senha123")
}

View file

@ -206,9 +206,10 @@ func (s *Server) Start(ctx context.Context) error {
} }
repo := postgres.New(s.db) repo := postgres.New(s.db)
if err := repo.ApplyMigrations(ctx); err != nil { // Migrações já aplicadas manualmente
return err // if err := repo.ApplyMigrations(ctx); err != nil {
} // return err
// }
// Seed Admin // Seed Admin
if s.cfg.AdminEmail != "" && s.cfg.AdminPassword != "" { if s.cfg.AdminEmail != "" && s.cfg.AdminPassword != "" {
@ -263,8 +264,9 @@ func (s *Server) Start(ctx context.Context) error {
if err != nil { if err != nil {
// If error is duplicate key on company, maybe we should fetch the company and try creating user only? // If error is duplicate key on company, maybe we should fetch the company and try creating user only?
// For now, let's log error but not fail startup hard, or fail hard to signal issue. // For now, let's log error but not fail startup hard
log.Printf("Failed to seed admin: %v", err) log.Printf("Failed to seed admin (may already exist): %v", err)
// Continue startup anyway
} else { } else {
// FORCE VERIFY the admin company // FORCE VERIFY the admin company
if _, err := s.svc.VerifyCompany(ctx, company.ID); err != nil { if _, err := s.svc.VerifyCompany(ctx, company.ID); err != nil {

View file

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { adminService, Company, CreateCompanyRequest } from '@/services/adminService' import { adminService, Company, CreateCompanyRequest } from '@/services/adminService'
import { useCepLookup } from '@/hooks/useCepLookup' import { useCepLookup } from '@/hooks/useCepLookup'
import { formatCNPJ, validateCNPJ } from '@/utils/cnpj' import { formatCNPJ, validateCNPJ } from '@/utils/cnpj'
@ -21,7 +21,7 @@ export function CompaniesPage() {
license_number: '', license_number: '',
latitude: -16.3281, latitude: -16.3281,
longitude: -48.9530, longitude: -48.9530,
city: 'Anápolis', city: 'An├ípolis',
state: 'GO', state: 'GO',
phone: '', phone: '',
email: '', email: '',
@ -73,16 +73,16 @@ export function CompaniesPage() {
// Only validate CNPJ on create (not on edit, since existing CNPJs may be legacy/test values) // Only validate CNPJ on create (not on edit, since existing CNPJs may be legacy/test values)
if (!editingCompany) { if (!editingCompany) {
if (!formData.cnpj) { if (!formData.cnpj) {
setCnpjError('CNPJ é obrigatório') setCnpjError('CNPJ ├® obrigat├│rio')
return return
} }
const cnpjDigits = formData.cnpj.replace(/\D/g, '') const cnpjDigits = formData.cnpj.replace(/\D/g, '')
if (cnpjDigits.length !== 14) { if (cnpjDigits.length !== 14) {
setCnpjError('CNPJ deve ter 14 dígitos') setCnpjError('CNPJ deve ter 14 d├¡gitos')
return return
} }
if (!validateCNPJ(formData.cnpj)) { if (!validateCNPJ(formData.cnpj)) {
setCnpjError('CNPJ inválido. Verifique o dígito verificador') setCnpjError('CNPJ inv├ílido. Verifique o d├¡gito verificador')
return return
} }
} }
@ -171,7 +171,7 @@ export function CompaniesPage() {
license_number: '', license_number: '',
latitude: -16.3281, latitude: -16.3281,
longitude: -48.9530, longitude: -48.9530,
city: 'Anápolis', city: 'An├ípolis',
state: 'GO', state: 'GO',
phone: '', phone: '',
email: '', email: '',
@ -233,12 +233,12 @@ export function CompaniesPage() {
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-gray-200">
<thead className="text-white" style={{ backgroundColor: '#0F4C81' }}> <thead className="text-white" style={{ backgroundColor: '#0F4C81' }}>
<tr> <tr>
<th className="px-4 py-3 text-left text-sm font-medium">Razão Social</th> <th className="px-4 py-3 text-left text-sm font-medium">Razúo Social</th>
<th className="px-4 py-3 text-left text-sm font-medium">CNPJ</th> <th className="px-4 py-3 text-left text-sm font-medium">CNPJ</th>
<th className="px-4 py-3 text-left text-sm font-medium">Categoria</th> <th className="px-4 py-3 text-left text-sm font-medium">Categoria</th>
<th className="px-4 py-3 text-left text-sm font-medium">Cidade</th> <th className="px-4 py-3 text-left text-sm font-medium">Cidade</th>
<th className="px-4 py-3 text-center text-sm font-medium">Verificada</th> <th className="px-4 py-3 text-center text-sm font-medium">Verificada</th>
<th className="px-4 py-3 text-right text-sm font-medium">Ações</th> <th className="px-4 py-3 text-right text-sm font-medium">AºÁes</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-gray-200">
@ -273,7 +273,7 @@ export function CompaniesPage() {
: 'bg-yellow-100 text-yellow-800' : 'bg-yellow-100 text-yellow-800'
}`} }`}
> >
{company.is_verified ? ' Verificada' : 'Pendente'} {company.is_verified ? 'Ô£ô Verificada' : 'Pendente'}
</button> </button>
</td> </td>
<td className="px-4 py-3 text-right"> <td className="px-4 py-3 text-right">
@ -316,7 +316,7 @@ export function CompaniesPage() {
disabled={page >= totalPages} disabled={page >= totalPages}
className="rounded border px-3 py-1 text-sm disabled:opacity-50" className="rounded border px-3 py-1 text-sm disabled:opacity-50"
> >
Próxima Prxima
</button> </button>
</div> </div>
</div> </div>
@ -335,7 +335,7 @@ export function CompaniesPage() {
<div className="col-span-2"> <div className="col-span-2">
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700">
CNPJ CNPJ
{editingCompany && <span className="ml-2 text-xs text-gray-400">(não editável)</span>} {editingCompany && <span className="ml-2 text-xs text-gray-400">(núo editível)</span>}
</label> </label>
<input <input
type="text" type="text"
@ -373,9 +373,9 @@ export function CompaniesPage() {
/> />
</div> </div>
{/* Razão Social */} {/* Raz├úo Social */}
<div className="col-span-2"> <div className="col-span-2">
<label className="block text-sm font-medium text-gray-700">Razão Social</label> <label className="block text-sm font-medium text-gray-700">Razúo Social</label>
<input <input
type="text" type="text"
value={formData.corporate_name} value={formData.corporate_name}
@ -404,7 +404,7 @@ export function CompaniesPage() {
onChange={(e) => setFormData({ ...formData, category: e.target.value })} onChange={(e) => setFormData({ ...formData, category: e.target.value })}
className="mt-1 w-full rounded border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" className="mt-1 w-full rounded border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none"
> >
<option value="farmacia">Farmácia</option> <option value="farmacia">Farmícia</option>
<option value="distribuidora">Distribuidora</option> <option value="distribuidora">Distribuidora</option>
<option value="admin">Admin</option> <option value="admin">Admin</option>
</select> </select>
@ -435,9 +435,9 @@ export function CompaniesPage() {
/> />
</div> </div>
{/* Licença Sanitária */} {/* Licen├ºa Sanit├íria */}
<div className="col-span-2"> <div className="col-span-2">
<label className="block text-sm font-medium text-gray-700">Licença Sanitária (mero)</label> <label className="block text-sm font-medium text-gray-700">Licenºa Sanitíria (Nmero)</label>
<input <input
type="text" type="text"
value={formData.license_number} value={formData.license_number}
@ -449,7 +449,7 @@ export function CompaniesPage() {
{editingCompany && ( {editingCompany && (
<div className="col-span-2"> <div className="col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Upload de Documento (Licença PDF/Imagem) Upload de Documento (Licenºa PDF/Imagem)
</label> </label>
<input <input
type="file" type="file"
@ -502,7 +502,7 @@ export function CompaniesPage() {
<div className="col-span-2"> <div className="col-span-2">
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700">
CEP CEP
{cepLoading && <span className="ml-2 text-xs text-blue-500 animate-pulse">🔍 Buscando...</span>} {cepLoading && <span className="ml-2 text-xs text-blue-500 animate-pulse">­ƒöì Buscando...</span>}
</label> </label>
<input <input
type="text" type="text"

View file

@ -38,6 +38,7 @@ export function DashboardHome() {
record.items, record.items,
record.users, record.users,
record.companies, record.companies,
record.tenants, // API retorna tenants para empresas
record.products, record.products,
record.orders record.orders
] ]

View file

@ -346,8 +346,12 @@ export function ProductsPage() {
<input <input
type="number" type="number"
step="0.01" step="0.01"
min="0"
value={formData.price_cents / 100} value={formData.price_cents / 100}
onChange={(e) => setFormData({ ...formData, price_cents: Math.round(parseFloat(e.target.value) * 100) })} onChange={(e) => {
const value = parseFloat(e.target.value)
setFormData({ ...formData, price_cents: isNaN(value) ? 0 : Math.round(value * 100) })
}}
className="mt-1 w-full rounded border px-3 py-2" className="mt-1 w-full rounded border px-3 py-2"
required required
/> />
@ -356,8 +360,12 @@ export function ProductsPage() {
<label className="block text-sm font-medium text-gray-700">Estoque</label> <label className="block text-sm font-medium text-gray-700">Estoque</label>
<input <input
type="number" type="number"
min="0"
value={formData.stock} value={formData.stock}
onChange={(e) => setFormData({ ...formData, stock: parseInt(e.target.value) })} onChange={(e) => {
const value = parseInt(e.target.value)
setFormData({ ...formData, stock: isNaN(value) ? 0 : value })
}}
className="mt-1 w-full rounded border px-3 py-2" className="mt-1 w-full rounded border px-3 py-2"
required required
/> />