mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
feat(auth): implement user authentication and password change functionality
This commit is contained in:
@@ -23,7 +23,7 @@ export const loadSettings = (): McpSettings => {
|
||||
return JSON.parse(settingsData);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load settings from ${settingsPath}:`, error);
|
||||
return { mcpServers: {} };
|
||||
return { mcpServers: {}, users: [] };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
179
src/controllers/authController.ts
Normal file
179
src/controllers/authController.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { Request, Response } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { validationResult } from 'express-validator';
|
||||
import { findUserByUsername, verifyPassword, createUser, updateUserPassword } from '../models/User.js';
|
||||
|
||||
// Default secret key - in production, use an environment variable
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-this';
|
||||
const TOKEN_EXPIRY = '24h';
|
||||
|
||||
// Login user
|
||||
export const login = async (req: Request, res: Response): Promise<void> => {
|
||||
// Validate request
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ success: false, errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
const { username, password } = req.body;
|
||||
|
||||
try {
|
||||
// Find user by username
|
||||
const user = findUserByUsername(username);
|
||||
|
||||
if (!user) {
|
||||
res.status(401).json({ success: false, message: 'Invalid credentials' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isPasswordValid = await verifyPassword(password, user.password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
res.status(401).json({ success: false, message: 'Invalid credentials' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const payload = {
|
||||
user: {
|
||||
username: user.username,
|
||||
isAdmin: user.isAdmin || false
|
||||
}
|
||||
};
|
||||
|
||||
jwt.sign(
|
||||
payload,
|
||||
JWT_SECRET,
|
||||
{ expiresIn: TOKEN_EXPIRY },
|
||||
(err, token) => {
|
||||
if (err) throw err;
|
||||
res.json({
|
||||
success: true,
|
||||
token,
|
||||
user: {
|
||||
username: user.username,
|
||||
isAdmin: user.isAdmin
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({ success: false, message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Register new user
|
||||
export const register = async (req: Request, res: Response): Promise<void> => {
|
||||
// Validate request
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ success: false, errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
const { username, password, isAdmin } = req.body;
|
||||
|
||||
try {
|
||||
// Create new user
|
||||
const newUser = await createUser({ username, password, isAdmin });
|
||||
|
||||
if (!newUser) {
|
||||
res.status(400).json({ success: false, message: 'User already exists' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const payload = {
|
||||
user: {
|
||||
username: newUser.username,
|
||||
isAdmin: newUser.isAdmin || false
|
||||
}
|
||||
};
|
||||
|
||||
jwt.sign(
|
||||
payload,
|
||||
JWT_SECRET,
|
||||
{ expiresIn: TOKEN_EXPIRY },
|
||||
(err, token) => {
|
||||
if (err) throw err;
|
||||
res.json({
|
||||
success: true,
|
||||
token,
|
||||
user: {
|
||||
username: newUser.username,
|
||||
isAdmin: newUser.isAdmin
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
res.status(500).json({ success: false, message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Get current user
|
||||
export const getCurrentUser = (req: Request, res: Response): void => {
|
||||
try {
|
||||
// User is already attached to request by auth middleware
|
||||
const user = (req as any).user;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: {
|
||||
username: user.username,
|
||||
isAdmin: user.isAdmin
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get current user error:', error);
|
||||
res.status(500).json({ success: false, message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Change password
|
||||
export const changePassword = async (req: Request, res: Response): Promise<void> => {
|
||||
// Validate request
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ success: false, errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
const username = (req as any).user.username;
|
||||
|
||||
try {
|
||||
// Find user by username
|
||||
const user = findUserByUsername(username);
|
||||
|
||||
if (!user) {
|
||||
res.status(404).json({ success: false, message: 'User not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
const isPasswordValid = await verifyPassword(currentPassword, user.password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
res.status(401).json({ success: false, message: 'Current password is incorrect' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the password
|
||||
const updated = await updateUserPassword(username, newPassword);
|
||||
|
||||
if (!updated) {
|
||||
res.status(500).json({ success: false, message: 'Failed to update password' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Password updated successfully' });
|
||||
} catch (error) {
|
||||
console.error('Change password error:', error);
|
||||
res.status(500).json({ success: false, message: 'Server error' });
|
||||
}
|
||||
};
|
||||
28
src/middlewares/auth.ts
Normal file
28
src/middlewares/auth.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
// Default secret key - in production, use an environment variable
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-this';
|
||||
|
||||
// Middleware to authenticate JWT token
|
||||
export const auth = (req: Request, res: Response, next: NextFunction): void => {
|
||||
// Get token from header
|
||||
const token = req.header('x-auth-token');
|
||||
|
||||
// Check if no token
|
||||
if (!token) {
|
||||
res.status(401).json({ success: false, message: 'No token, authorization denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify token
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
|
||||
// Add user from payload to request
|
||||
(req as any).user = (decoded as any).user;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ success: false, message: 'Token is not valid' });
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import path from 'path';
|
||||
import { auth } from './auth.js';
|
||||
import { initializeDefaultUser } from '../models/User.js';
|
||||
|
||||
export const errorHandler = (
|
||||
err: Error,
|
||||
@@ -25,6 +27,14 @@ export const initMiddlewares = (app: express.Application): void => {
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize default admin user if no users exist
|
||||
initializeDefaultUser().catch(err => {
|
||||
console.error('Error initializing default user:', err);
|
||||
});
|
||||
|
||||
// Protect all API routes with authentication middleware
|
||||
app.use('/api', auth);
|
||||
|
||||
app.get('/', (_req: Request, res: Response) => {
|
||||
res.sendFile(path.join(process.cwd(), 'frontend', 'dist', 'index.html'));
|
||||
});
|
||||
|
||||
103
src/models/User.ts
Normal file
103
src/models/User.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { IUser, McpSettings } from '../types/index.js';
|
||||
import { loadSettings, saveSettings } from '../config/index.js';
|
||||
|
||||
// Get all users
|
||||
export const getUsers = (): IUser[] => {
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
return settings.users || [];
|
||||
} catch (error) {
|
||||
console.error('Error reading users from settings:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Save users to settings
|
||||
const saveUsers = (users: IUser[]): void => {
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
settings.users = users;
|
||||
saveSettings(settings);
|
||||
} catch (error) {
|
||||
console.error('Error saving users to settings:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new user
|
||||
export const createUser = async (userData: IUser): Promise<IUser | null> => {
|
||||
const users = getUsers();
|
||||
|
||||
// Check if username already exists
|
||||
if (users.some(user => user.username === userData.username)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Hash the password
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(userData.password, salt);
|
||||
|
||||
const newUser = {
|
||||
username: userData.username,
|
||||
password: hashedPassword,
|
||||
isAdmin: userData.isAdmin || false
|
||||
};
|
||||
|
||||
users.push(newUser);
|
||||
saveUsers(users);
|
||||
|
||||
return newUser;
|
||||
};
|
||||
|
||||
// Find user by username
|
||||
export const findUserByUsername = (username: string): IUser | undefined => {
|
||||
const users = getUsers();
|
||||
return users.find(user => user.username === username);
|
||||
};
|
||||
|
||||
// Verify user password
|
||||
export const verifyPassword = async (
|
||||
plainPassword: string,
|
||||
hashedPassword: string
|
||||
): Promise<boolean> => {
|
||||
return await bcrypt.compare(plainPassword, hashedPassword);
|
||||
};
|
||||
|
||||
// Update user password
|
||||
export const updateUserPassword = async (
|
||||
username: string,
|
||||
newPassword: string
|
||||
): Promise<boolean> => {
|
||||
const users = getUsers();
|
||||
const userIndex = users.findIndex(user => user.username === username);
|
||||
|
||||
if (userIndex === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hash the new password
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(newPassword, salt);
|
||||
|
||||
// Update the user's password
|
||||
users[userIndex].password = hashedPassword;
|
||||
saveUsers(users);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Initialize with default admin user if no users exist
|
||||
export const initializeDefaultUser = async (): Promise<void> => {
|
||||
const users = getUsers();
|
||||
|
||||
if (users.length === 0) {
|
||||
await createUser({
|
||||
username: 'admin',
|
||||
password: 'admin123',
|
||||
isAdmin: true
|
||||
});
|
||||
console.log('Default admin user created');
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import express from 'express';
|
||||
import { check } from 'express-validator';
|
||||
import {
|
||||
getAllServers,
|
||||
getAllSettings,
|
||||
@@ -6,15 +7,43 @@ import {
|
||||
updateServer,
|
||||
deleteServer,
|
||||
} from '../controllers/serverController.js';
|
||||
import {
|
||||
login,
|
||||
register,
|
||||
getCurrentUser,
|
||||
changePassword
|
||||
} from '../controllers/authController.js';
|
||||
import { auth } from '../middlewares/auth.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
export const initRoutes = (app: express.Application): void => {
|
||||
// API routes protected by auth middleware in middlewares/index.ts
|
||||
router.get('/servers', getAllServers);
|
||||
router.get('/settings', getAllSettings);
|
||||
router.post('/servers', createServer);
|
||||
router.put('/servers/:name', updateServer);
|
||||
router.delete('/servers/:name', deleteServer);
|
||||
|
||||
// Auth routes (these will NOT be protected by auth middleware)
|
||||
app.post('/auth/login', [
|
||||
check('username', 'Username is required').not().isEmpty(),
|
||||
check('password', 'Password is required').not().isEmpty(),
|
||||
], login);
|
||||
|
||||
app.post('/auth/register', [
|
||||
check('username', 'Username is required').not().isEmpty(),
|
||||
check('password', 'Password must be at least 6 characters').isLength({ min: 6 }),
|
||||
], register);
|
||||
|
||||
app.get('/auth/user', auth, getCurrentUser);
|
||||
|
||||
// Add change password route
|
||||
app.post('/auth/change-password', [
|
||||
auth,
|
||||
check('currentPassword', 'Current password is required').not().isEmpty(),
|
||||
check('newPassword', 'New password must be at least 6 characters').isLength({ min: 6 }),
|
||||
], changePassword);
|
||||
|
||||
app.use('/api', router);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,8 @@ import { initMcpServer, registerAllTools } from './services/mcpService.js';
|
||||
import { initMiddlewares } from './middlewares/index.js';
|
||||
import { initRoutes } from './routes/index.js';
|
||||
import { handleSseConnection, handleSseMessage } from './services/sseService.js';
|
||||
import { migrateUserData } from './utils/migration.js';
|
||||
import { initializeDefaultUser } from './models/User.js';
|
||||
|
||||
export class AppServer {
|
||||
private app: express.Application;
|
||||
@@ -16,6 +18,12 @@ export class AppServer {
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
try {
|
||||
// Migrate user data from users.json to mcp_settings.json if needed
|
||||
migrateUserData();
|
||||
|
||||
// Initialize default admin user if no users exist
|
||||
await initializeDefaultUser();
|
||||
|
||||
const mcpServer = await initMcpServer(config.mcpHubName, config.mcpHubVersion);
|
||||
await registerAllTools(mcpServer, true);
|
||||
initMiddlewares(this.app);
|
||||
|
||||
@@ -2,8 +2,16 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
|
||||
// User interface
|
||||
export interface IUser {
|
||||
username: string;
|
||||
password: string;
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
// Represents the settings for MCP servers
|
||||
export interface McpSettings {
|
||||
users?: IUser[]; // Array of user credentials and permissions
|
||||
mcpServers: {
|
||||
[key: string]: ServerConfig; // Key-value pairs of server names and their configurations
|
||||
};
|
||||
|
||||
52
src/utils/migration.ts
Normal file
52
src/utils/migration.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// filepath: /Users/sunmeng/code/github/mcphub/src/utils/migration.ts
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { loadSettings, saveSettings } from '../config/index.js';
|
||||
import { IUser } from '../types/index.js';
|
||||
|
||||
/**
|
||||
* Migrates user data from the old users.json file to mcp_settings.json
|
||||
* This is a one-time migration to support the refactoring from separate
|
||||
* users.json to integrated user data in mcp_settings.json
|
||||
*/
|
||||
export const migrateUserData = (): void => {
|
||||
const oldUsersFilePath = path.join(process.cwd(), 'data', 'users.json');
|
||||
|
||||
// Check if the old users file exists
|
||||
if (fs.existsSync(oldUsersFilePath)) {
|
||||
try {
|
||||
// Read users from the old file
|
||||
const usersData = fs.readFileSync(oldUsersFilePath, 'utf8');
|
||||
const users = JSON.parse(usersData) as IUser[];
|
||||
|
||||
if (users && Array.isArray(users) && users.length > 0) {
|
||||
console.log(`Migrating ${users.length} users from users.json to mcp_settings.json`);
|
||||
|
||||
// Load current settings
|
||||
const settings = loadSettings();
|
||||
|
||||
// Merge users, giving priority to existing settings users
|
||||
const existingUsernames = new Set((settings.users || []).map(u => u.username));
|
||||
const newUsers = users.filter(u => !existingUsernames.has(u.username));
|
||||
|
||||
settings.users = [...(settings.users || []), ...newUsers];
|
||||
|
||||
// Save updated settings
|
||||
if (saveSettings(settings)) {
|
||||
console.log('User data migration completed successfully');
|
||||
|
||||
// Rename the old file as backup
|
||||
const backupPath = `${oldUsersFilePath}.bak.${Date.now()}`;
|
||||
fs.renameSync(oldUsersFilePath, backupPath);
|
||||
console.log(`Renamed old users file to ${backupPath}`);
|
||||
}
|
||||
} else {
|
||||
console.log('No users found in users.json, skipping migration');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during user data migration:', error);
|
||||
}
|
||||
} else {
|
||||
console.log('users.json not found, no migration needed');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user