Mainly auth re-thinking - just in thought

This commit is contained in:
Parker M. 2024-11-08 23:07:20 -06:00
parent 3cde652d52
commit 8941213c8d
Signed by: parker
GPG Key ID: 505ED36FC12B5D5E
7 changed files with 130 additions and 78 deletions

View File

@ -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;
}

View File

@ -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

View File

@ -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,

View File

@ -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)],

58
app/static/js/jwt.js Normal file
View File

@ -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;
}
}

View File

@ -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();
}
}
});

View File

@ -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")