59 Commits

Author SHA1 Message Date
Oscar Krause
139b669495 implemented origins & leases auto refresh button 2024-03-04 09:53:06 +01:00
Oscar Krause
e5ad0d0516 fixed setTimeout to setInterval 2024-03-04 09:52:56 +01:00
Oscar Krause
d5e14c8e2e updated bootstrap icons to 1.11.4 2024-03-04 09:52:43 +01:00
Oscar Krause
ee335cfbd7 updated bootstrap to 5.3.3 2024-03-04 09:50:42 +01:00
Oscar Krause
29ee3522b2 implemented origins & leases counter for sidebar 2024-03-04 09:18:11 +01:00
Oscar Krause
d7c8034672 code styling 2024-03-04 08:59:48 +01:00
Oscar Krause
a730a3ac05 improved dashboard config view 2024-03-04 08:59:36 +01:00
Oscar Krause
ffcf0d084e implemented endpoint for expired origins (origins without active leases) 2024-02-27 20:53:32 +01:00
Oscar Krause
128425e057 requirements.txt updated 2024-02-27 20:12:37 +01:00
Oscar Krause
3bd767cc85 Merge branch 'main' into ui
# Conflicts:
#	README.md
#	requirements.txt
2024-02-27 20:12:14 +01:00
Oscar Krause
68645b95cc removed "url_for" to prevent proxy http/https missmatch 2024-02-27 19:56:27 +01:00
Oscar Krause
6fbbf9f8a7 added meta tag for "Content-Security-Policy" 2024-02-27 08:21:40 +01:00
Oscar Krause
c7f5ae3d45 fixed layouts path 2023-07-04 13:45:27 +02:00
Oscar Krause
c461bed239 toggle api endpoints 2023-07-04 13:34:49 +02:00
Oscar Krause
a2db9ff446 typos 2023-07-04 13:34:49 +02:00
Oscar Krause
0cb1627447 added information about ipv6 may be must disabled 2023-07-04 13:34:49 +02:00
Oscar Krause
e8be60efb3 removed mysql from included docker drivers 2023-07-04 13:34:49 +02:00
Oscar Krause
db37bbc344 added docker command to logging section
thanks to @libreshare (https://gitea.publichub.eu/oscar.krause/fastapi-dls/issues/2)
2023-07-04 13:34:49 +02:00
Oscar Krause
117e66f0db improvements
thanks to @AbsolutelyFree (https://gitea.publichub.eu/oscar.krause/fastapi-dls/issues/1)
2023-07-04 13:34:49 +02:00
Oscar Krause
b298755faa fixed "deploy:pacman" 2023-07-04 13:34:49 +02:00
Oscar Krause
fb27616b93 fixed mariadb-client installation
ref. https://github.com/PyMySQL/mysqlclient/discussions/624
2023-07-04 13:34:49 +02:00
Oscar Krause
0a08b033ea added missing "pkg-config" for "mysqlclient==2.2.0"
ref. https://stackoverflow.com/questions/76533384/docker-alpine-build-fails-on-mysqlclient-installation-with-error-exception-can
2023-07-04 13:34:49 +02:00
Oscar Krause
8c6df6eb3a fixed versions 2023-07-04 13:34:49 +02:00
Oscar Krause
ef5eead1c3 refactored docker-compose.yml so very simple example, and moved proxy to "examples" directory 2023-07-04 13:34:49 +02:00
Oscar Krause
d8cae944e7 added 15.3 to supported drivers list 2023-07-04 13:34:49 +02:00
Oscar Krause
75a5ae7b29 updated compatibility list 2023-07-04 13:34:49 +02:00
Oscar Krause
5ff9d1a228 docker-compose.yml - added note for TZ 2023-07-04 13:34:49 +02:00
Oscar Krause
b46593f27e requirements.txt updated 2023-07-04 13:34:49 +02:00
Oscar Krause
16271ad00b implemented button for delete all expired leases 2023-06-12 11:41:01 +02:00
Oscar Krause
8d236af289 Merge branch 'dev' into ui
# Conflicts:
#	requirements.txt
2023-06-12 11:27:36 +02:00
Oscar Krause
50c996f199 fixed "x in y" statement 2023-01-23 07:43:39 +01:00
Oscar Krause
e0736b6199 fixed Origin.delete() 2023-01-23 07:19:18 +01:00
Oscar Krause
5a59864383 Merge remote-tracking branch 'origin/dev' into ui 2023-01-23 07:19:05 +01:00
Oscar Krause
024ca111a9 fixes 2023-01-18 12:04:42 +01:00
Oscar Krause
6dbd39b76c helper.js - added "lease_renewal" to table 2023-01-18 08:42:57 +01:00
Oscar Krause
21f49a2fb5 fixed return type 2023-01-18 08:42:38 +01:00
Oscar Krause
757bae849c fixed origin null pointer exception on leases table 2023-01-18 08:42:28 +01:00
Oscar Krause
e1259838db Merge branch 'dev' into ui
# Conflicts:
#	app/orm.py
2023-01-18 08:39:06 +01:00
Oscar Krause
4699c1770d Merge branch 'dev' into ui
# Conflicts:
#	app/main.py
2023-01-05 07:31:15 +01:00
Oscar Krause
0c5fb7566c PKGBUILD - fixed jinja dependency 2023-01-04 14:43:37 +01:00
Oscar Krause
995e959bb1 dashboard_origins.html - implemented confirmation before deleting all origins 2023-01-04 12:11:55 +01:00
Oscar Krause
7eb3d7434d implemented refresh button 2023-01-04 12:11:23 +01:00
Oscar Krause
efc8d9c564 display hostname as title on leases table 2023-01-04 12:00:15 +01:00
Oscar Krause
6ef1216db6 Merge branch 'dev' into ui
# Conflicts:
#	app/main.py
2023-01-04 11:19:32 +01:00
Oscar Krause
f48fc4ff98 display hostname as title on leases table 2023-01-04 11:17:00 +01:00
Oscar Krause
41cb7ae14f implemented delete origin endpoint for frontend 2023-01-04 09:52:51 +01:00
Oscar Krause
5035dd4947 fixed refreshing data after deleting some origin or lease 2023-01-04 09:52:15 +01:00
Oscar Krause
08440d242f implemented delete origin endpoint for frontend 2023-01-04 09:51:43 +01:00
Oscar Krause
f35a4c6145 fixed timezone handling 2023-01-03 21:49:24 +01:00
Oscar Krause
f199800040 sort leases by expires 2023-01-03 21:29:32 +01:00
Oscar Krause
ca96ae08a7 sort origins by hostname 2023-01-03 21:29:24 +01:00
Oscar Krause
49279b6b66 fixed origin table 2023-01-03 21:23:57 +01:00
Oscar Krause
d06e100bef .gitlab-ci.yml - rebuild docker image when requirements.txt has changed 2023-01-03 21:03:34 +01:00
Oscar Krause
c5e344b38a added jinja2 dependency 2023-01-03 21:01:29 +01:00
Oscar Krause
4b4979cf86 fixed imports and tests 2023-01-03 20:55:23 +01:00
Oscar Krause
139fdd3472 Merge branch 'dev' into ui 2023-01-03 20:45:51 +01:00
Oscar Krause
71c4972e6a added config values to dashboard 2023-01-03 15:03:09 +01:00
Oscar Krause
df43a598cd Merge branch 'dev' into ui 2023-01-03 14:55:26 +01:00
Oscar Krause
fc8ef174cd implemented basic ui 2023-01-02 18:01:44 +01:00
34 changed files with 850 additions and 67 deletions

View File

@@ -2,7 +2,7 @@ Package: fastapi-dls
Version: 0.0
Architecture: all
Maintainer: Oscar Krause oscar.krause@collinwebdesigns.de
Depends: python3, python3-fastapi, python3-uvicorn, python3-dotenv, python3-dateutil, python3-jose, python3-sqlalchemy, python3-pycryptodome, python3-markdown, uvicorn, openssl
Depends: python3, python3-fastapi, python3-uvicorn, python3-dotenv, python3-dateutil, python3-jose, python3-sqlalchemy, python3-pycryptodome, python3-markdown, python3-jinja2, uvicorn, openssl
Recommends: curl
Installed-Size: 10240
Homepage: https://git.collinwebdesigns.de/oscar.krause/fastapi-dls

View File

@@ -8,7 +8,7 @@ pkgdesc='NVIDIA DLS server implementation with FastAPI'
arch=('any')
url='https://git.collinwebdesigns.de/oscar.krause/fastapi-dls'
license=('MIT')
depends=('python' 'python-jose' 'python-starlette' 'python-httpx' 'python-fastapi' 'python-dotenv' 'python-dateutil' 'python-sqlalchemy' 'python-pycryptodome' 'uvicorn' 'python-markdown' 'openssl')
depends=('python' 'python-jose' 'python-starlette' 'python-httpx' 'python-fastapi' 'python-dotenv' 'python-dateutil' 'python-sqlalchemy' 'python-pycryptodome' 'python-jinja' 'uvicorn' 'python-markdown' 'openssl')
provider=("$pkgname")
install="$pkgname.install"
backup=('etc/default/fastapi-dls')
@@ -37,17 +37,52 @@ check() {
}
package() {
# create directories
install -d "$pkgdir/usr/share/doc/$pkgname"
install -d "$pkgdir/var/lib/$pkgname/cert"
# copy docs & static files
cp -r "$srcdir/$pkgname/doc"/* "$pkgdir/usr/share/doc/$pkgname/"
install -Dm644 "$srcdir/$pkgname/README.md" "$pkgdir/usr/share/doc/$pkgname/README.md"
install -Dm644 "$srcdir/$pkgname/version.env" "$pkgdir/usr/share/doc/$pkgname/version.env"
sed -i "s/README.md/\/usr\/share\/doc\/$pkgname\/README.md/g" "$srcdir/$pkgname/app/main.py"
# copy main app python files
sed -i "s/join(dirname(__file__), 'cert\//join('\/var\/lib\/$pkgname', 'cert\//g" "$srcdir/$pkgname/app/main.py"
install -Dm755 "$srcdir/$pkgname/app/main.py" "$pkgdir/opt/$pkgname/main.py"
install -Dm755 "$srcdir/$pkgname/app/orm.py" "$pkgdir/opt/$pkgname/orm.py"
install -Dm755 "$srcdir/$pkgname/app/util.py" "$pkgdir/opt/$pkgname/util.py"
# copy static asset files
install -Dm755 "$srcdir/$pkgname/app/static/assets/css/bootstrap.min.css" "$pkgdir/opt/$pkgname/static/assets/css/bootstrap.min.css"
install -Dm755 "$srcdir/$pkgname/app/static/assets/css/bootstrap-icons.min.css" "$pkgdir/opt/$pkgname/static/assets/css/bootstrap-icons.min.css"
install -Dm755 "$srcdir/$pkgname/app/static/assets/css/custom.css" "$pkgdir/opt/$pkgname/static/assets/css/custom.css"
install -Dm755 "$srcdir/$pkgname/app/static/assets/css/dashboard.css" "$pkgdir/opt/$pkgname/static/assets/css/dashboard.css"
install -Dm755 "$srcdir/$pkgname/app/static/assets/fonts/bootstrap-icons.woff" "$pkgdir/opt/$pkgname/static/assets/fonts/bootstrap-icons.woff"
install -Dm755 "$srcdir/$pkgname/app/static/assets/fonts/bootstrap-icons.woff2" "$pkgdir/opt/$pkgname/static/assets/fonts/bootstrap-icons.woff2"
install -Dm755 "$srcdir/$pkgname/app/static/assets/img/favicons/android-chrome-192x192.png" "$pkgdir/opt/$pkgname/static/assets/img/favicons/android-chrome-192x192.png"
install -Dm755 "$srcdir/$pkgname/app/static/assets/img/favicons/android-chrome-512x512.png" "$pkgdir/opt/$pkgname/static/assets/img/favicons/android-chrome-512x512.png"
install -Dm755 "$srcdir/$pkgname/app/static/assets/img/favicons/apple-touch-icon.png" "$pkgdir/opt/$pkgname/static/assets/img/favicons/apple-touch-icon.png"
install -Dm755 "$srcdir/$pkgname/app/static/assets/img/favicons/favicon.ico" "$pkgdir/opt/$pkgname/static/assets/img/favicons/favicon.ico"
install -Dm755 "$srcdir/$pkgname/app/static/assets/img/favicons/favicon-16x16.png" "$pkgdir/opt/$pkgname/static/assets/img/favicons/favicon-16x16.png"
install -Dm755 "$srcdir/$pkgname/app/static/assets/img/favicons/favicon-32x32.png" "$pkgdir/opt/$pkgname/static/assets/img/favicons/favicon-32x32.png"
install -Dm755 "$srcdir/$pkgname/app/static/assets/img/favicons/manifest.json" "$pkgdir/opt/$pkgname/static/assets/img/favicons/manifest.json"
install -Dm755 "$srcdir/$pkgname/app/static/assets/img/logo.png" "$pkgdir/opt/$pkgname/static/assets/img/logo.png"
install -Dm755 "$srcdir/$pkgname/app/static/assets/js/bootstrap.min.js" "$pkgdir/opt/$pkgname/static/assets/js/bootstrap.min.js"
install -Dm755 "$srcdir/$pkgname/app/static/assets/js/helper.js" "$pkgdir/opt/$pkgname/static/assets/js/helper.js"
install -Dm755 "$srcdir/$pkgname/app/static/assets/js/popper.min.js" "$pkgdir/opt/$pkgname/static/assets/js/popper.min.js"
install -Dm755 "$srcdir/$pkgname/app/templates/components/navbar.html" "$pkgdir/opt/$pkgname/templates/components/navbar.html"
install -Dm755 "$srcdir/$pkgname/app/templates/components/sidebar.html" "$pkgdir/opt/$pkgname/templates/components/sidebar.html"
install -Dm755 "$srcdir/$pkgname/app/templates/layouts/base.html" "$pkgdir/opt/$pkgname/templates/layouts/base.html"
install -Dm755 "$srcdir/$pkgname/app/templates/layouts/bootstrap.html" "$pkgdir/opt/$pkgname/templates/layouts/bootstrap.html"
install -Dm755 "$srcdir/$pkgname/app/templates/layouts/bootstrap-dashboard.html" "$pkgdir/opt/$pkgname/templates/layouts/bootstrap-dashboard.html"
install -Dm755 "$srcdir/$pkgname/app/templates/views/dashboard.html" "$pkgdir/opt/$pkgname/templates/views/dashboard.html"
install -Dm755 "$srcdir/$pkgname/app/templates/views/dashboard_leases.html" "$pkgdir/opt/$pkgname/templates/views/dashboard_leases.html"
install -Dm755 "$srcdir/$pkgname/app/templates/views/dashboard_origins.html" "$pkgdir/opt/$pkgname/templates/views/dashboard_origins.html"
install -Dm755 "$srcdir/$pkgname/app/templates/views/dashboard_readme.html" "$pkgdir/opt/$pkgname/templates/views/dashboard_readme.html"
install -Dm755 "$srcdir/$pkgname/app/templates/views/index.html" "$pkgdir/opt/$pkgname/templates/views/index.html"
# copy service files
install -Dm644 "$srcdir/$pkgname.default" "$pkgdir/etc/default/$pkgname"
install -Dm644 "$srcdir/$pkgname.service" "$pkgdir/usr/lib/systemd/system/$pkgname.service"
install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"

View File

@@ -20,6 +20,7 @@ build:docker:
changes:
- app/**/*
- Dockerfile
- requirements.txt
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
tags: [ docker ]
before_script:

View File

@@ -18,6 +18,8 @@ from starlette.middleware.cors import CORSMiddleware
from starlette.responses import StreamingResponse, JSONResponse as JSONr, HTMLResponse as HTMLr, Response, RedirectResponse
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from starlette.staticfiles import StaticFiles
from starlette.templating import Jinja2Templates
from util import load_key, load_file
from orm import Origin, Lease, init as db_init, migrate
@@ -30,6 +32,9 @@ VERSION, COMMIT, DEBUG = env('VERSION', 'unknown'), env('COMMIT', 'unknown'), bo
config = dict(openapi_url=None, docs_url=None, redoc_url=None) # dict(openapi_url='/-/openapi.json', docs_url='/-/docs', redoc_url='/-/redoc')
app = FastAPI(title='FastAPI-DLS', description='Minimal Delegated License Service (DLS).', version=VERSION, **config)
app.mount('/static', StaticFiles(directory=join(dirname(__file__), 'static'), html=True), name='static'),
templates = Jinja2Templates(directory=join(dirname(__file__), 'templates'))
db = create_engine(str(env('DATABASE', 'sqlite:///db.sqlite')))
db_init(db), migrate(db)
@@ -71,24 +76,8 @@ def __get_token(request: Request) -> dict:
return jwt.decode(token=token, key=jwt_decode_key, algorithms=ALGORITHMS.RS256, options={'verify_aud': False})
@app.get('/', summary='Index')
async def index():
return RedirectResponse('/-/readme')
@app.get('/-/', summary='* Index')
async def _index():
return RedirectResponse('/-/readme')
@app.get('/-/health', summary='* Health')
async def _health():
return JSONr({'status': 'up'})
@app.get('/-/config', summary='* Config', description='returns environment variables.')
async def _config():
return JSONr({
def __json_config() -> dict:
return {
'VERSION': str(VERSION),
'COMMIT': str(COMMIT),
'DEBUG': str(DEBUG),
@@ -102,52 +91,64 @@ async def _config():
'LEASE_RENEWAL_PERIOD': str(LEASE_RENEWAL_PERIOD),
'CORS_ORIGINS': str(CORS_ORIGINS),
'TZ': str(TZ),
})
# static / calculated
'LEASE_RENEWAL_DELTA': str(LEASE_RENEWAL_DELTA),
'LEASE_CALCULATED_RENEWAL': str(Lease.calculate_renewal(LEASE_RENEWAL_PERIOD, LEASE_RENEWAL_DELTA)),
'CLIENT_TOKEN_EXPIRE_DELTA': str(CLIENT_TOKEN_EXPIRE_DELTA),
}
@app.get('/', summary='* Index')
async def index():
return RedirectResponse('/-/')
@app.get('/-/', summary='* Index')
async def _index(request: Request):
return templates.TemplateResponse(name='views/index.html', context={'request': request, 'VERSION': VERSION})
@app.get('/-/health', summary='* Health')
async def _health():
return JSONr({'status': 'up'})
@app.get('/-/config', summary='* Config', description='returns environment variables.')
async def _config():
return JSONr(__json_config())
@app.get('/-/readme', summary='* Readme')
async def _readme():
async def _readme(request: Request):
from markdown import markdown
content = load_file('../README.md').decode('utf-8')
return HTMLr(markdown(text=content, extensions=['tables', 'fenced_code', 'md_in_html', 'nl2br', 'toc']))
markdown = markdown(text=content, extensions=['tables', 'fenced_code', 'md_in_html', 'nl2br', 'toc'])
context = {'request': request, 'VERSION': VERSION, 'markdown': markdown }
return templates.TemplateResponse(name='views/dashboard_readme.html', context=context)
@app.get('/-/manage', summary='* Management UI')
async def _manage(request: Request):
response = '''
<!DOCTYPE html>
<html>
<head>
<title>FastAPI-DLS Management</title>
</head>
<body>
<button onclick="deleteOrigins()">delete ALL origins and their leases</button>
<button onclick="deleteLease()">delete specific lease</button>
<script>
function deleteOrigins() {
const response = confirm('Are you sure you want to delete all origins and their leases?');
context = {'request': request, 'VERSION': VERSION}
return templates.TemplateResponse(name='views/manage.html', context=context)
if (response) {
var xhr = new XMLHttpRequest();
xhr.open("DELETE", '/-/origins', true);
xhr.send();
}
}
function deleteLease(lease_ref) {
if(lease_ref === undefined)
lease_ref = window.prompt("Please enter 'lease_ref' which should be deleted");
if(lease_ref === null || lease_ref === "")
return
var xhr = new XMLHttpRequest();
xhr.open("DELETE", `/-/lease/${lease_ref}`, true);
xhr.send();
}
</script>
</body>
</html>
'''
return HTMLr(response)
@app.get('/-/dashboard', summary='* Dashboard')
async def _dashboard(request: Request):
context = {'request': request, 'VERSION': VERSION, 'CONFIG': __json_config()}
return templates.TemplateResponse(name='views/dashboard.html', context=context)
@app.get('/-/dashboard/origins', summary='* Dashboard - Origins')
async def _dashboard_origins(request: Request):
context = {'request': request, 'VERSION': VERSION}
return templates.TemplateResponse(name='views/dashboard_origins.html', context=context)
@app.get('/-/dashboard/leases', summary='* Dashboard - Leases')
async def _dashboard_origins(request: Request):
context = {'request': request, 'VERSION': VERSION}
return templates.TemplateResponse(name='views/dashboard_leases.html', context=context)
@app.get('/-/origins', summary='* Origins')
@@ -170,6 +171,19 @@ async def _origins_delete(request: Request):
return Response(status_code=201)
@app.delete('/-/origins/expired', summary='* Delete all Origins without active Lease')
async def _origins_delete_expired(request: Request):
Origin.delete_expired(db)
return Response(status_code=201)
@app.delete('/-/origins/{origin_ref}', summary='* Delete specific Origin')
async def _origins_delete_origin_ref(request: Request, origin_ref: str):
if Origin.delete(db, [origin_ref]) == 1:
return Response(status_code=201)
return JSONr(status_code=404, content={'status': 404, 'detail': 'lease not found'})
@app.get('/-/leases', summary='* Leases')
async def _leases(request: Request, origin: bool = False):
session = sessionmaker(bind=db)()
@@ -186,13 +200,13 @@ async def _leases(request: Request, origin: bool = False):
return JSONr(response)
@app.delete('/-/leases/expired', summary='* Leases')
@app.delete('/-/leases/expired', summary='* Delete all expired Leases')
async def _lease_delete_expired(request: Request):
Lease.delete_expired(db)
return Response(status_code=201)
@app.delete('/-/lease/{lease_ref}', summary='* Lease')
@app.delete('/-/lease/{lease_ref}', summary='* Delete specific Lease')
async def _lease_delete(request: Request, lease_ref: str):
if Lease.delete(db, lease_ref) == 1:
return Response(status_code=201)

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from dateutil.relativedelta import relativedelta
from sqlalchemy import Column, VARCHAR, CHAR, ForeignKey, DATETIME, update, and_, inspect, text
@@ -61,7 +61,17 @@ class Origin(Base):
if origin_refs is None:
deletions = session.query(Origin).delete()
else:
deletions = session.query(Origin).filter(Origin.origin_ref in origin_refs).delete()
deletions = session.query(Origin).filter(Origin.origin_ref.in_(origin_refs)).delete()
session.commit()
session.close()
return deletions
@staticmethod
def delete_expired(engine: Engine) -> int:
session = sessionmaker(bind=engine)()
origins = session.query(Origin).join(Lease, Origin.origin_ref == Lease.origin_ref, isouter=True).filter(Lease.lease_ref.is_(None)).all()
origin_refs = [origin.origin_ref for origin in origins]
deletions = session.query(Origin).filter(Origin.origin_ref.in_(origin_refs)).delete()
session.commit()
session.close()
return deletions
@@ -89,10 +99,10 @@ class Lease(Base):
'lease_ref': self.lease_ref,
'origin_ref': self.origin_ref,
# 'scope_ref': self.scope_ref,
'lease_created': self.lease_created.isoformat(),
'lease_expires': self.lease_expires.isoformat(),
'lease_updated': self.lease_updated.isoformat(),
'lease_renewal': lease_renewal.isoformat(),
'lease_created': self.lease_created.replace(tzinfo=timezone.utc).isoformat(),
'lease_expires': self.lease_expires.replace(tzinfo=timezone.utc).isoformat(),
'lease_updated': self.lease_updated.replace(tzinfo=timezone.utc).isoformat(),
'lease_renewal': lease_renewal.replace(tzinfo=timezone.utc).isoformat(),
}
@staticmethod

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,56 @@
/*
Original: #76b900
Darken 1: #5DA000 (10%)
Darken 2: #438600 (20%)
Darken 3: #2A6D00 (30%)
Darken 4: #105300 (40%)
Darken 5: #003A00 (50%)
*/
.text-primary {
color: #76b900 !important;
}
.lead {
color: #105300 !important;
}
.navbar-green {
background-color: #76b900 !important;
}
.navbar-brand {
background-color: transparent;
color: #ffffff;
}
.navbar-brand:focus, .navbar-brand:hover {
color: #fcfcfc;
}
.btn-primary {
background-color: #76b900 !important;
border-color: #76b900 !important;
}
.btn-primary:focus, .btn-primary:hover {
background-color: #5DA000 !important;
border-color: #5DA000 !important;
}
code {
color: #105300 !important;
}
.sidebar .nav-link.active {
color: #76b900 !important;
}
.sidebar .nav-link:focus, .sidebar .nav-link:hover {
color: #105300 !important;
}
.navbar-nav .nav-item .nav-link {
color: white !important;
}

View File

@@ -0,0 +1,101 @@
body {
font-size: .875rem;
}
.feather {
width: 16px;
height: 16px;
vertical-align: text-bottom;
}
/*
* Sidebar
*/
.sidebar {
position: fixed;
top: 0;
/* rtl:raw:
right: 0;
*/
bottom: 0;
/* rtl:remove */
left: 0;
z-index: 100; /* Behind the navbar */
padding: 48px 0 0; /* Height of navbar */
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
@media (max-width: 767.98px) {
.sidebar {
top: 0;
}
}
.sidebar-sticky {
position: relative;
top: 0;
height: calc(100vh - 48px);
padding-top: .5rem;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
}
.sidebar .nav-link .feather {
margin-right: 4px;
color: #727272;
}
.sidebar .nav-link.active {
color: #2470dc;
}
.sidebar .nav-link:hover .feather,
.sidebar .nav-link.active .feather {
color: inherit;
}
.sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
}
/*
* Navbar
*/
.navbar-brand {
padding-top: .75rem;
padding-bottom: .75rem;
font-size: 1rem;
background-color: rgba(0, 0, 0, .25);
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
}
.navbar .navbar-toggler {
top: .25rem;
right: 1rem;
}
.navbar .form-control {
padding: .75rem 1rem;
border-width: 0;
border-radius: 0;
}
.form-control-dark {
color: #fff;
background-color: rgba(255, 255, 255, .1);
border-color: rgba(255, 255, 255, .1);
}
.form-control-dark:focus {
border-color: transparent;
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

7
app/static/assets/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,139 @@
async function fetchConfig(element) {
let xhr = new XMLHttpRequest();
xhr.open("GET", '/-/config', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
element.innerHTML = JSON.stringify(JSON.parse(xhr.response),null,2);
}
};
xhr.send();
}
async function fetchOriginsWithLeases(element) {
let xhr = new XMLHttpRequest();
xhr.open("GET", '/-/origins?leases=true', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
const x = JSON.parse(xhr.response)
console.debug(x)
element.innerHTML = ''
let table = document.createElement('table')
table.classList.add('table', 'mt-4');
let thead = document.createElement('thead');
thead.innerHTML = `
<tr>
<th scope="col">origin</th>
<th scope="col">hostname</th>
<th scope="col">OS</th>
<th scope="col">driver version</th>
<th scope="col">leases</th>
</tr>`
table.appendChild(thead)
let tbody = document.createElement('thead');
x.sort((a, b) => a.hostname.localeCompare(b.hostname)).forEach((o) => {
let row = document.createElement('tr');
row.innerHTML = `
<td><code>${o.origin_ref}</code></td>
<td>${o.hostname}</td>
<td>${o.os_platform} (${o.os_version})</td>
<td>${o.guest_driver_version}</td>
<td>${o.leases.map(x => `<code title="expires: ${x.lease_expires}">${x.lease_ref}</code>`).join(', ')}</td>`
tbody.appendChild(row);
})
table.appendChild(tbody)
element.appendChild(table)
}
};
xhr.send();
}
async function fetchLeases(element) {
// datetime config
const dtc = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
timeZoneName: "short"
}
let xhr = new XMLHttpRequest();
xhr.open("GET", '/-/leases?origin=true', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
const x = JSON.parse(xhr.response)
console.debug(x)
element.innerHTML = ''
let table = document.createElement('table')
table.classList.add('table', 'mt-4');
let thead = document.createElement('thead');
thead.innerHTML = `
<tr>
<th scope="col">lease</th>
<th scope="col">created</th>
<th scope="col">updated</th>
<th scope="col">next renew</th>
<th scope="col">expires</th>
<th scope="col">origin</th>
</tr>`
table.appendChild(thead)
let tbody = document.createElement('thead');
x.sort((a, b) => new Date(a.lease_expires) - new Date(b.lease_expires)).forEach((o) => {
let row = document.createElement('tr');
row.innerHTML = `
<td><code>${o.lease_ref}</code></td>
<td>${new Date(o.lease_created).toLocaleDateString('system', dtc)}</td>
<td>${new Date(o.lease_updated).toLocaleDateString('system', dtc)}</td>
<td>${new Date(o.lease_renewal).toLocaleDateString('system', dtc)}</td>
<td>${new Date(o.lease_expires).toLocaleDateString('system', dtc)}</td>
<td><code title="hostname: ${o.origin?.hostname}">${o.origin_ref}</code></td>`
tbody.appendChild(row);
})
table.appendChild(tbody)
element.appendChild(table)
}
};
xhr.send();
}
async function deleteOrigins() {
let xhr = new XMLHttpRequest();
xhr.open("DELETE", '/-/origins', true);
xhr.send();
}
async function deleteOrigin(origin_ref) {
if (origin_ref === undefined)
origin_ref = window.prompt("Please enter 'origin_ref' which should be deleted");
if (origin_ref === null || origin_ref === "")
return
let xhr = new XMLHttpRequest();
xhr.open("DELETE", `/-/origins/${origin_ref}`, true);
xhr.send();
}
async function deleteExpiredOrigins() {
let xhr = new XMLHttpRequest();
xhr.open("DELETE", `/-/origins/expired`, true);
xhr.send();
}
async function deleteLease(lease_ref) {
if (lease_ref === undefined)
lease_ref = window.prompt("Please enter 'lease_ref' which should be deleted");
if (lease_ref === null || lease_ref === "")
return
let xhr = new XMLHttpRequest();
xhr.open("DELETE", `/-/lease/${lease_ref}`, true);
xhr.send();
}
async function deleteExpiredLeases() {
let xhr = new XMLHttpRequest();
xhr.open("DELETE", `/-/leases/expired`, true);
xhr.send();
}

