mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-08-09 10:20:39 +00:00
Initial checkpoint
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90
This commit is contained in:
+936
@@ -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
@@ -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 };
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
@@ -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"));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user