From 966ad53bfe5dc8e1718c1f70001147824fc01c83 Mon Sep 17 00:00:00 2001
From: Robert Goldmann <deadlocker@gmx.de>
Date: Fri, 25 Sep 2020 09:13:55 +0200
Subject: [PATCH] Fixed #39 - render open api documentation with jinja

---
 api/Pipfile                 |  2 +-
 api/RoadmapAPIServer.py     | 14 +++++-
 api/docs/api.html           | 55 -----------------------
 api/docs/swagger-to-html.py | 87 -------------------------------------
 api/logic/Constants.py      |  3 ++
 api/settings-example.json   |  5 ++-
 api/templates/api.html      | 53 ++++++++++++++++++++++
 7 files changed, 73 insertions(+), 146 deletions(-)
 delete mode 100644 api/docs/api.html
 delete mode 100644 api/docs/swagger-to-html.py
 create mode 100644 api/templates/api.html

diff --git a/api/Pipfile b/api/Pipfile
index 48c2c74..462e30e 100644
--- a/api/Pipfile
+++ b/api/Pipfile
@@ -18,6 +18,6 @@ gevent = "==20.6.2"
 psycopg2 = "==2.8.5"
 TheCodeLabs-BaseUtils = "==1.3.1"
 TheCodeLabs-FlaskUtils = "==1.1.0"
+pyyaml = "==5.3.1"
 
 [dev-packages]
-pyyaml = "==5.3.1"
diff --git a/api/RoadmapAPIServer.py b/api/RoadmapAPIServer.py
index 8d3da52..5551c03 100644
--- a/api/RoadmapAPIServer.py
+++ b/api/RoadmapAPIServer.py
@@ -1,10 +1,12 @@
 import datetime
+import json
 import logging
 import os
 
+import yaml
 from TheCodeLabs_BaseUtils import DefaultLogger
 from TheCodeLabs_FlaskUtils import FlaskBaseApp
