Backend: - Adiciona endpoint `PUT /api/me` para permitir atualização de dados do usuário logado. - Implementa query `UpdateCadastroCliente` e função de serviço [UpdateClientData]para persistir alterações de clientes. - Atualiza handlers [Me], [Login] e [ListPending] para incluir e mapear corretamente campos de cliente (CPF, Endereço, Telefone). - Corrige mapeamento do campo `phone` na struct de resposta do usuário. Frontend: - Habilita o formulário de edição em [Profile.tsx] para usuários do tipo 'CLIENTE' (Event Owner). - Adiciona função [updateUserProfile] em [apiService.ts] para consumir o novo endpoint. - Atualiza [AuthContext] para persistir campos do cliente (CPF, Endereço, etc.) durante a restauração de sessão ([restoreSession], corrigindo o bug de perfil vazio ao recarregar a página. - Padroniza envio de dados no Registro e Aprovação para usar `snake_case` (ex: `cpf_cnpj`, `professional_type`). - Atualiza tipos em [types.ts] para incluir campos de endereço e documentos.
544 lines
16 KiB
Go
544 lines
16 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"photum-backend/internal/config"
|
|
"photum-backend/internal/db/generated"
|
|
"photum-backend/internal/profissionais"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
const (
|
|
RoleSuperAdmin = "SUPERADMIN"
|
|
RoleBusinessOwner = "BUSINESS_OWNER"
|
|
RolePhotographer = "PHOTOGRAPHER"
|
|
RoleEventOwner = "EVENT_OWNER"
|
|
RoleAgendaViewer = "AGENDA_VIEWER"
|
|
RoleResearcher = "RESEARCHER"
|
|
)
|
|
|
|
type Service struct {
|
|
queries *generated.Queries
|
|
profissionaisService *profissionais.Service
|
|
jwtAccessSecret string
|
|
jwtRefreshSecret string
|
|
jwtAccessTTLMinutes int
|
|
jwtRefreshTTLDays int
|
|
}
|
|
|
|
func NewService(queries *generated.Queries, profissionaisService *profissionais.Service, cfg *config.Config) *Service {
|
|
return &Service{
|
|
queries: queries,
|
|
profissionaisService: profissionaisService,
|
|
jwtAccessSecret: cfg.JwtAccessSecret,
|
|
jwtRefreshSecret: cfg.JwtRefreshSecret,
|
|
jwtAccessTTLMinutes: cfg.JwtAccessTTLMinutes,
|
|
jwtRefreshTTLDays: cfg.JwtRefreshTTLDays,
|
|
}
|
|
}
|
|
|
|
func (s *Service) Register(ctx context.Context, email, senha, role, nome, telefone, tipoProfissional string, empresaID *string, profissionalData *profissionais.CreateProfissionalInput, regiao, cpfCnpj, cep, endereco, numero, complemento, bairro, cidade, estado string) (*generated.Usuario, error) {
|
|
// Hash password
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(senha), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create user
|
|
user, err := s.queries.CreateUsuario(ctx, generated.CreateUsuarioParams{
|
|
Email: email,
|
|
SenhaHash: string(hashedPassword),
|
|
Role: role,
|
|
TipoProfissional: toPgText(&tipoProfissional),
|
|
Ativo: false,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If region is provided and different from default (or just force set it)
|
|
// We want to ensure the user has access to the region they registered in.
|
|
if regiao != "" {
|
|
err = s.queries.UpdateUsuarioRegions(ctx, generated.UpdateUsuarioRegionsParams{
|
|
ID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true},
|
|
RegioesPermitidas: []string{regiao}, // Set strictly to the selected region
|
|
})
|
|
if err != nil {
|
|
_ = s.queries.DeleteUsuario(ctx, user.ID)
|
|
return nil, err
|
|
}
|
|
// Update user object in memory to reflect this (for Login call later if it uses this object?
|
|
// Login fetches fresh from DB, so it's fine)
|
|
}
|
|
|
|
// If role is 'PHOTOGRAPHER' or 'BUSINESS_OWNER', create professional profile
|
|
if (role == RolePhotographer || role == RoleBusinessOwner) && profissionalData != nil {
|
|
userID := uuid.UUID(user.ID.Bytes).String()
|
|
_, err := s.profissionaisService.Create(ctx, userID, *profissionalData, regiao)
|
|
if err != nil {
|
|
// Rollback user creation (best effort)
|
|
_ = s.queries.DeleteUsuario(ctx, user.ID)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// If role is 'EVENT_OWNER', create client profile
|
|
if role == RoleEventOwner {
|
|
var empID pgtype.UUID
|
|
if empresaID != nil && *empresaID != "" {
|
|
parsedEmpID, err := uuid.Parse(*empresaID)
|
|
if err == nil {
|
|
empID = pgtype.UUID{Bytes: parsedEmpID, Valid: true}
|
|
}
|
|
}
|
|
|
|
_, err = s.queries.CreateCadastroCliente(ctx, generated.CreateCadastroClienteParams{
|
|
UsuarioID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true},
|
|
EmpresaID: empID,
|
|
Nome: pgtype.Text{String: nome, Valid: true},
|
|
Telefone: pgtype.Text{String: telefone, Valid: telefone != ""},
|
|
CpfCnpj: toPgText(&cpfCnpj),
|
|
Cep: toPgText(&cep),
|
|
Endereco: toPgText(&endereco),
|
|
Numero: toPgText(&numero),
|
|
Complemento: toPgText(&complemento),
|
|
Bairro: toPgText(&bairro),
|
|
Cidade: toPgText(&cidade),
|
|
Estado: toPgText(&estado),
|
|
})
|
|
if err != nil {
|
|
_ = s.queries.DeleteUsuario(ctx, user.ID)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &user, nil
|
|
}
|
|
|
|
type TokenPair struct {
|
|
AccessToken string
|
|
RefreshToken string
|
|
}
|
|
|
|
func (s *Service) Login(ctx context.Context, email, senha string) (*TokenPair, *generated.GetUsuarioByEmailRow, *generated.GetProfissionalByUsuarioIDRow, error) {
|
|
// The query now returns a Row with joined fields, not just Usuario struct
|
|
user, err := s.queries.GetUsuarioByEmail(ctx, email)
|
|
if err != nil {
|
|
fmt.Printf("[DEBUG] Login Failed: Email %s not found. Error: %v\n", email, err)
|
|
return nil, nil, nil, errors.New("invalid credentials")
|
|
}
|
|
|
|
err = bcrypt.CompareHashAndPassword([]byte(user.SenhaHash), []byte(senha))
|
|
if err != nil {
|
|
fmt.Printf("[DEBUG] Login Failed: Password mismatch for %s. DBHash: %s... Input: %s. Error: %v\n", email, user.SenhaHash[:10], senha, err)
|
|
return nil, nil, nil, errors.New("invalid credentials")
|
|
}
|
|
|
|
userUUID := uuid.UUID(user.ID.Bytes)
|
|
accessToken, _, err := GenerateAccessToken(userUUID, user.Role, user.RegioesPermitidas, s.jwtAccessSecret, s.jwtAccessTTLMinutes)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
refreshToken, err := GenerateRefreshToken(userUUID, s.jwtRefreshSecret, s.jwtRefreshTTLDays)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
var profData *generated.GetProfissionalByUsuarioIDRow
|
|
if user.Role == RolePhotographer || user.Role == RoleBusinessOwner {
|
|
p, err := s.queries.GetProfissionalByUsuarioID(ctx, user.ID)
|
|
if err == nil {
|
|
profData = &p
|
|
}
|
|
}
|
|
|
|
return &TokenPair{
|
|
AccessToken: accessToken,
|
|
RefreshToken: refreshToken,
|
|
}, &user, profData, nil
|
|
}
|
|
|
|
func (s *Service) Refresh(ctx context.Context, refreshTokenRaw string) (string, time.Time, error) {
|
|
hash := sha256.Sum256([]byte(refreshTokenRaw))
|
|
hashString := hex.EncodeToString(hash[:])
|
|
|
|
storedToken, err := s.queries.GetRefreshToken(ctx, hashString)
|
|
if err != nil {
|
|
return "", time.Time{}, errors.New("invalid refresh token")
|
|
}
|
|
|
|
if storedToken.Revogado {
|
|
return "", time.Time{}, errors.New("token revoked")
|
|
}
|
|
|
|
if time.Now().After(storedToken.ExpiraEm.Time) {
|
|
return "", time.Time{}, errors.New("token expired")
|
|
}
|
|
|
|
user, err := s.queries.GetUsuarioByID(ctx, storedToken.UsuarioID)
|
|
if err != nil {
|
|
return "", time.Time{}, errors.New("user not found")
|
|
}
|
|
|
|
userUUID := uuid.UUID(user.ID.Bytes)
|
|
return GenerateAccessToken(userUUID, user.Role, user.RegioesPermitidas, s.jwtAccessSecret, s.jwtAccessTTLMinutes)
|
|
}
|
|
|
|
func (s *Service) Logout(ctx context.Context, refreshTokenRaw string) error {
|
|
hash := sha256.Sum256([]byte(refreshTokenRaw))
|
|
hashString := hex.EncodeToString(hash[:])
|
|
return s.queries.RevokeRefreshToken(ctx, hashString)
|
|
}
|
|
|
|
func (s *Service) ListPendingUsers(ctx context.Context, regiao string) ([]generated.ListUsuariosPendingRow, error) {
|
|
if regiao == "" {
|
|
regiao = "SP"
|
|
}
|
|
return s.queries.ListUsuariosPending(ctx, regiao)
|
|
}
|
|
|
|
func (s *Service) ApproveUser(ctx context.Context, id string) error {
|
|
parsedUUID, err := uuid.Parse(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var pgID pgtype.UUID
|
|
pgID.Bytes = parsedUUID
|
|
pgID.Valid = true
|
|
|
|
_, err = s.queries.UpdateUsuarioAtivo(ctx, generated.UpdateUsuarioAtivoParams{
|
|
ID: pgID,
|
|
Ativo: true,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (s *Service) AdminCreateUser(ctx context.Context, email, senha, role, nome, tipoProfissional string, ativo bool, regiao string, empresaID string, telefone, cpfCnpj, cep, endereco, numero, complemento, bairro, cidade, estado string) (*generated.Usuario, error) {
|
|
// Hash password
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(senha), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create user
|
|
user, err := s.queries.CreateUsuario(ctx, generated.CreateUsuarioParams{
|
|
Email: email,
|
|
SenhaHash: string(hashedPassword),
|
|
Role: role,
|
|
TipoProfissional: toPgText(&tipoProfissional),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Update Regions if provided
|
|
if regiao != "" {
|
|
err = s.queries.UpdateUsuarioRegions(ctx, generated.UpdateUsuarioRegionsParams{
|
|
ID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true},
|
|
RegioesPermitidas: []string{regiao},
|
|
})
|
|
if err != nil {
|
|
_ = s.queries.DeleteUsuario(ctx, user.ID)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if ativo {
|
|
// Approve user immediately
|
|
err = s.ApproveUser(ctx, uuid.UUID(user.ID.Bytes).String())
|
|
if err != nil {
|
|
_ = s.queries.DeleteUsuario(ctx, user.ID)
|
|
return nil, err
|
|
}
|
|
user.Ativo = true
|
|
}
|
|
|
|
// Link Company if EVENT_OWNER
|
|
if role == RoleEventOwner && empresaID != "" {
|
|
empUuid, err := uuid.Parse(empresaID)
|
|
if err == nil {
|
|
_, err = s.queries.CreateCadastroCliente(ctx, generated.CreateCadastroClienteParams{
|
|
UsuarioID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true},
|
|
EmpresaID: pgtype.UUID{Bytes: empUuid, Valid: true},
|
|
Nome: pgtype.Text{String: nome, Valid: true},
|
|
Telefone: toPgText(&telefone),
|
|
CpfCnpj: toPgText(&cpfCnpj),
|
|
Cep: toPgText(&cep),
|
|
Endereco: toPgText(&endereco),
|
|
Numero: toPgText(&numero),
|
|
Complemento: toPgText(&complemento),
|
|
Bairro: toPgText(&bairro),
|
|
Cidade: toPgText(&cidade),
|
|
Estado: toPgText(&estado),
|
|
})
|
|
if err != nil {
|
|
// Log error but maybe don't fail user creation?
|
|
// Ideally rollback
|
|
_ = s.queries.DeleteUsuario(ctx, user.ID)
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create professional profile if applicable
|
|
if role == RolePhotographer || role == RoleBusinessOwner {
|
|
var funcaoID string
|
|
|
|
// If specific professional type is provided, resolve it
|
|
if tipoProfissional != "" {
|
|
// Resolve Function ID by Name using list scanning (since GetByName doesn't exist)
|
|
funcoes, err := s.queries.ListFuncoes(ctx)
|
|
if err == nil {
|
|
for _, f := range funcoes {
|
|
if f.Nome == tipoProfissional {
|
|
funcaoID = uuid.UUID(f.ID.Bytes).String()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prepare professional input
|
|
profInput := profissionais.CreateProfissionalInput{
|
|
Nome: nome,
|
|
Email: &email,
|
|
FuncaoProfissionalID: funcaoID,
|
|
}
|
|
|
|
// Create the professional
|
|
_, err = s.profissionaisService.Create(ctx, uuid.UUID(user.ID.Bytes).String(), profInput, regiao)
|
|
if err != nil {
|
|
// Try to delete user to rollback
|
|
_ = s.queries.DeleteUsuario(ctx, user.ID)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &user, nil
|
|
}
|
|
|
|
func (s *Service) UpdateUserRole(ctx context.Context, id, newRole string) error {
|
|
parsedUUID, err := uuid.Parse(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var pgID pgtype.UUID
|
|
pgID.Bytes = parsedUUID
|
|
pgID.Valid = true
|
|
|
|
_, err = s.queries.UpdateUsuarioRole(ctx, generated.UpdateUsuarioRoleParams{
|
|
ID: pgID,
|
|
Role: newRole,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (s *Service) DeleteUser(ctx context.Context, id string) error {
|
|
parsedUUID, err := uuid.Parse(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var pgID pgtype.UUID
|
|
pgID.Bytes = parsedUUID
|
|
pgID.Valid = true
|
|
|
|
return s.queries.DeleteUsuario(ctx, pgID)
|
|
}
|
|
|
|
func (s *Service) EnsureDemoUsers(ctx context.Context) error {
|
|
demoUsers := []struct {
|
|
Email string
|
|
Role string
|
|
Name string
|
|
Regions []string // Optional specific regions
|
|
}{
|
|
{"admin@photum.com", RoleSuperAdmin, "Dev Admin", []string{"SP", "MG"}},
|
|
{"empresa@photum.com", RoleBusinessOwner, "PHOTUM CEO", []string{"SP", "MG"}},
|
|
{"admin.sp@photum.com", RoleBusinessOwner, "ADMIN SP", []string{"SP"}},
|
|
{"admin.mg@photum.com", RoleBusinessOwner, "ADMIN MG", []string{"MG"}},
|
|
{"foto@photum.com", RolePhotographer, "COLABORADOR PHOTUM", []string{"SP"}},
|
|
{"cliente@photum.com", RoleEventOwner, "CLIENTE TESTE", []string{"SP"}},
|
|
{"pesquisa@photum.com", RoleResearcher, "PESQUISADOR", []string{"SP"}},
|
|
{"viewer@photum.com", RoleAgendaViewer, "VISUALIZADOR PHOTUM", []string{"SP", "MG"}},
|
|
}
|
|
|
|
for _, u := range demoUsers {
|
|
existingUser, err := s.queries.GetUsuarioByEmail(ctx, u.Email)
|
|
|
|
regions := u.Regions
|
|
if len(regions) == 0 {
|
|
regions = []string{"SP"} // Default fallback
|
|
}
|
|
|
|
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
|
|
|
|
if err != nil {
|
|
fmt.Printf("[DEBUG] Creating User %s\n", u.Email)
|
|
// Create if not exists
|
|
if errors.Is(err, pgx.ErrNoRows) { // Only create if user not found
|
|
fmt.Printf("Creating demo user: %s (%s)\n", u.Email, u.Role)
|
|
// Pass empty strings for new client fields for demo users
|
|
user, err := s.AdminCreateUser(ctx, u.Email, "123456", u.Role, u.Name, "", true, regions[0], "", "", "", "", "", "", "", "", "", "")
|
|
if err != nil {
|
|
fmt.Printf("Error creating demo user %s: %v\n", u.Email, err)
|
|
return err
|
|
}
|
|
// Update to include specific regions
|
|
err = s.queries.UpdateUsuarioRegions(ctx, generated.UpdateUsuarioRegionsParams{
|
|
ID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true},
|
|
RegioesPermitidas: regions,
|
|
})
|
|
if err != nil {
|
|
fmt.Printf("[DEBUG] Error updating regions for new user %s: %v\n", u.Email, err)
|
|
return err
|
|
}
|
|
} else { // If it's another error, return it
|
|
return err
|
|
}
|
|
} else {
|
|
// User exists, FORCE update password and ensure regions
|
|
fmt.Printf("[DEBUG] Updating Existing User %s\n", u.Email)
|
|
|
|
pgID := pgtype.UUID{Bytes: existingUser.ID.Bytes, Valid: true}
|
|
|
|
// Update Password
|
|
err = s.queries.UpdateUsuarioSenha(ctx, generated.UpdateUsuarioSenhaParams{
|
|
ID: pgID,
|
|
SenhaHash: string(hashedPassword),
|
|
})
|
|
if err != nil {
|
|
fmt.Printf("[DEBUG] Error updating password for %s: %v\n", u.Email, err)
|
|
return err
|
|
}
|
|
|
|
// Update Regions
|
|
err = s.queries.UpdateUsuarioRegions(ctx, generated.UpdateUsuarioRegionsParams{
|
|
ID: pgID,
|
|
RegioesPermitidas: regions,
|
|
})
|
|
if err != nil {
|
|
fmt.Printf("[DEBUG] Error updating regions for %s: %v\n", u.Email, err)
|
|
return err
|
|
}
|
|
|
|
// Update Role if mismatch
|
|
if existingUser.Role != u.Role {
|
|
fmt.Printf("[DEBUG] Updating Role from %s to %s for %s\n", existingUser.Role, u.Role, u.Email)
|
|
err = s.UpdateUserRole(ctx, uuid.UUID(existingUser.ID.Bytes).String(), u.Role)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type UpdateClientInput struct {
|
|
Nome string
|
|
Telefone string
|
|
CpfCnpj string
|
|
Cep string
|
|
Endereco string
|
|
Numero string
|
|
Complemento string
|
|
Bairro string
|
|
Cidade string
|
|
Estado string
|
|
}
|
|
|
|
func (s *Service) UpdateClientData(ctx context.Context, userID string, input UpdateClientInput) error {
|
|
uid, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Prepare params
|
|
params := generated.UpdateCadastroClienteParams{
|
|
UsuarioID: pgtype.UUID{Bytes: uid, Valid: true},
|
|
Nome: pgtype.Text{String: input.Nome, Valid: input.Nome != ""},
|
|
Telefone: pgtype.Text{String: input.Telefone, Valid: input.Telefone != ""},
|
|
CpfCnpj: pgtype.Text{String: input.CpfCnpj, Valid: input.CpfCnpj != ""},
|
|
Cep: pgtype.Text{String: input.Cep, Valid: input.Cep != ""},
|
|
Endereco: pgtype.Text{String: input.Endereco, Valid: input.Endereco != ""},
|
|
Numero: pgtype.Text{String: input.Numero, Valid: input.Numero != ""},
|
|
Complemento: pgtype.Text{String: input.Complemento, Valid: input.Complemento != ""},
|
|
Bairro: pgtype.Text{String: input.Bairro, Valid: input.Bairro != ""},
|
|
Cidade: pgtype.Text{String: input.Cidade, Valid: input.Cidade != ""},
|
|
Estado: pgtype.Text{String: input.Estado, Valid: input.Estado != ""},
|
|
}
|
|
|
|
_, err = s.queries.UpdateCadastroCliente(ctx, params)
|
|
return err
|
|
}
|
|
|
|
func (s *Service) ListUsers(ctx context.Context, regiao string) ([]generated.ListAllUsuariosRow, error) {
|
|
return s.queries.ListAllUsuarios(ctx, regiao)
|
|
}
|
|
|
|
func (s *Service) GetUser(ctx context.Context, id string) (*generated.GetUsuarioByIDRow, error) {
|
|
parsedUUID, err := uuid.Parse(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var pgID pgtype.UUID
|
|
pgID.Bytes = parsedUUID
|
|
pgID.Valid = true
|
|
|
|
user, err := s.queries.GetUsuarioByID(ctx, pgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
func (s *Service) GetProfessionalByUserID(ctx context.Context, userID string) (*generated.GetProfissionalByUsuarioIDRow, error) {
|
|
parsedUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var pgID pgtype.UUID
|
|
pgID.Bytes = parsedUUID
|
|
pgID.Valid = true
|
|
|
|
p, err := s.queries.GetProfissionalByUsuarioID(ctx, pgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &p, nil
|
|
}
|
|
|
|
func (s *Service) GetClientData(ctx context.Context, userID string) (*generated.CadastroCliente, error) {
|
|
parsedUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var pgID pgtype.UUID
|
|
pgID.Bytes = parsedUUID
|
|
pgID.Valid = true
|
|
|
|
c, err := s.queries.GetCadastroClienteByUsuarioID(ctx, pgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
func toPgText(s *string) pgtype.Text {
|
|
if s == nil {
|
|
return pgtype.Text{Valid: false}
|
|
}
|
|
return pgtype.Text{String: *s, Valid: true}
|
|
}
|