adding chat features, admin features and bug fixes
This commit is contained in:
193
backend/src/services/serviceMonitor.service.ts
Normal file
193
backend/src/services/serviceMonitor.service.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import axios from 'axios';
|
||||
import pool from '../config/database';
|
||||
import environment from '../config/environment';
|
||||
|
||||
export interface MonitoredService {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
type: 'internal' | 'custom';
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface PingResult {
|
||||
name: string;
|
||||
url: string;
|
||||
status: 'up' | 'down';
|
||||
latencyMs: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface StatusSummary {
|
||||
up: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
class ServiceMonitorService {
|
||||
async getAllServices(): Promise<MonitoredService[]> {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM monitored_services ORDER BY created_at'
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async createService(name: string, url: string, type: string = 'custom'): Promise<MonitoredService> {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO monitored_services (name, url, type) VALUES ($1, $2, $3) RETURNING *`,
|
||||
[name, url, type]
|
||||
);
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
async updateService(id: string, data: Partial<Pick<MonitoredService, 'name' | 'url' | 'is_active'>>): Promise<MonitoredService> {
|
||||
const fields: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (data.name !== undefined) {
|
||||
fields.push(`name = $${idx++}`);
|
||||
values.push(data.name);
|
||||
}
|
||||
if (data.url !== undefined) {
|
||||
fields.push(`url = $${idx++}`);
|
||||
values.push(data.url);
|
||||
}
|
||||
if (data.is_active !== undefined) {
|
||||
fields.push(`is_active = $${idx++}`);
|
||||
values.push(data.is_active);
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
throw new Error('No fields to update');
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
const result = await pool.query(
|
||||
`UPDATE monitored_services SET ${fields.join(', ')} WHERE id = $${idx} RETURNING *`,
|
||||
values
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
throw new Error('Service not found');
|
||||
}
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
async deleteService(id: string): Promise<boolean> {
|
||||
const result = await pool.query(
|
||||
`DELETE FROM monitored_services WHERE id = $1 AND type = 'custom'`,
|
||||
[id]
|
||||
);
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
}
|
||||
|
||||
async pingService(url: string, headers?: Record<string, string>): Promise<PingResult> {
|
||||
const start = Date.now();
|
||||
try {
|
||||
await axios.get(url, { timeout: 5000, headers });
|
||||
return {
|
||||
name: '',
|
||||
url,
|
||||
status: 'up',
|
||||
latencyMs: Date.now() - start,
|
||||
};
|
||||
} catch (error) {
|
||||
// Treat any HTTP response (even 4xx) as "service is reachable" only for status.php-type endpoints
|
||||
// For API endpoints requiring auth, a 401/403 still means the service is up
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
const status = error.response.status;
|
||||
// If we got a response, the service is running (auth errors = service is up)
|
||||
if (headers && (status === 401 || status === 403)) {
|
||||
return {
|
||||
name: '',
|
||||
url,
|
||||
status: 'up',
|
||||
latencyMs: Date.now() - start,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: '',
|
||||
url,
|
||||
status: 'down',
|
||||
latencyMs: Date.now() - start,
|
||||
error: axios.isAxiosError(error)
|
||||
? `${error.code ?? 'ERROR'}: ${error.message}`
|
||||
: String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async pingAll(): Promise<PingResult[]> {
|
||||
const services = await this.getAllServices();
|
||||
const activeServices = services.filter((s) => s.is_active);
|
||||
|
||||
const internalServices = this.getInternalServices();
|
||||
|
||||
const allTargets = [
|
||||
...activeServices.map((s) => ({ name: s.name, url: s.url, pingUrl: s.url, headers: undefined as Record<string, string> | undefined })),
|
||||
...internalServices,
|
||||
];
|
||||
|
||||
const results = await Promise.all(
|
||||
allTargets.map(async (target) => {
|
||||
const result = await this.pingService(target.pingUrl, target.headers);
|
||||
result.name = target.name;
|
||||
result.url = target.url;
|
||||
return result;
|
||||
})
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async getStatusSummary(): Promise<StatusSummary> {
|
||||
const results = await this.pingAll();
|
||||
return {
|
||||
up: results.filter((r) => r.status === 'up').length,
|
||||
total: results.length,
|
||||
};
|
||||
}
|
||||
|
||||
private getInternalServices(): Array<{ name: string; url: string; pingUrl: string; headers?: Record<string, string> }> {
|
||||
const internal: Array<{ name: string; url: string; pingUrl: string; headers?: Record<string, string> }> = [];
|
||||
|
||||
const { bookstack, vikunja, nextcloudUrl } = environment;
|
||||
|
||||
if (nextcloudUrl) {
|
||||
internal.push({
|
||||
name: 'Nextcloud',
|
||||
url: nextcloudUrl,
|
||||
pingUrl: `${nextcloudUrl}/status.php`,
|
||||
});
|
||||
}
|
||||
|
||||
if (bookstack.url) {
|
||||
internal.push({
|
||||
name: 'BookStack',
|
||||
url: bookstack.url,
|
||||
pingUrl: `${bookstack.url}/api/pages?count=1`,
|
||||
headers: {
|
||||
'Authorization': `Token ${bookstack.tokenId}:${bookstack.tokenSecret}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (vikunja.url) {
|
||||
internal.push({
|
||||
name: 'Vikunja',
|
||||
url: vikunja.url,
|
||||
pingUrl: `${vikunja.url}/api/v1/tasks/all?per_page=1`,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${vikunja.apiToken}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return internal;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ServiceMonitorService();
|
||||
Reference in New Issue
Block a user