-from flask import jsonify
+from flask import jsonify, render_template
 from flask import send_from_directory, request
 from flask_jwt_extended import (
     JWTManager, create_access_token
@@ -31,7 +33,15 @@ class RoadmapApi(FlaskBaseApp):
 
         @app.route('/')
         def index():
-            return send_from_directory('docs', 'api.html')
+            yamlPath = os.path.join(Constants.ROOT_DIR, 'docs', 'api.yml')
+            with open(yamlPath, 'r') as yamlFile:
+                specification = yaml.load(yamlFile, Loader=yaml.FullLoader)
+
+            specification['servers'][0]['url'] = self._settings['api']['url']
+            specification['info']['version'] = self._version['name']
+
+            specification = json.dumps(specification)
+            return render_template('api.html', appName="Roadmaps", openApiSpecification=specification)
 
         @app.route('/login', methods=['POST'])
         def login():
diff --git a/api/docs/api.html b/api/docs/api.html
deleted file mode 100644
index 1e38140..0000000
--- a/api/docs/api.html
+++ /dev/null
@@ -1,55 +0,0 @@
-
-<!DOCTYPE html>
-<html lang="en">
-<head>
-  <meta charset="UTF-8">
-  <title>Roadmaps API</title>
-  <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
-  <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.22.2/swagger-ui.css" >
-  <style>
-    html
-    {
-      box-sizing: border-box;
-      overflow: -moz-scrollbars-vertical;
-      overflow-y: scroll;
-    }
-    *,
-    *:before,
-    *:after
-    {
-      box-sizing: inherit;
-    }
-
-    body {
-      margin:0;
-      background: #fafafa;
-    }
-  </style>
-</head>
-<body>
-
-<div id="swagger-ui"></div>
-
-<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.22.2/swagger-ui-bundle.js"> </script>
-<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.22.2/swagger-ui-standalone-preset.js"> </script>
-<script>
-window.onload = function() {
-
-  var spec = {"openapi": "3.0.0", "servers": [{"description": "Roadmaps API Server", "url": "https://roadmaps.thecodelabs.de/api"}], "info": {"description": "The Official Roadmaps API", "version": "2.7.0", "title": "Roadmaps API"}, "tags": [{"name": "admins", "description": "Secured Admin-only calls"}, {"name": "public", "description": "Operations available to the public"}], "paths": {"/version": {"get": {"tags": ["public"], "summary": "Gets information about the server version", "operationId": "version", "responses": {"200": {"description": "The server version information", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Version"}}}}}}}, "/login": {"post": {"tags": ["public"], "summary": "Gets a bearer JSON Web Token", "operationId": "login", "requestBody": {"description": "Credentials", "required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Credentials"}}}}, "responses": {"200": {"description": "success response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/TokenResponse"}}}}, "401": {"description": "unauthorized response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UnauthorizedResponse"}}}}, "default": {"description": "error response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}}}}}, "/roadmaps": {"get": {"tags": ["public"], "summary": "Gets all roadmaps", "operationId": "roadmaps", "responses": {"200": {"description": "All available roadmaps", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/Roadmap"}}}}}}}}, "/roadmap/{roadmapID}": {"get": {"tags": ["public"], "summary": "Gets  a specific roadmap", "operationId": "roadmap", "parameters": [{"in": "path", "name": "roadmapID", "description": "The roadmap ID", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "The roadmap", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Roadmap"}}}}}}, "delete": {"tags": ["admins"], "description": "Deletes a roadmap based on the given ID", "operationId": "deleteRoadmap", "parameters": [{"in": "path", "name": "roadmapID", "description": "The roadmap ID", "required": true, "schema": {"type": "integer"}}], "security": [{"BearerAuth": []}], "responses": {"200": {"description": "success response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SuccessResponse"}}}}, "401": {"description": "unauthorized response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UnauthorizedResponse"}}}}, "default": {"description": "error response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}}}}}, "/roadmap": {"post": {"tags": ["admins"], "summary": "Adds a new roadmap", "operationId": "addRoadmap", "requestBody": {"description": "Roadmap to add", "required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/NewRoadmap"}}}}, "security": [{"BearerAuth": []}], "responses": {"200": {"description": "success response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SuccessResponse"}}}}, "401": {"description": "unauthorized response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UnauthorizedResponse"}}}}, "default": {"description": "error response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}}}}, "put": {"tags": ["admins"], "summary": "Updates a roadmap", "operationId": "updateRoadmap", "requestBody": {"description": "Roadmap to update", "required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Roadmap"}}}}, "security": [{"BearerAuth": []}], "responses": {"200": {"description": "success response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SuccessResponse"}}}}, "401": {"description": "unauthorized response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UnauthorizedResponse"}}}}, "default": {"description": "error response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}}}}}, "/roadmap/{roadmapID}/full": {"get": {"tags": ["public"], "summary": "Gets  a specific roadmap with all milestones, tasks and sub tasks", "operationId": "roadmapFull", "parameters": [{"in": "path", "name": "roadmapID", "description": "The roadmap ID", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "The roadmap", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/RoadmapFull"}}}}}}}, "/milestones/{roadmapID}": {"get": {"tags": ["public"], "summary": "Gets all milestones for a roadmap", "operationId": "milestones", "parameters": [{"in": "path", "name": "roadmapID", "description": "The roadmap ID", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "All milestones for the given roadmap ID", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/Milestone"}}}}}}}}, "/milestones/{roadmapID}/open": {"get": {"tags": ["public"], "summary": "Gets all open milestones for a roadmap", "operationId": "openMilestones", "parameters": [{"in": "path", "name": "roadmapID", "description": "The roadmap ID", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "All open milestones for the given roadmap ID", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/Milestone"}}}}}}}}, "/milestones/{roadmapID}/latest": {"get": {"tags": ["public"], "summary": "Gets the lastest finished milestone for a roadmap", "operationId": "latestMilestone", "parameters": [{"in": "path", "name": "roadmapID", "description": "The roadmap ID", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "The latest finished milestone for the given roadmap ID", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Milestone"}}}}}}}, "/milestone/{milestoneID}": {"get": {"tags": ["public"], "summary": "Gets  a specific milestone", "operationId": "milestone", "parameters": [{"in": "path", "name": "milestoneID", "description": "The milestone ID", "required": true, "schema": {"type": "string"}}], "responses": {"200": {"description": "The milestone", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Milestone"}}}}}}, "delete": {"tags": ["admins"], "description": "Deletes a milestone based on the given ID", "operationId": "deleteMilestone", "parameters": [{"in": "path", "name": "milestoneID", "description": "The milestone ID", "required": true, "schema": {"type": "integer"}}], "security": [{"BearerAuth": []}], "responses": {"200": {"description": "success response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SuccessResponse"}}}}, "401": {"description": "unauthorized response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UnauthorizedResponse"}}}}, "default": {"description": "error response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}}}}}, "/milestone": {"post": {"tags": ["admins"], "summary": "Adds a new milestone", "operationId": "addMilestone", "requestBody": {"description": "Milestone to add", "required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/NewMilestone"}}}}, "security": [{"BearerAuth": []}], "responses": {"200": {"description": "success response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SuccessResponse"}}}}, "401": {"description": "unauthorized response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UnauthorizedResponse"}}}}, "default": {"description": "error response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}}}}, "put": {"tags": ["admins"], "summary": "Updates a milestone", "operationId": "updateMilestone", "requestBody": {"description": "Milestone to update", "required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Milestone"}}}}, "security": [{"BearerAuth": []}], "responses": {"200": {"description": "success response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SuccessResponse"}}}}, "401": {"description": "unauthorized response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UnauthorizedResponse"}}}}, "default": {"description": "error response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}}}}}, "/milestone/{milestoneID}/close": {"post": {"tags": ["admins"], "summary": "Closes a milestone and all corresponding tasks and sub tasks", "operationId": "closeMilestone", "parameters": [{"in": "path", "name": "milestoneID", "description": "The milestone ID", "required": true, "schema": {"type": "integer"}}], "security": [{"BearerAuth": []}], "responses": {"200": {"description": "success response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SuccessResponse"}}}}, "401": {"description": "unauthorized response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UnauthorizedResponse"}}}}, "default": {"description": "error response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}}}}}, "/tasks/{milestoneID}": {"get": {"tags": ["public"], "summary": "Gets all tasks for a milestone", "operationId": "tasks", "parameters": [{"in": "path", "name": "milestoneID", "description": "The milestone ID", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "All tasks for the given milestone ID", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/Task"}}}}}}}}, "/tasks/{milestoneID}/open": {"get": {"tags": ["public"], "summary": "Gets all open tasks for a milestone", "operationId": "openTasks", "parameters": [{"in": "path", "name": "milestoneID", "description": "The milestone ID", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "All open tasks for the given milestone ID", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/Task"}}}}}}}}, "/task/{taskID}": {"get": {"tags": ["public"], "summary": "Gets  a specific task", "operationId": "task", "parameters": [{"in": "path", "name": "taskID", "description": "The task ID", "required": true, "schema": {"type": "string"}}], "responses": {"200": {"description": "The task", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Task"}}}}}}, "delete": {"tags": ["admins"], "description": "Deletes a task based on the given ID", "operationId": "deleteTask", "parameters": [{"in": "path", "name": "taskID", "description": "The task ID", "required": true, "schema": {"type": "integer"}}], "security": [{"BearerAuth": []}], "responses": {"200": {"description": "success response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SuccessResponse"}}}}, "401": {"description": "unauthorized response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UnauthorizedResponse"}}}}, "default": {"description": "error response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}}}}}, "/task": {"post": {"tags": ["admins"], "summary": "Adds a new task", "operationId": "addTask", "requestBody": {"description": "Task to add", "required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/NewTask"}}}}, "security": [{"BearerAuth": []}], "responses": {"200": {"description": "success response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SuccessResponse"}}}}, "401": {"description": "unauthorized response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UnauthorizedResponse"}}}}, "default": {"description": "error response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}}}}, "put": {"tags": ["admins"], "summary": "Updates a task", "operationId": "updateTask", "requestBody": {"description": "Task to update", "required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Task"}}}}, "security": [{"BearerAuth": []}], "responses": {"200": {"description": "success response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SuccessResponse"}}}}, "401": {"description": "unauthorized response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UnauthorizedResponse"}}}}, "default": {"description": "error response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}}}}}, "/subtasks/{taskID}": {"get": {"tags": ["public"], "summary": "Gets all sub taks for a task", "operationId": "substasks", "parameters": [{"in": "path", "name": "taskID", "description": "The task ID", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "All sub tasks for the given task ID", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/SubTask"}}}}}}}}, "/subtasks/{taskID}/open": {"get": {"tags": ["public"], "summary": "Gets all open sub taks for a task", "operationId": "openSubtasks", "parameters": [{"in": "path", "name": "taskID", "description": "The task ID", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "All open sub tasks for the given task ID", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/SubTask"}}}}}}}}, "/subtask/{subtaskID}": {"get": {"tags": ["public"], "summary": "Gets  a specific sub task", "operationId": "subtask", "parameters": [{"in": "path", "name": "subtaskID", "description": "The sub task ID", "required": true, "schema": {"type": "string"}}], "responses": {"200": {"description": "The sub task", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SubTask"}}}}}}, "delete": {"tags": ["admins"], "description": "Deletes a sub task based on the given ID", "operationId": "deleteSubTask", "parameters": [{"in": "path", "name": "subtaskID", "description": "The sub task ID", "required": true, "schema": {"type": "integer"}}], "security": [{"BearerAuth": []}], "responses": {"200": {"description": "success response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SuccessResponse"}}}}, "401": {"description": "unauthorized response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UnauthorizedResponse"}}}}, "default": {"description": "error response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}}}}}, "/subtask": {"post": {"tags": ["admins"], "summary": "Adds a new sub task", "operationId": "addSubTask", "requestBody": {"description": "Sub task to add", "required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/NewSubTask"}}}}, "security": [{"BearerAuth": []}], "responses": {"200": {"description": "success response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SuccessResponse"}}}}, "401": {"description": "unauthorized response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UnauthorizedResponse"}}}}, "default": {"description": "error response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}}}}, "put": {"tags": ["admins"], "summary": "Updates a sub task", "operationId": "updateSubTask", "requestBody": {"description": "Sub task to update", "required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SubTask"}}}}, "security": [{"BearerAuth": []}], "responses": {"200": {"description": "success response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SuccessResponse"}}}}, "401": {"description": "unauthorized response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UnauthorizedResponse"}}}}, "default": {"description": "error response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}}}}}}, "components": {"securitySchemes": {"BearerAuth": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}}, "schemas": {"Version": {"type": "object", "required": ["code", "name", "date"], "properties": {"code": {"type": "integer", "example": 1}, "name": {"type": "string", "example": "v1.0.0"}, "date": {"type": "string", "format": "date", "example": "30.05.19"}}}, "Credentials": {"type": "object", "required": ["username", "password"], "properties": {"username": {"type": "string", "example": "admin"}, "password": {"type": "string", "example": "123"}}}, "Roadmap": {"type": "object", "required": ["ID", "Projectname"], "properties": {"ID": {"type": "integer", "example": 1}, "Projectname": {"type": "string", "example": "Example Roadmap"}}}, "RoadmapFull": {"allOf": [{"$ref": "#/components/schemas/Roadmap"}, {"type": "object", "properties": {"milestones": {"type": "array", "items": {"allOf": [{"$ref": "#/components/schemas/Milestone"}, {"type": "object", "properties": {"tasks": {"type": "array", "items": {"allOf": [{"$ref": "#/components/schemas/Task"}, {"type": "object", "properties": {"subtasks": {"type": "array", "items": {"$ref": "#/components/schemas/SubTask"}}}}]}}}}]}}}}]}, "NewRoadmap": {"type": "object", "required": ["ID", "Projectname"], "properties": {"ID": {"type": "integer", "example": 1}, "Projectname": {"type": "string", "example": "Example Roadmap"}}}, "Milestone": {"type": "object", "required": ["ID", "RoadmapID", "VersionCode", "VersionName", "Title", "DueDate", "CompletionDate", "Status"], "properties": {"ID": {"type": "integer", "example": 1}, "RoadmapID": {"type": "integer", "example": 2}, "VersionCode": {"type": "integer", "example": 3}, "VersionName": {"type": "string", "example": "v1.0.0"}, "Title": {"type": "string", "example": "My awesome milestone"}, "DueDate": {"type": "string", "format": "date", "example": "2019-05-30"}, "CompletionDate": {"type": "string", "format": "date", "example": "2019-05-30"}, "Status": {"type": "integer", "enum": [0, 1]}}}, "NewMilestone": {"type": "object", "required": ["RoadmapID", "VersionCode", "VersionName", "Title", "DueDate", "CompletionDate", "Status"], "properties": {"RoadmapID": {"type": "integer", "example": 2}, "VersionCode": {"type": "integer", "example": 3}, "VersionName": {"type": "string", "example": "v1.0.0"}, "Title": {"type": "string", "example": "My awesome milestone"}, "DueDate": {"type": "string", "format": "date", "example": "2019-05-30"}, "CompletionDate": {"type": "string", "format": "date", "example": "2019-05-30"}, "Status": {"type": "integer", "enum": [0, 1]}}}, "Task": {"type": "object", "required": ["ID", "MilestoneID", "Title", "Description", "Status"], "properties": {"ID": {"type": "integer", "example": 1}, "MilestoneID": {"type": "integer", "example": 2}, "Title": {"type": "string", "example": "My awesome task"}, "Description": {"type": "string", "example": "Lorem Ipsum dolor sit amet"}, "Status": {"type": "integer", "enum": [0, 1]}}}, "NewTask": {"type": "object", "required": ["MilestoneID", "Title", "Description", "Status"], "properties": {"MilestoneID": {"type": "integer", "example": 2}, "Title": {"type": "string", "example": "My awesome task"}, "Description": {"type": "string", "example": "Lorem Ipsum dolor sit amet"}, "Status": {"type": "integer", "enum": [0, 1]}}}, "SubTask": {"type": "object", "required": ["ID", "TaskID", "Title", "Description", "Status"], "properties": {"ID": {"type": "integer", "example": 1}, "TaskID": {"type": "integer", "example": 2}, "Title": {"type": "string", "example": "My awesome sub task"}, "Description": {"type": "string", "example": "Lorem Ipsum dolor sit amet"}, "Status": {"type": "integer", "enum": [0, 1]}}}, "NewSubTask": {"type": "object", "required": ["TaskID", "Title", "Description", "Status"], "properties": {"TaskID": {"type": "integer", "example": 2}, "Title": {"type": "string", "example": "My awesome sub task"}, "Description": {"type": "string", "example": "Lorem Ipsum dolor sit amet"}, "Status": {"type": "integer", "enum": [0, 1]}}}, "UnauthorizedResponse": {"description": "Access token is missing or invalid", "properties": {"msg": {"type": "string", "example": "Missing Authorization Header"}}}, "TokenResponse": {"description": "JSON web token", "properties": {"access_token": {"type": "string"}}}, "SuccessResponse": {"required": ["success"], "properties": {"success": {"type": "boolean"}}}, "ErrorResponse": {"required": ["success", "msg"], "properties": {"success": {"type": "boolean"}, "msg": {"type": "string"}}}}}};
-
-  // Build a system
-  const ui = SwaggerUIBundle({
-    spec: spec,
-    dom_id: '#swagger-ui',
-    deepLinking: true,
-    presets: [
-      SwaggerUIBundle.presets.apis
-    ]
-  })
-
-  window.ui = ui
-}
-</script>
-</body>
-
-</html>
diff --git a/api/docs/swagger-to-html.py b/api/docs/swagger-to-html.py
deleted file mode 100644
index 674c13d..0000000
--- a/api/docs/swagger-to-html.py
+++ /dev/null
@@ -1,87 +0,0 @@
-#!/usr/bin/python
-
-import yaml
-import json
-import sys
-import re
-
-TEMPLATE = """
-<!DOCTYPE html>
-<html lang="en">
-<head>
-  <meta charset="UTF-8">
-  <title>%s</title>
-  <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
-  <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.22.2/swagger-ui.css" >
-  <style>
-    html
-    {
-      box-sizing: border-box;
-      overflow: -moz-scrollbars-vertical;
-      overflow-y: scroll;
-    }
-    *,
-    *:before,
-    *:after
-    {
-      box-sizing: inherit;
-    }
-
-    body {
-      margin:0;
-      background: #fafafa;
-    }
-  </style>
-</head>
-<body>
-
-<div id="swagger-ui"></div>
-
-<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.22.2/swagger-ui-bundle.js"> </script>
-<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.22.2/swagger-ui-standalone-preset.js"> </script>
-<script>
-window.onload = function() {
-
-  var spec = %s;
-
-  // Build a system
-  const ui = SwaggerUIBundle({
-    spec: spec,
-    dom_id: '#swagger-ui',
-    deepLinking: true,
-    presets: [
-      SwaggerUIBundle.presets.apis
-    ]
-  })
-
-  window.ui = ui
-}
-</script>
-</body>
-
-</html>
-"""
-
-VERSION_PATTERN = r'"version": "\d+.\d+.\d+"'
-
-if len(sys.argv) != 3:
-    print('Invalid number of args')
-    print('Usage: swagger-to-html.py source.yml destination.html')
-    exit()
-
-with open(sys.argv[1], "r") as inputFile:
-    spec = yaml.load(inputFile, Loader=yaml.FullLoader)
-    spec = json.dumps(spec)
-
-    with open('../version.json', 'r') as f:
-        VERSION = json.load(f)
-    VERSION = VERSION['version']
-
-    versionNameShort = VERSION['name'].replace('v', '')
-    print('Setting version number to "{}"'.format(versionNameShort))
-    spec = re.sub(VERSION_PATTERN, '"version": "{}"'.format(versionNameShort), spec, flags=re.S)
-
-    spec = TEMPLATE % ("Roadmaps API", spec)
-    with open(sys.argv[2], "w") as f:
-        f.write(spec)
-        print("DONE")
diff --git a/api/logic/Constants.py b/api/logic/Constants.py
index 4e1b264..5108aeb 100644
--- a/api/logic/Constants.py
+++ b/api/logic/Constants.py
@@ -1 +1,4 @@
+import os
+
 APP_NAME = 'RoadmapApi'
