diff --git a/api/routes/auth_routes.py b/api/routes/auth_routes.py index 24e1391..823feff 100644 --- a/api/routes/auth_routes.py +++ b/api/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=1) + access_token_expires = timedelta(minutes=180) access_token = create_access_token( data={"sub": user.id, "username": user.username}, expires_delta=access_token_expires, diff --git a/api/routes/log_routes.py b/api/routes/log_routes.py index 84b8c51..5875e3b 100644 --- a/api/routes/log_routes.py +++ b/api/routes/log_routes.py @@ -21,6 +21,9 @@ async def get_logs( current_user: Annotated[User, Depends(get_current_user)], db=Depends(get_db), ): + """ + Get all of the logs associated with the current user + """ logs = ( db.query(Log) .filter(Log.owner == current_user.id) @@ -34,12 +37,47 @@ async def get_logs( return logs +@router.get("/{link}", summary="Get all logs for a specific link") +async def get_logs_for_link( + link: Annotated[str, Path(title="Link to get logs for")], + current_user: Annotated[User, Depends(get_current_user)], + db=Depends(get_db), +): + """ + Get all of the logs associated with a specific link + - check to make sure the requester is the owner + """ + link = ( + db.query(Link) + .filter(Link.owner == current_user.id, Link.short == link) + .first() + ) + if not link: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Link not found" + ) + logs = ( + db.query(Log) + .filter(Log.link_id == link.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), ): + """ + Get a specific log (given the log ID) + """ log = ( db.query(Log) .filter(Log.id == log_id, Log.owner == current_user.id) @@ -58,6 +96,9 @@ async def delete_log( current_user: Annotated[User, Depends(get_current_user)], db=Depends(get_db), ): + """ + Delete a specific log (given the log ID) + """ log = ( db.query(Log) .filter(Log.id == log_id, Log.owner == current_user.id) diff --git a/app/src/App.tsx b/app/src/App.tsx index 54a500d..06ead3b 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -7,6 +7,7 @@ import { import Login from './components/Login'; import Signup from './components/Signup'; import Dashboard from './components/Dashboard'; +import CreateLink from './components/CreateLink'; function App() { return ( @@ -16,6 +17,7 @@ function App() { } /> } /> } /> + } /> ); diff --git a/app/src/components/CreateLink.tsx b/app/src/components/CreateLink.tsx new file mode 100644 index 0000000..a6456f1 --- /dev/null +++ b/app/src/components/CreateLink.tsx @@ -0,0 +1,100 @@ +import { useState, FormEvent, useEffect } from 'react'; +import createStyles from '../styles/Create.module.css'; +import styles from '../styles/Auth.module.css'; +import { Link } from 'react-router-dom'; +import axios from 'axios'; +import Navbar from './Navbar'; +import { useNavigate } from 'react-router-dom'; + +function CreateLink() { + document.title = 'LinkLogger | Create Short Link'; + + const [link, setLink] = useState(''); + const [url, setURL] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isCopied, setIsCopied] = useState(false); + const [error, setError] = useState(null); + const navigate = useNavigate(); + + // Get /api/users/me to make sure the user is logged in, and + // to get the username for rendering on screen + useEffect(() => { + axios + .get('/api/users/me') + .then((res) => { + if (res.status != 200) { + navigate('/login'); + } + }) + .catch(() => { + navigate('/login'); + }); + }, []); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + try { + const res = await axios.post('/api/links', { url }); + if (res.status === 200) { + setLink(res.data.link); + } + } catch (error) { + setError('STRANGE'); + } + }; + + const copyLink = () => { + navigator.clipboard.writeText(`${window.location.origin}/c/${link}`); + setIsCopied(true); + // Wait 5 seconds, then set isCopied back to false + setTimeout(() => { + setIsCopied(false); + }, 5000); + }; + + return ( + <> + +
+

Create a new short link by entering the long URL below

+

+ {error} +

+
+
+ setURL(e.target.value)} + required + /> + {link.length === 0 ? ( + + ) : ( + + )} +
+
+

+ + Click here + {' '} + to visit your dashboard. +

+
+ + ); +} + +export default CreateLink; diff --git a/app/src/components/Dashboard.tsx b/app/src/components/Dashboard.tsx index f3442e0..1b728d1 100644 --- a/app/src/components/Dashboard.tsx +++ b/app/src/components/Dashboard.tsx @@ -31,6 +31,8 @@ function Dashboard() { const [links, setLinks] = useState([]); const [logs, setLogs] = useState([]); const [visibleLog, setVisibleLog] = useState(null); + const [loadingLinks, setLoadingLinks] = useState(true); // Track loading state for links + const [loadingLogs, setLoadingLogs] = useState(true); // Track loading state for logs const navigate = useNavigate(); // Fetch links from API @@ -44,13 +46,15 @@ function Dashboard() { navigate('/login'); } }) - .catch((error: unknown) => { if (axios.isAxiosError(error)) { - if (error.response?.status != 404) { + if (error.response?.status !== 404) { navigate('/login'); } } + }) + .finally(() => { + setLoadingLinks(false); // Set loadingLinks to false once done }); }, []); @@ -67,10 +71,13 @@ function Dashboard() { }) .catch((error: unknown) => { if (axios.isAxiosError(error)) { - if (error.response?.status != 404) { + if (error.response?.status !== 404) { navigate('/login'); } } + }) + .finally(() => { + setLoadingLogs(false); // Set loadingLogs to false once done }); }, []); @@ -112,104 +119,117 @@ function Dashboard() { }); }; + // Loading spinner component + const LoadingSpinner = () => ( +
+
+

Loading...

+
+ ); + return ( <> - - - - - - - - - - - {/* If there are no links, put a special message */} - {links.length === 0 && ( - - + {/* Show loading spinner if either links or logs are still loading */} + {loadingLinks || loadingLogs ? ( + + ) : ( +
LinkVisitsRedirectExpire Date
-
- You do not have any shortened links - try creating one. -
-
+ + + + + + - )} - - {/* For every link and its logs */} - {links.map((link) => ( - - - + + {/* If there are no links, put a special message */} + {links.length === 0 && ( + + - - - + )} - {/* Conditionally render logs for this link */} - {visibleLog === link.link && ( - - +
LinkVisitsRedirectExpire Date
- +
+
+ You do not have any shortened links - try creating one. +
- {logs.filter((log) => log.link === link.link).length || 0} - {link.redirect_link}{link.expire_date}
- - - - - - - - - - - - {/* Render all logs for the link */} - {logs - .filter((log) => log.link === link.link) - .map((log, index, filteredLogs) => ( - - - - - - - + + + + + + + {/* Conditionally render logs for this link */} + {visibleLog === link.link && ( + + - - )} - - ))} - -
IDTimestampIPLocationISP
{filteredLogs.length - index}{log.timestamp}{log.ip}{log.location}{log.isp} - + {/* For every link and its logs */} + {links.map((link) => ( + +
+ + + {logs.filter((log) => log.link === link.link).length || 0} + {link.redirect_link}{link.expire_date}
+ + + + + + + + + + + + {/* Render all logs for the link */} + {logs + .filter((log) => log.link === link.link) + .map((log, index, filteredLogs) => ( + + + + + + + + + ))} + {/* If the link has no logs, put a special message */} + {logs.filter((log) => log.link === link.link) + .length === 0 && ( + + - ))} - {/* If the link has no logs, put a special message */} - {logs.filter((log) => log.link === link.link).length === - 0 && ( - - - - )} - -
IDTimestampIPLocationISP
{filteredLogs.length - index}{log.timestamp}{log.ip}{log.location}{log.isp} + +
+
+ No logs for this link +
-
- No logs for this link -
-
-
+ )} +
+ + + )} + + ))} + + + )} ); } diff --git a/app/src/components/Login.tsx b/app/src/components/Login.tsx index badb5b1..cc3a5bd 100644 --- a/app/src/components/Login.tsx +++ b/app/src/components/Login.tsx @@ -53,10 +53,13 @@ function Login() {

Log In

-

{error}

+

+ {error} +


diff --git a/app/src/components/Signup.tsx b/app/src/components/Signup.tsx index f4a3368..c01784e 100644 --- a/app/src/components/Signup.tsx +++ b/app/src/components/Signup.tsx @@ -66,10 +66,13 @@ function Signup() {

Sign up

-

{error}

+

+ {error} +