remocao do clerk

This commit is contained in:
2026-03-14 21:11:43 -03:00
parent c6f69e1c1d
commit 286867739d
13 changed files with 436 additions and 346 deletions

View File

@@ -1,7 +1,5 @@
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { SignedIn, SignedOut, useOrganization } from '@clerk/clerk-react';
import { AuthProvider } from './context/AuthContext';
import { useAuth } from './context/useAuth';
import { AuthProvider, useAuth } from './context/AuthContext';
import { SystemSettingsProvider } from './context/SystemSettingsContext';
import { NotificationProvider } from './contexts/NotificationContext';
import { Layout } from './components/Layout';
@@ -32,90 +30,98 @@ const DeveloperRoute: React.FC<{ children: React.ReactNode }> = ({ children }) =
};
const AppContent: React.FC = () => {
const { organization } = useOrganization();
const { appUser, isLoading } = useAuth();
if (isLoading) return <div className="flex h-screen items-center justify-center">Carregando...</div>;
console.log('AppContent rendered');
console.log('Current organization:', organization);
// If user is signed in but has no organization, show org selector
if (!organization) {
console.log('No organization - showing OrganizationSelector');
// AppUser exists but hasn't selected an org yet (if your business logic requires orgs)
if (appUser && !appUser.organizationId) {
return <OrganizationSelector />;
}
console.log('Organization exists - showing main app');
return (
<ToastProvider>
<AuthProvider>
<SystemSettingsProvider>
<NotificationProvider>
<Layout>
<Routes>
<Route path="/" element={<ProjectList />} />
<Route path="/guest-dashboard" element={<GuestDashboard />} />
<Route path="/projects" element={<ProjectList />} />
<Route path="/project/:id" element={<ProjectDetails />} />
<Route path="/schemes" element={<SchemesList />} />
<Route path="/inspections" element={<InspectionsList />} />
<Route path="/library" element={
<ProtectedRoute allowedRoles={['user', 'admin']}>
<DataSheetLibrary />
</ProtectedRoute>
} />
<Route path="/instruments" element={
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
<InstrumentList />
</ProtectedRoute>
} />
<Route path="/yield-study" element={
<ProtectedRoute allowedRoles={['user', 'admin']}>
<YieldStudyDashboard />
</ProtectedRoute>
} />
<Route path="/calculators" element={<CalculatorDashboard />} />
<Route
path="/admin"
element={
<ProtectedRoute allowedRoles={['admin']}>
<AdminDashboard />
</ProtectedRoute>
}
/>
<Route
path="/stock"
element={
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
<StockDashboard />
</ProtectedRoute>
}
/>
<Route
path="/developer"
element={
<DeveloperRoute>
<DeveloperDashboard />
</DeveloperRoute>
}
/>
</Routes>
</Layout>
</NotificationProvider>
</SystemSettingsProvider>
</AuthProvider>
</ToastProvider>
<Layout>
<Routes>
<Route path="/" element={<ProjectList />} />
<Route path="/guest-dashboard" element={<GuestDashboard />} />
<Route path="/projects" element={<ProjectList />} />
<Route path="/project/:id" element={<ProjectDetails />} />
<Route path="/schemes" element={<SchemesList />} />
<Route path="/inspections" element={<InspectionsList />} />
<Route path="/library" element={
<ProtectedRoute allowedRoles={['user', 'admin']}>
<DataSheetLibrary />
</ProtectedRoute>
} />
<Route path="/instruments" element={
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
<InstrumentList />
</ProtectedRoute>
} />
<Route path="/yield-study" element={
<ProtectedRoute allowedRoles={['user', 'admin']}>
<YieldStudyDashboard />
</ProtectedRoute>
} />
<Route path="/calculators" element={<CalculatorDashboard />} />
<Route
path="/admin"
element={
<ProtectedRoute allowedRoles={['admin']}>
<AdminDashboard />
</ProtectedRoute>
}
/>
<Route
path="/stock"
element={
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
<StockDashboard />
</ProtectedRoute>
}
/>
<Route
path="/developer"
element={
<DeveloperRoute>
<DeveloperDashboard />
</DeveloperRoute>
}
/>
</Routes>
</Layout>
);
};
const MainRouter: React.FC = () => {
const { appUser, isLoading } = useAuth();
if (isLoading) {
return <div className="flex h-screen items-center justify-center">Verificando sessão...</div>;
}
return (
<Router>
{!appUser ? (
<Login />
) : (
<AppContent />
)}
</Router>
);
};
function App() {
return (
<Router>
<SignedOut>
<Login />
</SignedOut>
<SignedIn>
<AppContent />
</SignedIn>
</Router>
<ToastProvider>
<AuthProvider>
<SystemSettingsProvider>
<NotificationProvider>
<MainRouter />
</NotificationProvider>
</SystemSettingsProvider>
</AuthProvider>
</ToastProvider>
);
}

View File

@@ -1,142 +1,108 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useUser, useOrganization } from '@clerk/clerk-react';
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import type { AppUser } from '../types';
import { AuthContext } from './AuthContextType';
import { setApiClerkUserId, setApiOrganizationId, getBaseUrl } from '../services/api';
import { setApiToken, setApiOrganizationId, getBaseUrl } from '../services/api';
const API_URL = getBaseUrl();
interface AuthProviderProps {
children: React.ReactNode;
export interface AuthContextType {
appUser: AppUser | null;
isLoading: boolean;
error: string | null;
token: string | null;
login: (token: string, user: AppUser) => void;
logout: () => void;
isAdmin: () => boolean;
isUser: () => boolean;
isGuest: () => boolean;
isDeveloper: () => boolean;
canEdit: () => boolean;
refetchUser: () => Promise<void>;
}
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const { user, isLoaded } = useUser();
const { organization, membership } = useOrganization();
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [appUser, setAppUser] = useState<AppUser | null>(null);
const [token, setToken] = useState<string | null>(localStorage.getItem('jwt_token'));
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const lastContextRef = useRef<{ clerkId?: string, orgId?: string | null }>({});
// Set the clerk user ID and organization ID for the API interceptor
// Initial load: se tem token, setar no interceptor e buscar dados do usuário
useEffect(() => {
setApiClerkUserId(user?.id || null);
setApiOrganizationId(organization?.id || null);
}, [user?.id, organization?.id]);
const syncUser = useCallback(async () => {
if (!user) {
setAppUser(null);
setIsLoading(false);
return;
}
try {
// Only set loading if the context has changed (new user or new organization)
// This prevents unmounting/remounting components on window focus revalidations
const isSameContext =
lastContextRef.current.clerkId === user.id &&
lastContextRef.current.orgId === (organization?.id || null);
if (!isSameContext) {
setIsLoading(true);
}
setError(null);
// Sync user with backend, including organization context
const response = await fetch(`${API_URL}/users/sync`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-clerk-user-id': user.id,
...(organization?.id && { 'x-organization-id': organization.id }),
},
body: JSON.stringify({
clerkId: user.id,
email: user.primaryEmailAddress?.emailAddress || '',
name: user.fullName || user.firstName || 'Usuário',
organizationId: organization?.id || null,
clerkRole: membership?.role || null, // org:admin, org:member, etc.
}),
});
if (!response.ok) {
const data = await response.json();
if (response.status === 403 && data.error?.includes('bloqueada')) {
setError('Sua conta foi bloqueada. Entre em contato com o administrador.');
setAppUser(null);
return;
}
throw new Error('Falha ao sincronizar usuário');
}
const syncedUser = await response.json();
// Use organizationRole if available (per-org role), otherwise fall back to global role
const effectiveRole = syncedUser.organizationRole || syncedUser.role || 'guest';
setAppUser({
...syncedUser,
id: syncedUser._id || syncedUser.id,
role: effectiveRole, // Override with organization-specific role
});
// Update last context ref
lastContextRef.current = { clerkId: user.id, orgId: organization?.id || null };
} catch (err) {
console.error('Error syncing user:', err);
setError('Erro ao carregar dados do usuário');
} finally {
if (token) {
setApiToken(token);
refetchUser();
} else {
setIsLoading(false);
}
}, [user, organization?.id, membership?.role]);
}, [token]);
const login = useCallback((newToken: string, user: AppUser) => {
localStorage.setItem('jwt_token', newToken);
setToken(newToken);
setAppUser(user);
setApiToken(newToken);
// Se a organização existir, setar o header
if (user.organizationId) {
setApiOrganizationId(user.organizationId);
}
}, []);
const logout = useCallback(() => {
localStorage.removeItem('jwt_token');
setToken(null);
setAppUser(null);
setApiToken(null);
setApiOrganizationId(null);
}, []);
const refetchUser = useCallback(async () => {
if (!user) return;
if (!token) return;
setIsLoading(true);
try {
const response = await fetch(`${API_URL}/users/me`, {
const response = await fetch(`${API_URL}/auth/me`, {
headers: {
'x-clerk-user-id': user.id,
...(organization?.id && { 'x-organization-id': organization.id }),
'Authorization': `Bearer ${token}`
},
});
if (response.ok) {
const userData = await response.json();
const effectiveRole = userData.organizationRole || userData.role || 'guest';
setAppUser({
...userData,
id: userData._id || userData.id,
role: effectiveRole,
});
setAppUser(userData);
if (userData.organizationId) {
setApiOrganizationId(userData.organizationId);
}
} else {
// Token inválido ou expirado
logout();
}
} catch (err) {
console.error('Error refetching user:', err);
setError('Falha na comunicação de autenticação.');
} finally {
setIsLoading(false);
}
}, [user, organization?.id]);
// Re-sync when organization changes
useEffect(() => {
if (isLoaded && user) {
syncUser();
}
}, [isLoaded, user, organization?.id, syncUser]);
}, [token, logout]);
const isDeveloper = useCallback(() => {
return user?.primaryEmailAddress?.emailAddress === 'admtracksteel@gmail.com';
}, [user]);
return appUser?.email === 'admtracksteel@gmail.com';
}, [appUser]);
const isAdmin = useCallback(() => appUser?.role === 'admin' || isDeveloper(), [appUser, isDeveloper]);
const isUser = useCallback(() => appUser?.role === 'user' || isAdmin(), [appUser, isAdmin]);
const isGuest = useCallback(() => appUser?.role === 'guest' && !isDeveloper(), [appUser, isDeveloper]);
const canEdit = useCallback(() => (appUser?.role !== 'guest' && appUser?.role !== undefined) || isDeveloper(), [appUser, isDeveloper]);
const canEdit = useCallback(() => (appUser?.role !== 'guest' && appUser !== null) || isDeveloper(), [appUser, isDeveloper]);
return (
<AuthContext.Provider
value={{
appUser,
isLoading,
isSignedIn: !!user,
error,
token,
login,
logout,
isAdmin,
isUser,
isGuest,
@@ -149,3 +115,11 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View File

@@ -1,47 +1,7 @@
import { createRoot } from 'react-dom/client'
import { ClerkProvider } from '@clerk/clerk-react'
import { ptBR } from '@clerk/localizations'
import './index.css'
import App from './App.tsx'
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
if (!PUBLISHABLE_KEY) {
throw new Error("Missing Publishable Key")
}
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.tsx';
createRoot(document.getElementById('root')!).render(
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
afterSignOutUrl="/"
localization={ptBR}
appearance={{
variables: {
colorPrimary: '#fb923c', // Cor primária do GPI (Laranja)
colorBackground: '#ffffff',
colorText: '#1c1917',
colorTextSecondary: '#57534e',
borderRadius: '0.75rem',
},
elements: {
card: "shadow-none border-0 bg-transparent", // Deixamos o container da página controlar o card
navbar: "hidden",
headerTitle: "text-2xl font-bold tracking-tight",
headerSubtitle: "text-text-muted font-medium",
formButtonPrimary: "bg-primary hover:bg-primary/90 text-white font-bold py-3 rounded-xl transition-all shadow-lg shadow-primary/20",
socialButtonsBlockButton: "bg-white hover:bg-surface-hover border-border/40 text-text-main font-semibold transition-all duration-300 rounded-xl",
footerActionLink: "text-primary hover:text-primary/80 font-bold",
formFieldInput: "bg-surface-soft border-border/40 focus:ring-2 focus:ring-primary/20 focus:border-primary rounded-xl",
organizationSwitcherTrigger: "hover:bg-surface-hover transition-colors rounded-xl",
organizationPreviewMainIdentifier: "font-bold",
// Personalização específica para a lista de organizações que aparece na imagem
organizationListPreview: "hover:bg-surface-soft rounded-xl transition-all p-3",
organizationListCreateOrganizationButton: "text-primary font-bold hover:text-primary/80",
}
}}
>
<App />
</ClerkProvider>,
)
<App />
);

View File

@@ -1,7 +1,45 @@
import { SignIn } from "@clerk/clerk-react";
import React, { useState } from "react";
import { Hammer } from "lucide-react";
import { useAuth } from "../context/useAuth";
import { getBaseUrl } from "../services/api";
const API_URL = getBaseUrl();
export const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [errorMsg, setErrorMsg] = useState("");
const [loading, setLoading] = useState(false);
const { login } = useAuth();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setErrorMsg("");
setLoading(true);
try {
const response = await fetch(`${API_URL}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (!response.ok) {
setErrorMsg(data.error || "Erro ao efetuar login");
setLoading(false);
return;
}
login(data.token, data.user);
} catch (err) {
setErrorMsg("Falha na conexão com o servidor.");
setLoading(false);
}
};
return (
<div className="min-h-screen w-full flex items-center justify-center bg-surface-soft relative overflow-hidden">
{/* Background decorative elements */}
@@ -18,13 +56,53 @@ export const Login = () => {
<p className="text-text-muted text-sm font-medium uppercase tracking-widest">Gestão de Pintura Industrial</p>
</div>
{/* Clerk SignIn Component - Customizado via Tema Global no main.tsx */}
<div className="w-full bg-surface rounded-[2rem] border border-border/40 shadow-2xl shadow-primary/5 p-4 animate-in slide-in-from-bottom-8 duration-1000">
<SignIn
afterSignInUrl="/"
afterSignUpUrl="/"
forceRedirectUrl="/"
/>
{/* Custom Login Form */}
<div className="w-full bg-surface rounded-[2rem] border border-border/40 shadow-2xl shadow-primary/5 p-8 animate-in slide-in-from-bottom-8 duration-1000">
<h2 className="text-xl font-bold text-text-main mb-6 text-center">Entrar na sua conta</h2>
{errorMsg && (
<div className="mb-4 p-3 rounded-lg bg-error/10 border border-error/20 text-error text-sm text-center">
{errorMsg}
</div>
)}
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<label className="text-sm font-semibold text-text-secondary" htmlFor="email">Email</label>
<input
id="email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="bg-surface-soft border border-border/40 focus:ring-2 focus:ring-primary/20 focus:border-primary rounded-xl px-4 py-3 text-text-main outline-none transition-all"
placeholder="seu@email.com"
/>
</div>
<div className="flex flex-col gap-2">
<div className="flex justify-between items-center">
<label className="text-sm font-semibold text-text-secondary" htmlFor="password">Senha</label>
</div>
<input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="bg-surface-soft border border-border/40 focus:ring-2 focus:ring-primary/20 focus:border-primary rounded-xl px-4 py-3 text-text-main outline-none transition-all"
placeholder="••••••••"
/>
</div>
<button
type="submit"
disabled={loading}
className="mt-4 bg-primary hover:bg-primary/90 text-white font-bold py-3 rounded-xl transition-all shadow-lg shadow-primary/20 disabled:opacity-70 disabled:cursor-not-allowed"
>
{loading ? "Entrando..." : "Entrar"}
</button>
</form>
</div>
<div className="mt-8 flex items-center gap-2 text-text-muted/60 text-xs font-medium">

View File

@@ -17,14 +17,13 @@ const api = axios.create({
},
});
// Store the current user's clerk ID and Organization ID/Name
let currentClerkUserId: string | null = null;
let currentToken: string | null = null;
let currentOrgId: string | null = null;
let currentOrgName: string | null = null;
// Function to set the clerk user ID (called from AuthContext)
export const setApiClerkUserId = (clerkId: string | null) => {
currentClerkUserId = clerkId;
// Function to set the JWT token
export const setApiToken = (token: string | null) => {
currentToken = token;
};
// Function to set the organization ID and Name (called from Layout/Context)
@@ -45,11 +44,10 @@ export const setApiOrganizationId = setApiOrgId;
api.interceptors.request.use(
(config) => {
console.log(`[API Request] ${config.method?.toUpperCase()} ${config.url}`, {
clerkId: currentClerkUserId,
orgId: currentOrgId
});
if (currentClerkUserId) {
config.headers['x-clerk-user-id'] = currentClerkUserId;
if (currentToken) {
config.headers['Authorization'] = `Bearer ${currentToken}`;
}
if (currentOrgId) {
config.headers['x-organization-id'] = currentOrgId;

View File

@@ -11,6 +11,7 @@ import yieldStudyRoutes from './routes/yieldStudyRoutes.js';
import userRoutes from './routes/userRoutes.js';
import systemSettingsRoutes from './routes/systemSettingsRoutes.js';
import geometryTypeRoutes from './routes/geometryTypeRoutes.js';
import authRoutes from './routes/authRoutes.js';
import stockRoutes from './routes/stockRoutes.js';
import notificationRoutes from './routes/notificationRoutes.js';
@@ -24,17 +25,19 @@ const app = express();
app.use(cors({
origin: '*', // Be more specific in production
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-clerk-user-id', 'x-organization-id']
allowedHeaders: ['Content-Type', 'Authorization', 'x-organization-id']
}));
app.use(express.json());
import { extractUser } from './middleware/roleMiddleware.js';
// LOG DE DEPURAÇÃO PARA CONEXÃO
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} - ClerkID: ${req.headers['x-clerk-user-id'] || 'None'}`);
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
import { authMiddleware } from './middleware/auth.js';
app.use(authMiddleware);
app.use(extractUser);
// Static Uploads
@@ -49,6 +52,7 @@ if (!fs.existsSync(uploadsPath)) {
app.use('/uploads', express.static(uploadsPath));
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
app.use('/api/projects', projectRoutes);
app.use('/api/parts', partRoutes);

View File

@@ -0,0 +1,99 @@
import { Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import User from '../models/User.js';
import { v4 as uuidv4 } from 'uuid';
const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key_change_in_prod';
export const register = async (req: Request, res: Response): Promise<void> => {
try {
const { name, email, password } = req.body;
if (!name || !email || !password) {
res.status(400).json({ error: 'Todos os campos são obrigatórios' });
return;
}
const existingUser = await User.findOne({ email });
if (existingUser) {
res.status(400).json({ error: 'Email já cadastrado' });
return;
}
const salt = await bcrypt.genSalt(10);
const passwordHash = await bcrypt.hash(password, salt);
// Gere um clerkId falso apenas para manter retrocompatibilidade no banco
const fakeClerkId = `user_${uuidv4().replace(/-/g, '')}`;
const newUser = new User({
name,
email,
passwordHash,
clerkId: fakeClerkId,
role: 'member',
isBanned: false
});
await newUser.save();
const token = jwt.sign(
{ userId: newUser._id.toString(), clerkId: newUser.clerkId, role: newUser.role, organizationId: newUser.organizationId },
JWT_SECRET,
{ expiresIn: '7d' }
);
res.status(201).json({
message: 'Usuário criado com sucesso',
token,
user: { id: newUser._id, name: newUser.name, email: newUser.email, role: newUser.role, clerkId: newUser.clerkId }
});
} catch (error) {
console.error('Register Error:', error);
res.status(500).json({ error: 'Erro no servidor' });
}
};
export const login = async (req: Request, res: Response): Promise<void> => {
try {
const { email, password } = req.body;
if (!email || !password) {
res.status(400).json({ error: 'Email e senha são obrigatórios' });
return;
}
const user = await User.findOne({ email });
if (!user) {
res.status(400).json({ error: 'Usuário não encontrado' });
return;
}
if (!user.passwordHash) {
res.status(400).json({ error: 'Usuário do sistema antigo. Por favor, solicite a redefinição de senha ou recrie sua conta se possível.' });
return;
}
const isMatch = await bcrypt.compare(password, user.passwordHash);
if (!isMatch) {
res.status(400).json({ error: 'Credenciais inválidas' });
return;
}
const token = jwt.sign(
{ userId: user._id.toString(), clerkId: user.clerkId, role: user.role, organizationId: user.organizationId },
JWT_SECRET,
{ expiresIn: '7d' }
);
res.status(200).json({
message: 'Login realizado com sucesso',
token,
user: { id: user._id, name: user.name, email: user.email, role: user.role, clerkId: user.clerkId }
});
} catch (error) {
console.error('Login Error:', error);
res.status(500).json({ error: 'Erro no servidor' });
}
};

View File

@@ -0,0 +1,26 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key_change_in_prod';
export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
// Se não houver token autêntico JWT, prossegue limpo
return next();
}
const token = authHeader.split(' ')[1];
const decoded = jwt.verify(token, JWT_SECRET) as any;
// Injeta o clerkId no header para que o extractUser (roleMiddleware)
// continue seu trabalho de carregar o usuário do banco instanciado e popular req.appUser
req.headers['x-clerk-user-id'] = decoded.clerkId;
next();
} catch (error) {
console.error('Auth Middleware Error:', error);
res.status(401).json({ error: 'Token inválido ou expirado' });
}
};

View File

@@ -8,6 +8,7 @@ export interface IUser extends Document {
name: string;
role: UserRole;
isBanned: boolean;
passwordHash?: string;
organizationId?: string;
createdAt: Date;
updatedAt: Date;
@@ -21,6 +22,10 @@ const UserSchema: Schema = new Schema({
unique: true,
index: true
},
passwordHash: {
type: String,
required: false
},
organizationId: {
type: String,
index: true

View File

@@ -0,0 +1,9 @@
import express from 'express';
import { login, register } from '../controllers/authController.js';
const router = express.Router();
router.post('/login', login);
router.post('/register', register);
export default router;