diff --git a/Dockerfile b/Dockerfile index c49ea8e..5f0a446 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,8 @@ LABEL org.opencontainers.image.vendor="JDB-NET" WORKDIR /app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt -COPY app.py db.py ./ +COPY app.py db.py openapi.json ./ +COPY static/swagger.html ./static/ COPY --from=frontend /app/static/dist ./static/dist ARG VERSION=unknown ENV VERSION=${VERSION} diff --git a/app.py b/app.py index 08daf19..23b1ab3 100644 --- a/app.py +++ b/app.py @@ -2925,14 +2925,14 @@ def api_dashboard(): }) cursor.execute(''' - SELECT HOUR(timestamp) AS hour, COUNT(*) AS count - FROM AuditLog - WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 24 HOUR) - GROUP BY HOUR(timestamp) - ORDER BY hour + SELECT timestamp FROM AuditLog + WHERE timestamp >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 24 HOUR) ''') - activity_by_hour = {row['hour']: row['count'] for row in cursor.fetchall()} - activity = [{'hour': h, 'count': activity_by_hour.get(h, 0)} for h in range(24)] + activity = [ + row['timestamp'].strftime('%Y-%m-%d %H:%M:%S') + if hasattr(row['timestamp'], 'strftime') else str(row['timestamp']) + for row in cursor.fetchall() + ] return jsonify({ 'stats': { @@ -3370,6 +3370,25 @@ def api_rack_export(rack_id): as_attachment=True, download_name=f"{rack['name']}_rack.csv".replace(' ', '_')) +# ── API documentation (OpenAPI + Swagger UI) ──────────────────────────────── +OPENAPI_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'openapi.json') +SWAGGER_HTML = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static', 'swagger.html') + + +@app.route('/api/openapi.json') +def api_openapi_spec(): + if not os.path.isfile(OPENAPI_PATH): + return jsonify({'error': 'OpenAPI spec not found'}), 404 + return send_file(OPENAPI_PATH, mimetype='application/json') + + +@app.route('/api/docs') +def api_docs(): + if not os.path.isfile(SWAGGER_HTML): + return jsonify({'error': 'Swagger UI not found'}), 404 + return send_file(SWAGGER_HTML) + + # ── SPA static files ────────────────────────────────────────────────────────── STATIC_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static') DIST = os.path.join(STATIC_ROOT, 'dist') diff --git a/frontend/src/api.ts b/frontend/src/api.ts index c6ad8ce..929f182 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -213,7 +213,7 @@ export const api = { available: number; status: "active" | "alerting"; }[]; - activity: { hour: number; count: number }[]; + activity: string[]; }>(await fetchApi("/api/v2/dashboard")); }, async search(q: string) { diff --git a/frontend/src/utils/datetime.ts b/frontend/src/utils/datetime.ts index 9494565..208d22d 100644 --- a/frontend/src/utils/datetime.ts +++ b/frontend/src/utils/datetime.ts @@ -21,3 +21,14 @@ export function formatLocalTime(ts?: string | null, fallback = "—"): string { if (!d) return ts?.trim() || fallback; return d.toLocaleString(); } + +/** Bucket audit timestamps from the last 24h by local hour of day (0–23). */ +export function bucketActivityByLocalHour(timestamps: string[]): { hour: number; count: number }[] { + const counts = new Array(24).fill(0); + for (const ts of timestamps) { + const d = parseApiTimestamp(ts); + if (!d) continue; + counts[d.getHours()]++; + } + return counts.map((count, hour) => ({ hour, count })); +} diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue index 1a273c8..e7b1f75 100644 --- a/frontend/src/views/DashboardView.vue +++ b/frontend/src/views/DashboardView.vue @@ -3,6 +3,7 @@ import { ref, onMounted, computed } from "vue"; import { RouterLink } from "vue-router"; import { Network, Wifi, Layers, Server } from "lucide-vue-next"; import { api } from "@/api"; +import { bucketActivityByLocalHour } from "@/utils/datetime"; interface DashboardStats { total_ips: number; @@ -46,7 +47,7 @@ onMounted(async () => { const d = await api.dashboard(); stats.value = d.stats; subnetOverview.value = d.subnet_overview; - activity.value = d.activity; + activity.value = bucketActivityByLocalHour(d.activity); } catch (e) { error.value = e instanceof Error ? e.message : "Failed to load dashboard"; } finally { diff --git a/frontend/src/views/UsersView.vue b/frontend/src/views/UsersView.vue index 8621def..f13fba9 100644 --- a/frontend/src/views/UsersView.vue +++ b/frontend/src/views/UsersView.vue @@ -135,9 +135,12 @@ async function delRole(id: number) {
-
+

Users

- +
+ API documentation + +
  • `, or `?api_key=` query parameter.\n Regenerate keys from the Users page in the web UI.\n\n**Authorization**\nEach endpoint requires a permission (see `x-permission`). API keys inherit permissions from the user's role.\n\n**List responses**\nMost list endpoints return `{ \"items\": [...] }`. Audit log returns `{ \"items\", \"total\" }`." + }, + "servers": [ + { + "url": "/api/v2", + "description": "API v2 base path" + } + ], + "tags": [ + { + "name": "Authentication" + }, + { + "name": "Account" + }, + { + "name": "Info" + }, + { + "name": "Dashboard" + }, + { + "name": "Search" + }, + { + "name": "Devices" + }, + { + "name": "Subnets" + }, + { + "name": "IP Addresses" + }, + { + "name": "Tags" + }, + { + "name": "Racks" + }, + { + "name": "Custom Fields" + }, + { + "name": "DHCP" + }, + { + "name": "Audit" + }, + { + "name": "Users" + }, + { + "name": "Roles" + }, + { + "name": "Permissions" + }, + { + "name": "Settings" + }, + { + "name": "Bulk" + } + ], + "components": { + "securitySchemes": { + "ApiKeyHeader": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + }, + "BearerAuth": { + "type": "http", + "scheme": "bearer", + "description": "API key as Bearer token" + }, + "ApiKeyQuery": { + "type": "apiKey", + "in": "query", + "name": "api_key" + }, + "SessionCookie": { + "type": "apiKey", + "in": "cookie", + "name": "session", + "description": "Flask session cookie after login (browser only)" + } + }, + "schemas": { + "Error": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "permission": { + "type": "string", + "description": "Required permission when 403" + } + } + }, + "OkResponse": { + "type": "object", + "properties": { + "ok": { + "type": "boolean" + } + } + }, + "ItemsResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": {} + } + } + }, + "LoginRequest": { + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "password": { + "type": "string", + "format": "password" + } + } + }, + "Verify2faRequest": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string" + }, + "use_backup": { + "type": "boolean", + "default": false + } + } + }, + "Setup2faRequest": { + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "type": "string", + "enum": [ + "generate", + "verify" + ] + }, + "code": { + "type": "string", + "description": "Required when action is verify" + } + } + }, + "ChangePasswordRequest": { + "type": "object", + "required": [ + "current_password", + "new_password" + ], + "properties": { + "current_password": { + "type": "string" + }, + "new_password": { + "type": "string" + } + } + }, + "PasswordConfirmRequest": { + "type": "object", + "required": [ + "password" + ], + "properties": { + "password": { + "type": "string" + } + } + }, + "DeviceCreate": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "DeviceUpdate": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "AssignIpRequest": { + "type": "object", + "required": [ + "ip_id" + ], + "properties": { + "ip_id": { + "type": "integer" + } + } + }, + "SubnetCreate": { + "type": "object", + "required": [ + "name", + "cidr" + ], + "properties": { + "name": { + "type": "string" + }, + "cidr": { + "type": "string", + "example": "10.0.0.0/24" + }, + "site": { + "type": "string" + }, + "vlan_id": { + "type": "integer", + "nullable": true + }, + "vlan_description": { + "type": "string" + }, + "vlan_notes": { + "type": "string" + } + } + }, + "SubnetUpdate": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "cidr": { + "type": "string" + }, + "site": { + "type": "string" + }, + "vlan_id": { + "type": "integer", + "nullable": true + }, + "vlan_description": { + "type": "string" + }, + "vlan_notes": { + "type": "string" + } + } + }, + "RackCreate": { + "type": "object", + "required": [ + "name", + "site", + "height_u" + ], + "properties": { + "name": { + "type": "string" + }, + "site": { + "type": "string" + }, + "height_u": { + "type": "integer", + "minimum": 1 + } + } + }, + "RackUpdate": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "site": { + "type": "string" + }, + "height_u": { + "type": "integer", + "minimum": 1 + } + } + }, + "RackDeviceCreate": { + "type": "object", + "required": [ + "position_u", + "side" + ], + "properties": { + "position_u": { + "type": "integer", + "minimum": 1 + }, + "side": { + "type": "string", + "enum": [ + "front", + "back" + ] + }, + "device_id": { + "type": "integer", + "description": "Networked device (mutually exclusive with nonnet_device_name)" + }, + "nonnet_device_name": { + "type": "string", + "description": "Label for non-networked equipment" + } + } + }, + "CustomFieldCreate": { + "type": "object", + "required": [ + "entity_type", + "name", + "field_key", + "field_type" + ], + "properties": { + "entity_type": { + "type": "string", + "enum": [ + "device", + "subnet" + ] + }, + "name": { + "type": "string" + }, + "field_key": { + "type": "string" + }, + "field_type": { + "type": "string", + "enum": [ + "text", + "textarea", + "number", + "select", + "checkbox", + "date" + ] + }, + "required": { + "type": "boolean", + "default": false + }, + "default_value": { + "type": "string", + "nullable": true + }, + "help_text": { + "type": "string", + "nullable": true + }, + "display_order": { + "type": "integer", + "default": 0 + }, + "validation_rules": { + "type": "object", + "additionalProperties": true + } + } + }, + "CustomFieldUpdate": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "field_type": { + "type": "string", + "enum": [ + "text", + "textarea", + "number", + "select", + "checkbox", + "date" + ] + }, + "required": { + "type": "boolean" + }, + "default_value": { + "type": "string", + "nullable": true + }, + "help_text": { + "type": "string", + "nullable": true + }, + "display_order": { + "type": "integer" + }, + "validation_rules": { + "type": "object", + "additionalProperties": true + } + } + }, + "CustomFieldsPatch": { + "type": "object", + "properties": { + "custom_fields": { + "type": "object", + "additionalProperties": true + } + } + }, + "CustomFieldReorder": { + "type": "object", + "required": [ + "entity_type", + "field_orders" + ], + "properties": { + "entity_type": { + "type": "string", + "enum": [ + "device", + "subnet" + ] + }, + "field_orders": { + "type": "object", + "additionalProperties": { + "type": "integer" + }, + "description": "Map of field definition ID to display order" + } + } + }, + "DhcpConfigure": { + "type": "object", + "properties": { + "remove": { + "type": "boolean", + "description": "If true, remove the DHCP pool for this subnet" + }, + "pools": { + "type": "array", + "items": { + "type": "object", + "required": [ + "start_ip", + "end_ip" + ], + "properties": { + "start_ip": { + "type": "string" + }, + "end_ip": { + "type": "string" + }, + "excluded_ips": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "TagCreate": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "color": { + "type": "string", + "default": "#6B7280" + }, + "description": { + "type": "string" + } + } + }, + "TagUpdate": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "color": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "AssignTagRequest": { + "type": "object", + "required": [ + "tag_id" + ], + "properties": { + "tag_id": { + "type": "integer" + } + } + }, + "IpNotesPatch": { + "type": "object", + "required": [ + "notes" + ], + "properties": { + "notes": { + "type": "string", + "nullable": true + } + } + }, + "UserCreate": { + "type": "object", + "required": [ + "name", + "email", + "password" + ], + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "password": { + "type": "string" + }, + "role_id": { + "type": "integer" + } + } + }, + "UserUpdate": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "password": { + "type": "string" + }, + "role_id": { + "type": "integer" + } + } + }, + "RoleCreate": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "require_2fa": { + "type": "boolean", + "default": false + }, + "permission_ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "RoleUpdate": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "require_2fa": { + "type": "boolean" + }, + "permission_ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "SettingsUpdate": { + "type": "object", + "properties": { + "org_name": { + "type": "string" + }, + "org_logo": { + "type": "string", + "description": "URL or path to PNG logo" + }, + "accent_color": { + "type": "string", + "example": "#1ebe8a" + } + } + }, + "BulkAssignIps": { + "type": "object", + "required": [ + "device_id", + "ip_ids" + ], + "properties": { + "device_id": { + "type": "integer" + }, + "ip_ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "BulkCreateDevices": { + "type": "object", + "required": [ + "names" + ], + "properties": { + "names": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "BulkAssignTags": { + "type": "object", + "required": [ + "device_ids", + "tag_id" + ], + "properties": { + "device_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "tag_id": { + "type": "integer" + } + } + }, + "BulkExportSubnets": { + "type": "object", + "required": [ + "subnet_ids" + ], + "properties": { + "subnet_ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + }, + "parameters": { + "deviceId": { + "name": "device_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + "subnetId": { + "name": "subnet_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + "rackId": { + "name": "rack_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + "rackDeviceId": { + "name": "rack_device_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + "tagId": { + "name": "tag_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + "ipId": { + "name": "ip_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + "fieldId": { + "name": "field_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + "userId": { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + "roleId": { + "name": "role_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + "entityType": { + "name": "entity_type", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "device", + "subnet" + ] + } + }, + "tagIdentifier": { + "name": "tag_identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Tag name or numeric tag ID" + }, + "ipAddress": { + "name": "ip_address", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "IPv4 address (e.g. 10.0.0.1)" + }, + "auditLimit": { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 100 + } + }, + "auditOffset": { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0 + } + }, + "auditUser": { + "name": "user", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Partial user name match" + }, + "auditAction": { + "name": "action", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Exact action name" + }, + "auditFrom": { + "name": "from", + "in": "query", + "schema": { + "type": "string", + "format": "date" + }, + "description": "Start date YYYY-MM-DD" + }, + "auditTo": { + "name": "to", + "in": "query", + "schema": { + "type": "string", + "format": "date" + }, + "description": "End date YYYY-MM-DD" + } + }, + "responses": { + "Unauthorized": { + "description": "Not authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "Forbidden": { + "description": "Missing permission", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "NotFound": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "BadRequest": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "security": [ + { + "ApiKeyHeader": [] + }, + { + "BearerAuth": [] + }, + { + "ApiKeyQuery": [] + }, + { + "SessionCookie": [] + } + ], + "paths": { + "/auth/login": { + "post": { + "summary": "Log in with email and password", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Login OK or 2FA required" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + }, + "tags": [ + "Authentication" + ] + } + }, + "/auth/logout": { + "post": { + "summary": "Log out and clear session", + "responses": { + "200": { + "description": "Session cleared", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OkResponse" + } + } + } + } + }, + "tags": [ + "Authentication" + ] + } + }, + "/auth/me": { + "get": { + "summary": "Current session, permissions, and org branding", + "responses": { + "200": { + "description": "Auth state" + } + }, + "tags": [ + "Authentication" + ] + } + }, + "/auth/verify-2fa": { + "post": { + "summary": "Complete login with TOTP or backup code", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Verify2faRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Login complete" + } + }, + "tags": [ + "Authentication" + ] + } + }, + "/auth/setup-2fa": { + "post": { + "summary": "Generate or verify TOTP setup", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Setup2faRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Secret/QR or setup complete" + } + }, + "tags": [ + "Authentication" + ] + } + }, + "/account": { + "get": { + "summary": "Account profile and 2FA status", + "security": [ + { + "ApiKeyHeader": [] + }, + { + "BearerAuth": [] + }, + { + "ApiKeyQuery": [] + }, + { + "SessionCookie": [] + } + ], + "responses": { + "200": { + "description": "Account details" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + }, + "tags": [ + "Account" + ] + } + }, + "/account/change-password": { + "post": { + "summary": "Change password", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangePasswordRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Password updated" + } + }, + "tags": [ + "Account" + ] + } + }, + "/account/disable-2fa": { + "post": { + "summary": "Disable two-factor authentication", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PasswordConfirmRequest" + } + } + } + }, + "responses": { + "200": { + "description": "2FA disabled" + } + }, + "tags": [ + "Account" + ] + } + }, + "/account/regenerate-backup-codes": { + "post": { + "summary": "Regenerate 2FA backup codes", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PasswordConfirmRequest" + } + } + } + }, + "responses": { + "200": { + "description": "New backup codes" + } + }, + "tags": [ + "Account" + ] + } + }, + "/info": { + "get": { + "summary": "API version and authenticated user info", + "responses": { + "200": { + "description": "API info" + } + }, + "tags": [ + "Info" + ] + } + }, + "/dashboard": { + "get": { + "summary": "Dashboard statistics and activity", + "responses": { + "200": { + "description": "Stats, subnet overview, activity timestamps" + } + }, + "x-permission": "view_index", + "tags": [ + "Dashboard" + ] + } + }, + "/search": { + "get": { + "summary": "Global search", + "parameters": [ + { + "name": "q", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "description": "Search query" + } + ], + "responses": { + "200": { + "description": "Matching subnets, IPs, devices, tags, racks, sites" + } + }, + "tags": [ + "Search" + ] + } + }, + "/devices": { + "get": { + "summary": "List devices", + "parameters": [ + { + "name": "tag", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Filter by tag name" + }, + { + "name": "site", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Filter by site (use Unassigned for null)" + } + ], + "responses": { + "200": { + "description": "Device list", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemsResponse" + } + } + } + } + }, + "x-permission": "view_devices", + "tags": [ + "Devices" + ] + }, + "post": { + "summary": "Create device", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Device created" + } + }, + "x-permission": "add_device", + "tags": [ + "Devices" + ] + } + }, + "/devices/{device_id}": { + "get": { + "summary": "Get device", + "parameters": [ + { + "$ref": "#/components/parameters/deviceId" + } + ], + "responses": { + "200": { + "description": "Device with IPs, tags, custom fields" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + }, + "x-permission": "view_device", + "tags": [ + "Devices" + ] + }, + "put": { + "summary": "Update device", + "parameters": [ + { + "$ref": "#/components/parameters/deviceId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Device updated" + } + }, + "x-permission": "edit_device", + "tags": [ + "Devices" + ] + }, + "delete": { + "summary": "Delete device", + "parameters": [ + { + "$ref": "#/components/parameters/deviceId" + } + ], + "responses": { + "200": { + "description": "Device deleted" + } + }, + "x-permission": "delete_device", + "tags": [ + "Devices" + ] + } + }, + "/devices/{device_id}/ips": { + "post": { + "summary": "Assign IP to device", + "parameters": [ + { + "$ref": "#/components/parameters/deviceId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssignIpRequest" + } + } + } + }, + "responses": { + "201": { + "description": "IP assigned" + } + }, + "x-permission": "add_device_ip", + "tags": [ + "Devices" + ] + } + }, + "/devices/{device_id}/ips/{ip_id}": { + "delete": { + "summary": "Remove IP from device", + "parameters": [ + { + "$ref": "#/components/parameters/deviceId" + }, + { + "$ref": "#/components/parameters/ipId" + } + ], + "responses": { + "200": { + "description": "IP removed" + } + }, + "x-permission": "remove_device_ip", + "tags": [ + "Devices" + ] + } + }, + "/devices/{device_id}/ip-history": { + "get": { + "summary": "IP assignment history for device", + "parameters": [ + { + "$ref": "#/components/parameters/deviceId" + } + ], + "responses": { + "200": { + "description": "History items" + } + }, + "x-permission": "view_device", + "tags": [ + "Devices" + ] + } + }, + "/devices/{device_id}/custom-fields": { + "patch": { + "summary": "Update device custom field values", + "parameters": [ + { + "$ref": "#/components/parameters/deviceId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFieldsPatch" + } + } + } + }, + "responses": { + "200": { + "description": "Fields updated" + } + }, + "x-permission": "edit_device", + "tags": [ + "Devices" + ] + } + }, + "/devices/{device_id}/tags": { + "get": { + "summary": "List tags on device", + "parameters": [ + { + "$ref": "#/components/parameters/deviceId" + } + ], + "responses": { + "200": { + "description": "Tags" + } + }, + "x-permission": "view_device", + "tags": [ + "Devices", + "Tags" + ] + }, + "post": { + "summary": "Assign tag to device", + "parameters": [ + { + "$ref": "#/components/parameters/deviceId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssignTagRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Tag assigned" + } + }, + "x-permission": "assign_device_tag", + "tags": [ + "Devices", + "Tags" + ] + } + }, + "/devices/{device_id}/tags/{tag_id}": { + "delete": { + "summary": "Remove tag from device", + "parameters": [ + { + "$ref": "#/components/parameters/deviceId" + }, + { + "$ref": "#/components/parameters/tagId" + } + ], + "responses": { + "200": { + "description": "Tag removed" + } + }, + "x-permission": "remove_device_tag", + "tags": [ + "Devices", + "Tags" + ] + } + }, + "/devices/by-tag/{tag_identifier}": { + "get": { + "summary": "List devices by tag name or ID", + "parameters": [ + { + "$ref": "#/components/parameters/tagIdentifier" + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "simple" + ] + }, + "description": "simple returns [{device, ip}] array for devices with at least one IP" + } + ], + "responses": { + "200": { + "description": "Devices (format varies by format param)" + } + }, + "x-permission": "view_devices", + "tags": [ + "Devices", + "Tags" + ] + } + }, + "/subnets": { + "get": { + "summary": "List subnets", + "parameters": [ + { + "name": "include", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "utilization" + ] + }, + "description": "Include utilization stats when set to utilization" + } + ], + "responses": { + "200": { + "description": "Subnet list" + } + }, + "x-permission": "view_subnet", + "tags": [ + "Subnets" + ] + }, + "post": { + "summary": "Create subnet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubnetCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Subnet created" + } + }, + "x-permission": "add_subnet", + "tags": [ + "Subnets" + ] + } + }, + "/subnets/{subnet_id}": { + "get": { + "summary": "Get subnet with IP addresses", + "parameters": [ + { + "$ref": "#/components/parameters/subnetId" + } + ], + "responses": { + "200": { + "description": "Subnet detail" + } + }, + "x-permission": "view_subnet", + "tags": [ + "Subnets" + ] + }, + "put": { + "summary": "Update subnet", + "parameters": [ + { + "$ref": "#/components/parameters/subnetId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubnetUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Subnet updated" + } + }, + "x-permission": "edit_subnet", + "tags": [ + "Subnets" + ] + }, + "delete": { + "summary": "Delete subnet", + "parameters": [ + { + "$ref": "#/components/parameters/subnetId" + } + ], + "responses": { + "200": { + "description": "Subnet deleted" + } + }, + "x-permission": "delete_subnet", + "tags": [ + "Subnets" + ] + } + }, + "/subnets/{subnet_id}/next_free_ip": { + "get": { + "summary": "First available IP outside DHCP pools", + "parameters": [ + { + "$ref": "#/components/parameters/subnetId" + } + ], + "responses": { + "200": { + "description": "Next free IP" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + }, + "x-permission": "view_subnet", + "tags": [ + "Subnets" + ] + } + }, + "/subnets/{subnet_id}/available-ips": { + "get": { + "summary": "All unassigned IPs outside DHCP pools", + "parameters": [ + { + "$ref": "#/components/parameters/subnetId" + } + ], + "responses": { + "200": { + "description": "Available IPs" + } + }, + "x-permission": "view_subnet", + "tags": [ + "Subnets" + ] + } + }, + "/subnets/{subnet_id}/export": { + "get": { + "summary": "Export subnet as CSV", + "parameters": [ + { + "$ref": "#/components/parameters/subnetId" + } + ], + "responses": { + "200": { + "description": "CSV file", + "content": { + "text/csv": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + }, + "x-permission": "export_subnet_csv", + "tags": [ + "Subnets" + ] + } + }, + "/subnets/{subnet_id}/custom-fields": { + "patch": { + "summary": "Update subnet custom field values", + "parameters": [ + { + "$ref": "#/components/parameters/subnetId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFieldsPatch" + } + } + } + }, + "responses": { + "200": { + "description": "Fields updated" + } + }, + "x-permission": "edit_subnet", + "tags": [ + "Subnets" + ] + } + }, + "/subnets/{subnet_id}/dhcp": { + "get": { + "summary": "Get DHCP pools for subnet", + "parameters": [ + { + "$ref": "#/components/parameters/subnetId" + } + ], + "responses": { + "200": { + "description": "DHCP pools" + } + }, + "x-permission": "view_dhcp", + "tags": [ + "DHCP" + ] + }, + "post": { + "summary": "Configure or remove DHCP pool", + "parameters": [ + { + "$ref": "#/components/parameters/subnetId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DhcpConfigure" + } + } + } + }, + "responses": { + "200": { + "description": "DHCP configured or removed" + } + }, + "x-permission": "configure_dhcp", + "tags": [ + "DHCP" + ] + } + }, + "/ip-addresses/{ip_id}": { + "patch": { + "summary": "Update IP address notes", + "parameters": [ + { + "$ref": "#/components/parameters/ipId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpNotesPatch" + } + } + } + }, + "responses": { + "200": { + "description": "Notes updated" + } + }, + "x-permission": "edit_subnet", + "tags": [ + "IP Addresses" + ] + } + }, + "/ips/{ip_address}/history": { + "get": { + "summary": "IP assignment history", + "parameters": [ + { + "$ref": "#/components/parameters/ipAddress" + } + ], + "responses": { + "200": { + "description": "History for IP" + } + }, + "x-permission": "view_subnet", + "tags": [ + "IP Addresses" + ] + } + }, + "/tags": { + "get": { + "summary": "List tags", + "responses": { + "200": { + "description": "Tag list" + } + }, + "x-permission": "view_tags", + "tags": [ + "Tags" + ] + }, + "post": { + "summary": "Create tag", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Tag created" + } + }, + "x-permission": "add_tag", + "tags": [ + "Tags" + ] + } + }, + "/tags/{tag_id}": { + "get": { + "summary": "Get tag with devices", + "parameters": [ + { + "$ref": "#/components/parameters/tagId" + } + ], + "responses": { + "200": { + "description": "Tag detail" + } + }, + "x-permission": "view_tags", + "tags": [ + "Tags" + ] + }, + "put": { + "summary": "Update tag", + "parameters": [ + { + "$ref": "#/components/parameters/tagId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Tag updated" + } + }, + "x-permission": "edit_tag", + "tags": [ + "Tags" + ] + }, + "delete": { + "summary": "Delete tag", + "parameters": [ + { + "$ref": "#/components/parameters/tagId" + } + ], + "responses": { + "200": { + "description": "Tag deleted" + } + }, + "x-permission": "delete_tag", + "tags": [ + "Tags" + ] + } + }, + "/racks": { + "get": { + "summary": "List racks", + "responses": { + "200": { + "description": "Rack list with devices" + } + }, + "x-permission": "view_racks", + "tags": [ + "Racks" + ] + }, + "post": { + "summary": "Create rack", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RackCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Rack created" + } + }, + "x-permission": "add_rack", + "tags": [ + "Racks" + ] + } + }, + "/racks/{rack_id}": { + "get": { + "summary": "Get rack", + "parameters": [ + { + "$ref": "#/components/parameters/rackId" + } + ], + "responses": { + "200": { + "description": "Rack detail" + } + }, + "x-permission": "view_rack", + "tags": [ + "Racks" + ] + }, + "put": { + "summary": "Update rack", + "parameters": [ + { + "$ref": "#/components/parameters/rackId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RackUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Rack updated" + } + }, + "x-permission": "add_rack", + "tags": [ + "Racks" + ] + }, + "delete": { + "summary": "Delete rack", + "parameters": [ + { + "$ref": "#/components/parameters/rackId" + } + ], + "responses": { + "200": { + "description": "Rack deleted" + } + }, + "x-permission": "delete_rack", + "tags": [ + "Racks" + ] + } + }, + "/racks/{rack_id}/devices": { + "post": { + "summary": "Add device to rack position", + "parameters": [ + { + "$ref": "#/components/parameters/rackId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RackDeviceCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Device placed in rack" + } + }, + "x-permission": "add_device_to_rack", + "tags": [ + "Racks" + ] + } + }, + "/racks/{rack_id}/devices/{rack_device_id}": { + "delete": { + "summary": "Remove device from rack", + "parameters": [ + { + "$ref": "#/components/parameters/rackId" + }, + { + "$ref": "#/components/parameters/rackDeviceId" + } + ], + "responses": { + "200": { + "description": "Removed from rack" + } + }, + "x-permission": "remove_device_from_rack", + "tags": [ + "Racks" + ] + } + }, + "/racks/{rack_id}/export": { + "get": { + "summary": "Export rack layout as CSV", + "parameters": [ + { + "$ref": "#/components/parameters/rackId" + } + ], + "responses": { + "200": { + "description": "CSV file", + "content": { + "text/csv": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + }, + "x-permission": "export_rack_csv", + "tags": [ + "Racks" + ] + } + }, + "/custom_fields/{entity_type}": { + "get": { + "summary": "List custom field definitions", + "parameters": [ + { + "$ref": "#/components/parameters/entityType" + } + ], + "responses": { + "200": { + "description": "Field definitions" + } + }, + "x-permission": "view_custom_fields", + "tags": [ + "Custom Fields" + ] + } + }, + "/custom_fields": { + "post": { + "summary": "Create custom field definition", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFieldCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Field created" + } + }, + "x-permission": "manage_custom_fields", + "tags": [ + "Custom Fields" + ] + } + }, + "/custom_fields/{field_id}": { + "put": { + "summary": "Update custom field definition", + "parameters": [ + { + "$ref": "#/components/parameters/fieldId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFieldUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Field updated" + } + }, + "x-permission": "manage_custom_fields", + "tags": [ + "Custom Fields" + ] + }, + "delete": { + "summary": "Delete custom field definition", + "parameters": [ + { + "$ref": "#/components/parameters/fieldId" + } + ], + "responses": { + "200": { + "description": "Field deleted" + } + }, + "x-permission": "manage_custom_fields", + "tags": [ + "Custom Fields" + ] + } + }, + "/custom-fields/reorder": { + "post": { + "summary": "Reorder custom fields", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFieldReorder" + } + } + } + }, + "responses": { + "200": { + "description": "Order updated" + } + }, + "x-permission": "manage_custom_fields", + "tags": [ + "Custom Fields" + ] + } + }, + "/audit/actions": { + "get": { + "summary": "List distinct audit action names", + "responses": { + "200": { + "description": "Action names" + } + }, + "x-permission": "view_audit", + "tags": [ + "Audit" + ] + } + }, + "/audit": { + "get": { + "summary": "Query audit log", + "parameters": [ + { + "$ref": "#/components/parameters/auditLimit" + }, + { + "$ref": "#/components/parameters/auditOffset" + }, + { + "$ref": "#/components/parameters/auditUser" + }, + { + "$ref": "#/components/parameters/auditAction" + }, + { + "$ref": "#/components/parameters/auditFrom" + }, + { + "$ref": "#/components/parameters/auditTo" + } + ], + "responses": { + "200": { + "description": "Paginated audit entries with total count" + } + }, + "x-permission": "view_audit", + "tags": [ + "Audit" + ] + } + }, + "/audit/export": { + "get": { + "summary": "Export audit log as CSV", + "parameters": [ + { + "$ref": "#/components/parameters/auditUser" + }, + { + "$ref": "#/components/parameters/auditAction" + }, + { + "$ref": "#/components/parameters/auditFrom" + }, + { + "$ref": "#/components/parameters/auditTo" + } + ], + "responses": { + "200": { + "description": "CSV export", + "content": { + "text/csv": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + }, + "x-permission": "view_audit", + "tags": [ + "Audit" + ] + } + }, + "/users": { + "get": { + "summary": "List users", + "responses": { + "200": { + "description": "User list (API keys not included)" + } + }, + "x-permission": "view_users", + "tags": [ + "Users" + ] + }, + "post": { + "summary": "Create user", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserCreate" + } + } + } + }, + "responses": { + "201": { + "description": "User created" + } + }, + "x-permission": "manage_users", + "tags": [ + "Users" + ] + } + }, + "/users/{user_id}": { + "put": { + "summary": "Update user", + "parameters": [ + { + "$ref": "#/components/parameters/userId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "User updated" + } + }, + "x-permission": "manage_users", + "tags": [ + "Users" + ] + }, + "delete": { + "summary": "Delete user", + "parameters": [ + { + "$ref": "#/components/parameters/userId" + } + ], + "responses": { + "200": { + "description": "User deleted" + } + }, + "x-permission": "manage_users", + "tags": [ + "Users" + ] + } + }, + "/users/{user_id}/regenerate-api-key": { + "post": { + "summary": "Regenerate user API key", + "parameters": [ + { + "$ref": "#/components/parameters/userId" + } + ], + "responses": { + "200": { + "description": "New API key (shown once)" + } + }, + "x-permission": "manage_users", + "tags": [ + "Users" + ] + } + }, + "/roles": { + "get": { + "summary": "List roles with permissions", + "responses": { + "200": { + "description": "Roles" + } + }, + "x-permission": "view_users", + "tags": [ + "Roles" + ] + }, + "post": { + "summary": "Create role", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Role created" + } + }, + "x-permission": "manage_roles", + "tags": [ + "Roles" + ] + } + }, + "/roles/{role_id}": { + "put": { + "summary": "Update role", + "parameters": [ + { + "$ref": "#/components/parameters/roleId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Role updated" + } + }, + "x-permission": "manage_roles", + "tags": [ + "Roles" + ] + }, + "delete": { + "summary": "Delete role", + "parameters": [ + { + "$ref": "#/components/parameters/roleId" + } + ], + "responses": { + "200": { + "description": "Role deleted" + } + }, + "x-permission": "manage_roles", + "tags": [ + "Roles" + ] + } + }, + "/permissions": { + "get": { + "summary": "List all permissions", + "responses": { + "200": { + "description": "Permission definitions" + } + }, + "x-permission": "manage_roles", + "tags": [ + "Permissions" + ] + } + }, + "/settings": { + "get": { + "summary": "Get organisation settings", + "responses": { + "200": { + "description": "org_name, org_logo, accent_color" + } + }, + "x-permission": "view_settings", + "tags": [ + "Settings" + ] + }, + "put": { + "summary": "Update organisation settings", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SettingsUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Settings saved" + } + }, + "x-permission": "manage_settings", + "tags": [ + "Settings" + ] + } + }, + "/bulk/assign-ips": { + "post": { + "summary": "Bulk assign IPs to a device", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkAssignIps" + } + } + } + }, + "responses": { + "200": { + "description": "Per-IP success and failure lists" + } + }, + "x-permission": "add_device_ip", + "tags": [ + "Bulk" + ] + } + }, + "/bulk/create-devices": { + "post": { + "summary": "Bulk create devices", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkCreateDevices" + } + } + } + }, + "responses": { + "200": { + "description": "Per-device success and failure lists" + } + }, + "x-permission": "add_device", + "tags": [ + "Bulk" + ] + } + }, + "/bulk/assign-tags": { + "post": { + "summary": "Bulk assign tag to devices", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkAssignTags" + } + } + } + }, + "responses": { + "200": { + "description": "Per-device success and failure lists" + } + }, + "x-permission": "assign_device_tag", + "tags": [ + "Bulk" + ] + } + }, + "/bulk/export-subnets": { + "post": { + "summary": "Export multiple subnets as CSV", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkExportSubnets" + } + } + } + }, + "responses": { + "200": { + "description": "Combined CSV export", + "content": { + "text/csv": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + }, + "x-permission": "export_subnet_csv", + "tags": [ + "Bulk" + ] + } + } + } +} diff --git a/static/swagger.html b/static/swagger.html new file mode 100644 index 0000000..c1ef8b0 --- /dev/null +++ b/static/swagger.html @@ -0,0 +1,32 @@ + + + + + + IPAM API Documentation + + + + +
    + + + + +