diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index fdc4443..c824342 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -136,15 +136,7 @@ func main() { api := r.Group("/api") api.Use(auth.AuthMiddleware(cfg)) { - api.GET("/me", func(c *gin.Context) { - userID, _ := c.Get("userID") - role, _ := c.Get("role") - c.JSON(200, gin.H{ - "user_id": userID, - "role": role, - "message": "You are authenticated", - }) - }) + api.GET("/me", authHandler.Me) profGroup := api.Group("/profissionais") { diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 4ea556f..debad85 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -1294,6 +1294,29 @@ const docTemplate = `{ } } }, + "/api/me": { + "get": { + "description": "Get current authenticated user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Get current user", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/auth.loginResponse" + } + } + } + } + }, "/api/profissionais": { "get": { "security": [ @@ -2209,6 +2232,10 @@ const docTemplate = `{ "email": { "type": "string" }, + "empresa_id": { + "description": "Optional, for EVENT_OWNER", + "type": "string" + }, "nome": { "type": "string" }, @@ -2242,12 +2269,21 @@ const docTemplate = `{ "ativo": { "type": "boolean" }, + "company_name": { + "type": "string" + }, "email": { "type": "string" }, "id": { "type": "string" }, + "name": { + "type": "string" + }, + "phone": { + "type": "string" + }, "role": { "type": "string" } diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 2a3c5e8..56bc797 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -1288,6 +1288,29 @@ } } }, + "/api/me": { + "get": { + "description": "Get current authenticated user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Get current user", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/auth.loginResponse" + } + } + } + } + }, "/api/profissionais": { "get": { "security": [ @@ -2203,6 +2226,10 @@ "email": { "type": "string" }, + "empresa_id": { + "description": "Optional, for EVENT_OWNER", + "type": "string" + }, "nome": { "type": "string" }, @@ -2236,12 +2263,21 @@ "ativo": { "type": "boolean" }, + "company_name": { + "type": "string" + }, "email": { "type": "string" }, "id": { "type": "string" }, + "name": { + "type": "string" + }, + "phone": { + "type": "string" + }, "role": { "type": "string" } diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index a3f6fda..bc6751e 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -42,6 +42,9 @@ definitions: properties: email: type: string + empresa_id: + description: Optional, for EVENT_OWNER + type: string nome: type: string role: @@ -69,10 +72,16 @@ definitions: properties: ativo: type: boolean + company_name: + type: string email: type: string id: type: string + name: + type: string + phone: + type: string role: type: string type: object @@ -1193,6 +1202,21 @@ paths: summary: Update function tags: - funcoes + /api/me: + get: + consumes: + - application/json + description: Get current authenticated user + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/auth.loginResponse' + summary: Get current user + tags: + - auth /api/profissionais: get: consumes: diff --git a/backend/internal/auth/handler.go b/backend/internal/auth/handler.go index bd9d4cd..878f724 100644 --- a/backend/internal/auth/handler.go +++ b/backend/internal/auth/handler.go @@ -19,11 +19,12 @@ func NewHandler(service *Service) *Handler { } type registerRequest struct { - Email string `json:"email" binding:"required,email"` - Senha string `json:"senha" binding:"required,min=6"` - Nome string `json:"nome" binding:"required"` - Telefone string `json:"telefone"` - Role string `json:"role" binding:"required"` // Role is now required + Email string `json:"email" binding:"required,email"` + Senha string `json:"senha" binding:"required,min=6"` + Nome string `json:"nome" binding:"required"` + Telefone string `json:"telefone"` + Role string `json:"role" binding:"required"` // Role is now required + EmpresaID string `json:"empresa_id"` // Optional, for EVENT_OWNER } // Register godoc @@ -46,15 +47,27 @@ func (h *Handler) Register(c *gin.Context) { // Create professional data only if role is appropriate var profData *profissionais.CreateProfissionalInput - // COMMENTED OUT to enable 2-step registration (User -> Full Profile) - // if req.Role == RolePhotographer || req.Role == RoleBusinessOwner { - // profData = &profissionais.CreateProfissionalInput{ - // Nome: req.Nome, - // Whatsapp: &req.Telefone, - // } - // } + // For PHOTOGRAPHER or BUSINESS_OWNER, we might populate this if we were doing 1-step, + // but actually 'nome' and 'telefone' are passed as args now. + // We keep passing nil for profData because Service logic for Professionals relies on 'CreateProfissionalInput' + // However, I updated Service to take nome/telefone directly. + // Wait, the Service code I JUST wrote takes (email, senha, role, nome, telefone, empresaID, profissionalData). + // If role is Photographer, the Service code checks `profissionalData`. + // I should probably populate `profissionalData` if it's a professional. - user, err := h.service.Register(c.Request.Context(), req.Email, req.Senha, req.Role, profData) + if req.Role == "PHOTOGRAPHER" || req.Role == "BUSINESS_OWNER" { + profData = &profissionais.CreateProfissionalInput{ + Nome: req.Nome, + Whatsapp: &req.Telefone, + } + } + + var empresaIDPtr *string + if req.EmpresaID != "" { + empresaIDPtr = &req.EmpresaID + } + + user, err := h.service.Register(c.Request.Context(), req.Email, req.Senha, req.Role, req.Nome, req.Telefone, empresaIDPtr, profData) if err != nil { if strings.Contains(err.Error(), "duplicate key") { c.JSON(http.StatusConflict, gin.H{"error": "email already registered"}) @@ -117,10 +130,13 @@ type loginResponse struct { } type userResponse struct { - ID string `json:"id"` - Email string `json:"email"` - Role string `json:"role"` - Ativo bool `json:"ativo"` + ID string `json:"id"` + Email string `json:"email"` + Role string `json:"role"` + Ativo bool `json:"ativo"` + Name string `json:"name,omitempty"` + Phone string `json:"phone,omitempty"` + CompanyName string `json:"company_name,omitempty"` } // Login godoc @@ -258,6 +274,52 @@ func (h *Handler) Logout(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "logged out"}) } +// Me godoc +// @Summary Get current user +// @Description Get current authenticated user +// @Tags auth +// @Accept json +// @Produce json +// @Success 200 {object} loginResponse +// @Router /api/me [get] +func (h *Handler) Me(c *gin.Context) { + // User ID comes from context (AuthMiddleware) + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + + // We can fetch fresh user data + user, err := h.service.GetUser(c.Request.Context(), userID.(string)) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"}) + return + } + + var empresaNome string + if user.EmpresaNome.Valid { + empresaNome = user.EmpresaNome.String + } + + resp := loginResponse{ + User: userResponse{ + ID: uuid.UUID(user.ID.Bytes).String(), + Email: user.Email, + Role: user.Role, + Ativo: user.Ativo, + Name: user.Nome, + Phone: user.Whatsapp, + CompanyName: empresaNome, + }, + } + // Note: We are not returning AccessToken/ExpiresAt here as they are already set/active. + // But to match loginResponse structure we can leave them empty or fill appropriately if we were refreshing. + // For session restore, we mainly need the User object. + + c.JSON(http.StatusOK, resp) +} + // ListPending godoc // @Summary List pending users // @Description List users with ativo=false @@ -287,23 +349,23 @@ func (h *Handler) ListPending(c *gin.Context) { resp := make([]map[string]interface{}, len(users)) for i, u := range users { - var nome string - if u.Nome.Valid { - nome = u.Nome.String - } - var whatsapp string - if u.Whatsapp.Valid { - whatsapp = u.Whatsapp.String + nome := u.Nome + whatsapp := u.Whatsapp + + var empresaNome string + if u.EmpresaNome.Valid { + empresaNome = u.EmpresaNome.String } resp[i] = map[string]interface{}{ - "id": uuid.UUID(u.ID.Bytes).String(), - "email": u.Email, - "role": u.Role, - "ativo": u.Ativo, - "created_at": u.CriadoEm.Time, - "name": nome, // Mapped to name for frontend compatibility - "phone": whatsapp, + "id": uuid.UUID(u.ID.Bytes).String(), + "email": u.Email, + "role": u.Role, + "ativo": u.Ativo, + "created_at": u.CriadoEm.Time, + "name": nome, // Mapped to name for frontend compatibility + "phone": whatsapp, + "company_name": empresaNome, } } @@ -503,12 +565,20 @@ func (h *Handler) GetUser(c *gin.Context) { return } + var empresaNome string + if user.EmpresaNome.Valid { + empresaNome = user.EmpresaNome.String + } + resp := map[string]interface{}{ - "id": uuid.UUID(user.ID.Bytes).String(), - "email": user.Email, - "role": user.Role, - "ativo": user.Ativo, - "created_at": user.CriadoEm.Time, + "id": uuid.UUID(user.ID.Bytes).String(), + "email": user.Email, + "role": user.Role, + "ativo": user.Ativo, + "created_at": user.CriadoEm.Time, + "name": user.Nome, + "phone": user.Whatsapp, + "company_name": empresaNome, } c.JSON(http.StatusOK, resp) diff --git a/backend/internal/auth/service.go b/backend/internal/auth/service.go index 9b763f7..34e2b05 100644 --- a/backend/internal/auth/service.go +++ b/backend/internal/auth/service.go @@ -43,7 +43,7 @@ func NewService(queries *generated.Queries, profissionaisService *profissionais. } } -func (s *Service) Register(ctx context.Context, email, senha, role string, profissionalData *profissionais.CreateProfissionalInput) (*generated.Usuario, error) { +func (s *Service) Register(ctx context.Context, email, senha, role, nome, telefone string, empresaID *string, profissionalData *profissionais.CreateProfissionalInput) (*generated.Usuario, error) { // Hash password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(senha), bcrypt.DefaultCost) if err != nil { @@ -71,6 +71,30 @@ func (s *Service) Register(ctx context.Context, email, senha, role string, profi } } + // 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 } @@ -237,6 +261,7 @@ func (s *Service) EnsureDemoUsers(ctx context.Context) error { {"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 { @@ -270,7 +295,7 @@ func (s *Service) ListUsers(ctx context.Context) ([]generated.ListAllUsuariosRow return s.queries.ListAllUsuarios(ctx) } -func (s *Service) GetUser(ctx context.Context, id string) (*generated.Usuario, error) { +func (s *Service) GetUser(ctx context.Context, id string) (*generated.GetUsuarioByIDRow, error) { parsedUUID, err := uuid.Parse(id) if err != nil { return nil, err diff --git a/backend/internal/db/generated/models.go b/backend/internal/db/generated/models.go index bbbf8bf..2e3ff71 100644 --- a/backend/internal/db/generated/models.go +++ b/backend/internal/db/generated/models.go @@ -14,6 +14,16 @@ type AnosFormatura struct { CriadoEm pgtype.Timestamptz `json:"criado_em"` } +type CadastroCliente struct { + ID pgtype.UUID `json:"id"` + UsuarioID pgtype.UUID `json:"usuario_id"` + EmpresaID pgtype.UUID `json:"empresa_id"` + Nome pgtype.Text `json:"nome"` + Telefone pgtype.Text `json:"telefone"` + CriadoEm pgtype.Timestamptz `json:"criado_em"` + AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"` +} + type CadastroFot struct { ID pgtype.UUID `json:"id"` Fot int32 `json:"fot"` diff --git a/backend/internal/db/generated/usuarios.sql.go b/backend/internal/db/generated/usuarios.sql.go index fad6bc6..6695e97 100644 --- a/backend/internal/db/generated/usuarios.sql.go +++ b/backend/internal/db/generated/usuarios.sql.go @@ -11,6 +11,39 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const createCadastroCliente = `-- name: CreateCadastroCliente :one +INSERT INTO cadastro_clientes (usuario_id, empresa_id, nome, telefone) +VALUES ($1, $2, $3, $4) +RETURNING id, usuario_id, empresa_id, nome, telefone, criado_em, atualizado_em +` + +type CreateCadastroClienteParams struct { + UsuarioID pgtype.UUID `json:"usuario_id"` + EmpresaID pgtype.UUID `json:"empresa_id"` + Nome pgtype.Text `json:"nome"` + Telefone pgtype.Text `json:"telefone"` +} + +func (q *Queries) CreateCadastroCliente(ctx context.Context, arg CreateCadastroClienteParams) (CadastroCliente, error) { + row := q.db.QueryRow(ctx, createCadastroCliente, + arg.UsuarioID, + arg.EmpresaID, + arg.Nome, + arg.Telefone, + ) + var i CadastroCliente + err := row.Scan( + &i.ID, + &i.UsuarioID, + &i.EmpresaID, + &i.Nome, + &i.Telefone, + &i.CriadoEm, + &i.AtualizadoEm, + ) + return i, err +} + const createUsuario = `-- name: CreateUsuario :one INSERT INTO usuarios (email, senha_hash, role, ativo) VALUES ($1, $2, $3, false) @@ -69,13 +102,33 @@ func (q *Queries) GetUsuarioByEmail(ctx context.Context, email string) (Usuario, } const getUsuarioByID = `-- name: GetUsuarioByID :one -SELECT id, email, senha_hash, role, ativo, criado_em, atualizado_em FROM usuarios -WHERE id = $1 LIMIT 1 +SELECT u.id, u.email, u.senha_hash, u.role, u.ativo, u.criado_em, u.atualizado_em, + COALESCE(cp.nome, cc.nome, '') as nome, + COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp, + e.nome as empresa_nome +FROM usuarios u +LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id +LEFT JOIN cadastro_clientes cc ON u.id = cc.usuario_id +LEFT JOIN empresas e ON cc.empresa_id = e.id +WHERE u.id = $1 LIMIT 1 ` -func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (Usuario, error) { +type GetUsuarioByIDRow struct { + ID pgtype.UUID `json:"id"` + Email string `json:"email"` + SenhaHash string `json:"senha_hash"` + Role string `json:"role"` + Ativo bool `json:"ativo"` + CriadoEm pgtype.Timestamptz `json:"criado_em"` + AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"` + Nome string `json:"nome"` + Whatsapp string `json:"whatsapp"` + EmpresaNome pgtype.Text `json:"empresa_nome"` +} + +func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (GetUsuarioByIDRow, error) { row := q.db.QueryRow(ctx, getUsuarioByID, id) - var i Usuario + var i GetUsuarioByIDRow err := row.Scan( &i.ID, &i.Email, @@ -84,6 +137,9 @@ func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (Usuario, &i.Ativo, &i.CriadoEm, &i.AtualizadoEm, + &i.Nome, + &i.Whatsapp, + &i.EmpresaNome, ) return i, err } @@ -132,21 +188,26 @@ func (q *Queries) ListAllUsuarios(ctx context.Context) ([]ListAllUsuariosRow, er const listUsuariosPending = `-- name: ListUsuariosPending :many SELECT u.id, u.email, u.role, u.ativo, u.criado_em, - cp.nome, cp.whatsapp + COALESCE(cp.nome, cc.nome, '') as nome, + COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp, + e.nome as empresa_nome FROM usuarios u LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id +LEFT JOIN cadastro_clientes cc ON u.id = cc.usuario_id +LEFT JOIN empresas e ON cc.empresa_id = e.id WHERE u.ativo = false ORDER BY u.criado_em DESC ` type ListUsuariosPendingRow struct { - ID pgtype.UUID `json:"id"` - Email string `json:"email"` - Role string `json:"role"` - Ativo bool `json:"ativo"` - CriadoEm pgtype.Timestamptz `json:"criado_em"` - Nome pgtype.Text `json:"nome"` - Whatsapp pgtype.Text `json:"whatsapp"` + ID pgtype.UUID `json:"id"` + Email string `json:"email"` + Role string `json:"role"` + Ativo bool `json:"ativo"` + CriadoEm pgtype.Timestamptz `json:"criado_em"` + Nome string `json:"nome"` + Whatsapp string `json:"whatsapp"` + EmpresaNome pgtype.Text `json:"empresa_nome"` } func (q *Queries) ListUsuariosPending(ctx context.Context) ([]ListUsuariosPendingRow, error) { @@ -166,6 +227,7 @@ func (q *Queries) ListUsuariosPending(ctx context.Context) ([]ListUsuariosPendin &i.CriadoEm, &i.Nome, &i.Whatsapp, + &i.EmpresaNome, ); err != nil { return nil, err } diff --git a/backend/internal/db/queries/usuarios.sql b/backend/internal/db/queries/usuarios.sql index 8e99984..44344cc 100644 --- a/backend/internal/db/queries/usuarios.sql +++ b/backend/internal/db/queries/usuarios.sql @@ -8,8 +8,15 @@ SELECT * FROM usuarios WHERE email = $1 LIMIT 1; -- name: GetUsuarioByID :one -SELECT * FROM usuarios -WHERE id = $1 LIMIT 1; +SELECT u.id, u.email, u.senha_hash, u.role, u.ativo, u.criado_em, u.atualizado_em, + COALESCE(cp.nome, cc.nome, '') as nome, + COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp, + e.nome as empresa_nome +FROM usuarios u +LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id +LEFT JOIN cadastro_clientes cc ON u.id = cc.usuario_id +LEFT JOIN empresas e ON cc.empresa_id = e.id +WHERE u.id = $1 LIMIT 1; -- name: DeleteUsuario :exec DELETE FROM usuarios @@ -17,9 +24,13 @@ WHERE id = $1; -- name: ListUsuariosPending :many SELECT u.id, u.email, u.role, u.ativo, u.criado_em, - cp.nome, cp.whatsapp + COALESCE(cp.nome, cc.nome, '') as nome, + COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp, + e.nome as empresa_nome FROM usuarios u LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id +LEFT JOIN cadastro_clientes cc ON u.id = cc.usuario_id +LEFT JOIN empresas e ON cc.empresa_id = e.id WHERE u.ativo = false ORDER BY u.criado_em DESC; @@ -38,3 +49,8 @@ RETURNING *; SELECT id, email, role, ativo, criado_em, atualizado_em FROM usuarios ORDER BY criado_em DESC; + +-- name: CreateCadastroCliente :one +INSERT INTO cadastro_clientes (usuario_id, empresa_id, nome, telefone) +VALUES ($1, $2, $3, $4) +RETURNING *; diff --git a/backend/internal/db/schema.sql b/backend/internal/db/schema.sql index cba3a31..422dae8 100644 --- a/backend/internal/db/schema.sql +++ b/backend/internal/db/schema.sql @@ -301,3 +301,15 @@ BEGIN ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; END $$; + +-- Cadastro Clientes (Representatives of Companies) +CREATE TABLE IF NOT EXISTS cadastro_clientes ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + usuario_id UUID REFERENCES usuarios(id) ON DELETE CASCADE, + empresa_id UUID REFERENCES empresas(id) ON DELETE SET NULL, + nome VARCHAR(255), -- Name of the representative + telefone VARCHAR(20), + criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(), + atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(usuario_id) +); diff --git a/frontend/contexts/AuthContext.tsx b/frontend/contexts/AuthContext.tsx index 65c0928..78138aa 100644 --- a/frontend/contexts/AuthContext.tsx +++ b/frontend/contexts/AuthContext.tsx @@ -1,5 +1,5 @@ -import React, { createContext, useContext, useState, ReactNode } from 'react'; +import React, { createContext, useContext, useState, ReactNode, useEffect } from 'react'; import { User, UserRole } from '../types'; // Mock Users Database @@ -38,14 +38,58 @@ interface AuthContextType { user: User | null; login: (email: string, password?: string) => Promise; logout: () => void; - register: (data: { nome: string; email: string; senha: string; telefone: string; role: string }) => Promise<{ success: boolean; userId?: string; token?: string }>; + register: (data: { nome: string; email: string; senha: string; telefone: string; role: string; empresaId?: string }) => Promise<{ success: boolean; userId?: string; token?: string }>; availableUsers: User[]; // Helper for the login screen demo + token: string | null; } const AuthContext = createContext(undefined); export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [user, setUser] = useState(null); + const [token, setToken] = useState(localStorage.getItem("token")); + + useEffect(() => { + const restoreSession = async () => { + if (!token) return; + + try { + const response = await fetch(`${import.meta.env.VITE_API_URL}/api/me`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + const data = await response.json(); + const backendUser = data.user; + const mappedUser: User = { + id: backendUser.id, + email: backendUser.email, + name: backendUser.email.split('@')[0], + role: backendUser.role as UserRole, + ativo: backendUser.ativo, + }; + if (!backendUser.ativo) { + console.warn("User is not active, logging out."); + logout(); + return; + } + setUser(mappedUser); + } else { + // Token invalid or expired + logout(); + } + } catch (err) { + console.error("Session restore error:", err); + logout(); + } + }; + + if (!user && token) { + restoreSession(); + } + }, [token]); // removed 'user' from dependency to avoid loop if user is set, though !user check handles it. safer to just depend on token mount. const getErrorMessage = (errorKey: string): string => { // Map backend error messages to Portuguese @@ -93,9 +137,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => const data = await response.json(); - // Store token (optional, if you need it for other requests outside cookies) - localStorage.setItem('token', data.access_token); - const backendUser = data.user; // Enforce active check @@ -103,6 +144,10 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => throw new Error("Cadastro pendente de aprovação."); } + // Store token (optional, if you need it for other requests outside cookies) + localStorage.setItem('token', data.access_token); + setToken(data.access_token); + // Map backend user to frontend User type const mappedUser: User = { id: backendUser.id, @@ -121,17 +166,41 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => } }; - const logout = () => { - localStorage.removeItem('token'); - setUser(null); + const logout = async () => { + try { + if (token) { + await fetch(`${import.meta.env.VITE_API_URL}/auth/logout`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` // Though refresh token is in cookie + }, + // body: JSON.stringify({ refresh_token: ... }) // If we had it in client state, but it is in cookie. + }); + } + } catch (error) { + console.error("Logout failed", error); + } finally { + localStorage.removeItem('token'); + setToken(null); + setUser(null); + // Force redirect or let app router handle it safely + window.location.href = '/'; + } }; - const register = async (data: { nome: string; email: string; senha: string; telefone: string; role: string }) => { + const register = async (data: { nome: string; email: string; senha: string; telefone: string; role: string; empresaId?: string }) => { try { + // Destructure to separate empresaId from the rest + const { empresaId, ...rest } = data; + const payload = { + ...rest, + empresa_id: empresaId + }; const response = await fetch(`${import.meta.env.VITE_API_URL}/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) + body: JSON.stringify(payload) }); if (!response.ok) { @@ -142,14 +211,15 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => const responseData = await response.json(); - if (responseData.access_token) { - localStorage.setItem('token', responseData.access_token); - } - // IF user is returned (auto-login), logic: - // Only set user if they are ACTIVE (which they won't be for standard clients) + // Only set user/token if they are ACTIVE (which they won't be for standard clients/professionals) // This allows the "Pending Approval" modal to show instead of auto-redirecting. if (responseData.user && responseData.user.ativo) { + if (responseData.access_token) { + localStorage.setItem('token', responseData.access_token); + setToken(responseData.access_token); + } + const backendUser = responseData.user; const mappedUser: User = { id: backendUser.id, @@ -160,6 +230,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => }; setUser(mappedUser); } + // If user is NOT active, we do NOT set the token/user state, preventing auto-login. return { success: true, @@ -173,7 +244,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => }; return ( - + {children} ); diff --git a/frontend/pages/Login.tsx b/frontend/pages/Login.tsx index 66bbe39..acb8779 100644 --- a/frontend/pages/Login.tsx +++ b/frontend/pages/Login.tsx @@ -201,6 +201,7 @@ export const Login: React.FC = ({ onNavigate }) => { { id: "1", name: "Dev Admin", email: "admin@photum.com", role: UserRole.SUPERADMIN }, { id: "2", name: "PHOTUM CEO", email: "empresa@photum.com", role: UserRole.BUSINESS_OWNER }, { id: "3", name: "COLABORADOR PHOTUM", email: "foto@photum.com", role: UserRole.PHOTOGRAPHER }, + { id: "4", name: "CLIENTE TESTE", email: "cliente@photum.com", role: UserRole.EVENT_OWNER }, ].map((user) => (