Initial checkpoint

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90
This commit is contained in:
alphaeusmote
2025-04-08 01:35:07 +00:00
parent bfb75c0e91
commit e871381332
75 changed files with 16269 additions and 0 deletions
+936
View File
@@ -0,0 +1,936 @@
import { Router } from 'express';
import { storage } from './storage';
import { ldapClient } from './ldap';
import { randomBytes } from 'crypto';
import { z } from 'zod';
import { insertUserSchema, insertApiTokenSchema, insertLdapConnectionSchema } from '@shared/schema';
export function registerAPIRoutes(router: Router, verifyApiToken: any) {
/**
* @swagger
* /users:
* get:
* summary: Get all users
* description: Retrieve a list of all users
* tags: [Users]
* parameters:
* - in: query
* name: properties
* schema:
* type: string
* description: Comma-separated list of properties to return
* responses:
* 200:
* description: A list of users
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/User'
*/
router.get('/users', verifyApiToken, async (req, res) => {
try {
// Extract properties filter
const propertiesParam = req.query.properties as string;
const properties = propertiesParam ? propertiesParam.split(',') : null;
const users = await storage.listUsers();
// Filter out password field
const safeUsers = users.map(user => {
const { password, ...userWithoutPassword } = user;
return userWithoutPassword;
});
// Apply property selection if specified
if (properties) {
const filteredUsers = safeUsers.map(user => {
const filteredUser: Record<string, any> = {};
properties.forEach(prop => {
if (user.hasOwnProperty(prop)) {
filteredUser[prop] = user[prop as keyof typeof user];
}
});
return filteredUser;
});
return res.json(filteredUsers);
}
res.json(safeUsers);
} catch (error) {
res.status(500).json({ message: error instanceof Error ? error.message : 'An error occurred' });
}
});
/**
* @swagger
* /users/{id}:
* get:
* summary: Get a user by ID
* description: Retrieve a single user by ID
* tags: [Users]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: User ID
* - in: query
* name: properties
* schema:
* type: string
* description: Comma-separated list of properties to return
* responses:
* 200:
* description: User found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/User'
* 404:
* description: User not found
*/
router.get('/users/:id', verifyApiToken, async (req, res) => {
try {
const userId = parseInt(req.params.id);
if (isNaN(userId)) {
return res.status(400).json({ message: 'Invalid user ID' });
}
// Extract properties filter
const propertiesParam = req.query.properties as string;
const properties = propertiesParam ? propertiesParam.split(',') : null;
const user = await storage.getUser(userId);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
// Filter out password
const { password, ...userWithoutPassword } = user;
// Apply property selection if specified
if (properties) {
const filteredUser: Record<string, any> = {};
properties.forEach(prop => {
if (userWithoutPassword.hasOwnProperty(prop)) {
filteredUser[prop] = userWithoutPassword[prop as keyof typeof userWithoutPassword];
}
});
return res.json(filteredUser);
}
res.json(userWithoutPassword);
} catch (error) {
res.status(500).json({ message: error instanceof Error ? error.message : 'An error occurred' });
}
});
// API Tokens Routes
/**
* @swagger
* /api-tokens:
* get:
* summary: Get all API tokens
* description: Retrieve a list of all API tokens for the authenticated user
* tags: [API Tokens]
* responses:
* 200:
* description: A list of API tokens
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/ApiToken'
*/
router.get('/api-tokens', async (req, res) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ message: 'Not authenticated' });
}
const userId = (req.user as any).id;
const tokens = await storage.listApiTokens(userId);
res.json(tokens);
} catch (error) {
res.status(500).json({ message: error instanceof Error ? error.message : 'An error occurred' });
}
});
/**
* @swagger
* /api-tokens:
* post:
* summary: Create a new API token
* description: Generate a new API token for the authenticated user
* tags: [API Tokens]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* expiresAt:
* type: string
* format: date-time
* responses:
* 201:
* description: Token created successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiToken'
*/
router.post('/api-tokens', async (req, res) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ message: 'Not authenticated' });
}
const schema = insertApiTokenSchema.extend({
name: z.string().min(1, 'Name is required'),
expiresAt: z.string().optional().transform(val => val ? new Date(val) : undefined)
});
const validateResult = schema.safeParse(req.body);
if (!validateResult.success) {
return res.status(400).json({ message: 'Invalid data', errors: validateResult.error.errors });
}
const { name, expiresAt } = validateResult.data;
const userId = (req.user as any).id;
// Generate a secure token
const token = randomBytes(32).toString('hex');
const apiToken = await storage.createApiToken({
name,
token,
userId,
expiresAt
});
// Log this activity
await storage.createActivityLog({
action: 'Create',
resource: name,
resourceType: 'API Token',
userId,
username: (req.user as any).username,
status: 'Success',
details: { id: apiToken.id }
});
res.status(201).json(apiToken);
} catch (error) {
res.status(500).json({ message: error instanceof Error ? error.message : 'An error occurred' });
}
});
/**
* @swagger
* /api-tokens/{id}:
* delete:
* summary: Delete an API token
* description: Revoke and delete an API token
* tags: [API Tokens]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Token ID
* responses:
* 204:
* description: Token deleted successfully
* 404:
* description: Token not found
*/
router.delete('/api-tokens/:id', async (req, res) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ message: 'Not authenticated' });
}
const tokenId = parseInt(req.params.id);
if (isNaN(tokenId)) {
return res.status(400).json({ message: 'Invalid token ID' });
}
const token = await storage.getApiToken(tokenId);
if (!token) {
return res.status(404).json({ message: 'Token not found' });
}
// Only allow users to delete their own tokens
if (token.userId !== (req.user as any).id) {
return res.status(403).json({ message: 'Not authorized to delete this token' });
}
await storage.deleteApiToken(tokenId);
// Log this activity
await storage.createActivityLog({
action: 'Delete',
resource: token.name,
resourceType: 'API Token',
userId: (req.user as any).id,
username: (req.user as any).username,
status: 'Success',
details: { id: tokenId }
});
res.status(204).send();
} catch (error) {
res.status(500).json({ message: error instanceof Error ? error.message : 'An error occurred' });
}
});
// LDAP Connections Routes
/**
* @swagger
* /ldap-connections:
* get:
* summary: Get all LDAP connections
* description: Retrieve a list of all LDAP connections
* tags: [LDAP Connections]
* responses:
* 200:
* description: A list of LDAP connections
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/LdapConnection'
*/
router.get('/ldap-connections', async (req, res) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ message: 'Not authenticated' });
}
const connections = await storage.listLdapConnections();
// Don't send passwords back
const safeConnections = connections.map(connection => {
const { password, ...connectionWithoutPassword } = connection;
return connectionWithoutPassword;
});
res.json(safeConnections);
} catch (error) {
res.status(500).json({ message: error instanceof Error ? error.message : 'An error occurred' });
}
});
/**
* @swagger
* /ldap-connections:
* post:
* summary: Create a new LDAP connection
* description: Add a new LDAP connection
* tags: [LDAP Connections]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* server:
* type: string
* port:
* type: integer
* authType:
* type: string
* username:
* type: string
* password:
* type: string
* baseDN:
* type: string
* useTLS:
* type: boolean
* responses:
* 201:
* description: Connection created successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/LdapConnection'
*/
router.post('/ldap-connections', async (req, res) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ message: 'Not authenticated' });
}
const schema = insertLdapConnectionSchema.extend({
name: z.string().min(1, 'Name is required'),
server: z.string().min(1, 'Server is required'),
port: z.number().int().positive('Port must be a positive integer'),
authType: z.string().min(1, 'Authentication type is required'),
username: z.string().min(1, 'Username is required'),
password: z.string().min(1, 'Password is required'),
});
const validateResult = schema.safeParse(req.body);
if (!validateResult.success) {
return res.status(400).json({ message: 'Invalid data', errors: validateResult.error.errors });
}
const connection = await storage.createLdapConnection(validateResult.data);
// Log this activity
await storage.createActivityLog({
action: 'Create',
resource: connection.name,
resourceType: 'LDAP Connection',
userId: (req.user as any).id,
username: (req.user as any).username,
status: 'Success',
details: { id: connection.id }
});
// Don't send password back
const { password, ...connectionWithoutPassword } = connection;
res.status(201).json(connectionWithoutPassword);
} catch (error) {
res.status(500).json({ message: error instanceof Error ? error.message : 'An error occurred' });
}
});
/**
* @swagger
* /ldap-connections/{id}:
* put:
* summary: Update an LDAP connection
* description: Update an existing LDAP connection
* tags: [LDAP Connections]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Connection ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/LdapConnection'
* responses:
* 200:
* description: Connection updated successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/LdapConnection'
* 404:
* description: Connection not found
*/
router.put('/ldap-connections/:id', async (req, res) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ message: 'Not authenticated' });
}
const connectionId = parseInt(req.params.id);
if (isNaN(connectionId)) {
return res.status(400).json({ message: 'Invalid connection ID' });
}
const connection = await storage.getLdapConnection(connectionId);
if (!connection) {
return res.status(404).json({ message: 'Connection not found' });
}
const schema = insertLdapConnectionSchema.partial();
const validateResult = schema.safeParse(req.body);
if (!validateResult.success) {
return res.status(400).json({ message: 'Invalid data', errors: validateResult.error.errors });
}
const updatedConnection = await storage.updateLdapConnection(connectionId, validateResult.data);
// Log this activity
await storage.createActivityLog({
action: 'Update',
resource: connection.name,
resourceType: 'LDAP Connection',
userId: (req.user as any).id,
username: (req.user as any).username,
status: 'Success',
details: { id: connectionId }
});
// Don't send password back
const { password, ...connectionWithoutPassword } = updatedConnection!;
res.json(connectionWithoutPassword);
} catch (error) {
res.status(500).json({ message: error instanceof Error ? error.message : 'An error occurred' });
}
});
/**
* @swagger
* /ldap-connections/{id}:
* delete:
* summary: Delete an LDAP connection
* description: Delete an existing LDAP connection
* tags: [LDAP Connections]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Connection ID
* responses:
* 204:
* description: Connection deleted successfully
* 404:
* description: Connection not found
*/
router.delete('/ldap-connections/:id', async (req, res) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ message: 'Not authenticated' });
}
const connectionId = parseInt(req.params.id);
if (isNaN(connectionId)) {
return res.status(400).json({ message: 'Invalid connection ID' });
}
const connection = await storage.getLdapConnection(connectionId);
if (!connection) {
return res.status(404).json({ message: 'Connection not found' });
}
// Disconnect from LDAP if connected
try {
await ldapClient.disconnect(connectionId);
} catch (err) {
// Ignore disconnect errors
}
await storage.deleteLdapConnection(connectionId);
// Log this activity
await storage.createActivityLog({
action: 'Delete',
resource: connection.name,
resourceType: 'LDAP Connection',
userId: (req.user as any).id,
username: (req.user as any).username,
status: 'Success',
details: { id: connectionId }
});
res.status(204).send();
} catch (error) {
res.status(500).json({ message: error instanceof Error ? error.message : 'An error occurred' });
}
});
/**
* @swagger
* /ldap-connections/{id}/test:
* post:
* summary: Test an LDAP connection
* description: Test the connection to an LDAP server
* tags: [LDAP Connections]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Connection ID
* responses:
* 200:
* description: Connection test successful
* 400:
* description: Connection test failed
* 404:
* description: Connection not found
*/
router.post('/ldap-connections/:id/test', async (req, res) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ message: 'Not authenticated' });
}
const connectionId = parseInt(req.params.id);
if (isNaN(connectionId)) {
return res.status(400).json({ message: 'Invalid connection ID' });
}
const connection = await storage.getLdapConnection(connectionId);
if (!connection) {
return res.status(404).json({ message: 'Connection not found' });
}
try {
// Disconnect first if already connected
await ldapClient.disconnect(connectionId);
// Try to connect
const success = await ldapClient.connect(connection);
if (success) {
res.json({ message: 'Connection successful', status: 'connected' });
} else {
res.status(400).json({ message: 'Connection failed', status: 'disconnected' });
}
} catch (error) {
res.status(400).json({
message: 'Connection failed',
error: error instanceof Error ? error.message : String(error),
status: 'disconnected'
});
}
} catch (error) {
res.status(500).json({ message: error instanceof Error ? error.message : 'An error occurred' });
}
});
/**
* @swagger
* /ad/users:
* get:
* summary: Get Active Directory users
* description: Retrieve users from Active Directory
* tags: [Active Directory]
* parameters:
* - in: query
* name: connectionId
* schema:
* type: integer
* required: true
* description: LDAP Connection ID
* - in: query
* name: filter
* schema:
* type: string
* description: LDAP filter expression
* - in: query
* name: properties
* schema:
* type: string
* description: Comma-separated list of properties to return
* responses:
* 200:
* description: A list of users
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/LdapUser'
*/
router.get('/ad/users', verifyApiToken, async (req, res) => {
try {
const connectionId = parseInt(req.query.connectionId as string);
if (isNaN(connectionId)) {
return res.status(400).json({ message: 'Invalid connection ID' });
}
const connection = await storage.getLdapConnection(connectionId);
if (!connection) {
return res.status(404).json({ message: 'Connection not found' });
}
// Connect if not already connected
if (!ldapClient.isConnectionActive(connectionId)) {
try {
await ldapClient.connect(connection);
} catch (error) {
return res.status(500).json({
message: 'Failed to connect to LDAP server',
error: error instanceof Error ? error.message : String(error)
});
}
}
const filter = req.query.filter as string || '(objectClass=user)';
const propertiesParam = req.query.properties as string;
const properties = propertiesParam ? propertiesParam.split(',') : undefined;
const users = await ldapClient.searchUsers(connectionId, filter, properties);
res.json(users);
} catch (error) {
res.status(500).json({ message: error instanceof Error ? error.message : 'An error occurred' });
}
});
/**
* @swagger
* /ad/groups:
* get:
* summary: Get Active Directory groups
* description: Retrieve groups from Active Directory
* tags: [Active Directory]
* parameters:
* - in: query
* name: connectionId
* schema:
* type: integer
* required: true
* description: LDAP Connection ID
* - in: query
* name: filter
* schema:
* type: string
* description: LDAP filter expression
* - in: query
* name: properties
* schema:
* type: string
* description: Comma-separated list of properties to return
* responses:
* 200:
* description: A list of groups
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/LdapGroup'
*/
router.get('/ad/groups', verifyApiToken, async (req, res) => {
try {
const connectionId = parseInt(req.query.connectionId as string);
if (isNaN(connectionId)) {
return res.status(400).json({ message: 'Invalid connection ID' });
}
const connection = await storage.getLdapConnection(connectionId);
if (!connection) {
return res.status(404).json({ message: 'Connection not found' });
}
// Connect if not already connected
if (!ldapClient.isConnectionActive(connectionId)) {
try {
await ldapClient.connect(connection);
} catch (error) {
return res.status(500).json({
message: 'Failed to connect to LDAP server',
error: error instanceof Error ? error.message : String(error)
});
}
}
const filter = req.query.filter as string || '(objectClass=group)';
const propertiesParam = req.query.properties as string;
const properties = propertiesParam ? propertiesParam.split(',') : undefined;
const groups = await ldapClient.searchGroups(connectionId, filter, properties);
res.json(groups);
} catch (error) {
res.status(500).json({ message: error instanceof Error ? error.message : 'An error occurred' });
}
});
/**
* @swagger
* /ad/organizational-units:
* get:
* summary: Get Active Directory organizational units
* description: Retrieve organizational units from Active Directory
* tags: [Active Directory]
* parameters:
* - in: query
* name: connectionId
* schema:
* type: integer
* required: true
* description: LDAP Connection ID
* - in: query
* name: filter
* schema:
* type: string
* description: LDAP filter expression
* - in: query
* name: properties
* schema:
* type: string
* description: Comma-separated list of properties to return
* responses:
* 200:
* description: A list of organizational units
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/LdapOU'
*/
router.get('/ad/organizational-units', verifyApiToken, async (req, res) => {
try {
const connectionId = parseInt(req.query.connectionId as string);
if (isNaN(connectionId)) {
return res.status(400).json({ message: 'Invalid connection ID' });
}
const connection = await storage.getLdapConnection(connectionId);
if (!connection) {
return res.status(404).json({ message: 'Connection not found' });
}
// Connect if not already connected
if (!ldapClient.isConnectionActive(connectionId)) {
try {
await ldapClient.connect(connection);
} catch (error) {
return res.status(500).json({
message: 'Failed to connect to LDAP server',
error: error instanceof Error ? error.message : String(error)
});
}
}
const filter = req.query.filter as string || '(objectClass=organizationalUnit)';
const propertiesParam = req.query.properties as string;
const properties = propertiesParam ? propertiesParam.split(',') : undefined;
const ous = await ldapClient.searchOUs(connectionId, filter, properties);
res.json(ous);
} catch (error) {
res.status(500).json({ message: error instanceof Error ? error.message : 'An error occurred' });
}
});
/**
* @swagger
* /ad/computers:
* get:
* summary: Get Active Directory computers
* description: Retrieve computers from Active Directory
* tags: [Active Directory]
* parameters:
* - in: query
* name: connectionId
* schema:
* type: integer
* required: true
* description: LDAP Connection ID
* - in: query
* name: filter
* schema:
* type: string
* description: LDAP filter expression
* - in: query
* name: properties
* schema:
* type: string
* description: Comma-separated list of properties to return
* responses:
* 200:
* description: A list of computers
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/LdapComputer'
*/
router.get('/ad/computers', verifyApiToken, async (req, res) => {
try {
const connectionId = parseInt(req.query.connectionId as string);
if (isNaN(connectionId)) {
return res.status(400).json({ message: 'Invalid connection ID' });
}
const connection = await storage.getLdapConnection(connectionId);
if (!connection) {
return res.status(404).json({ message: 'Connection not found' });
}
// Connect if not already connected
if (!ldapClient.isConnectionActive(connectionId)) {
try {
await ldapClient.connect(connection);
} catch (error) {
return res.status(500).json({
message: 'Failed to connect to LDAP server',
error: error instanceof Error ? error.message : String(error)
});
}
}
const filter = req.query.filter as string || '(objectClass=computer)';
const propertiesParam = req.query.properties as string;
const properties = propertiesParam ? propertiesParam.split(',') : undefined;
const computers = await ldapClient.searchComputers(connectionId, filter, properties);
res.json(computers);
} catch (error) {
res.status(500).json({ message: error instanceof Error ? error.message : 'An error occurred' });
}
});
/**
* @swagger
* /activity-logs:
* get:
* summary: Get activity logs
* description: Retrieve a list of recent activity logs
* tags: [Activity Logs]
* parameters:
* - in: query
* name: limit
* schema:
* type: integer
* description: Maximum number of logs to return
* responses:
* 200:
* description: A list of activity logs
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/ActivityLog'
*/
router.get('/activity-logs', async (req, res) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ message: 'Not authenticated' });
}
const limit = req.query.limit ? parseInt(req.query.limit as string) : 10;
const logs = await storage.listActivityLogs(limit);
res.json(logs);
} catch (error) {
res.status(500).json({ message: error instanceof Error ? error.message : 'An error occurred' });
}
});
}
+175
View File
@@ -0,0 +1,175 @@
import passport from "passport";
import { Strategy as LocalStrategy } from "passport-local";
import { Express } from "express";
import session from "express-session";
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
import { promisify } from "util";
import { storage } from "./storage";
import { User as SelectUser } from "@shared/schema";
import jwt from "jsonwebtoken";
declare global {
namespace Express {
interface User extends SelectUser {}
}
}
const scryptAsync = promisify(scrypt);
export async function hashPassword(password: string) {
const salt = randomBytes(16).toString("hex");
const buf = (await scryptAsync(password, salt, 64)) as Buffer;
return `${buf.toString("hex")}.${salt}`;
}
export async function comparePasswords(supplied: string, stored: string) {
const [hashed, salt] = stored.split(".");
const hashedBuf = Buffer.from(hashed, "hex");
const suppliedBuf = (await scryptAsync(supplied, salt, 64)) as Buffer;
return timingSafeEqual(hashedBuf, suppliedBuf);
}
export function setupAuth(app: Express) {
const jwtSecret = process.env.JWT_SECRET || 'default_jwt_secret_key_change_in_production';
const sessionSecret = process.env.SESSION_SECRET || 'default_session_secret_key_change_in_production';
const sessionSettings: session.SessionOptions = {
secret: sessionSecret,
resave: false,
saveUninitialized: false,
store: storage.sessionStore,
cookie: {
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
};
app.set("trust proxy", 1);
app.use(session(sessionSettings));
app.use(passport.initialize());
app.use(passport.session());
passport.use(
new LocalStrategy(async (username, password, done) => {
try {
const user = await storage.getUserByUsername(username);
if (!user || !(await comparePasswords(password, user.password))) {
return done(null, false);
} else {
return done(null, user);
}
} catch (error) {
return done(error);
}
}),
);
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id: number, done) => {
try {
const user = await storage.getUser(id);
done(null, user);
} catch (error) {
done(error);
}
});
app.post("/api/register", async (req, res, next) => {
try {
const existingUser = await storage.getUserByUsername(req.body.username);
if (existingUser) {
return res.status(400).json({ message: "Username already exists" });
}
const hashedPassword = await hashPassword(req.body.password);
const user = await storage.createUser({
...req.body,
password: hashedPassword,
});
// Log this activity
await storage.createActivityLog({
action: 'Create',
resource: user.username,
resourceType: 'User',
userId: null,
username: 'System',
status: 'Success',
details: { id: user.id }
});
req.login(user, (err) => {
if (err) return next(err);
// Don't send password back
const { password, ...userWithoutPassword } = user;
res.status(201).json(userWithoutPassword);
});
} catch (error) {
next(error);
}
});
app.post("/api/login", passport.authenticate("local"), (req, res) => {
// Don't send password back
const { password, ...userWithoutPassword } = req.user as SelectUser;
res.status(200).json(userWithoutPassword);
});
app.post("/api/logout", (req, res, next) => {
req.logout((err) => {
if (err) return next(err);
res.sendStatus(200);
});
});
app.get("/api/user", (req, res) => {
if (!req.isAuthenticated()) return res.sendStatus(401);
// Don't send password back
const { password, ...userWithoutPassword } = req.user as SelectUser;
res.json(userWithoutPassword);
});
// Middleware to verify API token
const verifyApiToken = async (req: any, res: any, next: any) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ message: 'API token is required' });
}
try {
// Check if token exists in storage
const apiToken = await storage.getApiTokenByToken(token);
if (!apiToken) {
return res.status(401).json({ message: 'Invalid API token' });
}
// Check if token is expired
if (apiToken.expiresAt && new Date(apiToken.expiresAt) < new Date()) {
return res.status(401).json({ message: 'API token has expired' });
}
// Update the last used timestamp
await storage.updateApiToken(apiToken.id, { lastUsedAt: new Date() });
// Get the user associated with this token
const user = await storage.getUser(apiToken.userId);
if (!user) {
return res.status(401).json({ message: 'User associated with token not found' });
}
// Set the user in the request
req.user = user;
next();
} catch (error) {
return res.status(500).json({ message: 'Error verifying API token' });
}
};
return { verifyApiToken };
}
+70
View File
@@ -0,0 +1,70 @@
import express, { type Request, Response, NextFunction } from "express";
import { registerRoutes } from "./routes";
import { setupVite, serveStatic, log } from "./vite";
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use((req, res, next) => {
const start = Date.now();
const path = req.path;
let capturedJsonResponse: Record<string, any> | undefined = undefined;
const originalResJson = res.json;
res.json = function (bodyJson, ...args) {
capturedJsonResponse = bodyJson;
return originalResJson.apply(res, [bodyJson, ...args]);
};
res.on("finish", () => {
const duration = Date.now() - start;
if (path.startsWith("/api")) {
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
if (capturedJsonResponse) {
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
}
if (logLine.length > 80) {
logLine = logLine.slice(0, 79) + "…";
}
log(logLine);
}
});
next();
});
(async () => {
const server = await registerRoutes(app);
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
const status = err.status || err.statusCode || 500;
const message = err.message || "Internal Server Error";
res.status(status).json({ message });
throw err;
});
// importantly only setup vite in development and after
// setting up all the other routes so the catch-all route
// doesn't interfere with the other routes
if (app.get("env") === "development") {
await setupVite(app, server);
} else {
serveStatic(app);
}
// ALWAYS serve the app on port 5000
// this serves both the API and the client.
// It is the only port that is not firewalled.
const port = 5000;
server.listen({
port,
host: "0.0.0.0",
reusePort: true,
}, () => {
log(`serving on port ${port}`);
});
})();
+201
View File
@@ -0,0 +1,201 @@
import { EventEmitter } from 'events';
import ldap from 'ldapjs';
import { LdapConnection } from '@shared/schema';
import { storage } from './storage';
class LdapClient extends EventEmitter {
private clients: Map<number, ldap.Client> = new Map();
private isConnected: Map<number, boolean> = new Map();
async connect(connection: LdapConnection): Promise<boolean> {
try {
const clientOptions: ldap.ClientOptions = {
url: `${connection.useTLS ? 'ldaps' : 'ldap'}://${connection.server}:${connection.port}`,
reconnect: {
initialDelay: 1000,
maxDelay: 10000,
failAfter: 10
},
timeout: 5000,
connectTimeout: 10000
};
const client = ldap.createClient(clientOptions);
return new Promise((resolve, reject) => {
client.on('error', async (err) => {
console.error(`LDAP connection error for ${connection.name}:`, err);
this.isConnected.set(connection.id, false);
await storage.updateLdapConnection(connection.id, { status: 'disconnected' });
this.emit('status', {
connectionId: connection.id,
status: 'disconnected',
error: err.message
});
});
client.bind(connection.username, connection.password, async (err) => {
if (err) {
console.error(`LDAP bind error for ${connection.name}:`, err);
this.isConnected.set(connection.id, false);
await storage.updateLdapConnection(connection.id, { status: 'disconnected' });
this.emit('status', {
connectionId: connection.id,
status: 'disconnected',
error: err.message
});
reject(err);
return;
}
this.clients.set(connection.id, client);
this.isConnected.set(connection.id, true);
await storage.updateLdapConnection(connection.id, {
status: 'connected',
lastConnected: new Date()
});
this.emit('status', {
connectionId: connection.id,
status: 'connected'
});
resolve(true);
});
});
} catch (error) {
console.error(`LDAP connection error for ${connection.name}:`, error);
this.isConnected.set(connection.id, false);
await storage.updateLdapConnection(connection.id, { status: 'disconnected' });
this.emit('status', {
connectionId: connection.id,
status: 'disconnected',
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
async disconnect(connectionId: number): Promise<void> {
const client = this.clients.get(connectionId);
if (client) {
return new Promise((resolve) => {
client.unbind(() => {
this.clients.delete(connectionId);
this.isConnected.set(connectionId, false);
resolve();
});
});
}
}
getClient(connectionId: number): ldap.Client | undefined {
return this.clients.get(connectionId);
}
isConnectionActive(connectionId: number): boolean {
return this.isConnected.get(connectionId) || false;
}
// LDAP CRUD operations
async searchUsers(connectionId: number, filter = '(objectClass=user)', attributes?: string[]): Promise<any[]> {
const client = this.getClient(connectionId);
if (!client) throw new Error('LDAP connection not established');
const connection = await storage.getLdapConnection(connectionId);
if (!connection) throw new Error('LDAP connection not found');
const baseDN = connection.baseDN || '';
const defaultAttributes = ['cn', 'sAMAccountName', 'mail', 'distinguishedName'];
const searchAttributes = attributes?.length ? attributes : defaultAttributes;
return new Promise((resolve, reject) => {
const results: any[] = [];
client.search(baseDN, {
filter,
scope: 'sub',
attributes: searchAttributes
}, (err, res) => {
if (err) {
reject(err);
return;
}
res.on('searchEntry', (entry) => {
results.push(entry.object);
});
res.on('error', (err) => {
reject(err);
});
res.on('end', (result) => {
resolve(results);
});
});
});
}
async searchGroups(connectionId: number, filter = '(objectClass=group)', attributes?: string[]): Promise<any[]> {
const defaultAttributes = ['cn', 'distinguishedName', 'member'];
return this.searchUsers(connectionId, filter, attributes || defaultAttributes);
}
async searchOUs(connectionId: number, filter = '(objectClass=organizationalUnit)', attributes?: string[]): Promise<any[]> {
const defaultAttributes = ['ou', 'distinguishedName'];
return this.searchUsers(connectionId, filter, attributes || defaultAttributes);
}
async searchComputers(connectionId: number, filter = '(objectClass=computer)', attributes?: string[]): Promise<any[]> {
const defaultAttributes = ['cn', 'distinguishedName', 'operatingSystem'];
return this.searchUsers(connectionId, filter, attributes || defaultAttributes);
}
async createEntry(connectionId: number, dn: string, attributes: any): Promise<boolean> {
const client = this.getClient(connectionId);
if (!client) throw new Error('LDAP connection not established');
return new Promise((resolve, reject) => {
client.add(dn, attributes, (err) => {
if (err) {
reject(err);
return;
}
resolve(true);
});
});
}
async updateEntry(connectionId: number, dn: string, changes: any[]): Promise<boolean> {
const client = this.getClient(connectionId);
if (!client) throw new Error('LDAP connection not established');
return new Promise((resolve, reject) => {
client.modify(dn, changes, (err) => {
if (err) {
reject(err);
return;
}
resolve(true);
});
});
}
async deleteEntry(connectionId: number, dn: string): Promise<boolean> {
const client = this.getClient(connectionId);
if (!client) throw new Error('LDAP connection not established');
return new Promise((resolve, reject) => {
client.del(dn, (err) => {
if (err) {
reject(err);
return;
}
resolve(true);
});
});
}
}
export const ldapClient = new LdapClient();
+15
View File
@@ -0,0 +1,15 @@
import type { Express } from "express";
import { createServer, type Server } from "http";
import { storage } from "./storage";
export async function registerRoutes(app: Express): Promise<Server> {
// put application routes here
// prefix all routes with /api
// use storage to perform CRUD operations on the storage interface
// e.g. storage.insertUser(user) or storage.getUserByUsername(username)
const httpServer = createServer(app);
return httpServer;
}
+215
View File
@@ -0,0 +1,215 @@
import { users, apiTokens, ldapConnections, activityLogs } from "@shared/schema";
import type {
User, InsertUser,
ApiToken, InsertApiToken,
LdapConnection, InsertLdapConnection,
ActivityLog, InsertActivityLog
} from "@shared/schema";
import session from "express-session";
import createMemoryStore from "memorystore";
const MemoryStore = createMemoryStore(session);
export interface IStorage {
// Users
getUser(id: number): Promise<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>;
createUser(user: InsertUser): Promise<User>;
updateUser(id: number, user: Partial<User>): Promise<User | undefined>;
deleteUser(id: number): Promise<boolean>;
listUsers(filters?: any): Promise<User[]>;
// API Tokens
getApiToken(id: number): Promise<ApiToken | undefined>;
getApiTokenByToken(token: string): Promise<ApiToken | undefined>;
createApiToken(token: InsertApiToken & { token: string }): Promise<ApiToken>;
updateApiToken(id: number, token: Partial<ApiToken>): Promise<ApiToken | undefined>;
deleteApiToken(id: number): Promise<boolean>;
listApiTokens(userId?: number): Promise<ApiToken[]>;
// LDAP Connections
getLdapConnection(id: number): Promise<LdapConnection | undefined>;
createLdapConnection(connection: InsertLdapConnection): Promise<LdapConnection>;
updateLdapConnection(id: number, connection: Partial<LdapConnection>): Promise<LdapConnection | undefined>;
deleteLdapConnection(id: number): Promise<boolean>;
listLdapConnections(): Promise<LdapConnection[]>;
// Activity Logs
createActivityLog(log: InsertActivityLog): Promise<ActivityLog>;
listActivityLogs(limit?: number): Promise<ActivityLog[]>;
// Session Store
sessionStore: session.SessionStore;
}
export class MemStorage implements IStorage {
private users: Map<number, User>;
private apiTokens: Map<number, ApiToken>;
private ldapConnections: Map<number, LdapConnection>;
private activityLogs: ActivityLog[];
currentUserId: number;
currentTokenId: number;
currentConnectionId: number;
currentLogId: number;
sessionStore: session.SessionStore;
constructor() {
this.users = new Map();
this.apiTokens = new Map();
this.ldapConnections = new Map();
this.activityLogs = [];
this.currentUserId = 1;
this.currentTokenId = 1;
this.currentConnectionId = 1;
this.currentLogId = 1;
this.sessionStore = new MemoryStore({
checkPeriod: 86400000, // 24 hours
});
}
// Users
async getUser(id: number): Promise<User | undefined> {
return this.users.get(id);
}
async getUserByUsername(username: string): Promise<User | undefined> {
return Array.from(this.users.values()).find(
(user) => user.username.toLowerCase() === username.toLowerCase(),
);
}
async createUser(insertUser: InsertUser): Promise<User> {
const id = this.currentUserId++;
const createdAt = new Date();
const user: User = { ...insertUser, id, createdAt };
this.users.set(id, user);
return user;
}
async updateUser(id: number, updates: Partial<User>): Promise<User | undefined> {
const user = this.users.get(id);
if (!user) return undefined;
const updatedUser = { ...user, ...updates };
this.users.set(id, updatedUser);
return updatedUser;
}
async deleteUser(id: number): Promise<boolean> {
return this.users.delete(id);
}
async listUsers(filters?: any): Promise<User[]> {
const users = Array.from(this.users.values());
if (!filters) return users;
// Apply filters if provided
return users.filter(user => {
for (const [key, value] of Object.entries(filters)) {
if (user[key as keyof User] !== value) {
return false;
}
}
return true;
});
}
// API Tokens
async getApiToken(id: number): Promise<ApiToken | undefined> {
return this.apiTokens.get(id);
}
async getApiTokenByToken(token: string): Promise<ApiToken | undefined> {
return Array.from(this.apiTokens.values()).find(
(apiToken) => apiToken.token === token,
);
}
async createApiToken(tokenData: InsertApiToken & { token: string }): Promise<ApiToken> {
const id = this.currentTokenId++;
const createdAt = new Date();
const apiToken: ApiToken = {
...tokenData,
id,
createdAt,
lastUsedAt: null
};
this.apiTokens.set(id, apiToken);
return apiToken;
}
async updateApiToken(id: number, updates: Partial<ApiToken>): Promise<ApiToken | undefined> {
const token = this.apiTokens.get(id);
if (!token) return undefined;
const updatedToken = { ...token, ...updates };
this.apiTokens.set(id, updatedToken);
return updatedToken;
}
async deleteApiToken(id: number): Promise<boolean> {
return this.apiTokens.delete(id);
}
async listApiTokens(userId?: number): Promise<ApiToken[]> {
const tokens = Array.from(this.apiTokens.values());
if (userId === undefined) return tokens;
return tokens.filter(token => token.userId === userId);
}
// LDAP Connections
async getLdapConnection(id: number): Promise<LdapConnection | undefined> {
return this.ldapConnections.get(id);
}
async createLdapConnection(connectionData: InsertLdapConnection): Promise<LdapConnection> {
const id = this.currentConnectionId++;
const ldapConnection: LdapConnection = {
...connectionData,
id,
status: "disconnected",
lastConnected: null
};
this.ldapConnections.set(id, ldapConnection);
return ldapConnection;
}
async updateLdapConnection(id: number, updates: Partial<LdapConnection>): Promise<LdapConnection | undefined> {
const connection = this.ldapConnections.get(id);
if (!connection) return undefined;
const updatedConnection = { ...connection, ...updates };
this.ldapConnections.set(id, updatedConnection);
return updatedConnection;
}
async deleteLdapConnection(id: number): Promise<boolean> {
return this.ldapConnections.delete(id);
}
async listLdapConnections(): Promise<LdapConnection[]> {
return Array.from(this.ldapConnections.values());
}
// Activity Logs
async createActivityLog(logData: InsertActivityLog): Promise<ActivityLog> {
const id = this.currentLogId++;
const timestamp = new Date();
const log: ActivityLog = { ...logData, id, timestamp };
this.activityLogs.push(log);
return log;
}
async listActivityLogs(limit = 10): Promise<ActivityLog[]> {
// Sort by timestamp descending (newest first)
return [...this.activityLogs]
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
.slice(0, limit);
}
}
export const storage = new MemStorage();
+283
View File
@@ -0,0 +1,283 @@
import swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
import { Express } from 'express';
export const setupSwagger = (app: Express) => {
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Active Directory Management API',
version: '1.0.0',
description: 'RESTful API for managing Active Directory resources',
contact: {
name: 'API Support',
email: 'support@example.com'
}
},
servers: [
{
url: '/api',
description: 'API Server'
}
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
},
schemas: {
User: {
type: 'object',
properties: {
id: {
type: 'integer',
description: 'User ID'
},
username: {
type: 'string',
description: 'Username'
},
email: {
type: 'string',
description: 'Email address'
},
isAdmin: {
type: 'boolean',
description: 'Admin status'
},
createdAt: {
type: 'string',
format: 'date-time',
description: 'Creation date'
}
}
},
ApiToken: {
type: 'object',
properties: {
id: {
type: 'integer',
description: 'Token ID'
},
name: {
type: 'string',
description: 'Token name'
},
token: {
type: 'string',
description: 'The actual token'
},
userId: {
type: 'integer',
description: 'User ID that owns the token'
},
createdAt: {
type: 'string',
format: 'date-time',
description: 'Creation date'
},
lastUsedAt: {
type: 'string',
format: 'date-time',
description: 'Last used date'
},
expiresAt: {
type: 'string',
format: 'date-time',
description: 'Expiration date'
}
}
},
LdapConnection: {
type: 'object',
properties: {
id: {
type: 'integer',
description: 'Connection ID'
},
name: {
type: 'string',
description: 'Connection name'
},
server: {
type: 'string',
description: 'Server hostname/IP'
},
port: {
type: 'integer',
description: 'Server port'
},
authType: {
type: 'string',
description: 'Authentication type'
},
username: {
type: 'string',
description: 'Username'
},
baseDN: {
type: 'string',
description: 'Base Distinguished Name'
},
useTLS: {
type: 'boolean',
description: 'Use TLS/SSL'
},
status: {
type: 'string',
description: 'Connection status'
},
lastConnected: {
type: 'string',
format: 'date-time',
description: 'Last connected timestamp'
}
}
},
ActivityLog: {
type: 'object',
properties: {
id: {
type: 'integer',
description: 'Log ID'
},
action: {
type: 'string',
description: 'Action performed'
},
resource: {
type: 'string',
description: 'Resource name'
},
resourceType: {
type: 'string',
description: 'Resource type'
},
userId: {
type: 'integer',
description: 'User ID'
},
username: {
type: 'string',
description: 'Username'
},
status: {
type: 'string',
description: 'Status'
},
details: {
type: 'object',
description: 'Additional details'
},
timestamp: {
type: 'string',
format: 'date-time',
description: 'Timestamp'
}
}
},
LdapUser: {
type: 'object',
properties: {
cn: {
type: 'string',
description: 'Common Name'
},
sAMAccountName: {
type: 'string',
description: 'SAM Account Name'
},
mail: {
type: 'string',
description: 'Email address'
},
distinguishedName: {
type: 'string',
description: 'Distinguished Name'
}
}
},
LdapGroup: {
type: 'object',
properties: {
cn: {
type: 'string',
description: 'Common Name'
},
distinguishedName: {
type: 'string',
description: 'Distinguished Name'
},
member: {
type: 'array',
items: {
type: 'string'
},
description: 'Group members'
}
}
},
LdapOU: {
type: 'object',
properties: {
ou: {
type: 'string',
description: 'Organizational Unit name'
},
distinguishedName: {
type: 'string',
description: 'Distinguished Name'
}
}
},
LdapComputer: {
type: 'object',
properties: {
cn: {
type: 'string',
description: 'Computer name'
},
distinguishedName: {
type: 'string',
description: 'Distinguished Name'
},
operatingSystem: {
type: 'string',
description: 'Operating System'
}
}
},
Error: {
type: 'object',
properties: {
message: {
type: 'string',
description: 'Error message'
}
}
}
}
},
security: [
{
bearerAuth: []
}
]
},
apis: ['./server/api.ts']
};
const swaggerSpec = swaggerJsdoc(options);
app.use('/swagger-ui', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Serve the OpenAPI spec as JSON
app.get('/swagger.json', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.send(swaggerSpec);
});
};
+85
View File
@@ -0,0 +1,85 @@
import express, { type Express } from "express";
import fs from "fs";
import path from "path";
import { createServer as createViteServer, createLogger } from "vite";
import { type Server } from "http";
import viteConfig from "../vite.config";
import { nanoid } from "nanoid";
const viteLogger = createLogger();
export function log(message: string, source = "express") {
const formattedTime = new Date().toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: true,
});
console.log(`${formattedTime} [${source}] ${message}`);
}
export async function setupVite(app: Express, server: Server) {
const serverOptions = {
middlewareMode: true,
hmr: { server },
allowedHosts: true,
};
const vite = await createViteServer({
...viteConfig,
configFile: false,
customLogger: {
...viteLogger,
error: (msg, options) => {
viteLogger.error(msg, options);
process.exit(1);
},
},
server: serverOptions,
appType: "custom",
});
app.use(vite.middlewares);
app.use("*", async (req, res, next) => {
const url = req.originalUrl;
try {
const clientTemplate = path.resolve(
import.meta.dirname,
"..",
"client",
"index.html",
);
// always reload the index.html file from disk incase it changes
let template = await fs.promises.readFile(clientTemplate, "utf-8");
template = template.replace(
`src="/src/main.tsx"`,
`src="/src/main.tsx?v=${nanoid()}"`,
);
const page = await vite.transformIndexHtml(url, template);
res.status(200).set({ "Content-Type": "text/html" }).end(page);
} catch (e) {
vite.ssrFixStacktrace(e as Error);
next(e);
}
});
}
export function serveStatic(app: Express) {
const distPath = path.resolve(import.meta.dirname, "public");
if (!fs.existsSync(distPath)) {
throw new Error(
`Could not find the build directory: ${distPath}, make sure to build the client first`,
);
}
app.use(express.static(distPath));
// fall through to index.html if the file doesn't exist
app.use("*", (_req, res) => {
res.sendFile(path.resolve(distPath, "index.html"));
});
}