diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/js/jwt.js | 22 | ||||
-rw-r--r-- | app/main.py | 2 | ||||
-rw-r--r-- | app/routes/auth_routes.py | 4 | ||||
-rw-r--r-- | app/routes/links_routes.py | 4 | ||||
-rw-r--r-- | app/static/js/jwt.js | 58 | ||||
-rw-r--r-- | app/templates/dashboard.html | 95 | ||||
-rw-r--r-- | app/util/authentication.py | 23 |
7 files changed, 130 insertions, 78 deletions
diff --git a/app/js/jwt.js b/app/js/jwt.js deleted file mode 100644 index 43c2e6c..0000000 --- a/app/js/jwt.js +++ /dev/null @@ -1,22 +0,0 @@ -function parseJwt (token) { - var base64Url = token.split('.')[1]; - var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); - var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); - }).join('')); - - return JSON.parse(jsonPayload); -} - -function isJwtExpired (token) { - var jwt = parseJwt(token); - return jwt.exp < Date.now() / 1000; -} - -async function refreshAccessToken (refreshToken) { - const data = await fetch('/api/refresh', { - method: 'POST', - headers: {'Authorization': 'Bearer ' + refreshToken} - }); - return data.access_token; -}
\ No newline at end of file diff --git a/app/main.py b/app/main.py index 4280275..9ef3e03 100644 --- a/app/main.py +++ b/app/main.py @@ -2,6 +2,7 @@ from fastapi import FastAPI, Depends, Request, Path from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse, JSONResponse from fastapi.templating import Jinja2Templates +from fastapi.staticfiles import StaticFiles from app.routes.auth_routes import router as auth_router from app.routes.links_routes import router as links_router from app.routes.user_routes import router as user_router @@ -35,6 +36,7 @@ app.add_middleware( allow_credentials=True, ) +app.mount("/static", StaticFiles(directory="app/static"), name="static") templates = Jinja2Templates(directory="app/templates") # Import routes diff --git a/app/routes/auth_routes.py b/app/routes/auth_routes.py index 3054e22..ceb68b1 100644 --- a/app/routes/auth_routes.py +++ b/app/routes/auth_routes.py @@ -33,7 +33,7 @@ async def login_for_access_token( detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) - access_token_expires = timedelta(minutes=15) + access_token_expires = timedelta(minutes=1) access_token = create_access_token( data={"sub": user.id, "username": user.username, "refresh": False}, expires_delta=access_token_expires, @@ -63,7 +63,7 @@ async def refresh_access_token( """ Return a new access token if the refresh token is valid """ - access_token_expires = timedelta(minutes=30) + access_token_expires = timedelta(minutes=1) access_token = create_access_token( data={"sub": current_user.id, "refresh": False}, expires_delta=access_token_expires, diff --git a/app/routes/links_routes.py b/app/routes/links_routes.py index 833c699..874f3c2 100644 --- a/app/routes/links_routes.py +++ b/app/routes/links_routes.py @@ -16,7 +16,7 @@ from app.util.authentication import get_current_user router = APIRouter(prefix="/links", tags=["links"]) -@router.get("/", summary="Get all of the links associated with your account") +@router.get("", summary="Get all of the links associated with your account") async def get_links( current_user: Annotated[User, Depends(get_current_user)], db=Depends(get_db), @@ -35,7 +35,7 @@ async def get_links( return links -@router.post("/", summary="Create a new link") +@router.post("", summary="Create a new link") async def create_link( url: URLSchema, current_user: Annotated[User, Depends(get_current_user)], diff --git a/app/static/js/jwt.js b/app/static/js/jwt.js new file mode 100644 index 0000000..1e07703 --- /dev/null +++ b/app/static/js/jwt.js @@ -0,0 +1,58 @@ +// Description: This file contains functions to access the API with JWT authentication. + +/** + * Accept a full URL, method, and body to send to the API. + * - If successful, return the response + * - If first fail, attempt to refresh JWT token and try again + * - If second fail, return false + * @param {*} endpoint API endpoint + * @param {*} method String (GET, POST, PUT, DELETE) + * @param {*} body Data to send to the API + * @returns boolean + */ +async function accessAPI(endpoint, method, body) { + let response = await fetch(`/api${endpoint}`, { + method: method, + body: body, + }); + + if (response.ok) { + let data = await response.json(); + data = await data; + console.log(data); + return data; + } else if (response.status === 401) { + console.log('REFRESHING TOKEN') + if (await refreshAccessToken()) { + // Try the request again + let response = await fetch(`/api${endpoint}`, { + method: method, + body: body, + }); + if (response.ok) { + let data = await response.json(); + data = await data; + console.log("REFRESHED DATA") + return data; + } + } + } + return false; +} + +/** + * Attempt to refresh the JWT token + * @returns boolean + */ +async function refreshAccessToken () { + const response = await fetch('/api/auth/refresh', { + method: 'POST', + }); + if (response.ok) { + console.log("TOKEN REFRESH") + return true; + } else { + console.log("TOKEN REFRESH FAILED") + return false; + } +}
\ No newline at end of file diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 0a383af..fa7458a 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -6,12 +6,13 @@ <title>LinkLogger | Dashboard</title> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet"> + <script src="/static/js/jwt.js"></script> </head> <body> <div> <!-- Create a table with 4 columns with a total of 1000px width --> <table> - <tr> + <tr style="border: 2px solid #ccc;"> <th>Link</th> <th>Visits</th> <th>Redirect</th> @@ -36,20 +37,30 @@ margin-top: 100px; } + table { margin: 20px 0 20px 0; text-align: center; font-size: 25px; - width: 1250px; + width: 1000px; color: #ccc; border-collapse: collapse; overflow: hidden; } + /* Center all sub tables */ + .log-table-row table { + margin: 0 auto; + } + + .log-table-row table { + width: 90%; + } + table th { background-color: #415eac; - padding: 10px; border: 2px solid #ccc; + padding: 10px; } .link-table-row { @@ -91,25 +102,6 @@ </style> <script> - async function fetchLinks() { - const response = await fetch('/api/links'); - if (!response.ok) { - throw new Error('Failed to fetch links'); - } - const links = await response.json(); - return links; - } - - async function fetchLogs(link) { - const response = await fetch(`/api/links/${link}/logs`); - if (!response.ok) { - throw new Error('Failed to fetch logs'); - } - const logs = await response.json(); - return logs; - } - - function createRow(index, link, logs) { // Create the sub-table with the logs let subTable = ` @@ -124,10 +116,20 @@ `; // Loop through the logs and create a row for each one logs.forEach((log, index) => { + let logTimestamp = new Date(log.timestamp).toLocaleString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }).replace(',', ''); + let row = ` <tr id="${log.id}-log"> <td>${logs.length - index}</td> - <td>${log.timestamp}</td> + <td>${logTimestamp}</td> <td>${log.ip}</td> <td>${log.location}</td> <td>${log.isp}</td> @@ -172,21 +174,23 @@ async function getData() { - let links = fetchLinks(); - links = await links; - - console.log(links); + const links = await accessAPI(`/links`, 'GET') + if (!links) { + throw new Error('Failed to fetch links'); + } // Links is an Array of objects with the link data // Loop through the links and create a row for each one - links.forEach(async (link, index) => { - // Fetch the logs for the link - let logs = await fetchLogs(link.link); - logs = await logs; - // Create the entire row with sub-table and logs - let row = createRow(index, link, logs); - // Add the new table row to the main table + // Do not use async because then the order or data in the + // table will change from time to time + for (let i = 0; i < links.length; i++) { + let link = links[i]; + let logs = await accessAPI(`/links/${link.link}/logs`, 'GET') + if (!logs) { + throw new Error('Failed to fetch logs'); + } + let row = createRow(i, link, logs); document.querySelector('table').innerHTML += row; - }); + } } // hideLogRows to all log-table-rows @@ -203,6 +207,7 @@ let id = event.target.id; let logTR = document.getElementById(`${id}-logTR`); if (logTR.style.display === 'none') { + // Hide any open log tables hideLogRows(); logTR.style.display = 'table-row'; } else { @@ -214,14 +219,18 @@ // Add an event listen to all trash bins document.addEventListener('click', (event) => { if (event.target.classList.contains('fa-trash')) { - let id = event.target.id; - let link = id.split('/')[1]; - let logId = id.split('/')[0]; - fetch(`/api/links/${link}/logs/${logId}`, { - method: 'DELETE' - }); - let logRow = document.getElementById(`${logId}-log`) - logRow.remove(); + // Confirm the user wants to delete the log + let confirmDelete = confirm('Are you sure you want to delete this log?'); + if (confirmDelete) { + let id = event.target.id; + let link = id.split('/')[1]; + let logId = id.split('/')[0]; + fetch(`/api/links/${link}/logs/${logId}`, { + method: 'DELETE' + }); + let logRow = document.getElementById(`${logId}-log`) + logRow.remove(); + } } }); diff --git a/app/util/authentication.py b/app/util/authentication.py index 0bc7e09..a8f7aff 100644 --- a/app/util/authentication.py +++ b/app/util/authentication.py @@ -72,6 +72,10 @@ async def refresh_get_current_user( return await get_current_user(token, is_refresh=True, db=db) +def process_refresh_token(token: str, db: Session): + return False + + async def get_current_user( request: Request, db=Depends(get_db), @@ -84,15 +88,6 @@ async def get_current_user( Otherwise, the request is from an API and we should return a 401 """ - # If the request is from /api/auth/refresh, it is a request to get - # a new access token using a refresh token - if request.url.path == "/api/auth/refresh": - token = request.cookies.get("refresh_token") - is_refresh = True - else: - token = request.cookies.get("access_token") - is_refresh = False - def raise_unauthorized(): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -100,6 +95,16 @@ async def get_current_user( headers={"WWW-Authenticate": "Bearer"}, ) + # If the request is from /api/auth/refresh, it is a request to get + # a new access token using a refresh token + if request.url.path == "/api/auth/refresh": + token = request.cookies.get("refresh_token") + user = process_refresh_token(token, db) + if user is None: + raise_unauthorized() + else: + token = request.cookies.get("access_token") + try: payload = jwt.decode(token, secret_key, algorithms=[algorithm]) id: int = payload.get("sub") |