5
app/static/assets/js/popper.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
<header class="navbar navbar-expand-md navbar-green sticky-top bg-dark flex-md-nowrap p-0 shadow">
<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3" href="/-/">FastAPI-DLS {{ VERSION }}</a>
<button class="navbar-toggler position-absolute d-lg-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</header>

View File

@@ -0,0 +1,93 @@
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
<div class="position-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {{ 'active' if request.url.path == '/-/dashboard' }}" aria-current="page" href="/-/dashboard">
<i class="bi-house-door"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.url.path == '/-/dashboard/origins' }}" aria-current="page" href="/-/dashboard/origins">
<i class="bi-pc-display-horizontal"></i> Origins <span id="origin-cnt" class="badge text-bg-secondary"></span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.url.path == '/-/dashboard/leases' }}" aria-current="page" href="/-/dashboard/leases">
<i class="bi-layers"></i> Leases <span id="lease-cnt" class="badge text-bg-secondary"></span>
</a>
</li>
</ul>
<script type="application/javascript">
function loadLOriginCnt() {
let xhr = new XMLHttpRequest();
xhr.open("GET", '/-/origins?leases=false', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
const x = JSON.parse(xhr.response)
document.getElementById('origin-cnt').innerHTML = x.length
}
};
xhr.send();
}
function loadLeaseCnt() {
let xhr = new XMLHttpRequest();
xhr.open("GET", '/-/leases?origin=false', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
const x = JSON.parse(xhr.response)
document.getElementById('lease-cnt').innerHTML = x.length
}
};
xhr.send();
}
// load initial
loadLOriginCnt()
loadLeaseCnt()
// refresh every 5 seconds
setInterval(() => {
loadLOriginCnt()
loadLeaseCnt()
}, 5000);
</script>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted text-uppercase">
<span>Help</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {{ 'active' if request.url.path == '/-/readme' }}" aria-current="page" href="/-/readme">
<i class="bi-question-circle"></i> Readme
</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="https://git.collinwebdesigns.de/oscar.krause/fastapi-dls" target="_blank">
<i class="bi-git"></i> Git Repo
</a>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted text-uppercase">
<span>Integrations</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" aria-current="page" href="/-/doc" target="_blank">
<i class="bi-file-text"></i> Swagger UI
</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="/-/redoc" target="_blank">
<i class="bi-file-text"></i> Redoc
</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="/-/openapi.json" target="_blank">
<i class="bi bi-filetype-json"></i> OpenAPI JSON
</a>
</li>
</ul>
</div>
</nav>

