commit c78ad6f2353029028a8a9f69a5827e4fbc21c9b6
Author: Matej Zeman <zeman339@gmail.com>
Date:   Wed May 4 13:30:59 2022 +0200

    re #9708 Login logic with fastapi-jwt

diff --git a/server/requirements.txt b/server/requirements.txt
index acad635..a1e5719 100644
--- a/server/requirements.txt
+++ b/server/requirements.txt
@@ -4,4 +4,5 @@ SQLAlchemy==1.4.32
 uvicorn==0.17.6
 psycopg2-binary==2.8.6
 jinja2==3.1.1
-python-multipart==0.0.5
\ No newline at end of file
+python-multipart==0.0.5
+fastapi-jwt-auth=0.5.0
\ No newline at end of file
diff --git a/server/sql_app/api/devices_web.py b/server/sql_app/api/devices_web.py
index 0c6fd0a..f47bc28 100644
--- a/server/sql_app/api/devices_web.py
+++ b/server/sql_app/api/devices_web.py
@@ -1,14 +1,15 @@
-from typing import List
-
-from fastapi import Depends, FastAPI, HTTPException, APIRouter, Form
-from sqlalchemy.orm import Session
-from sql_app import crud, models, schemas
 from datetime import datetime
-from ..database import SessionLocal, engine
-from fastapi import FastAPI, Request
+
+from fastapi import Depends, APIRouter, Form
+from fastapi import Request
 from fastapi.responses import HTMLResponse
-from fastapi.staticfiles import StaticFiles
 from fastapi.templating import Jinja2Templates
+from fastapi_jwt_auth import AuthJWT
+from pydantic import BaseModel
+from sqlalchemy.orm import Session
+
+from sql_app import crud, models
+from ..database import SessionLocal, engine
 
 models.Base.metadata.create_all(bind=engine)
 
@@ -19,6 +20,32 @@ templates = Jinja2Templates(directory="templates/devices")
 device_web = APIRouter(prefix="")
 
 
+
+class Settings(BaseModel):
+    authjwt_secret_key: str = "secret"
+    # Configure application to store and get JWT from cookies
+    authjwt_token_location: set = {"cookies"}
+    # Disable CSRF Protection for this example. default is True
+    authjwt_cookie_csrf_protect: bool = False
+
+
+@AuthJWT.load_config
+def get_config():
+    return Settings()
+
+
+fake_users_db = {
+    "admin": {
+        "username": "admin",
+        "password": "admin"
+    },
+    "editor": {
+        "username": "editor",
+        "password": "editor"
+    },
+}
+
+
 # Dependency
 def get_db():
     db = SessionLocal()
@@ -28,16 +55,86 @@ def get_db():
         db.close()
 
 
+@device_web.get("/token", response_class=HTMLResponse)
+async def login_get(request: Request):
+    return templates.TemplateResponse("login.html", {"request": request})
+
+
+@device_web.post("/token", response_class=HTMLResponse)
+async def login(username: str = Form(...), password: str = Form(...), Authorize: AuthJWT = Depends()):
+    user_dict = fake_users_db.get(username)
+
+    access_token = Authorize.create_access_token(subject=username, expires_time=False)
+    refresh_token = Authorize.create_refresh_token(subject=username, expires_time=False)
+
+    # Set the JWT cookies in the response
+    Authorize.set_access_cookies(access_token)
+    Authorize.set_refresh_cookies(refresh_token)
+    return """
+    <html>
+        <head>
+            <title>Some HTML in here</title>
+        </head>
+        <body>
+            <h1>Look ma! HTML!</h1>
+            <form action="/devices-web" method="get">
+                <input type="submit" value="Login" />
+            </form>
+        </body>
+    </html>
+    """
+
+
+@device_web.post('/refresh')
+def refresh(Authorize: AuthJWT = Depends()):
+    Authorize.jwt_refresh_token_required()
+
+    current_user = Authorize.get_jwt_subject()
+    new_access_token = Authorize.create_access_token(subject=current_user)
+    # Set the JWT cookies in the response
+    Authorize.set_access_cookies(new_access_token)
+    return {"msg": "The token has been refresh"}
+
+
+@device_web.get('/logout', response_class=HTMLResponse)
+def logout(Authorize: AuthJWT = Depends()):
+    """
+    Because the JWT are stored in an httponly cookie now, we cannot
+    log the user out by simply deleting the cookies in the frontend.
+    We need the backend to send us a response to delete the cookies.
+    """
+    Authorize.jwt_required()
+
+    Authorize.unset_jwt_cookies()
+    return """
+        <html>
+            <head>
+                <title>Some HTML in here</title>
+            </head>
+            <body>
+                <h1>Look ma! HTML!</h1>
+                <form action="/devices-web" method="get">
+                    <input type="submit" value="Login" />
+                </form>
+            </body>
+        </html>
+        """
+
+
 @device_web.get("/devices-web", response_class=HTMLResponse)
-async def read_devices(request: Request, skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
+async def read_devices(request: Request, skip: int = 0, limit: int = 100, db: Session = Depends(get_db),
+                       Authorize: AuthJWT = Depends()):
     """
     Returns template with all devices and its current states
     """
+    Authorize.jwt_optional()
+    current_user = Authorize.get_jwt_subject()
+
     devices = crud.get_devices(db, skip=skip, limit=limit)
     statuses = []
     # adding state for each device in list
     for i in range(0, len(devices)):
-        statuses.append(devices[i].logs[len(devices[i].logs)-1].status)
+        statuses.append(devices[i].logs[len(devices[i].logs) - 1].status)
     licenses = crud.get_licenses(db, skip=skip, limit=limit)
     return templates.TemplateResponse("devices.html", {"request": request, "devs": len(devices), "devices": devices,
                                                        "statuses": statuses, "licenses": licenses})
@@ -63,8 +160,9 @@ async def filter_devices(request: Request, skip: int = 0, limit: int = 100, lic:
     for i in range(0, len(def_devices)):
         statuses.append(def_devices[i].logs[len(def_devices[i].logs) - 1].status)
     licenses = crud.get_licenses(db, skip=skip, limit=limit)
-    return templates.TemplateResponse("devices.html", {"request": request, "devs": len(def_devices), "devices": def_devices,
-                                                       "statuses": statuses, "licenses": licenses})
+    return templates.TemplateResponse("devices.html",
+                                      {"request": request, "devs": len(def_devices), "devices": def_devices,
+                                       "statuses": statuses, "licenses": licenses})
 
 
 @device_web.get("/device-license/{device_id}", response_class=HTMLResponse)
diff --git a/server/templates/devices/devices.html b/server/templates/devices/devices.html
index 4d4bd6e..4c4774e 100644
--- a/server/templates/devices/devices.html
+++ b/server/templates/devices/devices.html
@@ -14,6 +14,9 @@
   </select>
   <input type="submit" value="OK">
 </form>
+<form action="/token" method="get">
+    <input type="submit" value="Login" />
+</form>
 <form action="/devices-web" method="post">
     <label for="lic">License:</label>
     <input id="lic" name="lic" type="text" list="licenses" value="" placeholder="all">
diff --git a/server/templates/devices/login.html b/server/templates/devices/login.html
new file mode 100644
index 0000000..622990b
--- /dev/null
+++ b/server/templates/devices/login.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Login</title>
+</head>
+<body>
+<form action="/token" method="post">
+  <label for="username">Username:</label><br>
+  <input type="text" id="username" name="username"><br><br>
+    <label for="password">Expiration Date</label>
+  <input type="text" id="password" name="password">
+  <input type="submit" value="Submit">
+</form>
+</body>
+</html>
\ No newline at end of file
