package auth import ( "context" "crypto/sha256" "encoding/hex" "errors" "time" "photum-backend/internal/config" "photum-backend/internal/db/generated" "photum-backend/internal/profissionais" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "golang.org/x/crypto/bcrypt" ) const ( RoleSuperAdmin = "SUPERADMIN" RoleBusinessOwner = "BUSINESS_OWNER" RolePhotographer = "PHOTOGRAPHER" RoleEventOwner = "EVENT_OWNER" ) 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) (*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 } // 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) 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 { userID := user.ID var empID pgtype.UUID if empresaID != nil && *empresaID != "" { parsedEmpID, err := uuid.Parse(*empresaID) if err == nil { empID.Bytes = parsedEmpID empID.Valid = true } } _, err := s.queries.CreateCadastroCliente(ctx, generated.CreateCadastroClienteParams{ UsuarioID: userID, EmpresaID: empID, Nome: pgtype.Text{String: nome, Valid: nome != ""}, Telefone: pgtype.Text{String: telefone, Valid: telefone != ""}, }) 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 { return nil, nil, nil, errors.New("invalid credentials") } err = bcrypt.CompareHashAndPassword([]byte(user.SenhaHash), []byte(senha)) if err != nil { return nil, nil, nil, errors.New("invalid credentials") } userUUID := uuid.UUID(user.ID.Bytes) accessToken, _, err := GenerateAccessToken(userUUID, user.Role, 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, 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) ([]generated.ListUsuariosPendingRow, error) { return s.queries.ListUsuariosPending(ctx) } 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 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, }) if err != nil { return nil, err } // If needed, create partial professional profile or just ignore for admin creation // For simplicity, if name is provided and it's a professional role, we can stub it. // But let's stick to basic user creation first as per plan. // If the Admin creates a user, they might need to go to another endpoint to set profile. // Or we can optionally create if name is present. if (role == RolePhotographer || role == RoleBusinessOwner) && nome != "" { userID := uuid.UUID(user.ID.Bytes).String() _, _ = s.profissionaisService.Create(ctx, userID, profissionais.CreateProfissionalInput{ Nome: nome, }) } 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 }{ {"admin@photum.com", RoleSuperAdmin, "Dev Admin"}, {"empresa@photum.com", RoleBusinessOwner, "PHOTUM CEO"}, {"foto@photum.com", RolePhotographer, "COLABORADOR PHOTUM"}, {"cliente@photum.com", RoleEventOwner, "CLIENTE TESTE"}, } for _, u := range demoUsers { existingUser, err := s.queries.GetUsuarioByEmail(ctx, u.Email) if err != nil { // User not found (or error), try to create user, err := s.AdminCreateUser(ctx, u.Email, "123456", u.Role, u.Name) if err != nil { return err } // Auto approve them err = s.ApproveUser(ctx, uuid.UUID(user.ID.Bytes).String()) if err != nil { return err } } else { // User exists, check if role matches if existingUser.Role != u.Role { // Update role if mismatch err = s.UpdateUserRole(ctx, uuid.UUID(existingUser.ID.Bytes).String(), u.Role) if err != nil { return err } } } } return nil } func (s *Service) ListUsers(ctx context.Context) ([]generated.ListAllUsuariosRow, error) { return s.queries.ListAllUsuarios(ctx) } 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 toPgText(s *string) pgtype.Text { if s == nil { return pgtype.Text{Valid: false} } return pgtype.Text{String: *s, Valid: true} }