+ROOT_DIR = os.path.dirname(os.path.dirname(__file__))
diff --git a/api/settings-example.json b/api/settings-example.json
index fc577dc..685e943 100644
--- a/api/settings-example.json
+++ b/api/settings-example.json
@@ -19,5 +19,8 @@
             "name": "admin",
             "password": "123"
         }
-    ]
+    ],
+    "api": {
+        "url": "http://localhost:10003"
+    }
 }
\ No newline at end of file
diff --git a/api/templates/api.html b/api/templates/api.html
new file mode 100644
index 0000000..86f2231
--- /dev/null
+++ b/api/templates/api.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+    <html lang="en">
+    <head>
+      <meta charset="UTF-8">
+      <title>{{ appName }} API</title>
+      <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
+      <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.22.2/swagger-ui.css" >
+      <style>
+        html
+        {
+          box-sizing: border-box;
+          overflow: -moz-scrollbars-vertical;
+          overflow-y: scroll;
+        }
+        *,
+        *:before,
+        *:after
+        {
+          box-sizing: inherit;
+        }
+
+        body {
+          margin:0;
+          background: #fafafa;
+        }
+      </style>
+    </head>
+    <body>
+
+        <div id="swagger-ui"></div>
+
+        <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.22.2/swagger-ui-bundle.js"> </script>
+        <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.22.2/swagger-ui-standalone-preset.js"> </script>
+        <script>
+        window.onload = function() {
+
+          var spec = {{ openApiSpecification|safe }};
+
+          // Build a system
+          const ui = SwaggerUIBundle({
+            spec: spec,
+            dom_id: '#swagger-ui',
+            deepLinking: true,
+            presets: [
+              SwaggerUIBundle.presets.apis
+            ]
+          })
+
+          window.ui = ui
+        }
+        </script>
+    </body>
+</html>
-- 
GitLab