Implement secure user authentication with role-based access control

Integrates user authentication using bcrypt, session management with PostgreSQL, and role-based access control for enhanced security and user management.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 91318273-c764-4fd4-be04-bdc12c38af32
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0715008c-7244-40f4-befc-26c014852236/ab880d72-1a34-4d36-b5be-9f44df85ed1a.jpg
This commit is contained in:
michaelschal committed 2025-07-09 18:44:20 +00:00
1 parent 42b249e5b0
commit 5c01234fc4
16 files changed
+2384 -827

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
import { Pool, neonConfig } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-serverless';
import ws from "ws";
import * as schema from "@shared/schema";
neonConfig.webSocketConstructor = ws;
if (!process.env.DATABASE_URL) {
throw new Error(
"DATABASE_URL must be set. Did you forget to provision a database?",
);
}
export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle({ client: pool, schema });
+256 -6
View File
@@ -1,15 +1,265 @@
import type { Express } from "express";
import type { Express, Request, Response, NextFunction } from "express";
import { createServer, type Server } from "http";
import { storage } from "./storage";
import session from "express-session";
import connectPg from "connect-pg-simple";
import { loginSchema, insertUserSchema, insertStoreSchema, insertSaleSchema } from "@shared/schema";
import { z } from "zod";
declare module 'express-session' {
interface SessionData {
userId?: number;
}
}
// Authentication middleware
const requireAuth = (req: Request, res: Response, next: NextFunction) => {
if (!req.session?.userId) {
return res.status(401).json({ error: "Authentication required" });
}
next();
};
// Role-based authorization middleware
const requireRole = (roles: string[]) => {
return async (req: Request, res: Response, next: NextFunction) => {
if (!req.session?.userId) {
return res.status(401).json({ error: "Authentication required" });
}
const user = await storage.getUser(req.session.userId);
if (!user || !roles.includes(user.role)) {
return res.status(403).json({ error: "Insufficient permissions" });
}
req.user = user;
next();
};
};
declare global {
namespace Express {
interface Request {
user?: any;
}
}
}
export async function registerRoutes(app: Express): Promise<Server> {
// put application routes here
// prefix all routes with /api
// Session configuration with PostgreSQL store
const pgStore = connectPg(session);
app.use(session({
store: new pgStore({
conString: process.env.DATABASE_URL,
createTableIfMissing: false,
tableName: 'sessions',
}),
secret: process.env.SESSION_SECRET || 'fireworks-secret-key-2024',
resave: false,
saveUninitialized: false,
cookie: {
secure: false, // Set to true in production with HTTPS
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// use storage to perform CRUD operations on the storage interface
// e.g. storage.insertUser(user) or storage.getUserByUsername(username)
// Initialize default admin if needed
const initResult = await storage.initializeDefaults();
if (initResult.defaultAdminCredentials) {
console.log('\n=== DEFAULT ADMIN CREATED ===');
console.log('Username:', initResult.defaultAdminCredentials.username);
console.log('Password:', initResult.defaultAdminCredentials.password);
console.log('Please change this password after first login!');
console.log('==============================\n');
}
// Auth routes
app.post('/api/auth/login', async (req, res) => {
try {
const { username, password } = loginSchema.parse(req.body);
const user = await storage.authenticateUser(username, password);
if (!user) {
return res.status(401).json({ error: "Invalid credentials" });
}
req.session.userId = user.id;
res.json({ user: { ...user, password: undefined } });
} catch (error) {
console.error('Login error:', error);
if (error instanceof z.ZodError) {
return res.status(400).json({ error: "Invalid input", details: error.errors });
}
res.status(500).json({ error: "Login failed" });
}
});
app.post('/api/auth/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
console.error('Logout error:', err);
return res.status(500).json({ error: "Logout failed" });
}
res.json({ message: "Logged out successfully" });
});
});
app.get('/api/auth/me', requireAuth, async (req, res) => {
try {
const user = await storage.getUser(req.session.userId!);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
res.json({ ...user, password: undefined });
} catch (error) {
console.error('Get user error:', error);
res.status(500).json({ error: "Failed to get user info" });
}
});
// Sales routes
app.get('/api/sales', requireAuth, async (req, res) => {
try {
const user = await storage.getUser(req.session.userId!);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
const { startDate, endDate } = req.query;
const sales = await storage.getSalesByStore(
user.storeId!,
startDate as string,
endDate as string
);
res.json(sales);
} catch (error) {
console.error('Get sales error:', error);
res.status(500).json({ error: "Failed to get sales" });
}
});
app.post('/api/sales', requireAuth, async (req, res) => {
try {
const user = await storage.getUser(req.session.userId!);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
const saleData = insertSaleSchema.parse({
...req.body,
storeId: user.storeId,
userId: user.id
});
const sale = await storage.createSale(saleData);
res.status(201).json(sale);
} catch (error) {
console.error('Create sale error:', error);
if (error instanceof z.ZodError) {
return res.status(400).json({ error: "Invalid input", details: error.errors });
}
res.status(500).json({ error: "Failed to create sale" });
}
});
app.delete('/api/sales/:id', requireRole(['admin', 'manager']), async (req, res) => {
try {
const saleId = parseInt(req.params.id);
await storage.deleteSale(saleId);
res.json({ message: "Sale deleted successfully" });
} catch (error) {
console.error('Delete sale error:', error);
res.status(500).json({ error: "Failed to delete sale" });
}
});
// Admin routes - Users
app.get('/api/admin/users', requireRole(['admin']), async (req, res) => {
try {
const users = await storage.getAllUsers();
const safeUsers = users.map(user => ({ ...user, password: undefined }));
res.json(safeUsers);
} catch (error) {
console.error('Get users error:', error);
res.status(500).json({ error: "Failed to get users" });
}
});
app.post('/api/admin/users', requireRole(['admin']), async (req, res) => {
try {
const userData = insertUserSchema.parse(req.body);
const user = await storage.createUser(userData);
res.status(201).json({ ...user, password: undefined });
} catch (error) {
console.error('Create user error:', error);
if (error instanceof z.ZodError) {
return res.status(400).json({ error: "Invalid input", details: error.errors });
}
res.status(500).json({ error: "Failed to create user" });
}
});
app.put('/api/admin/users/:id', requireRole(['admin']), async (req, res) => {
try {
const userId = parseInt(req.params.id);
const userData = insertUserSchema.partial().parse(req.body);
const user = await storage.updateUser(userId, userData);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
res.json({ ...user, password: undefined });
} catch (error) {
console.error('Update user error:', error);
if (error instanceof z.ZodError) {
return res.status(400).json({ error: "Invalid input", details: error.errors });
}
res.status(500).json({ error: "Failed to update user" });
}
});
// Admin routes - Stores
app.get('/api/admin/stores', requireRole(['admin']), async (req, res) => {
try {
const stores = await storage.getAllStores();
res.json(stores);
} catch (error) {
console.error('Get stores error:', error);
res.status(500).json({ error: "Failed to get stores" });
}
});
app.post('/api/admin/stores', requireRole(['admin']), async (req, res) => {
try {
const storeData = insertStoreSchema.parse(req.body);
const store = await storage.createStore(storeData);
res.status(201).json(store);
} catch (error) {
console.error('Create store error:', error);
if (error instanceof z.ZodError) {
return res.status(400).json({ error: "Invalid input", details: error.errors });
}
res.status(500).json({ error: "Failed to create store" });
}
});
app.put('/api/admin/stores/:id', requireRole(['admin']), async (req, res) => {
try {
const storeId = parseInt(req.params.id);
const storeData = insertStoreSchema.partial().parse(req.body);
const store = await storage.updateStore(storeId, storeData);
if (!store) {
return res.status(404).json({ error: "Store not found" });
}
res.json(store);
} catch (error) {
console.error('Update store error:', error);
if (error instanceof z.ZodError) {
return res.status(400).json({ error: "Invalid input", details: error.errors });
}
res.status(500).json({ error: "Failed to update store" });
}
});
const httpServer = createServer(app);
return httpServer;
}
+181 -22
View File
@@ -1,39 +1,198 @@
import { users, type User, type InsertUser } from "@shared/schema";
// modify the interface with any CRUD methods
// you might need
import {
users,
stores,
sales,
type User,
type InsertUser,
type Store,
type InsertStore,
type Sale,
type InsertSale,
} from "@shared/schema";
import { db } from "./db";
import { eq, and, gte, lte, desc } from "drizzle-orm";
import bcrypt from "bcryptjs";
export interface IStorage {
// User operations
getUser(id: number): Promise<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>;
createUser(user: InsertUser): Promise<User>;
updateUser(id: number, user: Partial<InsertUser>): Promise<User | undefined>;
getAllUsers(): Promise<User[]>;
// Store operations
getStore(id: number): Promise<Store | undefined>;
createStore(store: InsertStore): Promise<Store>;
getAllStores(): Promise<Store[]>;
updateStore(id: number, store: Partial<InsertStore>): Promise<Store | undefined>;
// Sale operations
getSale(id: number): Promise<Sale | undefined>;
createSale(sale: InsertSale): Promise<Sale>;
getSalesByStore(storeId: number, startDate?: string, endDate?: string): Promise<Sale[]>;
deleteSale(id: number): Promise<void>;
// Auth operations
authenticateUser(username: string, password: string): Promise<User | null>;
hashPassword(password: string): Promise<string>;
initializeDefaults(): Promise<{ defaultAdminCredentials?: { username: string; password: string } }>;
}
export class MemStorage implements IStorage {
private users: Map<number, User>;
currentId: number;
constructor() {
this.users = new Map();
this.currentId = 1;
}
export class DatabaseStorage implements IStorage {
// User operations
async getUser(id: number): Promise<User | undefined> {
return this.users.get(id);
const [user] = await db.select().from(users).where(eq(users.id, id));
return user;
}
async getUserByUsername(username: string): Promise<User | undefined> {
return Array.from(this.users.values()).find(
(user) => user.username === username,
);
const [user] = await db.select().from(users).where(eq(users.username, username));
return user;
}
async createUser(insertUser: InsertUser): Promise<User> {
const id = this.currentId++;
const user: User = { ...insertUser, id };
this.users.set(id, user);
async createUser(userData: InsertUser): Promise<User> {
// Hash password if provided
if (userData.password) {
userData.password = await this.hashPassword(userData.password);
}
const [user] = await db
.insert(users)
.values(userData)
.returning();
return user;
}
async updateUser(id: number, userData: Partial<InsertUser>): Promise<User | undefined> {
// Hash password if provided
if (userData.password) {
userData.password = await this.hashPassword(userData.password);
}
const [user] = await db
.update(users)
.set(userData)
.where(eq(users.id, id))
.returning();
return user;
}
async getAllUsers(): Promise<User[]> {
return await db.select().from(users).orderBy(desc(users.createdAt));
}
// Store operations
async getStore(id: number): Promise<Store | undefined> {
const [store] = await db.select().from(stores).where(eq(stores.id, id));
return store;
}
async createStore(storeData: InsertStore): Promise<Store> {
const [store] = await db
.insert(stores)
.values(storeData)
.returning();
return store;
}
async getAllStores(): Promise<Store[]> {
return await db.select().from(stores).orderBy(desc(stores.createdAt));
}
async updateStore(id: number, storeData: Partial<InsertStore>): Promise<Store | undefined> {
const [store] = await db
.update(stores)
.set(storeData)
.where(eq(stores.id, id))
.returning();
return store;
}
// Sale operations
async getSale(id: number): Promise<Sale | undefined> {
const [sale] = await db.select().from(sales).where(eq(sales.id, id));
return sale;
}
async createSale(saleData: InsertSale): Promise<Sale> {
const [sale] = await db
.insert(sales)
.values(saleData)
.returning();
return sale;
}
async getSalesByStore(storeId: number, startDate?: string, endDate?: string): Promise<Sale[]> {
let query = db.select().from(sales).where(eq(sales.storeId, storeId));
if (startDate && endDate) {
query = db.select().from(sales).where(
and(
eq(sales.storeId, storeId),
gte(sales.dateVente, startDate),
lte(sales.dateVente, endDate)
)
);
}
return await query.orderBy(desc(sales.timestamp));
}
async deleteSale(id: number): Promise<void> {
await db.delete(sales).where(eq(sales.id, id));
}
// Auth operations
async authenticateUser(username: string, password: string): Promise<User | null> {
const user = await this.getUserByUsername(username);
if (!user || !user.isActive) {
return null;
}
const isValid = await bcrypt.compare(password, user.password);
return isValid ? user : null;
}
async hashPassword(password: string): Promise<string> {
return await bcrypt.hash(password, 12);
}
// Initialize default admin user and store if none exist
async initializeDefaults(): Promise<{ defaultAdminCredentials?: { username: string; password: string } }> {
const existingUsers = await this.getAllUsers();
if (existingUsers.length === 0) {
// Create default store
const defaultStore = await this.createStore({
name: "Magasin Principal",
address: "À définir",
isActive: true,
});
// Create default admin user
const defaultPassword = "admin123";
const adminUser = await this.createUser({
username: "admin",
password: defaultPassword,
email: "admin@example.com",
firstName: "Administrateur",
lastName: "Principal",
role: "admin",
storeId: defaultStore.id,
isActive: true,
});
return {
defaultAdminCredentials: {
username: "admin",
password: defaultPassword
}
};
}
return {};
}
}
export const storage = new MemStorage();
export const storage = new DatabaseStorage();