diff options
-rw-r--r-- | api/main.py | 2 | ||||
-rw-r--r-- | api/routes/auth_routes.py | 13 | ||||
-rw-r--r-- | api/routes/log_routes.py | 72 | ||||
-rw-r--r-- | api/routes/user_routes.py | 4 | ||||
-rw-r--r-- | app/package.json | 3 | ||||
-rw-r--r-- | app/src/App.tsx | 1 | ||||
-rw-r--r-- | app/src/components/Dashboard.tsx | 142 | ||||
-rw-r--r-- | app/src/styles/Dashboard.module.css | 33 | ||||
-rw-r--r-- | app/yarn.lock | 47 |
9 files changed, 268 insertions, 49 deletions
diff --git a/api/main.py b/api/main.py index c8c1296..c9a6d38 100644 --- a/api/main.py +++ b/api/main.py @@ -4,6 +4,7 @@ from fastapi.responses import JSONResponse, RedirectResponse from api.routes.auth_routes import router as auth_router from api.routes.links_routes import router as links_router from api.routes.user_routes import router as user_router +from api.routes.log_routes import router as log_router from typing import Annotated from fastapi.exceptions import HTTPException from starlette.status import HTTP_404_NOT_FOUND @@ -44,6 +45,7 @@ app.add_middleware( app.include_router(auth_router, prefix="/api") app.include_router(links_router, prefix="/api") app.include_router(user_router, prefix="/api") +app.include_router(log_router, prefix="/api") @app.get("/c/{link}") diff --git a/api/routes/auth_routes.py b/api/routes/auth_routes.py index c51557f..24e1391 100644 --- a/api/routes/auth_routes.py +++ b/api/routes/auth_routes.py @@ -7,8 +7,10 @@ from typing import Annotated from api.util.authentication import ( create_access_token, authenticate_user, + get_current_user, ) from api.util.db_dependency import get_db +from api.schemas.auth_schemas import User router = APIRouter(prefix="/auth", tags=["auth"]) @@ -44,3 +46,14 @@ async def login_for_access_token( # secure=True, # Cookies are only sent over HTTPS ) return response + + +# Check if the user is logged in +@router.get("/check", summary="Check if the user is logged in") +async def check_login( + current_user: Annotated[User, Depends(get_current_user)], +): + """ + If the user actually makes it to this endpoint, they are logged in + """ + return {"success": True} diff --git a/api/routes/log_routes.py b/api/routes/log_routes.py new file mode 100644 index 0000000..84b8c51 --- /dev/null +++ b/api/routes/log_routes.py @@ -0,0 +1,72 @@ +from fastapi import APIRouter, status, Path, Depends +from fastapi.exception_handlers import HTTPException +from typing import Annotated +import string +import random +import datetime +import validators + +from api.util.db_dependency import get_db +from models import Link, Log +from api.schemas.links_schemas import URLSchema +from api.schemas.auth_schemas import User +from api.util.authentication import get_current_user + + +router = APIRouter(prefix="/logs", tags=["logs"]) + + +@router.get("", summary="Get all of the logs associated with your account") +async def get_logs( + current_user: Annotated[User, Depends(get_current_user)], + db=Depends(get_db), +): + logs = ( + db.query(Log) + .filter(Log.owner == current_user.id) + .order_by(Log.timestamp.desc()) + .all() + ) + if not logs: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="No logs found" + ) + return logs + + +@router.get("/{log_id}", summary="Get a specific log") +async def get_log( + log_id: Annotated[int, Path(title="ID of log to delete")], + current_user: Annotated[User, Depends(get_current_user)], + db=Depends(get_db), +): + log = ( + db.query(Log) + .filter(Log.id == log_id, Log.owner == current_user.id) + .first() + ) + if not log: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Log not found" + ) + return log + + +@router.delete("/{log_id}", summary="Delete a log") +async def delete_log( + log_id: Annotated[int, Path(title="ID of log to delete")], + current_user: Annotated[User, Depends(get_current_user)], + db=Depends(get_db), +): + log = ( + db.query(Log) + .filter(Log.id == log_id, Log.owner == current_user.id) + .first() + ) + if not log: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Log not found" + ) + db.delete(log) + db.commit() + return status.HTTP_204_NO_CONTENT diff --git a/api/routes/user_routes.py b/api/routes/user_routes.py index cf9be52..618f721 100644 --- a/api/routes/user_routes.py +++ b/api/routes/user_routes.py @@ -22,7 +22,7 @@ router = APIRouter(prefix="/users", tags=["users"]) @router.delete("/{user_id}", summary="Delete your account") async def delete_user( - user_id: Annotated[int, Path(title="Link to delete")], + user_id: Annotated[int, Path(title="ID of user to delete")], current_user: Annotated[User, Depends(get_current_user)], db=Depends(get_db), ): @@ -51,7 +51,7 @@ async def delete_user( @router.post("/{user_id}/password", summary="Update your account password") async def update_pass( - user_id: Annotated[int, Path(title="Link to update")], + user_id: Annotated[int, Path(title="ID of user to update")], update_data: UpdatePasswordSchema, current_user: Annotated[User, Depends(get_current_user)], db=Depends(get_db), diff --git a/app/package.json b/app/package.json index 0ad2c45..4ba8808 100644 --- a/app/package.json +++ b/app/package.json @@ -10,6 +10,9 @@ "preview": "vite preview" }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.6.0", + "@fortawesome/free-solid-svg-icons": "^6.6.0", + "@fortawesome/react-fontawesome": "^0.2.2", "axios": "^1.7.7", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/app/src/App.tsx b/app/src/App.tsx index 75cd203..14fa571 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,5 +1,4 @@ import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; -// Import components import Login from './components/Login' import Signup from './components/Signup' import Dashboard from './components/Dashboard' diff --git a/app/src/components/Dashboard.tsx b/app/src/components/Dashboard.tsx index bcab092..373a07f 100644 --- a/app/src/components/Dashboard.tsx +++ b/app/src/components/Dashboard.tsx @@ -1,46 +1,126 @@ -import { useState, useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import Axios from 'axios'; import styles from '../styles/Dashboard.module.css'; -// import { accessAPI } from '../helpers/api'; - +import { useNavigate } from 'react-router-dom'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTrash } from '@fortawesome/free-solid-svg-icons'; function Dashboard() { - // Get the links from the API - const [links, setLinks] = useState([]); + interface Log { + id: number; + link: string; + timestamp: string; + ip: string; + location: string; + browser: string; + os: string; + userAgent: string; + isp: string; + } + + interface Link { + link: string; + owner: number; + redirect_link: string; + expire_date: string; + } + + const [links, setLinks] = useState<Link[]>([]); + const [logs, setLogs] = useState<Log[]>([]); + const [visibleLog, setVisibleLog] = useState<string | null>(null); + const navigate = useNavigate(); + + // Fetch links from API useEffect(() => { - Axios.get('/api/links') - .then((res) => { + Axios.get('/api/links').then((res) => { + if (res.status === 200) { setLinks(res.data); - }) - .catch((err) => { - console.log(err); - }); + } else { + navigate('/login'); + } + }).catch(() => { + navigate('/login'); + }); + }, []); + + // Fetch logs from API + useEffect(() => { + Axios.get('/api/logs').then((res) => { + if (res.status === 200) { + setLogs(res.data); + } else { + navigate('/login'); + } + }).catch(() => { + navigate('/login'); + }); }, []); + const toggleLogRow = (link: string) => { + setVisibleLog(visibleLog === link ? null : link); + }; + return ( - <div id={styles.container}> - <table> - <thead> - <tr style={{ border: '2px solid #ccc' }}> - <th>Link</th> - <th>Visits</th> - <th>Redirect</th> - <th>Expire Date</th> - </tr> - </thead> - <tbody> - {/* {links.map((link: any) => ( - <tr key={link.id}> - <td>{link.url}</td> - <td>{link.visits}</td> - <td>{link.redirect}</td> + <table id={styles.mainTable}> + <thead> + <tr style={{ border: '2px solid #ccc' }}> + <th>Link</th> + <th>Visits</th> + <th>Redirect</th> + <th>Expire Date</th> + </tr> + </thead> + <tbody> + {/* For every link and its logs */} + {links.map((link) => ( + <React.Fragment key={link.link}> + <tr className={styles.linkTableRow}> + <td> + <button onClick={() => toggleLogRow(link.link)} className={styles.linkButton}>{link.link}</button> + </td> + <td>{logs.filter((log) => log.link === link.link).length || 0}</td> + <td>{link.redirect_link}</td> <td>{link.expire_date}</td> </tr> - ))} */} - </tbody> - </table> - </div> + + {/* Conditionally render logs for this link */} + {visibleLog === link.link && ( + <tr className={styles.logTableRow}> + <td colSpan={6}> + <table> + <thead> + <tr> + <th>ID</th> + <th>Timestamp</th> + <th>IP</th> + <th>Location</th> + <th colSpan={2}>ISP</th> + </tr> + </thead> + <tbody> + {/* Render logs only if visibleLog matches the link */} + {logs + .filter((log) => log.link === link.link) + .map((log, index, filteredLogs) => ( + <tr key={log.id}> + <td>{filteredLogs.length - index}</td> + <td>{log.timestamp}</td> + <td>{log.ip}</td> + <td>{log.location}</td> + <td>{log.isp}</td> + <td><FontAwesomeIcon icon={faTrash} className={styles.trashBin}/></td> + </tr> + ))} + </tbody> + </table> + </td> + </tr> + )} + </React.Fragment> + ))} + </tbody> + </table> ) } diff --git a/app/src/styles/Dashboard.module.css b/app/src/styles/Dashboard.module.css index 042a2f1..ef6b451 100644 --- a/app/src/styles/Dashboard.module.css +++ b/app/src/styles/Dashboard.module.css @@ -5,15 +5,15 @@ body { background-color: #2c3338; } -#container { - display: flex; - justify-content: center; - margin-top: 100px; +#mainTable { + position: absolute; + top: 100px; + left: 50%; + transform: translateX(-50%); } - table { - margin: 20px 0 20px 0; + margin: 0 auto; text-align: center; font-size: 25px; width: 1000px; @@ -23,11 +23,11 @@ table { } /* Center all sub tables */ -.log-table-row table { +.logTableRow table { margin: 0 auto; } -.log-table-row table { +.logTableRow table { width: 90%; } @@ -37,7 +37,7 @@ table th { padding: 10px; } -.link-table-row { +.linkTableRow { border: 2px solid #ccc; } @@ -45,20 +45,20 @@ table td { padding: 10px; } -.link-table-row td { +.linkTableRow td { padding: 20px; } -.log-table-row table td { +.logTableRow table td { background-color: #3b4148; padding: 10px; } -.log-table-row table tr { +.logTableRow table tr { border: 2px solid #ccc; } -.link-button { +.linkButton { background-color: #3b4148; color: #ccc; border: none; @@ -68,7 +68,12 @@ table td { border-radius: 5px; } -.fa-trash:hover { +.trashBin:hover { color: rgb(238, 86, 86); cursor: pointer; + transition: color 0.2s ease; +} + +.trashBin:active { + transform: scale(0.95); }
\ No newline at end of file diff --git a/app/yarn.lock b/app/yarn.lock index 23ce4d2..189fe6d 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -336,6 +336,32 @@ dependencies: levn "^0.4.1" +"@fortawesome/fontawesome-common-types@6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz#31ab07ca6a06358c5de4d295d4711b675006163f" + integrity sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw== + +"@fortawesome/fontawesome-svg-core@^6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz#2a24c32ef92136e98eae2ff334a27145188295ff" + integrity sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg== + dependencies: + "@fortawesome/fontawesome-common-types" "6.6.0" + +"@fortawesome/free-solid-svg-icons@^6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz#061751ca43be4c4d814f0adbda8f006164ec9f3b" + integrity sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA== + dependencies: + "@fortawesome/fontawesome-common-types" "6.6.0" + +"@fortawesome/react-fontawesome@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz#68b058f9132b46c8599875f6a636dad231af78d4" + integrity sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g== + dependencies: + prop-types "^15.8.1" + "@humanfs/core@^0.19.1": version "0.19.1" resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" @@ -1209,7 +1235,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -loose-envify@^1.1.0: +loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -1282,6 +1308,11 @@ node-releases@^2.0.18: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -1349,6 +1380,15 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" @@ -1372,6 +1412,11 @@ react-dom@^18.3.1: loose-envify "^1.1.0" scheduler "^0.23.2" +react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + react-refresh@^0.14.2: version "0.14.2" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" |