├── api ├── data │ └── .gitkeep ├── first_init.py ├── main.py ├── f2b.py └── _defs.py ├── static ├── .gitkeep └── main.css ├── config ├── prod.env.js ├── dev.env.js └── index.js ├── src ├── assets │ └── logo.png ├── router │ └── index.js ├── main.js ├── App.vue └── components │ ├── Home.vue │ ├── Jails.vue │ └── Jail.vue ├── requirements.txt ├── jail.local.sample ├── index.html ├── package.json ├── .gitignore └── README.md /api/data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunderrrrrr/Fail2ban-FastAPI/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /api/first_init.py: -------------------------------------------------------------------------------- 1 | from f2b import generate_files 2 | 3 | if __name__ == "__main__": 4 | generate_files() -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2019.11.28 2 | chardet==3.0.4 3 | Click==7.0 4 | dataclasses==0.7 5 | fastapi==0.49.0 6 | h11==0.9.0 7 | httptools==0.1.1 8 | idna==2.9 9 | pydantic==1.4 10 | requests==2.23.0 11 | starlette==0.12.9 12 | urllib3==1.25.8 13 | uvicorn==0.11.3 14 | uvloop==0.14.0 15 | websockets==8.1 16 | -------------------------------------------------------------------------------- /jail.local.sample: -------------------------------------------------------------------------------- 1 | # 2 | # THIS FILE IS LOCATED IN /etc/fail2ban/jail.local 3 | # 4 | 5 | 6 | # DEFAULTS 7 | [DEFAULT] 8 | bantime = 1h 9 | findtime = 15m 10 | 11 | # SSH 12 | [sshd] 13 | enabled = true 14 | port = 22 15 | filter = sshd 16 | logpath = /var/log/auth.log 17 | maxretry = 3 18 | 19 | # NGINX 20 | [nginx-http-auth] 21 | enabled = true 22 | filter = nginx-http-auth 23 | port = http,https 24 | logpath = /var/log/nginx/error.log 25 | /var/log/nginx/site01.com/error.log 26 | /var/log/nginx/site02.com/error.log 27 | maxretry = 3 28 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from '@/components/Home' 4 | import Jails from '@/components/Jails' 5 | import Jail from '@/components/Jail' 6 | 7 | Vue.use(Router) 8 | 9 | export default new Router({ 10 | mode: 'history', 11 | routes: [ 12 | { 13 | path: '/', 14 | name: 'Home', 15 | component: Home 16 | }, 17 | { 18 | path: '/jails', 19 | name: 'Jails', 20 | component: Jails 21 | }, 22 | { 23 | path: '/jail/:jailname', 24 | name: 'Jail', 25 | component: Jail 26 | } 27 | ] 28 | }) 29 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | import { library } from '@fortawesome/fontawesome-svg-core' 7 | import { faBookDead, faSkull, faCaretRight, faArchway, faDownload, faSyncAlt } from '@fortawesome/free-solid-svg-icons' 8 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' 9 | import Notifications from 'vue-notification' 10 | 11 | Vue.config.productionTip = false 12 | 13 | Vue.use(Notifications) 14 | 15 | library.add( 16 | faBookDead, 17 | faSkull, 18 | faCaretRight, 19 | faArchway, 20 | faDownload, 21 | faSyncAlt 22 | ) 23 | 24 | Vue.component( 25 | 'font-awesome-icon', 26 | FontAwesomeIcon 27 | ) 28 | 29 | /* eslint-disable no-new */ 30 | new Vue({ 31 | el: '#app', 32 | router, 33 | template: '', 34 | components: { App } 35 | }) 36 | -------------------------------------------------------------------------------- /api/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | import requests 3 | from fastapi import FastAPI 4 | from pydantic import BaseModel 5 | from starlette.middleware.cors import CORSMiddleware 6 | from _defs import main, get_jail 7 | from f2b import ban_ip, unban_ip, generate_files 8 | 9 | app = FastAPI() 10 | api_host = "localhost" 11 | api_port = 8000 12 | 13 | origins = [ 14 | "*" 15 | ] 16 | 17 | app.add_middleware( 18 | CORSMiddleware, 19 | allow_origins=origins, 20 | allow_credentials=True, 21 | allow_methods=["*"], 22 | allow_headers=["*"], 23 | ) 24 | 25 | class Item(BaseModel): 26 | ip: str 27 | jail: str 28 | 29 | @app.get("/") 30 | def read_root(): 31 | uri = 'http://{}:{}/openapi.json'.format(api_host, api_port) 32 | r = requests.get(uri) 33 | return(r.json()['paths']) 34 | 35 | @app.get("/jails") 36 | def read_jails(): 37 | m = main() 38 | return(m) 39 | 40 | @app.get("/jail/{jail}") 41 | def read_jail(jail): 42 | jail_data = get_jail(jail) 43 | return (jail_data) 44 | 45 | @app.post("/ban") 46 | async def ip_ban(item: Item): 47 | ban = ban_ip(item.ip, item.jail) 48 | return item 49 | 50 | @app.post("/unban") 51 | async def ip_unban(item: Item): 52 | unban = unban_ip(item.ip, item.jail) 53 | return item 54 | 55 | @app.get("/refresh") 56 | def refresh(): 57 | generate_files() 58 | return "OK" 59 | 60 | if __name__ == "__main__": 61 | uvicorn.run("main:app", host=api_host, port=api_port, reload=True) -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report 23 | }, 24 | dev: { 25 | env: require('./dev.env'), 26 | port: 8080, 27 | autoOpenBrowser: true, 28 | assetsSubDirectory: 'static', 29 | assetsPublicPath: '/', 30 | proxyTable: {}, 31 | // CSS Sourcemaps off by default because relative paths are "buggy" 32 | // with this option, according to the CSS-Loader README 33 | // (https://github.com/webpack/css-loader#sourcemaps) 34 | // In our experience, they generally work as expected, 35 | // just be aware of this issue when enabling this option. 36 | cssSourceMap: false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Fail2ban FastAPI 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /api/f2b.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | data_path = os.path.dirname(os.path.realpath(__file__)) + "/data/" 5 | 6 | def create_status(): 7 | print("Running fail2ban-client status") 8 | filename = "jails.txt" 9 | data = os.popen('fail2ban-client status').read() 10 | file = open(data_path + filename, 'w') 11 | file.write(data) 12 | print("Wrote to {}{}\n".format(data_path, filename)) 13 | 14 | def get_jails(): 15 | f = open(data_path + "jails.txt", "r") 16 | l = [] 17 | for line in f: 18 | l.append(line) 19 | jail_names = l[2].split(':')[1].strip() 20 | jails = jail_names.split(', ') 21 | return(jails) 22 | 23 | def create_jail(jail): 24 | print("Running fail2ban-client status {}".format(jail)) 25 | data = os.popen('fail2ban-client status {}'.format(jail)).read() 26 | print("JAILDATA: ", data) 27 | file = open(data_path + "jail_" + jail + ".txt", 'w') 28 | file.write(data) 29 | print("wrote to {}jail_{}.txt\n".format(data_path, jail)) 30 | 31 | def ban_ip(ip, jail): 32 | print("Banning {} in jail {}".format(ip, jail)) 33 | data = os.popen('fail2ban-client set {} banip {}'.format(jail, ip)).read() 34 | print(data) 35 | time.sleep(1.5) # takes time to refresh jail (got diff in vue frontend) 36 | create_jail(jail) 37 | 38 | def unban_ip(ip, jail): 39 | print("Unbanning {} in jail {}".format(ip, jail)) 40 | data = os.popen('fail2ban-client set {} unbanip {}'.format(jail, ip)).read() 41 | print(data) 42 | time.sleep(1.5) # takes time to refresh jail (got diff in vue frontend) 43 | create_jail(jail) 44 | 45 | def generate_files(): 46 | status = create_status() 47 | jails = get_jails() 48 | for jail in jails: 49 | create_jail(jail) 50 | 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "router-app", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "node build/dev-server.js", 10 | "build": "node build/build.js" 11 | }, 12 | "dependencies": { 13 | "@fortawesome/fontawesome-svg-core": "^1.2.27", 14 | "@fortawesome/free-solid-svg-icons": "^5.12.1", 15 | "@fortawesome/vue-fontawesome": "^0.1.9", 16 | "axios": "^0.16.2", 17 | "vue": "^2.2.6", 18 | "vue-notification": "^1.3.20", 19 | "vue-router": "^2.3.1" 20 | }, 21 | "devDependencies": { 22 | "autoprefixer": "^6.7.2", 23 | "babel-core": "^6.22.1", 24 | "babel-loader": "^6.2.10", 25 | "babel-plugin-transform-runtime": "^6.22.0", 26 | "babel-preset-env": "^1.3.2", 27 | "babel-preset-stage-2": "^6.22.0", 28 | "babel-register": "^6.22.0", 29 | "chalk": "^1.1.3", 30 | "connect-history-api-fallback": "^1.3.0", 31 | "copy-webpack-plugin": "^4.0.1", 32 | "css-loader": "^0.28.0", 33 | "eventsource-polyfill": "^0.9.6", 34 | "express": "^4.14.1", 35 | "extract-text-webpack-plugin": "^2.0.0", 36 | "file-loader": "^0.11.1", 37 | "friendly-errors-webpack-plugin": "^1.1.3", 38 | "html-webpack-plugin": "^2.28.0", 39 | "http-proxy-middleware": "^0.17.3", 40 | "webpack-bundle-analyzer": "^2.2.1", 41 | "semver": "^5.3.0", 42 | "shelljs": "^0.7.6", 43 | "opn": "^4.0.2", 44 | "optimize-css-assets-webpack-plugin": "^1.3.0", 45 | "ora": "^1.2.0", 46 | "rimraf": "^2.6.0", 47 | "url-loader": "^0.5.8", 48 | "vue-loader": "^11.3.4", 49 | "vue-style-loader": "^2.0.5", 50 | "vue-template-compiler": "^2.2.6", 51 | "webpack": "^2.3.3", 52 | "webpack-dev-middleware": "^1.10.0", 53 | "webpack-hot-middleware": "^2.18.0", 54 | "webpack-merge": "^4.1.0" 55 | }, 56 | "engines": { 57 | "node": ">= 4.0.0", 58 | "npm": ">= 3.0.0" 59 | }, 60 | "browserslist": [ 61 | "> 1%", 62 | "last 2 versions", 63 | "not ie <= 8" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /src/components/Home.vue: -------------------------------------------------------------------------------- 1 | 46 | 78 | -------------------------------------------------------------------------------- /src/components/Jails.vue: -------------------------------------------------------------------------------- 1 | 45 | 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | api/data/*.txt 2 | node_modules/ 3 | npm-debug.log 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | 136 | # pytype static type analyzer 137 | .pytype/ 138 | node_modules 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fail2ban-fastapi 2 | ---------- 3 | 4 | Fail2Ban operates by monitoring log files (e.g. `/var/log/auth.log`, `/var/log/apache/access.log`, etc.) for selected entries and running scripts based on them. Most commonly this is used to block selected IP addresses that may belong to hosts that are trying to breach the system's security. 5 | 6 | **What is fail2ban-fastapi?** 7 | Frontend (vuejs) and backend (fastapi). FastAPI reads it's data from files created by this script (since Fail2ban's internal sqlite database is not human readable). The script user must have permissions to execute `$ fail2ban-client` to generate this data. 8 | ``` 9 | . 10 | ├── api 11 | │ └── data/ 12 | │ └── jails.txt 13 | │ └── jail_.txt 14 | │ └── jail_.txt 15 | ``` 16 | 17 | ![Fail2ban-FastAPI Demo](https://i.imgur.com/7sQhGeh.gif) 18 | 19 | ## Installation 20 | 21 | ### Fail2ban Setup 22 | 23 | First of all, the script needs to be able to run `fail2ban-client` command. This is usually done with root permissions. To solve this, you need to create a new (non-root) user and run fail2ban as this user. 24 | 25 | ```bash 26 | $ sudo apt install fail2ban 27 | $ sudo adduser fail2ban 28 | ``` 29 | 30 | Set `User` to "fail2ban" in the [Service] section in `/lib/systemd/system/fail2ban.service` 31 | ```text 32 | ... 33 | [Service] 34 | User=fail2ban 35 | ... 36 | ``` 37 | 38 | Change permissions of `/var/run/fail2ban`. 39 | ```bash 40 | $ sudo chown -R fail2ban:root /var/run/fail2ban 41 | ``` 42 | 43 | Verify permissions with `sudo ls -la /var/run/fail2ban/`. Make sure fail2ban user can read and write to this location. 44 | 45 | Depending on your jails, the fail2ban user needs to be able read your logfiles (ex. `/var/log/auth.log`). An example configuration can be found in [jail.local.sample](./jail.local.sample). 46 | 47 | So change permisions of your logfiles. 48 | ```bash 49 | $ sudo chown fail2ban:root /var/log/auth.log 50 | ``` 51 | 52 | Now you should be all set to reload `systemctl daemon` and restart fail2ban. 53 | 54 | ```bash 55 | $ sudo systemctl daemon-reload 56 | $ sudo systemctl restart fail2ban.service 57 | ``` 58 | 59 | Check status of fail2ban.service to make sure we're all set and you have no errors. 60 | 61 | ## Install Fail2ban-FastAPI 62 | 63 | Clone rep, create virtualenv and install requirements. 64 | ```bash 65 | $ git clone git@github.com:dunderrrrrr/fail2ban-fastapi.git 66 | $ mkvirtualenv --python=/usr/bin/python3 fail2ban-fastapi 67 | $ pip install -r requirements.txt 68 | ``` 69 | 70 | Before starting FastAPI backend we'll need to generate some Fail2ban data. This will only be necessary once when setting up Fail2ban-FastAPI for the first time on. 71 | ```bash 72 | $ cd api/ 73 | $ python first_init.py 74 | ``` 75 | 76 | Start FastAPI backend. 77 | ```bash 78 | $ python main.py 79 | ``` 80 | 81 | Start vuejs frontend. 82 | ```bash 83 | $ npm install 84 | $ npm run dev 85 | ``` 86 | 87 | FastAPI backend can be access at `http://localhost:8000`. 88 | Frontend access at `http://localhost:8080`. -------------------------------------------------------------------------------- /api/_defs.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | data_path = 'data/' 4 | data_format = '.txt' 5 | files = { 6 | "jails": "{}jails{}".format(data_path, data_format) 7 | } 8 | 9 | def get_jaildata(jail, jail_path): 10 | data = {} 11 | f = open(jail_path, "r") 12 | l = [] 13 | for line in f: 14 | l.append(line) 15 | data['fail_current'] = re.findall(r'\d+', l[2])[0] 16 | data['fail_total'] = re.findall(r'\d+', l[3])[0] 17 | data['bans_current'] = re.findall(r'\d+', l[6])[0] 18 | data['bans_total'] = re.findall(r'\d+', l[7])[0] 19 | data['log_file'] = l[4].split(':')[1].strip() 20 | data['bans_iplist'] = l[8].split(':')[1].strip().split(' ') 21 | return(data) 22 | 23 | def summary(obj_jails, jailnums): 24 | tot_bans_current = [] 25 | tot_bans_total = [] 26 | for k,v in obj_jails['jail'].items(): 27 | tot_bans_current.append(int(v['bans_current'])) 28 | tot_bans_total.append(int(v['bans_total'])) 29 | obj_jails['sum'] = {} 30 | obj_jails['sum']['jail_nums'] = int(jailnums) 31 | obj_jails['sum']['total_bans_current'] = sum(tot_bans_current) 32 | obj_jails['sum']['total_bans_total'] = sum(tot_bans_total) 33 | return(obj_jails) 34 | 35 | def main(): 36 | obj_jails = {} 37 | f = open(files['jails'], "r") 38 | l = [] 39 | for line in f: 40 | l.append(line) 41 | jailnums = re.findall(r'\d+', l[1])[0] 42 | jail_names = l[2].split(':')[1].strip() 43 | obj_jails['jail'] = {} 44 | for jail in jail_names.split(', '): 45 | jail_path = data_path + 'jail_' + jail + data_format 46 | jail_data = get_jaildata(jail, jail_path) 47 | obj_jails['jail'][jail] = { 48 | "jail_name": jail, 49 | "fails_current": jail_data['fail_current'], 50 | "fails_total": jail_data['fail_total'], 51 | "bans_current": jail_data['bans_current'], 52 | "bans_total": jail_data['bans_total'], 53 | "log_file": jail_data['log_file'], 54 | "bans_iplist": jail_data['bans_iplist'] 55 | } 56 | data = summary(obj_jails, jailnums) 57 | return(data) 58 | 59 | def get_jail(jail): 60 | f = open(files['jails'], "r") 61 | l = [] 62 | for line in f: 63 | l.append(line) 64 | jail_names = l[2].split(':')[1].strip().split(', ') 65 | if jail in jail_names: 66 | obj_jail = { 67 | jail: {} 68 | } 69 | jail_path = data_path + 'jail_' + jail + data_format 70 | jail_data = get_jaildata(jail, jail_path) 71 | obj_jail[jail] = { 72 | "fails_current": jail_data['fail_current'], 73 | "fails_total": jail_data['fail_total'], 74 | "bans_current": jail_data['bans_current'], 75 | "bans_total": jail_data['bans_total'], 76 | "log_file": jail_data['log_file'], 77 | "bans_iplist": jail_data['bans_iplist'] 78 | } 79 | return(obj_jail) 80 | else: 81 | return {"error": 404, "msg": "Jail name '{}' not found".format(jail)} -------------------------------------------------------------------------------- /static/main.css: -------------------------------------------------------------------------------- 1 | @import "https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700"; 2 | body { 3 | font-family: 'Poppins', sans-serif; 4 | background: #fafafa; 5 | } 6 | 7 | p { 8 | font-family: 'Poppins', sans-serif; 9 | font-size: 1.1em; 10 | font-weight: 300; 11 | line-height: 1.7em; 12 | color: #999; 13 | } 14 | 15 | a, 16 | a:hover, 17 | a:focus { 18 | color: inherit; 19 | text-decoration: none; 20 | transition: all 0.3s; 21 | } 22 | 23 | .navbar { 24 | padding: 15px 10px; 25 | background: #fff; 26 | border: none; 27 | border-radius: 0; 28 | margin-bottom: 40px; 29 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1); 30 | } 31 | 32 | .navbar-btn { 33 | box-shadow: none; 34 | outline: none !important; 35 | border: none; 36 | } 37 | 38 | .line { 39 | width: 100%; 40 | height: 1px; 41 | border-bottom: 1px dashed #ddd; 42 | margin: 40px 0; 43 | } 44 | 45 | .wrapper { 46 | display: flex; 47 | width: 100%; 48 | align-items: stretch; 49 | } 50 | 51 | #sidebar { 52 | min-width: 250px; 53 | max-width: 250px; 54 | background: #7386D5; 55 | color: #fff; 56 | transition: all 0.3s; 57 | } 58 | 59 | #sidebar.active { 60 | margin-left: -250px; 61 | } 62 | 63 | #sidebar .sidebar-header { 64 | padding: 20px; 65 | background: #6d7fcc; 66 | } 67 | 68 | #sidebar ul.components { 69 | padding: 20px 0; 70 | border-bottom: 1px solid #47748b; 71 | } 72 | 73 | #sidebar ul p { 74 | color: #fff; 75 | padding: 10px; 76 | } 77 | 78 | #sidebar ul li a { 79 | padding: 10px; 80 | font-size: 1.1em; 81 | display: block; 82 | } 83 | 84 | #sidebar ul li a:hover { 85 | color: #7386D5; 86 | background: #fff; 87 | } 88 | 89 | #sidebar ul li.active>a, 90 | a[aria-expanded="true"] { 91 | color: #fff; 92 | background: #6d7fcc; 93 | } 94 | 95 | a[data-toggle="collapse"] { 96 | position: relative; 97 | } 98 | 99 | .dropdown-toggle::after { 100 | display: block; 101 | position: absolute; 102 | top: 50%; 103 | right: 20px; 104 | transform: translateY(-50%); 105 | } 106 | 107 | ul ul a { 108 | font-size: 0.9em !important; 109 | padding-left: 30px !important; 110 | background: #6d7fcc; 111 | } 112 | 113 | ul.CTAs { 114 | padding: 20px; 115 | } 116 | 117 | ul.CTAs a { 118 | text-align: center; 119 | font-size: 0.9em !important; 120 | display: block; 121 | border-radius: 5px; 122 | margin-bottom: 5px; 123 | } 124 | 125 | a.download { 126 | background: #fff; 127 | color: #7386D5; 128 | } 129 | 130 | a.article, 131 | a.article:hover { 132 | background: #6d7fcc !important; 133 | color: #fff !important; 134 | } 135 | 136 | #content { 137 | width: 100%; 138 | padding: 20px; 139 | min-height: 100vh; 140 | transition: all 0.3s; 141 | } 142 | 143 | .delObjects { 144 | float:left; 145 | border:1px solid #CCC; 146 | } 147 | .delObjects:hover { 148 | background-color:#FFF; 149 | border:1px solid #999; 150 | 151 | } 152 | .logPath { 153 | font-size:12px; 154 | } 155 | .clearBoth { 156 | clear:both; 157 | } 158 | @media (max-width: 768px) { 159 | #sidebar { 160 | margin-left: -250px; 161 | } 162 | #sidebar.active { 163 | margin-left: 0; 164 | } 165 | #sidebarCollapse span { 166 | display: none; 167 | } 168 | } -------------------------------------------------------------------------------- /src/components/Jail.vue: -------------------------------------------------------------------------------- 1 | 71 | --------------------------------------------------------------------------------