Merge pull request #87 from rede5/feature/reset-password
Feature/reset password
This commit is contained in:
commit
c1c1dd83a5
5 changed files with 92 additions and 25 deletions
56
backend/cmd/reset_password/main.go
Normal file
56
backend/cmd/reset_password/main.go
Normal 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")
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
Pr├│xima
|
||||||
</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 (Número)</label>
|
<label className="block text-sm font-medium text-gray-700">Licença Sanitária (Número)</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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue