mirror of
https://github.com/R0m1k3/Regisflow.git
synced 2026-10-17 04:08:52 +02:00
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:
1 parent
42b249e5b0
commit
5c01234fc4
16 files changed
+2384
-827
No files matched your search
@@ -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
@@ -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
@@ -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();
|
||||
Reference in new issue
Block a user