View File

@@ -0,0 +1,33 @@
<!doctype html>
<html lang="en" class="h-100">
<head>
{% block title %}
<title>FastAPI-DLS</title>
{% endblock %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
<link rel="icon" href="/static/assets/img/favicons/favicon-32x32.png" sizes="32x32" type="image/png">
<link rel="icon" href="/static/assets/img/favicons/favicon-16x16.png" sizes="16x16" type="image/png">
<link rel="manifest" href="/static/assets/img/favicons/manifest.json">
<link rel="icon" href="/static/assets/img/favicons/favicon.ico">
<link rel="apple-touch-icon" href="/static/assets/img/favicons/apple-touch-icon.png" sizes="180x180">
{% block styles %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="/static/assets/css/custom.css">
</head>
<body class="d-flex flex-column {% block body_class %}{% endblock %}">
{% block body %}
{% endblock %}
<script src="/static/assets/js/helper.js"></script>
{% block scripts %}
{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,16 @@
{% extends 'layouts/bootstrap.html' %}
{% block body %}
{% include 'components/navbar.html' %}
<div class="container-fluid">
<div class="row">
{% include 'components/sidebar.html' %}
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
{% block content %}
{% endblock %}
</main>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends 'layouts/base.html' %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" type="text/css" href="/static/assets/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/static/assets/css/bootstrap-icons.min.css">
<link rel="stylesheet" type="text/css" href="/static/assets/css/dashboard.css">
<script src="/static/assets/js/popper.min.js"></script>
<script src="/static/assets/js/bootstrap.min.js"></script>
{% endblock %}

View File

@@ -0,0 +1,69 @@
{% extends 'layouts/bootstrap-dashboard.html' %}
{% block title %}
<title>Dashboard</title>
{% endblock %}
{% block content %}
<div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Dashboard</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="downloadClientToken()">
<i class="bi bi-download"></i>
Client Token
</button>
</div>
</div>
</div>
<div class="p-5 mb-4 bg-light rounded-3">
<div class="container-fluid py-5">
<h1 class="display-5 fw-bold">FastAPI-DLS</h1>
<p class="col-md-8 fs-4">Minimal Delegated License Service (DLS).</p>
<a href="https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/-/releases" class="btn btn-primary btn-lg" target="_blank">
Releases &raquo;
</a>
</div>
</div>
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Configuration</h5>
<h6 class="card-subtitle mb-2 text-body-secondary">
Using timezone: {{ CONFIG.TZ }}. Make sure this is correct and match your clients!
</h6>
<p class="card-text">
Your clients renew their license every {{ CONFIG.LEASE_CALCULATED_RENEWAL }}.<br/>
If the renewal fails, the license is {{ CONFIG.LEASE_RENEWAL_DELTA }} valid.<br/>
<br/>
Your client-token file (.tok) is valid for {{ CONFIG.CLIENT_TOKEN_EXPIRE_DELTA }}.
</p>
</div>
</div>
<div class="card">
<div class="card-body">
<pre id="config"></pre>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script type="application/javascript">
function downloadClientToken() {
window.open('/-/client-token', "_blank")
}
function load() {
const config = document.getElementById('config')
fetchConfig(config)
}
load()
</script>
{% endblock %}

View File

@@ -0,0 +1,60 @@
{% extends 'layouts/bootstrap-dashboard.html' %}
{% block title %}
<title>Origins</title>
{% endblock %}
{% block content %}
<div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Leases <small>with origin</small></h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteLease().finally(() => load())">
delete lease
</button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteExpiredLeases().finally(() => load())">
delete all expired leases
</button>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="load()" title="refresh">
<i class="bi bi-arrow-clockwise"></i>
</button>
<button id="btn-auto-refresh" type="button" class="btn btn-sm active">auto-refresh</button>
</div>
</div>
<div id="leases" class="mt-3"></div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script type="application/javascript">
let autoRefresh = true
function load() {
const leases = document.getElementById('leases')
fetchLeases(leases)
}
load()
setInterval(() => {
if(autoRefresh)
load()
}, 5000);
const btnAutoRefresh = document.getElementById('btn-auto-refresh')
btnAutoRefresh.addEventListener("click", () => {
if(btnAutoRefresh.classList.contains('active')) {
autoRefresh = false
btnAutoRefresh.classList.remove('active')
} else {
autoRefresh = false
btnAutoRefresh.classList.add('active')
}
}, true);
</script>
{% endblock %}

View File

@@ -0,0 +1,70 @@
{% extends 'layouts/bootstrap-dashboard.html' %}
{% block title %}
<title>Origins</title>
{% endblock %}
{% block content %}
<div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Origins <small>with leases</small></h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteOrigin().finally(() => load())">
delete origin
</button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteExpiredOrigins().finally(() => load())">
delete all expired origins
</button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteOriginsWrapper()">
delete all
</button>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="load()" title="refresh">
<i class="bi bi-arrow-clockwise"></i>
</button>
<button id="btn-auto-refresh" type="button" class="btn btn-sm active">auto-refresh</button>
</div>
</div>
<div id="origins" class="mt-3"></div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script type="application/javascript">
let autoRefresh = true
function load() {
const origins = document.getElementById('origins')
fetchOriginsWithLeases(origins)
}
load()
function deleteOriginsWrapper() {
const response = confirm('Are you sure you want to delete all origins and their leases?');
if (response)
deleteOrigins().finally(() => load())
}
setInterval(() => {
if(autoRefresh)
load()
}, 5000);
const btnAutoRefresh = document.getElementById('btn-auto-refresh')
btnAutoRefresh.addEventListener("click", () => {
if(btnAutoRefresh.classList.contains('active')) {
autoRefresh = false
btnAutoRefresh.classList.remove('active')
} else {
autoRefresh = false
btnAutoRefresh.classList.add('active')
}
}, true);
</script>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends 'layouts/bootstrap-dashboard.html' %}
{% block title %}
<title>Origins</title>
{% endblock %}
{% block content %}
<div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<div class="overflow-hidden">
{{ markdown|safe }}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends 'layouts/bootstrap.html' %}
{% block title %}
<title>Index</title>
{% endblock %}
{% block body_class %}h-100{% endblock %}
{% block body %}
<main class="flex-shrink-0">
<div class="container">
<h1 class="mt-5 text-primary">FastAPI-DLS</h1>
<p class="lead">Minimal Delegated License Service (DLS).</p>
<p>
<a href="/-/dashboard">Dashboard</a>,
<a href="/-/readme">Readme</a>
</p>
</div>
</main>
<footer class="footer mt-auto py-3 bg-light">
<div class="container">
<span class="text-muted">FastAPI-DLS Version {{ VERSION }}</span>
</div>
</footer>
{% endblock %}

View File

@@ -6,3 +6,4 @@ python-dateutil==2.8.2
sqlalchemy==2.0.27
markdown==3.5.2
python-dotenv==1.0.1
jinja2==3.1.3

View File

@@ -59,8 +59,8 @@ def test_readme():
assert response.status_code == 200
def test_manage():
response = client.get('/-/manage')
def test_dashboard():
response = client.get('/-/dashboard')
assert response.status_code == 200