security: correcoes semana 1 - session secret, CORS, filtro tenant, protecao /api/tenants
- Session secret com randomBytes(32), obrigatorio em producao - CORS whitelist configurado (origens permitidas) - getUserByUsername com validacao opcional de tenantId - /api/tenants restrito: admin ve todos, user ve so seu tenant - Metodo getTenant(id) adicionado a interface IStorage
This commit is contained in:
parent
4fb3f87dc3
commit
a2aff30b8c
|
|
@ -30,12 +30,14 @@ async function comparePasswords(supplied: string, stored: string) {
|
||||||
return timingSafeEqual(hashedBuf, suppliedBuf);
|
return timingSafeEqual(hashedBuf, suppliedBuf);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.env.SESSION_SECRET) {
|
// CORREÇÃO: Session secret seguro - obrigatório em produção
|
||||||
console.warn("[auth] WARNING: SESSION_SECRET env var not set. Using insecure fallback. Set SESSION_SECRET in production.");
|
const SESSION_SECRET = process.env.SESSION_SECRET ||
|
||||||
}
|
(process.env.NODE_ENV === 'production'
|
||||||
|
? (() => { throw new Error('SESSION_SECRET obrigatório em produção'); })()
|
||||||
|
: randomBytes(32).toString('hex'));
|
||||||
|
|
||||||
const sessionSettings: session.SessionOptions = {
|
const sessionSettings: session.SessionOptions = {
|
||||||
secret: process.env.SESSION_SECRET || `arcadia-dev-${Math.random().toString(36)}`,
|
secret: SESSION_SECRET,
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
store: storage.sessionStore,
|
store: storage.sessionStore,
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,26 @@ import ws from "ws";
|
||||||
neonConfig.webSocketConstructor = ws;
|
neonConfig.webSocketConstructor = ws;
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 8006;
|
const PORT = process.env.PORT || 8006;
|
||||||
|
|
||||||
app.use(cors());
|
// CORREÇÃO: CORS restrito - whitelist de origens permitidas
|
||||||
|
const allowedOrigins = [
|
||||||
|
'http://localhost:5000',
|
||||||
|
'https://localhost:5000',
|
||||||
|
process.env.FRONTEND_URL,
|
||||||
|
process.env.DOMAIN ? `https://${process.env.DOMAIN}` : null,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
app.use(cors({
|
||||||
|
origin: (origin, callback) => {
|
||||||
|
if (!origin || allowedOrigins.includes(origin)) {
|
||||||
|
callback(null, true);
|
||||||
|
} else {
|
||||||
|
callback(new Error('CORS não permitido'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
credentials: true
|
||||||
|
}));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||||
|
|
|
||||||
|
|
@ -182,11 +182,22 @@ export async function registerRoutes(
|
||||||
// Arcádia Plus - SSO routes (proxy already registered at top)
|
// Arcádia Plus - SSO routes (proxy already registered at top)
|
||||||
app.use("/api/plus/sso", plusSsoRoutes);
|
app.use("/api/plus/sso", plusSsoRoutes);
|
||||||
|
|
||||||
|
// CORREÇÃO: /api/tenants protegido - apenas admin vê todos
|
||||||
app.get("/api/tenants", async (req: any, res) => {
|
app.get("/api/tenants", async (req: any, res) => {
|
||||||
if (!req.isAuthenticated()) {
|
if (!req.isAuthenticated()) {
|
||||||
return res.status(401).json({ error: "Authentication required" });
|
return res.status(401).json({ error: "Authentication required" });
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
// Se não for admin, retornar apenas o tenant do usuário
|
||||||
|
if (req.user?.role !== 'admin') {
|
||||||
|
if (req.user?.tenantId) {
|
||||||
|
const tenant = await storage.getTenant(req.user.tenantId);
|
||||||
|
return res.json(tenant ? [tenant] : []);
|
||||||
|
}
|
||||||
|
return res.status(403).json({ error: "Access denied" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin vê todos
|
||||||
const tenants = await storage.getTenants();
|
const tenants = await storage.getTenants();
|
||||||
res.json(tenants);
|
res.json(tenants);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export interface IStorage {
|
||||||
sessionStore: session.Store;
|
sessionStore: session.Store;
|
||||||
|
|
||||||
getUser(id: string): Promise<User | undefined>;
|
getUser(id: string): Promise<User | undefined>;
|
||||||
getUserByUsername(username: string): Promise<User | undefined>;
|
getUserByUsername(username: string, tenantId?: number): Promise<User | undefined>;
|
||||||
createUser(user: InsertUser): Promise<User>;
|
createUser(user: InsertUser): Promise<User>;
|
||||||
getEnrichedUser(user: User): Promise<any>;
|
getEnrichedUser(user: User): Promise<any>;
|
||||||
|
|
||||||
|
|
@ -28,6 +28,7 @@ export interface IStorage {
|
||||||
getUserApplications(userId: string): Promise<Application[]>;
|
getUserApplications(userId: string): Promise<Application[]>;
|
||||||
assignApplicationToUser(userId: string, applicationId: string): Promise<void>;
|
assignApplicationToUser(userId: string, applicationId: string): Promise<void>;
|
||||||
removeApplicationFromUser(userId: string, applicationId: string): Promise<void>;
|
removeApplicationFromUser(userId: string, applicationId: string): Promise<void>;
|
||||||
|
getTenant(id: number): Promise<{ id: number; name: string; slug: string; tenantType?: string; plan?: string; status?: string } | undefined>;
|
||||||
getTenants(): Promise<{ id: number; name: string; slug: string }[]>;
|
getTenants(): Promise<{ id: number; name: string; slug: string }[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,8 +47,21 @@ export class DatabaseStorage implements IStorage {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserByUsername(username: string): Promise<User | undefined> {
|
// CORREÇÃO: getUserByUsername com validação de tenant
|
||||||
|
async getUserByUsername(username: string, tenantId?: number): Promise<User | undefined> {
|
||||||
const [user] = await db.select().from(users).where(eq(users.username, username));
|
const [user] = await db.select().from(users).where(eq(users.username, username));
|
||||||
|
|
||||||
|
// Se tenantId foi passado, validar que usuário pertence a esse tenant
|
||||||
|
if (tenantId && user) {
|
||||||
|
const [tenantUser] = await db.select()
|
||||||
|
.from(tenantUsers)
|
||||||
|
.where(and(
|
||||||
|
eq(tenantUsers.userId, user.id),
|
||||||
|
eq(tenantUsers.tenantId, tenantId)
|
||||||
|
));
|
||||||
|
if (!tenantUser) return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,6 +160,19 @@ export class DatabaseStorage implements IStorage {
|
||||||
return enriched;
|
return enriched;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CORREÇÃO: getTenant para buscar tenant específico
|
||||||
|
async getTenant(id: number): Promise<{ id: number; name: string; slug: string; tenantType?: string; plan?: string; status?: string } | undefined> {
|
||||||
|
const [tenant] = await db.select({
|
||||||
|
id: tenants.id,
|
||||||
|
name: tenants.name,
|
||||||
|
slug: tenants.slug,
|
||||||
|
tenantType: tenants.tenantType,
|
||||||
|
plan: tenants.plan,
|
||||||
|
status: tenants.status
|
||||||
|
}).from(tenants).where(eq(tenants.id, id));
|
||||||
|
return tenant;
|
||||||
|
}
|
||||||
|
|
||||||
async getTenants(): Promise<{ id: number; name: string; slug: string }[]> {
|
async getTenants(): Promise<{ id: number; name: string; slug: string }[]> {
|
||||||
const result = await db.select({
|
const result = await db.select({
|
||||||
id: tenants.id,
|
id: tenants.id,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue