fix: resolve seeder connection, backoffice scroll and auth session refresh

This commit is contained in:
Yamamoto 2026-01-03 12:11:38 -03:00
parent 928997c9ce
commit 621e4594c6
6 changed files with 94 additions and 24 deletions

View file

@ -68,9 +68,9 @@ export default function BackofficePage() {
loadBackoffice() loadBackoffice()
}, [router]) }, [router])
const loadBackoffice = async () => { const loadBackoffice = async (silent = false) => {
try { try {
setLoading(true) if (!silent) setLoading(true)
const [rolesData, auditData, companiesData, jobsData, tagsData] = await Promise.all([ const [rolesData, auditData, companiesData, jobsData, tagsData] = await Promise.all([
adminAccessApi.listRoles(), adminAccessApi.listRoles(),
adminAuditApi.listLogins(20), adminAuditApi.listLogins(20),
@ -87,7 +87,7 @@ export default function BackofficePage() {
console.error("Error loading backoffice:", error) console.error("Error loading backoffice:", error)
toast.error("Failed to load backoffice data") toast.error("Failed to load backoffice data")
} finally { } finally {
setLoading(false) if (!silent) setLoading(false)
} }
} }
@ -95,7 +95,7 @@ export default function BackofficePage() {
try { try {
await adminCompaniesApi.updateStatus(companyId, { verified: true }) await adminCompaniesApi.updateStatus(companyId, { verified: true })
toast.success("Company approved") toast.success("Company approved")
loadBackoffice() loadBackoffice(true)
} catch (error) { } catch (error) {
console.error("Error approving company:", error) console.error("Error approving company:", error)
toast.error("Failed to approve company") toast.error("Failed to approve company")
@ -106,7 +106,7 @@ export default function BackofficePage() {
try { try {
await adminCompaniesApi.updateStatus(companyId, { active: false }) await adminCompaniesApi.updateStatus(companyId, { active: false })
toast.success("Company deactivated") toast.success("Company deactivated")
loadBackoffice() loadBackoffice(true)
} catch (error) { } catch (error) {
console.error("Error deactivating company:", error) console.error("Error deactivating company:", error)
toast.error("Failed to deactivate company") toast.error("Failed to deactivate company")
@ -117,7 +117,7 @@ export default function BackofficePage() {
try { try {
await adminJobsApi.updateStatus(jobId, status) await adminJobsApi.updateStatus(jobId, status)
toast.success("Job status updated") toast.success("Job status updated")
loadBackoffice() loadBackoffice(true)
} catch (error) { } catch (error) {
console.error("Error updating job status:", error) console.error("Error updating job status:", error)
toast.error("Failed to update job status") toast.error("Failed to update job status")
@ -128,7 +128,7 @@ export default function BackofficePage() {
try { try {
await adminJobsApi.duplicate(jobId) await adminJobsApi.duplicate(jobId)
toast.success("Job duplicated as draft") toast.success("Job duplicated as draft")
loadBackoffice() loadBackoffice(true)
} catch (error) { } catch (error) {
console.error("Error duplicating job:", error) console.error("Error duplicating job:", error)
toast.error("Failed to duplicate job") toast.error("Failed to duplicate job")
@ -145,7 +145,7 @@ export default function BackofficePage() {
await adminTagsApi.create({ name: tagForm.name.trim(), category: tagForm.category }) await adminTagsApi.create({ name: tagForm.name.trim(), category: tagForm.category })
toast.success("Tag created") toast.success("Tag created")
setTagForm({ name: "", category: "area" }) setTagForm({ name: "", category: "area" })
loadBackoffice() loadBackoffice(true)
} catch (error) { } catch (error) {
console.error("Error creating tag:", error) console.error("Error creating tag:", error)
toast.error("Failed to create tag") toast.error("Failed to create tag")
@ -158,7 +158,7 @@ export default function BackofficePage() {
try { try {
await adminTagsApi.update(tag.id, { active: !tag.active }) await adminTagsApi.update(tag.id, { active: !tag.active })
toast.success("Tag updated") toast.success("Tag updated")
loadBackoffice() loadBackoffice(true)
} catch (error) { } catch (error) {
console.error("Error updating tag:", error) console.error("Error updating tag:", error)
toast.error("Failed to update tag") toast.error("Failed to update tag")
@ -180,7 +180,7 @@ export default function BackofficePage() {
<h1 className="text-3xl font-bold text-foreground">Backoffice</h1> <h1 className="text-3xl font-bold text-foreground">Backoffice</h1>
<p className="text-muted-foreground mt-1">Controle administrativo do GoHorse Jobs</p> <p className="text-muted-foreground mt-1">Controle administrativo do GoHorse Jobs</p>
</div> </div>
<Button variant="outline" onClick={loadBackoffice} className="gap-2"> <Button variant="outline" onClick={() => loadBackoffice(false)} className="gap-2">
<RefreshCw className="h-4 w-4" /> <RefreshCw className="h-4 w-4" />
Refresh Refresh
</Button> </Button>

View file

@ -11,6 +11,7 @@ import { I18nProvider } from "@/lib/i18n"
import "./globals.css" import "./globals.css"
import { Suspense } from "react" import { Suspense } from "react"
import { LoadingScreen } from "@/components/ui/loading-spinner" import { LoadingScreen } from "@/components/ui/loading-spinner"
import { AuthProvider } from "@/contexts/AuthContext"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "GoHorseJobs - Find your next opportunity", title: "GoHorseJobs - Find your next opportunity",
@ -37,16 +38,18 @@ export default function RootLayout({
<ConfigProvider> <ConfigProvider>
<I18nProvider> <I18nProvider>
<ThemeProvider> <ThemeProvider>
<NotificationProvider> <AuthProvider>
<Suspense fallback={<LoadingScreen text="GoHorse Jobs" />}>{children}</Suspense> <NotificationProvider>
<Toaster <Suspense fallback={<LoadingScreen text="GoHorse Jobs" />}>{children}</Suspense>
position="top-right" <Toaster
richColors position="top-right"
closeButton richColors
expand={false} closeButton
duration={4000} expand={false}
/> duration={4000}
</NotificationProvider> />
</NotificationProvider>
</AuthProvider>
</ThemeProvider> </ThemeProvider>
</I18nProvider> </I18nProvider>
</ConfigProvider> </ConfigProvider>

View file

@ -0,0 +1,58 @@
"use client";
import { createContext, useContext, useEffect, useState } from "react";
import { refreshSession, User, getCurrentUser } from "@/lib/auth";
interface AuthContextType {
user: User | null;
loading: boolean;
checkSession: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType>({
user: null,
loading: true,
checkSession: async () => { },
});
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const checkSession = async () => {
try {
// First check local storage for immediate feedback (optimistic)
const stored = getCurrentUser();
if (stored) {
setUser(stored);
}
// Then verify with backend (httpOnly cookie)
const refreshedUser = await refreshSession();
if (refreshedUser) {
setUser(refreshedUser);
} else {
// If backend fails but we had local storage, it means session invalid/expired
// refreshSession already clears localStorage in that case
setUser(null);
}
} catch (error) {
console.error("Auth initialization failed", error);
setUser(null);
} finally {
setLoading(false);
}
};
useEffect(() => {
checkSession();
}, []);
return (
<AuthContext.Provider value={{ user, loading, checkSession }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);

View file

@ -96,7 +96,7 @@ export function getCurrentUser(): User | null {
const stored = localStorage.getItem(AUTH_KEY); const stored = localStorage.getItem(AUTH_KEY);
if (stored) { if (stored) {
const user = JSON.parse(stored); const user = JSON.parse(stored);
console.log("%c[AUTH] User Loaded from Storage", "color: #10b981", user.email); // console.log("%c[AUTH] User Loaded from Storage", "color: #10b981", user.email);
return user; return user;
} }
// User not in storage (normal state) // User not in storage (normal state)

View file

@ -119,9 +119,9 @@ async function seedDatabase() {
} catch (error) { } catch (error) {
console.error('\n❌ Seeding failed:', error.message); console.error('\n❌ Seeding failed:', error.message);
console.error(error); console.error(error);
} finally { throw error;
await closePool();
} }
// Do NOT close pool here
} }
// Lite version (skips cities for faster seeding) // Lite version (skips cities for faster seeding)
@ -228,6 +228,8 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
} catch (error) { } catch (error) {
console.error('❌ Execution failed:', error); console.error('❌ Execution failed:', error);
process.exit(1); process.exit(1);
} finally {
await import('./db.js').then(m => m.closePool());
} }
})(); })();
} }

View file

@ -327,6 +327,13 @@ async function executeSqlFile(filename, tableName) {
pgStmt = transformStatesInsert(pgStmt); pgStmt = transformStatesInsert(pgStmt);
} }
// prevent duplicate key errors
if (pgStmt.trim().endsWith(';')) {
pgStmt = pgStmt.trim().slice(0, -1) + ' ON CONFLICT DO NOTHING;';
} else {
pgStmt += ' ON CONFLICT DO NOTHING;';
}
await pool.query(pgStmt); await pool.query(pgStmt);
} }
@ -436,7 +443,7 @@ async function executeGzippedSqlFile(filename, tableName) {
for (let i = 0; i < valueBatches.length; i++) { for (let i = 0; i < valueBatches.length; i++) {
const batch = valueBatches[i]; const batch = valueBatches[i];
const query = `INSERT INTO ${tableName} ${columns} VALUES ${batch.join(', ')}`; const query = `INSERT INTO ${tableName} ${columns} VALUES ${batch.join(', ')} ON CONFLICT DO NOTHING`;
await pool.query(query); await pool.query(query);
if ((i + 1) % 10 === 0 || i === valueBatches.length - 1) { if ((i + 1) % 10 === 0 || i === valueBatches.length - 1) {