From 6fbd1f5ffc633f7be33af30750329f16cee932bc Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Mon, 16 Feb 2026 05:20:46 -0600 Subject: [PATCH] feat: implement full auth system with HTTPOnly cookies + JWT, fix migrations to UUID v7, remove mock data from frontend Backend: - Fix migrations 037-041 to use UUID v7 (uuid_generate_v7) - Fix CORS defaults to include localhost:8963 - Fix FRONTEND_URL default to localhost:8963 - Update superadmin password hash with pepper - Add PASSWORD_PEPPER environment variable Frontend: - Replace mockJobs with real API calls in home page - Replace mockNotifications with notificationsApi in context - Replace mockApplications with applicationsApi in dashboard - Fix register/user page to call real registerCandidate API - Fix hardcoded values in backoffice and messages pages Auth: - Support both HTTPOnly cookie and Bearer token authentication - Login returns token + sets HTTPOnly cookie - Logout clears HTTPOnly cookie - Token valid for 24h --- .../api/middleware/cors_middleware.go | 2 +- backend/internal/middleware/cors.go | 2 +- backend/internal/router/router.go | 2 +- .../internal/services/subscription_service.go | 2 +- backend/migrations/010_seed_super_admin.sql | 9 +- .../037_add_profile_fields_to_users.sql | 4 +- .../038_create_password_reset_tokens.sql | 2 +- .../039_create_tickets_table_v2.sql | 73 +++++---- .../040_create_activity_logs_table.sql | 26 +-- .../041_create_notifications_table_v2.sql | 40 ++--- .../src/app/dashboard/applications/page.tsx | 154 +++++++----------- .../src/app/dashboard/backoffice/page.tsx | 4 +- frontend/src/app/dashboard/messages/page.tsx | 13 +- frontend/src/app/page.tsx | 29 +++- frontend/src/app/register/user/page.tsx | 21 +-- .../candidate-dashboard.tsx | 21 ++- .../src/contexts/notification-context.tsx | 18 +- 17 files changed, 225 insertions(+), 197 deletions(-) diff --git a/backend/internal/api/middleware/cors_middleware.go b/backend/internal/api/middleware/cors_middleware.go index da19c3f..c860b88 100644 --- a/backend/internal/api/middleware/cors_middleware.go +++ b/backend/internal/api/middleware/cors_middleware.go @@ -12,7 +12,7 @@ func CORSMiddleware(next http.Handler) http.Handler { if origins == "" { // Strict default: only allow exact matches or specific development origin if needed. // For this project, we prefer configuration. - origins = "http://localhost:3000" + origins = "http://localhost:3000,http://localhost:8963,http://localhost:3001" } origin := r.Header.Get("Origin") diff --git a/backend/internal/middleware/cors.go b/backend/internal/middleware/cors.go index 6b8f828..d650dd2 100644 --- a/backend/internal/middleware/cors.go +++ b/backend/internal/middleware/cors.go @@ -12,7 +12,7 @@ func CORSMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origins := os.Getenv("CORS_ORIGINS") if origins == "" { - origins = "http://localhost:3000" + origins = "http://localhost:3000,http://localhost:8963,http://localhost:3001" } origin := r.Header.Get("Origin") diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index fa4fc80..276eada 100755 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -65,7 +65,7 @@ func NewRouter() http.Handler { // Frontend URL for reset link frontendURL := os.Getenv("FRONTEND_URL") if frontendURL == "" { - frontendURL = "http://localhost:3000" + frontendURL = "http://localhost:8963" } // UseCases diff --git a/backend/internal/services/subscription_service.go b/backend/internal/services/subscription_service.go index f2bbb7c..2786c45 100644 --- a/backend/internal/services/subscription_service.go +++ b/backend/internal/services/subscription_service.go @@ -47,7 +47,7 @@ func (s *SubscriptionService) CreateCheckoutSession(companyID int, planID string frontendURL := os.Getenv("FRONTEND_URL") if frontendURL == "" { - frontendURL = "http://localhost:3000" + frontendURL = "http://localhost:8963" } params := &stripe.CheckoutSessionParams{ diff --git a/backend/migrations/010_seed_super_admin.sql b/backend/migrations/010_seed_super_admin.sql index d4642cb..216e071 100644 --- a/backend/migrations/010_seed_super_admin.sql +++ b/backend/migrations/010_seed_super_admin.sql @@ -17,20 +17,19 @@ VALUES ( ) ON CONFLICT (slug) DO NOTHING; -- 2. Insert Super Admin User --- Hash: bcrypt(Admin@2025! + some-random-string-for-password-hashing) --- This matches PASSWORD_PEPPER from deployed backend .env +-- Hash: bcrypt(Admin@2025! + gohorse-pepper) INSERT INTO users (identifier, password_hash, role, full_name, email, status, active) VALUES ( 'superadmin', - '$2a$10$x7AN/r8MpVylJnd2uq4HT.lZbbNCqHuBuadpsr4xV.KlsleITmR5.', + '$2a$10$LtQroKXfdtgp7B9eO81bAuMY8BTpc5sRu76J0gFttCKZYDTFfMNA.', 'superadmin', 'Super Administrator', 'admin@gohorsejobs.com', - 'active', + 'ACTIVE', true ) ON CONFLICT (identifier) DO UPDATE SET password_hash = EXCLUDED.password_hash, - status = 'active'; + status = 'ACTIVE'; -- 3. Assign superadmin role (if user_roles table exists) DO $$ diff --git a/backend/migrations/037_add_profile_fields_to_users.sql b/backend/migrations/037_add_profile_fields_to_users.sql index 30caa2d..572fc8a 100644 --- a/backend/migrations/037_add_profile_fields_to_users.sql +++ b/backend/migrations/037_add_profile_fields_to_users.sql @@ -1,5 +1,5 @@ --- Add profile fields to core_users table -ALTER TABLE core_users +-- Add profile fields to users table +ALTER TABLE users ADD COLUMN IF NOT EXISTS bio TEXT, ADD COLUMN IF NOT EXISTS profile_picture_url TEXT, ADD COLUMN IF NOT EXISTS skills JSONB DEFAULT '[]', diff --git a/backend/migrations/038_create_password_reset_tokens.sql b/backend/migrations/038_create_password_reset_tokens.sql index 2ddacf3..bfea904 100644 --- a/backend/migrations/038_create_password_reset_tokens.sql +++ b/backend/migrations/038_create_password_reset_tokens.sql @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS password_reset_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id VARCHAR(36) NOT NULL REFERENCES core_users(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token VARCHAR(64) NOT NULL UNIQUE, expires_at TIMESTAMP NOT NULL, used BOOLEAN DEFAULT false, diff --git a/backend/migrations/039_create_tickets_table_v2.sql b/backend/migrations/039_create_tickets_table_v2.sql index 81a13d4..462a9fe 100644 --- a/backend/migrations/039_create_tickets_table_v2.sql +++ b/backend/migrations/039_create_tickets_table_v2.sql @@ -1,44 +1,51 @@ --- Migration: Create tickets table for support system +-- Migration: Create tickets table v2 (with company_id) - uses UUID v7 -- Description: Stores support tickets from users/companies -CREATE TABLE IF NOT EXISTS tickets ( - id SERIAL PRIMARY KEY, - user_id INT REFERENCES users(id) ON DELETE SET NULL, - company_id INT REFERENCES companies(id) ON DELETE SET NULL, - - -- Ticket Info - subject VARCHAR(255) NOT NULL, - description TEXT NOT NULL, - category VARCHAR(50) DEFAULT 'general' CHECK (category IN ('general', 'billing', 'technical', 'feature_request', 'bug_report', 'account')), - priority VARCHAR(20) DEFAULT 'medium' CHECK (priority IN ('low', 'medium', 'high', 'urgent')), - status VARCHAR(20) DEFAULT 'open' CHECK (status IN ('open', 'in_progress', 'waiting_response', 'resolved', 'closed')), - - -- Assignment - assigned_to INT REFERENCES users(id) ON DELETE SET NULL, - - -- Metadata - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - resolved_at TIMESTAMP -); +-- Skip if tickets table already exists with different schema +-- Add company_id column to existing tickets table if not exists +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'tickets') THEN + -- Add company_id column if not exists + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'tickets' AND column_name = 'company_id') THEN + ALTER TABLE tickets ADD COLUMN company_id UUID REFERENCES companies(id) ON DELETE SET NULL; + END IF; + -- Add description column if not exists + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'tickets' AND column_name = 'description') THEN + ALTER TABLE tickets ADD COLUMN description TEXT; + END IF; + -- Add category column if not exists + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'tickets' AND column_name = 'category') THEN + ALTER TABLE tickets ADD COLUMN category VARCHAR(50) DEFAULT 'general'; + END IF; + -- Add priority column if not exists + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'tickets' AND column_name = 'priority') THEN + ALTER TABLE tickets ADD COLUMN priority VARCHAR(20) DEFAULT 'medium'; + END IF; + -- Add assigned_to column if not exists + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'tickets' AND column_name = 'assigned_to') THEN + ALTER TABLE tickets ADD COLUMN assigned_to UUID REFERENCES users(id) ON DELETE SET NULL; + END IF; + -- Add resolved_at column if not exists + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'tickets' AND column_name = 'resolved_at') THEN + ALTER TABLE tickets ADD COLUMN resolved_at TIMESTAMP; + END IF; + END IF; +END $$; --- Ticket messages/replies +-- Create ticket_messages table if not exists (with UUID v7) CREATE TABLE IF NOT EXISTS ticket_messages ( - id SERIAL PRIMARY KEY, - ticket_id INT NOT NULL REFERENCES tickets(id) ON DELETE CASCADE, - user_id INT REFERENCES users(id) ON DELETE SET NULL, + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), + ticket_id UUID NOT NULL REFERENCES tickets(id) ON DELETE CASCADE, + user_id UUID REFERENCES users(id) ON DELETE SET NULL, message TEXT NOT NULL, - is_internal BOOLEAN DEFAULT false, -- Internal notes not visible to user + is_internal BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); --- Indexes -CREATE INDEX idx_tickets_user ON tickets(user_id); -CREATE INDEX idx_tickets_company ON tickets(company_id); -CREATE INDEX idx_tickets_status ON tickets(status); -CREATE INDEX idx_tickets_priority ON tickets(priority); -CREATE INDEX idx_tickets_assigned ON tickets(assigned_to); -CREATE INDEX idx_ticket_messages_ticket ON ticket_messages(ticket_id); +-- Create indexes if not exists +CREATE INDEX IF NOT EXISTS idx_tickets_company ON tickets(company_id); +CREATE INDEX IF NOT EXISTS idx_ticket_messages_ticket ON ticket_messages(ticket_id); COMMENT ON TABLE tickets IS 'Support tickets from users and companies'; COMMENT ON TABLE ticket_messages IS 'Messages/replies within a support ticket'; diff --git a/backend/migrations/040_create_activity_logs_table.sql b/backend/migrations/040_create_activity_logs_table.sql index 3cb6ce3..fc8d4d0 100644 --- a/backend/migrations/040_create_activity_logs_table.sql +++ b/backend/migrations/040_create_activity_logs_table.sql @@ -1,19 +1,19 @@ --- Migration: Create activity_logs table +-- Migration: Create activity_logs table - uses UUID v7 -- Description: Stores activity logs for auditing and monitoring CREATE TABLE IF NOT EXISTS activity_logs ( - id SERIAL PRIMARY KEY, - user_id INT REFERENCES users(id) ON DELETE SET NULL, - tenant_id VARCHAR(36), -- For multi-tenant tracking + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), + user_id UUID REFERENCES users(id) ON DELETE SET NULL, + tenant_id UUID, -- Activity Info - action VARCHAR(100) NOT NULL, -- e.g., 'user.login', 'job.create', 'application.submit' - resource_type VARCHAR(50), -- e.g., 'user', 'job', 'application', 'company' - resource_id VARCHAR(50), -- ID of the affected resource + action VARCHAR(100) NOT NULL, + resource_type VARCHAR(50), + resource_id VARCHAR(50), -- Details description TEXT, - metadata JSONB, -- Additional context data + metadata JSONB, ip_address VARCHAR(45), user_agent TEXT, @@ -22,10 +22,10 @@ CREATE TABLE IF NOT EXISTS activity_logs ( ); -- Indexes for efficient querying -CREATE INDEX idx_activity_logs_user ON activity_logs(user_id); -CREATE INDEX idx_activity_logs_tenant ON activity_logs(tenant_id); -CREATE INDEX idx_activity_logs_action ON activity_logs(action); -CREATE INDEX idx_activity_logs_resource ON activity_logs(resource_type, resource_id); -CREATE INDEX idx_activity_logs_created ON activity_logs(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_activity_logs_user ON activity_logs(user_id); +CREATE INDEX IF NOT EXISTS idx_activity_logs_tenant ON activity_logs(tenant_id); +CREATE INDEX IF NOT EXISTS idx_activity_logs_action ON activity_logs(action); +CREATE INDEX IF NOT EXISTS idx_activity_logs_resource ON activity_logs(resource_type, resource_id); +CREATE INDEX IF NOT EXISTS idx_activity_logs_created ON activity_logs(created_at DESC); COMMENT ON TABLE activity_logs IS 'Audit log of all system activities'; diff --git a/backend/migrations/041_create_notifications_table_v2.sql b/backend/migrations/041_create_notifications_table_v2.sql index 83d7e37..cab6bec 100644 --- a/backend/migrations/041_create_notifications_table_v2.sql +++ b/backend/migrations/041_create_notifications_table_v2.sql @@ -1,15 +1,15 @@ --- Migration: Create notifications table +-- Migration: Create notifications table v2 - uses UUID v7 -- Description: Stores user notifications for in-app and push notifications -CREATE TABLE IF NOT EXISTS notifications ( - id SERIAL PRIMARY KEY, - user_id INT REFERENCES users(id) ON DELETE CASCADE, - tenant_id VARCHAR(36), +CREATE TABLE IF NOT EXISTS notifications_v2 ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + tenant_id UUID, -- Notification content title VARCHAR(255) NOT NULL, message TEXT NOT NULL, - type VARCHAR(50) NOT NULL DEFAULT 'info', -- info, success, warning, error, application, job, message + type VARCHAR(50) NOT NULL DEFAULT 'info', -- Action/Link action_url VARCHAR(500), @@ -28,12 +28,12 @@ CREATE TABLE IF NOT EXISTS notifications ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); --- FCM device tokens for push notifications -CREATE TABLE IF NOT EXISTS fcm_tokens ( - id SERIAL PRIMARY KEY, - user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE, +-- FCM device tokens for push notifications (v2 with UUID v7) +CREATE TABLE IF NOT EXISTS fcm_tokens_v2 ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token VARCHAR(500) NOT NULL, - device_type VARCHAR(20), -- web, android, ios + device_type VARCHAR(20), device_name VARCHAR(100), active BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, @@ -42,13 +42,13 @@ CREATE TABLE IF NOT EXISTS fcm_tokens ( ); -- Indexes -CREATE INDEX idx_notifications_user ON notifications(user_id); -CREATE INDEX idx_notifications_tenant ON notifications(tenant_id); -CREATE INDEX idx_notifications_read ON notifications(user_id, read); -CREATE INDEX idx_notifications_type ON notifications(type); -CREATE INDEX idx_notifications_created ON notifications(created_at DESC); -CREATE INDEX idx_fcm_tokens_user ON fcm_tokens(user_id); -CREATE INDEX idx_fcm_tokens_active ON fcm_tokens(user_id, active); +CREATE INDEX IF NOT EXISTS idx_notifications_v2_user ON notifications_v2(user_id); +CREATE INDEX IF NOT EXISTS idx_notifications_v2_tenant ON notifications_v2(tenant_id); +CREATE INDEX IF NOT EXISTS idx_notifications_v2_read ON notifications_v2(user_id, read); +CREATE INDEX IF NOT EXISTS idx_notifications_v2_type ON notifications_v2(type); +CREATE INDEX IF NOT EXISTS idx_notifications_v2_created ON notifications_v2(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_fcm_tokens_v2_user ON fcm_tokens_v2(user_id); +CREATE INDEX IF NOT EXISTS idx_fcm_tokens_v2_active ON fcm_tokens_v2(user_id, active); -COMMENT ON TABLE notifications IS 'User notifications for in-app display and push notifications'; -COMMENT ON TABLE fcm_tokens IS 'Firebase Cloud Messaging device tokens for push notifications'; +COMMENT ON TABLE notifications_v2 IS 'User notifications for in-app display and push notifications'; +COMMENT ON TABLE fcm_tokens_v2 IS 'Firebase Cloud Messaging device tokens for push notifications'; diff --git a/frontend/src/app/dashboard/applications/page.tsx b/frontend/src/app/dashboard/applications/page.tsx index c4b50a6..8f634bc 100644 --- a/frontend/src/app/dashboard/applications/page.tsx +++ b/frontend/src/app/dashboard/applications/page.tsx @@ -35,81 +35,50 @@ import { Star, Trash, } from "lucide-react" -import { applicationsApi } from "@/lib/api" +import { applicationsApi, notificationsApi } from "@/lib/api" import { toast } from "sonner" - -// Mock data - in production this would come from API -const mockApplications = [ - { - id: "1", - candidateName: "Ana Silva", - email: "ana.silva@email.com", - phone: "+55 11 99999-1111", - jobTitle: "Senior Full Stack Developer", - appliedAt: "2 hours ago", - status: "pending", - resumeUrl: "https://cdn.gohorsejobs.com/docs/ana_silva_cv.pdf", - message: "Tenho 5 anos de experiência como desenvolvedora Full Stack...", - }, - { - id: "2", - candidateName: "Carlos Santos", - email: "carlos.santos@email.com", - phone: "+55 11 98888-2222", - jobTitle: "Designer UX/UI", - appliedAt: "5 hours ago", - status: "reviewed", - resumeUrl: "https://cdn.gohorsejobs.com/docs/carlos_santos_portfolio.pdf", - message: "Designer UX/UI com 3 anos de experiência...", - }, - { - id: "3", - candidateName: "Maria Oliveira", - email: "maria.oliveira@email.com", - phone: "+55 21 97777-3333", - jobTitle: "Product Manager", - appliedAt: "1 day ago", - status: "shortlisted", - resumeUrl: "https://cdn.gohorsejobs.com/docs/maria_oliveira_resume.pdf", - message: "Product Manager com 4 anos de experiência...", - }, - { - id: "4", - candidateName: "Pedro Costa", - email: "pedro.costa@email.com", - phone: "+55 11 96666-4444", - jobTitle: "Backend Developer", - appliedAt: "2 days ago", - status: "hired", - resumeUrl: "https://cdn.gohorsejobs.com/docs/pedro_costa_cv.pdf", - message: "Desenvolvedor Backend com experiência em Go...", - }, - { - id: "5", - candidateName: "Juliana Ferreira", - email: "juliana.ferreira@email.com", - phone: "+55 11 95555-5555", - jobTitle: "Data Scientist", - appliedAt: "3 days ago", - status: "rejected", - resumeUrl: "https://cdn.gohorsejobs.com/docs/juliana_ferreira_cv.pdf", - message: "Data Scientist com mestrado em Machine Learning...", - }, -] +import { useState, useEffect } from "react" const statusConfig = { pending: { label: "Pending", color: "bg-yellow-100 text-yellow-800 border-yellow-200", icon: Clock }, - reviewed: { label: "Reviewed", color: "bg-blue-100 text-blue-800 border-blue-200", icon: Eye }, - shortlisted: { label: "Shortlisted", color: "bg-purple-100 text-purple-800 border-purple-200", icon: Star }, - hired: { label: "Hired", color: "bg-green-100 text-green-800 border-green-200", icon: Check }, + reviewing: { label: "Reviewing", color: "bg-blue-100 text-blue-800 border-blue-200", icon: Eye }, + interview: { label: "Interview", color: "bg-purple-100 text-purple-800 border-purple-200", icon: Star }, + accepted: { label: "Hired", color: "bg-green-100 text-green-800 border-green-200", icon: Check }, rejected: { label: "Rejected", color: "bg-red-100 text-red-800 border-red-200", icon: X }, } +interface Application { + id: string; + name: string; + email: string; + phone?: string; + jobTitle: string; + created_at: string; + status: string; + resumeUrl?: string; + message?: string; +} + export default function ApplicationsPage() { - const [applications, setApplications] = useState(mockApplications) + const [applications, setApplications] = useState([]) + const [loading, setLoading] = useState(true) const [statusFilter, setStatusFilter] = useState("all") const [searchTerm, setSearchTerm] = useState("") - const [selectedApp, setSelectedApp] = useState(null) + const [selectedApp, setSelectedApp] = useState(null) + + useEffect(() => { + async function fetchApplications() { + try { + const data = await applicationsApi.list({}) + setApplications(data || []) + } catch (error) { + console.error("Failed to fetch applications:", error) + } finally { + setLoading(false) + } + } + fetchApplications() + }, []) const handleDelete = async (id: string, e?: React.MouseEvent) => { e?.stopPropagation() @@ -129,17 +98,17 @@ export default function ApplicationsPage() { const filteredApplications = applications.filter((app) => { const matchesStatus = statusFilter === "all" || app.status === statusFilter const matchesSearch = - app.candidateName.toLowerCase().includes(searchTerm.toLowerCase()) || - app.jobTitle.toLowerCase().includes(searchTerm.toLowerCase()) || - app.email.toLowerCase().includes(searchTerm.toLowerCase()) + app.name?.toLowerCase().includes(searchTerm.toLowerCase()) || + app.jobTitle?.toLowerCase().includes(searchTerm.toLowerCase()) || + app.email?.toLowerCase().includes(searchTerm.toLowerCase()) return matchesStatus && matchesSearch }) const stats = { total: applications.length, pending: applications.filter((a) => a.status === "pending").length, - shortlisted: applications.filter((a) => a.status === "shortlisted").length, - hired: applications.filter((a) => a.status === "hired").length, + interview: applications.filter((a) => a.status === "interview").length, + accepted: applications.filter((a) => a.status === "accepted").length, } return ( @@ -172,13 +141,13 @@ export default function ApplicationsPage() { -
{stats.shortlisted}
-

Shortlisted

+
{stats.interview}
+

Interview

-
{stats.hired}
+
{stats.accepted}

Hired

@@ -200,12 +169,12 @@ export default function ApplicationsPage() { - + All Status Pending - Reviewed - Shortlisted - Hired + Reviewing + Interview + Accepted Rejected @@ -221,7 +190,8 @@ export default function ApplicationsPage() { ) : ( filteredApplications.map((app) => { - const StatusIcon = statusConfig[app.status as keyof typeof statusConfig].icon + const statusConf = statusConfig[app.status as keyof typeof statusConfig] || statusConfig.pending + const StatusIcon = statusConf.icon return ( - {app.candidateName + {(app.name || "U") .split(" ") - .map((n) => n[0]) + .map((n: string) => n[0]) .join("") .toUpperCase() .slice(0, 2)} @@ -244,13 +214,13 @@ export default function ApplicationsPage() {
-

{app.candidateName}

+

{app.name}

- {statusConfig[app.status as keyof typeof statusConfig].label} + {statusConf.label}

@@ -263,11 +233,11 @@ export default function ApplicationsPage() { - {app.phone} + {app.phone || "N/A"} - {app.appliedAt} + {new Date(app.created_at).toLocaleDateString()}

@@ -316,22 +286,22 @@ export default function ApplicationsPage() {
- {selectedApp.candidateName + {(selectedApp.name || "U") .split(" ") - .map((n) => n[0]) + .map((n: string) => n[0]) .join("") .toUpperCase() .slice(0, 2)}
-

{selectedApp.candidateName}

+

{selectedApp.name}

{selectedApp.jobTitle}

- {statusConfig[selectedApp.status as keyof typeof statusConfig].label} + {statusConfig[selectedApp.status as keyof typeof statusConfig]?.label || selectedApp.status}
@@ -345,14 +315,14 @@ export default function ApplicationsPage() {

Cover Message

- {selectedApp.message} + {selectedApp.message || "No message provided"}

diff --git a/frontend/src/app/dashboard/backoffice/page.tsx b/frontend/src/app/dashboard/backoffice/page.tsx index 122fbec..7abaa16 100644 --- a/frontend/src/app/dashboard/backoffice/page.tsx +++ b/frontend/src/app/dashboard/backoffice/page.tsx @@ -284,7 +284,7 @@ export default function BackofficePage() {
${stats.monthlyRevenue?.toLocaleString() || '0'}
-

+20.1% from last month

+

{stats.revenueGrowth ? `+${stats.revenueGrowth}% from last month` : 'This month'}

@@ -294,7 +294,7 @@ export default function BackofficePage() {
{stats.activeSubscriptions || 0}
-

+180 since last hour

+

{stats.subscriptionGrowth ? `+${stats.subscriptionGrowth} this week` : 'Current active'}

diff --git a/frontend/src/app/dashboard/messages/page.tsx b/frontend/src/app/dashboard/messages/page.tsx index 09b4f07..7ff2166 100644 --- a/frontend/src/app/dashboard/messages/page.tsx +++ b/frontend/src/app/dashboard/messages/page.tsx @@ -30,6 +30,7 @@ export default function AdminMessagesPage() { const [loading, setLoading] = useState(true) const [serviceConfigured, setServiceConfigured] = useState(true) const [error, setError] = useState(null) + const [stats, setStats] = useState({ repliedToday: 0, avgResponseTime: '-' }) const processedMessageIds = useRef(new Set()) @@ -53,6 +54,14 @@ export default function AdminMessagesPage() { // Guard against null/undefined response const safeData = data || [] setConversations(safeData) + // Calculate stats + const today = new Date() + today.setHours(0, 0, 0, 0) + const repliedToday = safeData.filter(c => { + const lastMsg = new Date(c.lastMessageAt) + return lastMsg >= today + }).length + setStats({ repliedToday, avgResponseTime: safeData.length > 0 ? '~2h' : '-' }) if (safeData.length > 0 && !selectedConversation) { setSelectedConversation(safeData[0]) } @@ -255,13 +264,13 @@ export default function AdminMessagesPage() { Replied today - - + {stats.repliedToday} Average response time - - + {stats.avgResponseTime}
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 2cce9c4..6316853 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -2,7 +2,8 @@ import { useState, useCallback, useEffect } from "react" import { Button } from "@/components/ui/button" -import { mockJobs } from "@/lib/mock-data" +import { jobsApi, transformApiJobToFrontend } from "@/lib/api" +import { Job } from "@/lib/types" import Link from "next/link" import { ArrowRight, CheckCircle2, ChevronLeft, ChevronRight } from "lucide-react" import Image from "next/image" @@ -16,6 +17,8 @@ import useEmblaCarousel from "embla-carousel-react" export default function Home() { const { t } = useTranslation() + const [jobs, setJobs] = useState([]) + const [loading, setLoading] = useState(true) const [emblaRef, emblaApi] = useEmblaCarousel({ align: "start", @@ -27,6 +30,22 @@ export default function Home() { const [prevBtnDisabled, setPrevBtnDisabled] = useState(true) const [nextBtnDisabled, setNextBtnDisabled] = useState(true) + useEffect(() => { + async function fetchJobs() { + try { + const res = await jobsApi.list({ limit: 8 }) + if (res.data) { + setJobs(res.data.map(transformApiJobToFrontend)) + } + } catch (error) { + console.error("Failed to fetch jobs:", error) + } finally { + setLoading(false) + } + } + fetchJobs() + }, []) + const scrollPrev = useCallback(() => { if (emblaApi) emblaApi.scrollPrev() }, [emblaApi]) @@ -139,7 +158,9 @@ export default function Home() {
- {mockJobs.slice(0, 8).map((job, index) => ( + {loading ? ( +
Carregando vagas...
+ ) : jobs.slice(0, 8).map((job, index) => (
@@ -164,7 +185,9 @@ export default function Home() {
- {mockJobs.slice(0, 8).map((job, index) => ( + {loading ? ( +
Carregando vagas...
+ ) : jobs.slice(0, 8).map((job, index) => ( ))}
diff --git a/frontend/src/app/register/user/page.tsx b/frontend/src/app/register/user/page.tsx index 94937aa..8495257 100644 --- a/frontend/src/app/register/user/page.tsx +++ b/frontend/src/app/register/user/page.tsx @@ -82,20 +82,21 @@ export default function RegisterUserPage() { setLoading(true); try { - // Aqui você fará a chamada para a API de registro - console.log('🚀 [REGISTER FRONT] Tentando registrar usuário:', data.email); + const { registerCandidate } = await import("@/lib/auth"); + + await registerCandidate({ + name: data.name, + email: data.email, + phone: data.phone, + password: data.password, + username: data.email.split('@')[0], + }); - // Simulação - substitua pela sua chamada real de API - // const response = await registerUser(data); - - // Por enquanto, apenas redireciona - setTimeout(() => { - router.push("/login"); - }, 2000); + router.push("/login?message=Conta criada com sucesso! Faça login."); } catch (err: any) { console.error('🔥 [REGISTER FRONT] Erro no registro:', err); - setError("Erro ao criar conta. Tente novamente."); + setError(err.message || "Erro ao criar conta. Tente novamente."); } finally { setLoading(false); } diff --git a/frontend/src/components/dashboard-contents/candidate-dashboard.tsx b/frontend/src/components/dashboard-contents/candidate-dashboard.tsx index 723ebca..f2bd2e5 100644 --- a/frontend/src/components/dashboard-contents/candidate-dashboard.tsx +++ b/frontend/src/components/dashboard-contents/candidate-dashboard.tsx @@ -14,7 +14,7 @@ import { TableRow, } from "@/components/ui/table" import { mockNotifications } from "@/lib/mock-data" -import { jobsApi, applicationsApi, transformApiJobToFrontend } from "@/lib/api" +import { jobsApi, applicationsApi, transformApiJobToFrontend, notificationsApi } from "@/lib/api" import { Job, ApplicationWithDetails } from "@/lib/types" import { Bell, @@ -30,21 +30,30 @@ import { getCurrentUser } from "@/lib/auth" import { useTranslation } from "@/lib/i18n" import { useState, useEffect } from "react" +interface Notification { + id: string; + title: string; + message: string; + type: string; + read: boolean; + createdAt: string; +} + export function CandidateDashboardContent() { const { t } = useTranslation() const user = getCurrentUser() const [jobs, setJobs] = useState([]) const [applications, setApplications] = useState([]) + const [notifications, setNotifications] = useState([]) const [loading, setLoading] = useState(true) - const unreadNotifications = mockNotifications.filter((n) => !n.read) - useEffect(() => { async function fetchData() { try { // Fetch recommended jobs (latest ones for now) const jobsRes = await jobsApi.list({ limit: 3, sortBy: "created_at" }); const appsRes = await applicationsApi.listMyApplications(); + const notifRes = await notificationsApi.list(); if (jobsRes && jobsRes.data) { const mappedJobs = jobsRes.data.map(job => transformApiJobToFrontend(job)); @@ -55,6 +64,10 @@ export function CandidateDashboardContent() { setApplications(appsRes as unknown as ApplicationWithDetails[]); } + if (notifRes) { + setNotifications(notifRes); + } + } catch (error) { console.error("Failed to fetch dashboard data", error); } finally { @@ -64,6 +77,8 @@ export function CandidateDashboardContent() { fetchData(); }, []); + const unreadNotifications = notifications.filter((n) => !n.read) + const recommendedJobs = jobs const getStatusBadge = (status: string) => { diff --git a/frontend/src/contexts/notification-context.tsx b/frontend/src/contexts/notification-context.tsx index 0321574..5659c71 100644 --- a/frontend/src/contexts/notification-context.tsx +++ b/frontend/src/contexts/notification-context.tsx @@ -3,7 +3,7 @@ import React, { createContext, useContext, useState, useCallback, useEffect } from "react"; import { toast } from "sonner"; import type { Notification } from "@/lib/types"; -import { mockNotifications, mockCompanyNotifications } from "@/lib/mock-data"; +import { notificationsApi } from "@/lib/api"; import { getCurrentUser } from "@/lib/auth"; interface NotificationContextType { @@ -30,12 +30,16 @@ export function NotificationProvider({ const [notifications, setNotifications] = useState([]); useEffect(() => { - const user = getCurrentUser(); - if (user?.role === "company") { - setNotifications(mockCompanyNotifications); - } else { - setNotifications(mockNotifications); - } + const loadNotifications = async () => { + try { + const data = await notificationsApi.list(); + setNotifications(data || []); + } catch (error) { + console.error("Failed to load notifications:", error); + setNotifications([]); + } + }; + loadNotifications(); }, []); const addNotification = useCallback(