Biome format

This commit is contained in:
Kizuren 2025-12-09 01:51:46 +01:00
parent 47969209eb
commit 8729e57def
85 changed files with 2467 additions and 1983 deletions

View file

@ -96,5 +96,11 @@
"organizeImports": "on"
}
}
},
"css": {
"parser": {
"cssModules": true,
"tailwindDirectives": true
}
}
}

View file

@ -1,4 +1,4 @@
import type { NextConfig } from "next";
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
allowedDevOrigins: ['localhost', '*.marcus7i.net'],

View file

@ -7,6 +7,7 @@
"build": "next build",
"start": "next start",
"lint": "biome lint",
"lint:fix": "biome check --write .",
"format": "biome format . --write"
},
"dependencies": {

View file

@ -1,5 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
plugins: ['@tailwindcss/postcss'],
};
export default config;

View file

@ -76,7 +76,9 @@
padding: 1.5rem;
box-shadow: var(--card-shadow);
border: 1px solid var(--border-color);
transition: transform 0.2s, box-shadow 0.2s;
transition:
transform 0.2s,
box-shadow 0.2s;
text-align: center;
}
@ -129,7 +131,9 @@
padding: 1.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border: 1px solid var(--border-color);
transition: transform 0.2s, box-shadow 0.2s;
transition:
transform 0.2s,
box-shadow 0.2s;
height: 20rem;
display: flex;
flex-direction: column;

View file

@ -23,7 +23,8 @@
gap: 1rem;
}
.dashboardButton, .statsButton {
.dashboardButton,
.statsButton {
padding: 0.5rem 1rem;
border-radius: 4px;
font-weight: 500;
@ -42,7 +43,8 @@
color: white;
}
.dashboardButton:hover, .statsButton:hover {
.dashboardButton:hover,
.statsButton:hover {
opacity: 0.9;
transform: translateY(-2px);
}
@ -132,7 +134,8 @@
gap: 0.5rem;
}
.viewButton, .deleteButton {
.viewButton,
.deleteButton {
padding: 0.4rem 0.6rem;
border-radius: 4px;
font-size: 0.8rem;
@ -152,7 +155,8 @@
color: white;
}
.viewButton:hover, .deleteButton:hover {
.viewButton:hover,
.deleteButton:hover {
opacity: 0.9;
}
@ -172,7 +176,8 @@
font-size: 1rem;
}
.makeAdminButton, .removeAdminButton {
.makeAdminButton,
.removeAdminButton {
padding: 0.4rem 0.6rem;
border-radius: 4px;
font-size: 0.8rem;
@ -193,7 +198,8 @@
color: white;
}
.makeAdminButton:hover, .removeAdminButton:hover {
.makeAdminButton:hover,
.removeAdminButton:hover {
opacity: 0.9;
}

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import { useState, useEffect } from 'react';
import { useSession } from 'next-auth/react';
@ -23,7 +23,7 @@ export default function AdminDashboard() {
const [isRecreatingStats, setIsRecreatingStats] = useState(false);
useEffect(() => {
if (status === "unauthenticated" || (status === "authenticated" && !session?.user?.isAdmin)) {
if (status === 'unauthenticated' || (status === 'authenticated' && !session?.user?.isAdmin)) {
router.push('/dashboard');
}
}, [status, session, router]);
@ -41,10 +41,11 @@ export default function AdminDashboard() {
}
const term = searchTerm.toLowerCase();
const filtered = users.filter(user =>
user.account_id.toLowerCase().includes(term) ||
user.created_at.toLocaleString().toLowerCase().includes(term) ||
(user.is_admin ? "admin" : "user").includes(term)
const filtered = users.filter(
user =>
user.account_id.toLowerCase().includes(term) ||
user.created_at.toLocaleString().toLowerCase().includes(term) ||
(user.is_admin ? 'admin' : 'user').includes(term)
);
setFilteredUsers(filtered);
@ -65,7 +66,7 @@ export default function AdminDashboard() {
const processedUsers = data.users.map((user: User) => ({
account_id: user.account_id,
is_admin: user.is_admin,
created_at: new Date(user.created_at)
created_at: new Date(user.created_at),
}));
setUsers(processedUsers);
@ -119,7 +120,7 @@ export default function AdminDashboard() {
try {
setIsRecreatingStats(true);
const response = await fetch('/api/admin/statistics/rebuild', {
method: 'POST'
method: 'POST',
});
const data = await response.json();
@ -149,9 +150,7 @@ export default function AdminDashboard() {
showToast(data.message, 'success');
setUsers(prevUsers =>
prevUsers.map(user =>
user.account_id === accountId
? { ...user, is_admin: data.is_admin }
: user
user.account_id === accountId ? { ...user, is_admin: data.is_admin } : user
)
);
} else {
@ -163,11 +162,11 @@ export default function AdminDashboard() {
}
};
if (status === "loading" || loading) {
if (status === 'loading' || loading) {
return <div className={styles.loading}>Loading...</div>;
}
if (status === "unauthenticated" || (status === "authenticated" && !session?.user?.isAdmin)) {
if (status === 'unauthenticated' || (status === 'authenticated' && !session?.user?.isAdmin)) {
return null;
}
@ -176,7 +175,7 @@ export default function AdminDashboard() {
<header className={styles.adminHeader}>
<h1 className={styles.adminTitle}>Admin Dashboard</h1>
<div className={styles.actionButtons}>
<Link href="/dashboard">
<Link href='/dashboard'>
<button className={styles.dashboardButton}>Back to Dashboard</button>
</Link>
<button
@ -194,17 +193,17 @@ export default function AdminDashboard() {
<h2>Manage Users</h2>
<div className={styles.searchContainer}>
<input
type="text"
placeholder="Search users..."
type='text'
placeholder='Search users...'
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onChange={e => setSearchTerm(e.target.value)}
className={styles.searchInput}
/>
{searchTerm && (
<button
className={styles.clearSearchButton}
onClick={() => setSearchTerm('')}
title="Clear search"
title='Clear search'
>
</button>
@ -228,21 +227,21 @@ export default function AdminDashboard() {
</tr>
</thead>
<tbody>
{filteredUsers.map((user) => (
{filteredUsers.map(user => (
<tr key={user.account_id}>
<td>{user.account_id}</td>
<td>{user.created_at.toLocaleString()}</td>
<td>{user.is_admin ? 'Admin' : 'User'}</td>
<td className={styles.actions}>
<Link href={`/admin/user/${user.account_id}`}>
<button className={styles.viewButton}>
View Details
</button>
<button className={styles.viewButton}>View Details</button>
</Link>
{user.account_id !== session?.user?.accountId && (
<button
className={user.is_admin ? styles.removeAdminButton : styles.makeAdminButton}
className={
user.is_admin ? styles.removeAdminButton : styles.makeAdminButton
}
onClick={() => handleToggleAdminStatus(user.account_id)}
>
{user.is_admin ? 'Remove Admin' : 'Make Admin'}
@ -268,8 +267,8 @@ export default function AdminDashboard() {
<ConfirmModal
isOpen={isDeleteModalOpen}
title="Delete User"
message="Are you sure you want to delete this user? This will permanently remove their account and all associated data including links and analytics. This action cannot be undone."
title='Delete User'
message='Are you sure you want to delete this user? This will permanently remove their account and all associated data including links and analytics. This action cannot be undone.'
onConfirm={handleDeleteUser}
onCancel={() => setIsDeleteModalOpen(false)}
/>

View file

@ -23,7 +23,8 @@
gap: 1rem;
}
.backButton, .deleteButton {
.backButton,
.deleteButton {
padding: 0.5rem 1rem;
border-radius: 4px;
font-weight: 500;
@ -42,12 +43,15 @@
color: white;
}
.backButton:hover, .deleteButton:hover {
.backButton:hover,
.deleteButton:hover {
opacity: 0.9;
transform: translateY(-2px);
}
.userInfo, .linksSection, .sessionsSection {
.userInfo,
.linksSection,
.sessionsSection {
background-color: var(--card-bg);
border-radius: 8px;
padding: 1.5rem;

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import { useState, useEffect, useRef } from 'react';
import { useSession } from 'next-auth/react';
@ -44,19 +44,19 @@ export default function AdminLinkDetailPage() {
const [ipVersionStats, setIpVersionStats] = useState<StatItem[]>([]);
useEffect(() => {
if (status === "unauthenticated" || (status === "authenticated" && !session?.user?.isAdmin)) {
if (status === 'unauthenticated' || (status === 'authenticated' && !session?.user?.isAdmin)) {
router.push('/dashboard');
}
}, [status, session, router]);
function isValidUrl(urlStr: string) : boolean {
if(urlStr.trim() === "") {
function isValidUrl(urlStr: string): boolean {
if (urlStr.trim() === '') {
return false;
}
try {
const parsedUrl = new URL(urlStr);
return parsedUrl.protocol !== "" && parsedUrl.hostname !== "";
return parsedUrl.protocol !== '' && parsedUrl.hostname !== '';
} catch {
return false;
}
@ -71,7 +71,7 @@ export default function AdminLinkDetailPage() {
}, [isEditing]);
useEffect(() => {
if (isRedirecting.current || status !== "authenticated" || !session?.user?.isAdmin) return;
if (isRedirecting.current || status !== 'authenticated' || !session?.user?.isAdmin) return;
async function fetchLinkData() {
try {
@ -106,11 +106,13 @@ export default function AdminLinkDetailPage() {
}, [shortId, accountId, router, showToast, session, status]);
useEffect(() => {
if (!link || status !== "authenticated" || !session?.user?.isAdmin) return;
if (!link || status !== 'authenticated' || !session?.user?.isAdmin) return;
async function fetchAllAnalytics() {
try {
const response = await fetch(`/api/admin/users/${accountId}/links/${shortId}/analytics?all=true`);
const response = await fetch(
`/api/admin/users/${accountId}/links/${shortId}/analytics?all=true`
);
if (!response.ok) {
showToast('Failed to load complete analytics data', 'error');
return;
@ -130,11 +132,13 @@ export default function AdminLinkDetailPage() {
}, [link, shortId, accountId, showToast, session, status]);
useEffect(() => {
if (!link || status !== "authenticated" || !session?.user?.isAdmin) return;
if (!link || status !== 'authenticated' || !session?.user?.isAdmin) return;
async function fetchPaginatedAnalytics() {
try {
const response = await fetch(`/api/admin/users/${accountId}/links/${shortId}/analytics?page=${page}&limit=${limit}`);
const response = await fetch(
`/api/admin/users/${accountId}/links/${shortId}/analytics?page=${page}&limit=${limit}`
);
if (!response.ok) {
showToast('Failed to load analytics page', 'error');
return;
@ -187,21 +191,29 @@ export default function AdminLinkDetailPage() {
}, {});
// Convert to StatItem[] and sort by count
setBrowserStats(Object.entries(browsers)
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count));
setBrowserStats(
Object.entries(browsers)
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count)
);
setOsStats(Object.entries(oses)
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count));
setOsStats(
Object.entries(oses)
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count)
);
setCountryStats(Object.entries(countries)
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count));
setCountryStats(
Object.entries(countries)
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count)
);
setIpVersionStats(Object.entries(ipVersions)
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count));
setIpVersionStats(
Object.entries(ipVersions)
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count)
);
} catch {
showToast('An error occurred while processing analytics data', 'error');
} finally {
@ -229,7 +241,7 @@ export default function AdminLinkDetailPage() {
'Content-Type': 'application/json',
},
body: JSON.stringify({
target_url: targetUrl
target_url: targetUrl,
}),
});
@ -242,7 +254,7 @@ export default function AdminLinkDetailPage() {
setLink({
...link,
target_url: targetUrl,
last_modified: new Date()
last_modified: new Date(),
});
}
} else {
@ -261,7 +273,7 @@ export default function AdminLinkDetailPage() {
'Content-Type': 'application/json',
},
body: JSON.stringify({
analytics_id: analyticsToDelete
analytics_id: analyticsToDelete,
}),
});
@ -292,7 +304,7 @@ export default function AdminLinkDetailPage() {
'Content-Type': 'application/json',
},
body: JSON.stringify({
delete_all: true
delete_all: true,
}),
});
@ -317,7 +329,7 @@ export default function AdminLinkDetailPage() {
}
};
if (status === "loading" || isLoading) {
if (status === 'loading' || isLoading) {
return (
<div className={styles.loadingContainer}>
<div className={styles.loader}></div>
@ -326,13 +338,15 @@ export default function AdminLinkDetailPage() {
);
}
if (status === "unauthenticated" || (status === "authenticated" && !session?.user?.isAdmin)) {
if (status === 'unauthenticated' || (status === 'authenticated' && !session?.user?.isAdmin)) {
return null;
}
if (!link) {
return (
<div className={styles.error}>Link not found or you don&apos;t have permission to view it.</div>
<div className={styles.error}>
Link not found or you don&apos;t have permission to view it.
</div>
);
}
@ -342,8 +356,14 @@ export default function AdminLinkDetailPage() {
<div className={styles.titleSection}>
<h1 className={styles.title}>Admin Link Management</h1>
<div className={styles.breadcrumbs}>
<Link href="/admin" className={styles.breadcrumbLink}>Admin</Link> &gt;
<Link href={`/admin/user/${accountId}`} className={styles.breadcrumbLink}>User</Link> &gt;
<Link href='/admin' className={styles.breadcrumbLink}>
Admin
</Link>{' '}
&gt;
<Link href={`/admin/user/${accountId}`} className={styles.breadcrumbLink}>
User
</Link>{' '}
&gt;
<span className={styles.breadcrumbCurrent}>Link {shortId}</span>
</div>
</div>
@ -371,8 +391,8 @@ export default function AdminLinkDetailPage() {
<span className={styles.label}>Short URL:</span>
<a
href={`${window.location.origin}/l/${shortId}`}
target="_blank"
rel="noopener noreferrer"
target='_blank'
rel='noopener noreferrer'
className={styles.shortUrl}
>
{`${window.location.origin}/l/${shortId}`}
@ -411,10 +431,7 @@ export default function AdminLinkDetailPage() {
<div className={styles.targetUrlHeader}>
<span className={styles.label}>Target URL:</span>
{!isEditing && (
<button
className={styles.editButton}
onClick={() => setIsEditing(true)}
>
<button className={styles.editButton} onClick={() => setIsEditing(true)}>
Edit
</button>
)}
@ -422,41 +439,43 @@ export default function AdminLinkDetailPage() {
{isEditing ? (
<form
onSubmit={(e) => {
onSubmit={e => {
e.preventDefault();
handleEditLink();
}}
className={styles.editForm}
>
<input
ref={inputRef}
type="url"
value={targetUrl}
onChange={(e) => setTargetUrl(e.target.value)}
className={styles.urlInput}
placeholder="https://example.com"
<input
ref={inputRef}
type='url'
value={targetUrl}
onChange={e => setTargetUrl(e.target.value)}
className={styles.urlInput}
placeholder='https://example.com'
/>
<div className={styles.editActions}>
<button
type="button"
type='button'
className={styles.cancelButton}
onClick={() => {
setIsEditing(false);
setTargetUrl(link?.target_url || '');
setIsEditing(false);
setTargetUrl(link?.target_url || '');
}}
>
Cancel
</button>
<button
type="submit"
className={styles.saveButton}
>
<button type='submit' className={styles.saveButton}>
Save
</button>
</div>
</form>
) : (
<a href={link.target_url} target="_blank" rel="noopener noreferrer" className={styles.targetUrl}>
<a
href={link.target_url}
target='_blank'
rel='noopener noreferrer'
className={styles.targetUrl}
>
{link.target_url}
</a>
)}
@ -468,14 +487,9 @@ export default function AdminLinkDetailPage() {
<div className={styles.analyticsSection}>
<div className={styles.analyticsHeader}>
<h2>Analytics</h2>
<span className={styles.totalClicks}>
Total Clicks: {totalAnalytics}
</span>
<span className={styles.totalClicks}>Total Clicks: {totalAnalytics}</span>
{totalAnalytics > 0 && (
<button
className={styles.deleteAllButton}
onClick={() => setShowDeleteAllModal(true)}
>
<button className={styles.deleteAllButton} onClick={() => setShowDeleteAllModal(true)}>
Delete All Analytics
</button>
)}
@ -486,38 +500,23 @@ export default function AdminLinkDetailPage() {
<div className={styles.graphs}>
<div className={styles.graphCard}>
<h3>Browsers</h3>
<Graph
type="doughnut"
data={browserStats}
loading={isLoadingStats}
height={200}
/>
<Graph type='doughnut' data={browserStats} loading={isLoadingStats} height={200} />
</div>
<div className={styles.graphCard}>
<h3>Operating Systems</h3>
<Graph
type="doughnut"
data={osStats}
loading={isLoadingStats}
height={200}
/>
<Graph type='doughnut' data={osStats} loading={isLoadingStats} height={200} />
</div>
<div className={styles.graphCard}>
<h3>Countries</h3>
<Graph
type="doughnut"
data={countryStats}
loading={isLoadingStats}
height={200}
/>
<Graph type='doughnut' data={countryStats} loading={isLoadingStats} height={200} />
</div>
<div className={styles.graphCard}>
<h3>IP Versions</h3>
<Graph
type="doughnut"
type='doughnut'
data={ipVersionStats}
loading={isLoadingStats}
height={200}
@ -532,7 +531,7 @@ export default function AdminLinkDetailPage() {
currentPage={page}
itemsPerPage={limit}
onPageChange={handlePageChange}
onDeleteClick={(id) => {
onDeleteClick={id => {
setAnalyticsToDelete(id);
setShowDeleteModal(true);
}}
@ -548,10 +547,10 @@ export default function AdminLinkDetailPage() {
{/* Confirm Delete Modal */}
<ConfirmModal
isOpen={showDeleteModal}
title="Delete Analytics Entry"
message="Are you sure you want to delete this analytics entry? This action cannot be undone."
confirmLabel="Delete"
cancelLabel="Cancel"
title='Delete Analytics Entry'
message='Are you sure you want to delete this analytics entry? This action cannot be undone.'
confirmLabel='Delete'
cancelLabel='Cancel'
onConfirm={handleDeleteAnalytics}
onCancel={() => {
setShowDeleteModal(false);
@ -562,10 +561,10 @@ export default function AdminLinkDetailPage() {
{/* Confirm Delete All Modal */}
<ConfirmModal
isOpen={showDeleteAllModal}
title="Delete All Analytics"
message="Are you sure you want to delete all analytics for this link? This action cannot be undone."
confirmLabel="Delete All"
cancelLabel="Cancel"
title='Delete All Analytics'
message='Are you sure you want to delete all analytics for this link? This action cannot be undone.'
confirmLabel='Delete All'
cancelLabel='Cancel'
onConfirm={handleDeleteAllAnalytics}
onCancel={() => setShowDeleteAllModal(false)}
/>

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
@ -26,13 +26,13 @@ export default function UserDetailPage() {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
useEffect(() => {
if (status === "unauthenticated" || (status === "authenticated" && !session?.user?.isAdmin)) {
if (status === 'unauthenticated' || (status === 'authenticated' && !session?.user?.isAdmin)) {
router.push('/dashboard');
}
}, [status, session, router]);
useEffect(() => {
if (status === "authenticated" && session?.user?.isAdmin) {
if (status === 'authenticated' && session?.user?.isAdmin) {
fetchUserData();
}
}, [status, session, accountId]);
@ -83,16 +83,14 @@ export default function UserDetailPage() {
const response = await fetch(`/api/admin/users/${accountId}/sessions/revoke`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId, accountId })
body: JSON.stringify({ sessionId, accountId }),
});
const data = await response.json();
if (response.ok && data.success) {
showToast('Session revoked successfully', 'success');
setSessions(prevSessions =>
prevSessions.filter(s => s.id !== sessionId)
);
setSessions(prevSessions => prevSessions.filter(s => s.id !== sessionId));
} else {
showToast(data.message || 'Failed to revoke session', 'error');
}
@ -135,7 +133,7 @@ export default function UserDetailPage() {
return date.toLocaleString();
};
if (status === "loading" || loading) {
if (status === 'loading' || loading) {
return <div className={styles.loading}>Loading user details...</div>;
}
@ -148,14 +146,11 @@ export default function UserDetailPage() {
<header className={styles.header}>
<h1 className={styles.title}>User Details</h1>
<div className={styles.actionButtons}>
<Link href="/admin">
<Link href='/admin'>
<button className={styles.backButton}>Back to Admin</button>
</Link>
{!user.is_admin && (
<button
className={styles.deleteButton}
onClick={() => setIsDeleteModalOpen(true)}
>
<button className={styles.deleteButton} onClick={() => setIsDeleteModalOpen(true)}>
Delete User
</button>
)}
@ -206,7 +201,7 @@ export default function UserDetailPage() {
</tr>
</thead>
<tbody>
{sessions.map((s) => (
{sessions.map(s => (
<tr key={s.id}>
<td>{s.userAgent.split(' ').slice(0, 3).join(' ')}</td>
<td>{s.ipAddress}</td>
@ -231,7 +226,7 @@ export default function UserDetailPage() {
<ConfirmModal
isOpen={isDeleteModalOpen}
title="Delete User"
title='Delete User'
message={`Are you sure you want to delete user ${accountId}? This will permanently remove their account and all associated data including links and analytics. This action cannot be undone.`}
onConfirm={handleDeleteUser}
onCancel={() => setIsDeleteModalOpen(false)}

View file

@ -9,10 +9,13 @@ export async function POST() {
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json({
message: "Unauthorized",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized',
success: false,
},
{ status: 401 }
);
}
const result = await updateStats();
@ -23,9 +26,12 @@ export async function POST() {
});
} catch (error) {
logger.error('Error rebuilding statistics:', { error });
return NextResponse.json({
message: "Failed to rebuild statistics",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Failed to rebuild statistics',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -13,43 +13,55 @@ export async function POST(
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json({
message: "Unauthorized",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized',
success: false,
},
{ status: 401 }
);
}
const { accountId } = await params;
if (!accountId) {
return NextResponse.json({
message: "Account ID is required",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Account ID is required',
success: false,
},
{ status: 400 }
);
}
if (accountId === session.user.accountId) {
return NextResponse.json({
message: "You cannot change your own admin status",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'You cannot change your own admin status',
success: false,
},
{ status: 400 }
);
}
const isAdmin = await isUserAdmin(accountId);
const result = await makeUserAdmin(accountId, !isAdmin);
if (!result.success) {
return NextResponse.json({
message: result.status,
success: false,
}, { status: result.status === "User not found" ? 404 : 500 });
return NextResponse.json(
{
message: result.status,
success: false,
},
{ status: result.status === 'User not found' ? 404 : 500 }
);
}
await removeAllSessionsByAccountId(accountId);
logger.info(`Admin status toggled for user ${accountId}, all sessions revoked`, {
accountId,
newStatus: !isAdmin
newStatus: !isAdmin,
});
return NextResponse.json({
@ -59,9 +71,12 @@ export async function POST(
});
} catch (error) {
logger.error('Error toggling admin status:', error);
return NextResponse.json({
message: "Failed to toggle admin status",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Failed to toggle admin status',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -7,36 +7,38 @@ import logger from '@/lib/logger';
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ accountId: string, shortId: string }> }
{ params }: { params: Promise<{ accountId: string; shortId: string }> }
) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json({
message: "Unauthorized",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized',
success: false,
},
{ status: 401 }
);
}
const { accountId, shortId } = await params;
if (!accountId || !shortId) {
return NextResponse.json({
message: "Account ID and Short ID are required",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Account ID and Short ID are required',
success: false,
},
{ status: 400 }
);
}
const url = new URL(req.url);
const page = parseInt(url.searchParams.get('page') || '1');
const limit = parseInt(url.searchParams.get('limit') || '50');
const { analytics, total } = await getAllAnalytics(
accountId,
shortId,
{ page, limit }
);
const { analytics, total } = await getAllAnalytics(accountId, shortId, { page, limit });
const sanitizedAnalytics = analytics.map(item => sanitizeMongoDocument(item));
@ -46,40 +48,53 @@ export async function GET(
total,
page,
limit,
pages: Math.ceil(total / limit)
pages: Math.ceil(total / limit),
},
success: true,
});
} catch (error) {
logger.error('Error getting analytics:', { error, accountId: (await params).accountId, shortId: (await params).shortId });
return NextResponse.json({
message: "Failed to retrieve analytics",
success: false,
}, { status: 500 });
logger.error('Error getting analytics:', {
error,
accountId: (await params).accountId,
shortId: (await params).shortId,
});
return NextResponse.json(
{
message: 'Failed to retrieve analytics',
success: false,
},
{ status: 500 }
);
}
}
export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ accountId: string, shortId: string }> }
{ params }: { params: Promise<{ accountId: string; shortId: string }> }
) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json({
message: "Unauthorized",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized',
success: false,
},
{ status: 401 }
);
}
const { accountId, shortId } = await params;
if (!accountId || !shortId) {
return NextResponse.json({
message: "Account ID and Short ID are required",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Account ID and Short ID are required',
success: false,
},
{ status: 400 }
);
}
const body = await req.json();
@ -87,30 +102,46 @@ export async function DELETE(
if (body.delete_all) {
const result = await removeAllAnalytics(accountId, shortId);
return NextResponse.json({
message: result.status,
success: result.success,
}, { status: result.success ? 200 : 400 });
return NextResponse.json(
{
message: result.status,
success: result.success,
},
{ status: result.success ? 200 : 400 }
);
}
if (body.analytics_id) {
const result = await removeAnalytics(accountId, shortId, body.analytics_id);
return NextResponse.json({
message: result.status,
success: result.success,
}, { status: result.success ? 200 : 400 });
return NextResponse.json(
{
message: result.status,
success: result.success,
},
{ status: result.success ? 200 : 400 }
);
}
return NextResponse.json({
message: "Either delete_all or analytics_id must be provided",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Either delete_all or analytics_id must be provided',
success: false,
},
{ status: 400 }
);
} catch (error) {
logger.error('Error deleting analytics:', { error, accountId: (await params).accountId, shortId: (await params).shortId });
return NextResponse.json({
message: "Failed to delete analytics",
success: false,
}, { status: 500 });
logger.error('Error deleting analytics:', {
error,
accountId: (await params).accountId,
shortId: (await params).shortId,
});
return NextResponse.json(
{
message: 'Failed to delete analytics',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -7,34 +7,43 @@ import logger from '@/lib/logger';
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ accountId: string, shortId: string }> }
{ params }: { params: Promise<{ accountId: string; shortId: string }> }
) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json({
message: "Unauthorized",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized',
success: false,
},
{ status: 401 }
);
}
const { accountId, shortId } = await params;
if (!accountId || !shortId) {
return NextResponse.json({
message: "Account ID and Short ID are required",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Account ID and Short ID are required',
success: false,
},
{ status: 400 }
);
}
const { link, return: linkReturn } = await getLinkById(accountId, shortId);
if (!linkReturn.success || !link) {
return NextResponse.json({
message: linkReturn.status,
success: false,
}, { status: 404 });
return NextResponse.json(
{
message: linkReturn.status,
success: false,
},
{ status: 404 }
);
}
return NextResponse.json({
@ -42,96 +51,138 @@ export async function GET(
success: true,
});
} catch (error) {
logger.error('Error getting link details:', { error, accountId: (await params).accountId, shortId: (await params).shortId });
return NextResponse.json({
message: "Failed to retrieve link details",
success: false,
}, { status: 500 });
logger.error('Error getting link details:', {
error,
accountId: (await params).accountId,
shortId: (await params).shortId,
});
return NextResponse.json(
{
message: 'Failed to retrieve link details',
success: false,
},
{ status: 500 }
);
}
}
export async function PATCH(
req: NextRequest,
{ params }: { params: Promise<{ accountId: string, shortId: string }> }
{ params }: { params: Promise<{ accountId: string; shortId: string }> }
) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json({
message: "Unauthorized",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized',
success: false,
},
{ status: 401 }
);
}
const { accountId, shortId } = await params;
if (!accountId || !shortId) {
return NextResponse.json({
message: "Account ID and Short ID are required",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Account ID and Short ID are required',
success: false,
},
{ status: 400 }
);
}
const body = await req.json();
const { target_url } = body;
if (!target_url) {
return NextResponse.json({
message: "Target URL is required",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Target URL is required',
success: false,
},
{ status: 400 }
);
}
const result = await editLink(accountId, shortId, target_url);
return NextResponse.json({
message: result.status,
success: result.success,
}, { status: result.success ? 200 : 400 });
return NextResponse.json(
{
message: result.status,
success: result.success,
},
{ status: result.success ? 200 : 400 }
);
} catch (error) {
logger.error('Error updating link:', { error, accountId: (await params).accountId, shortId: (await params).shortId });
return NextResponse.json({
message: "Failed to update link",
success: false,
}, { status: 500 });
logger.error('Error updating link:', {
error,
accountId: (await params).accountId,
shortId: (await params).shortId,
});
return NextResponse.json(
{
message: 'Failed to update link',
success: false,
},
{ status: 500 }
);
}
}
export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ accountId: string, shortId: string }> }
{ params }: { params: Promise<{ accountId: string; shortId: string }> }
) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json({
message: "Unauthorized",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized',
success: false,
},
{ status: 401 }
);
}
const { accountId, shortId } = await params;
if (!accountId || !shortId) {
return NextResponse.json({
message: "Account ID and Short ID are required",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Account ID and Short ID are required',
success: false,
},
{ status: 400 }
);
}
const result = await removeLink(accountId, shortId);
return NextResponse.json({
message: result.status,
success: result.success,
}, { status: result.success ? 200 : 400 });
return NextResponse.json(
{
message: result.status,
success: result.success,
},
{ status: result.success ? 200 : 400 }
);
} catch (error) {
logger.error('Error deleting link:', { error, accountId: (await params).accountId, shortId: (await params).shortId });
return NextResponse.json({
message: "Failed to delete link",
success: false,
}, { status: 500 });
logger.error('Error deleting link:', {
error,
accountId: (await params).accountId,
shortId: (await params).shortId,
});
return NextResponse.json(
{
message: 'Failed to delete link',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -13,23 +13,29 @@ export async function GET(
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json({
message: "Unauthorized",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized',
success: false,
},
{ status: 401 }
);
}
const { accountId } = await params;
if (!accountId) {
return NextResponse.json({
message: "Account ID is required",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Account ID is required',
success: false,
},
{ status: 400 }
);
}
const { links } = await getLinks(accountId);
const sanitizedLinks = links.map(link => (sanitizeMongoDocument(link)));
const sanitizedLinks = links.map(link => sanitizeMongoDocument(link));
return NextResponse.json({
links: sanitizedLinks,
@ -37,9 +43,12 @@ export async function GET(
});
} catch (error) {
logger.error('Error getting user links:', error);
return NextResponse.json({
message: "Failed to retrieve user links",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Failed to retrieve user links',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -13,28 +13,37 @@ export async function GET(
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json({
message: "Unauthorized",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized',
success: false,
},
{ status: 401 }
);
}
const { accountId } = await params;
if (!accountId) {
return NextResponse.json({
message: "Account ID is required",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Account ID is required',
success: false,
},
{ status: 400 }
);
}
const user = await getUserById(accountId);
if (!user) {
return NextResponse.json({
message: "User not found",
success: false,
}, { status: 404 });
return NextResponse.json(
{
message: 'User not found',
success: false,
},
{ status: 404 }
);
}
const sanitizedUser = sanitizeMongoDocument(user);
@ -45,9 +54,12 @@ export async function GET(
});
} catch (error) {
logger.error('Error getting user:', error);
return NextResponse.json({
message: "Failed to retrieve user",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Failed to retrieve user',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -9,19 +9,25 @@ export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json({
message: "Unauthorized",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized',
success: false,
},
{ status: 401 }
);
}
const { sessionId, accountId } = await req.json();
if (!sessionId || !accountId) {
return NextResponse.json({
message: "Session ID and Account ID are required",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Session ID and Account ID are required',
success: false,
},
{ status: 400 }
);
}
const result = await revokeSession(sessionId, accountId);
@ -31,9 +37,12 @@ export async function POST(req: NextRequest) {
});
} catch (error) {
logger.error('Error revoking session:', { error });
return NextResponse.json({
message: "Failed to revoke session",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Failed to revoke session',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -13,19 +13,25 @@ export async function GET(
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json({
message: "Unauthorized",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized',
success: false,
},
{ status: 401 }
);
}
const { accountId } = await params;
if (!accountId) {
return NextResponse.json({
message: "Account ID is required",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Account ID is required',
success: false,
},
{ status: 400 }
);
}
const sessions = await getSessions(accountId);
@ -39,9 +45,12 @@ export async function GET(
});
} catch (error) {
logger.error('Error getting user sessions:', error);
return NextResponse.json({
message: "Failed to retrieve user sessions",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Failed to retrieve user sessions',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -10,16 +10,19 @@ export async function GET() {
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json({
message: "Unauthorized",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized',
success: false,
},
{ status: 401 }
);
}
const result = await listUsers();
if (result.users) {
const sanitizedUsers = result.users.map(user => (sanitizeMongoDocument(user)));
const sanitizedUsers = result.users.map(user => sanitizeMongoDocument(user));
return NextResponse.json({
users: sanitizedUsers,
@ -28,16 +31,22 @@ export async function GET() {
});
}
return NextResponse.json({
message: result.return.status,
success: result.return.success,
}, { status: result.return.success ? 200 : 500 });
return NextResponse.json(
{
message: result.return.status,
success: result.return.success,
},
{ status: result.return.success ? 200 : 500 }
);
} catch (error) {
logger.error('Error getting users:', { error });
return NextResponse.json({
message: "Failed to retrieve users",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Failed to retrieve users',
success: false,
},
{ status: 500 }
);
}
}
@ -46,33 +55,45 @@ export async function DELETE(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json({
message: "Unauthorized",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized',
success: false,
},
{ status: 401 }
);
}
const { account_id } = await req.json();
if (!account_id) {
return NextResponse.json({
message: "Account ID is required",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Account ID is required',
success: false,
},
{ status: 400 }
);
}
if (account_id === session.user.accountId) {
return NextResponse.json({
message: "You cannot delete your own account from admin panel",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'You cannot delete your own account from admin panel',
success: false,
},
{ status: 400 }
);
}
if (await isUserAdmin(account_id)) {
return NextResponse.json({
message: "Cannot delete admin accounts",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Cannot delete admin accounts',
success: false,
},
{ status: 400 }
);
}
const result = await removeUser(account_id);
@ -83,9 +104,12 @@ export async function DELETE(req: NextRequest) {
});
} catch (error) {
logger.error('Error deleting user:', { error });
return NextResponse.json({
message: "Failed to delete user",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Failed to delete user',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -12,10 +12,13 @@ export async function GET(req: NextRequest) {
if (!session?.user?.accountId) {
logger.info('Analytics request failed due to unauthorized access', { url: req.url });
return NextResponse.json({
message: "Unauthorized. Please sign in first.",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized. Please sign in first.',
success: false,
},
{ status: 401 }
);
}
const account_id = session.user.accountId;
@ -24,10 +27,13 @@ export async function GET(req: NextRequest) {
const link_id = url.searchParams.get('link_id');
if (!link_id) {
logger.info('Analytics request failed due to missing link_id', { account_id, url: req.url });
return NextResponse.json({
message: "Missing link_id parameter",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Missing link_id parameter',
success: false,
},
{ status: 400 }
);
}
const all = url.searchParams.get('all') === 'true';
@ -56,28 +62,34 @@ export async function GET(req: NextRequest) {
page,
limit,
startDate,
endDate
endDate,
};
const { analytics, total } = await getAllAnalytics(account_id, link_id, queryOptions);
return NextResponse.json({
message: "Analytics retrieved successfully",
success: true,
analytics,
pagination: {
total,
page,
limit,
totalPages: Math.ceil(total / limit)
}
}, { status: 200 });
return NextResponse.json(
{
message: 'Analytics retrieved successfully',
success: true,
analytics,
pagination: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
},
{ status: 200 }
);
} catch (error) {
logger.error('Analytics retrieval error:', { error, url: req.url });
return NextResponse.json({
message: "Failed to retrieve analytics",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Failed to retrieve analytics',
success: false,
},
{ status: 500 }
);
}
}
@ -88,10 +100,13 @@ export async function DELETE(req: NextRequest) {
if (!session?.user?.accountId) {
logger.info('Analytics deletion request failed due to unauthorized access', { url: req.url });
return NextResponse.json({
message: "Unauthorized. Please sign in first.",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized. Please sign in first.',
success: false,
},
{ status: 401 }
);
}
const account_id = session.user.accountId;
@ -107,18 +122,27 @@ export async function DELETE(req: NextRequest) {
}
} catch {
logger.info('Analytics deletion request failed due to missing parameters', { url: req.url });
return NextResponse.json({
message: "Missing required parameters",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Missing required parameters',
success: false,
},
{ status: 400 }
);
}
if (!link_id) {
logger.info('Analytics deletion request failed due to missing link_id', { account_id, url: req.url });
return NextResponse.json({
message: "Missing link_id parameter",
success: false,
}, { status: 400 });
logger.info('Analytics deletion request failed due to missing link_id', {
account_id,
url: req.url,
});
return NextResponse.json(
{
message: 'Missing link_id parameter',
success: false,
},
{ status: 400 }
);
}
// Delete all analytics for a link
@ -126,27 +150,49 @@ export async function DELETE(req: NextRequest) {
const result = await removeAllAnalytics(account_id, link_id);
if (result.success) {
logger.info('All analytics deletion request succeeded', { account_id, link_id, url: req.url });
return NextResponse.json({
message: "All analytics records deleted successfully",
success: true,
}, { status: 200 });
logger.info('All analytics deletion request succeeded', {
account_id,
link_id,
url: req.url,
});
return NextResponse.json(
{
message: 'All analytics records deleted successfully',
success: true,
},
{ status: 200 }
);
} else {
logger.info('All analytics deletion request failed', { error: result.status, account_id, link_id, url: req.url });
return NextResponse.json({
message: result.status,
success: false,
}, { status: 404 });
logger.info('All analytics deletion request failed', {
error: result.status,
account_id,
link_id,
url: req.url,
});
return NextResponse.json(
{
message: result.status,
success: false,
},
{ status: 404 }
);
}
}
// Delete single analytics record
if (!analytics_id) {
logger.info('Analytics deletion request failed due to missing analytics_id', { account_id, link_id, url: req.url });
return NextResponse.json({
message: "Missing analytics_id parameter for single record deletion",
success: false,
}, { status: 400 });
logger.info('Analytics deletion request failed due to missing analytics_id', {
account_id,
link_id,
url: req.url,
});
return NextResponse.json(
{
message: 'Missing analytics_id parameter for single record deletion',
success: false,
},
{ status: 400 }
);
}
const result = await removeAnalytics(account_id, link_id, analytics_id);
@ -156,30 +202,39 @@ export async function DELETE(req: NextRequest) {
account_id,
link_id,
analytics_id,
url: req.url
url: req.url,
});
return NextResponse.json({
message: "Analytics record deleted successfully",
success: true,
}, { status: 200 });
return NextResponse.json(
{
message: 'Analytics record deleted successfully',
success: true,
},
{ status: 200 }
);
} else {
logger.info('Single analytics record deletion request failed', {
error: result.status,
account_id,
link_id,
analytics_id,
url: req.url
url: req.url,
});
return NextResponse.json({
message: result.status,
success: false,
}, { status: 404 });
return NextResponse.json(
{
message: result.status,
success: false,
},
{ status: 404 }
);
}
} catch (error) {
logger.error('Analytics deletion error:', { error, url: req.url });
return NextResponse.json({
message: "Failed to delete analytics",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Failed to delete analytics',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -1,18 +1,18 @@
import NextAuth, { NextAuthOptions, User } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import NextAuth, { NextAuthOptions, User } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import logger from '@/lib/logger';
import { existsUser, isUserAdmin } from "@/lib/userdb";
import { JWT } from "next-auth/jwt";
import { Session } from "next-auth";
import { existsUser, isUserAdmin } from '@/lib/userdb';
import { JWT } from 'next-auth/jwt';
import { Session } from 'next-auth';
import { createSession, updateSessionActivity, revokeSession } from '@/lib/sessiondb';
import { headers } from 'next/headers';
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: "Account ID",
name: 'Account ID',
credentials: {
accountId: { label: "Account ID", type: "text", placeholder: "Enter your Account ID" }
accountId: { label: 'Account ID', type: 'text', placeholder: 'Enter your Account ID' },
},
async authorize(credentials) {
const { accountId } = credentials as { accountId: string };
@ -30,26 +30,20 @@ export const authOptions: NextAuthOptions = {
}
return null;
}
},
}),
],
callbacks: {
async jwt({ token, user, trigger }: { token: JWT; user: User, trigger?: string }) {
async jwt({ token, user, trigger }: { token: JWT; user: User; trigger?: string }) {
if (user) {
token.accountId = user.accountId;
token.isAdmin = user.isAdmin;
const headersList = await headers();
const userAgent = headersList.get('user-agent') || 'Unknown';
const ip = headersList.get('x-forwarded-for') ||
headersList.get('x-real-ip') ||
'Unknown';
const ip = headersList.get('x-forwarded-for') || headersList.get('x-real-ip') || 'Unknown';
const { sessionId } = await createSession(
user.accountId,
userAgent,
ip
);
const { sessionId } = await createSession(user.accountId, userAgent, ip);
token.sessionId = sessionId;
}
@ -66,11 +60,11 @@ export const authOptions: NextAuthOptions = {
...session.user,
accountId: token.accountId as string,
isAdmin: token.isAdmin as boolean,
sessionId: token.sessionId as string
sessionId: token.sessionId as string,
};
}
return session;
}
},
},
events: {
async signOut({ token }) {
@ -81,20 +75,20 @@ export const authOptions: NextAuthOptions = {
logger.error('Error terminating session on signOut:', error);
}
}
}
},
},
pages: {
signIn: '/',
error: '/',
},
session: {
strategy: "jwt" as const,
strategy: 'jwt' as const,
maxAge: 30 * 24 * 60 * 60, // 30 days (session lifetime)
},
jwt: {
maxAge: 5 * 60, // 5 min (token lifetime)
},
secret: process.env.NEXTAUTH_SECRET || "/JZ9N+lqRtvspbAfs0HK41RkthPYuUdqxb+cuimYOXw=",
secret: process.env.NEXTAUTH_SECRET || '/JZ9N+lqRtvspbAfs0HK41RkthPYuUdqxb+cuimYOXw=',
};
const handler = NextAuth(authOptions);

View file

@ -9,35 +9,41 @@ export async function GET() {
const session = await getServerSession(authOptions);
if (!session?.user?.accountId || !session?.user?.sessionId) {
return NextResponse.json({
valid: false,
message: "No active session"
}, { status: 401 });
return NextResponse.json(
{
valid: false,
message: 'No active session',
},
{ status: 401 }
);
}
const isValid = await isSessionValid(
session.user.sessionId,
session.user.accountId
);
const isValid = await isSessionValid(session.user.sessionId, session.user.accountId);
if (!isValid) {
logger.info('Session check failed - revoked or expired session', {
sessionId: session.user.sessionId,
accountId: session.user.accountId
accountId: session.user.accountId,
});
return NextResponse.json({
valid: false,
message: "Session has been revoked"
}, { status: 401 });
return NextResponse.json(
{
valid: false,
message: 'Session has been revoked',
},
{ status: 401 }
);
}
return NextResponse.json({ valid: true });
} catch (error) {
logger.error('Error checking session:', error);
return NextResponse.json({
valid: false,
message: "Error checking session"
}, { status: 500 });
return NextResponse.json(
{
valid: false,
message: 'Error checking session',
},
{ status: 500 }
);
}
}

View file

@ -18,10 +18,13 @@ export async function POST(req: NextRequest) {
}
} catch {
logger.info('Registration request failed due to missing parameters', { url: req.url });
return NextResponse.json({
message: "Missing required parameters",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Missing required parameters',
success: false,
},
{ status: 400 }
);
}
if (is_admin) {
@ -30,32 +33,44 @@ export async function POST(req: NextRequest) {
if (isAuthorized) {
const account = await createUser(is_admin);
logger.info('Account creation request succeeded (admin)', { is_admin, url: req.url });
return NextResponse.json({
message: "Admin registration successful",
success: true,
account_id: account.account_id,
}, { status: 200 });
return NextResponse.json(
{
message: 'Admin registration successful',
success: true,
account_id: account.account_id,
},
{ status: 200 }
);
} else {
logger.info('Registration request failed due to missing rights', { is_admin, url: req.url });
return NextResponse.json({
message: "Unauthorized admin registration attempt",
success: false,
}, { status: 401 });
logger.info('Registration request failed due to missing rights', {
is_admin,
url: req.url,
});
return NextResponse.json(
{
message: 'Unauthorized admin registration attempt',
success: false,
},
{ status: 401 }
);
}
}
const account = await createUser(false);
return NextResponse.json({
message: "Registration successful",
message: 'Registration successful',
success: true,
account_id: account.account_id,
});
} catch (error) {
logger.error('Registration error:', { error, url: req.url });
return NextResponse.json({
message: "Registration failed",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Registration failed',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -18,30 +18,42 @@ export async function DELETE(req: NextRequest) {
}
} catch {
logger.info('Account removal request failed due to missing parameters', { url: req.url });
return NextResponse.json({
message: "Missing required parameters",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Missing required parameters',
success: false,
},
{ status: 400 }
);
}
if (session?.user?.accountId === account_id || await isUserAdmin(session?.user?.accountId)) {
if (session?.user?.accountId === account_id || (await isUserAdmin(session?.user?.accountId))) {
const accountRemovalResponse = await removeUser(account_id);
return NextResponse.json({
message: accountRemovalResponse.status,
success: accountRemovalResponse.success,
}, { status: accountRemovalResponse.success ? 200 : 500 });
return NextResponse.json(
{
message: accountRemovalResponse.status,
success: accountRemovalResponse.success,
},
{ status: accountRemovalResponse.success ? 200 : 500 }
);
}
logger.info('Account removal request failed due to missing rights', { url: req.url });
return NextResponse.json({
message: "Unauthorized account removal attempt",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized account removal attempt',
success: false,
},
{ status: 401 }
);
} catch (error) {
logger.error('Account removal error:', { error, url: req.url });
return NextResponse.json({
message: "Account removal failed",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Account removal failed',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -10,26 +10,32 @@ export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user?.accountId) {
return NextResponse.json({
message: "Unauthorized",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized',
success: false,
},
{ status: 401 }
);
}
const { sessionId } = await req.json();
if (!sessionId) {
return NextResponse.json({
message: "Session ID is required",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Session ID is required',
success: false,
},
{ status: 400 }
);
}
const result = await revokeSession(sessionId, session.user.accountId);
const isCurrentSession = sessionId === session.user.sessionId;
if (isCurrentSession) {
signOut({ redirect: false });
signOut({ redirect: false });
}
return NextResponse.json({
@ -38,9 +44,12 @@ export async function POST(req: NextRequest) {
});
} catch (error) {
logger.error('Error revoking session:', { error });
return NextResponse.json({
message: "Failed to revoking session",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Failed to revoking session',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -9,10 +9,13 @@ export async function GET() {
const session = await getServerSession(authOptions);
if (!session?.user?.accountId) {
return NextResponse.json({
message: "Unauthorized",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized',
success: false,
},
{ status: 401 }
);
}
const sessions = await getSessions(session.user.accountId);
@ -21,7 +24,7 @@ export async function GET() {
const currentSessionId = session.user.sessionId;
const sessionsWithCurrent = sessions.map(s => ({
...s,
isCurrentSession: s.id === currentSessionId
isCurrentSession: s.id === currentSessionId,
}));
return NextResponse.json({
@ -30,9 +33,12 @@ export async function GET() {
});
} catch (error) {
logger.error('Error getting sessions:', { error });
return NextResponse.json({
message: "Failed to retrieve sessions",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Failed to retrieve sessions',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -14,10 +14,13 @@ export async function GET(req: NextRequest) {
if (!session?.user?.accountId) {
logger.info('Link retrieval request failed due to unauthorized access', { url: req.url });
return NextResponse.json({
message: "Unauthorized. Please sign in first.",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized. Please sign in first.',
success: false,
},
{ status: 401 }
);
}
const account_id = session.user.accountId;
@ -26,34 +29,54 @@ export async function GET(req: NextRequest) {
const shortId = url.searchParams.get('shortId');
if (!shortId) {
logger.info('Link retrieval request failed due to missing shortId', { account_id, url: req.url });
return NextResponse.json({
message: "Missing shortId parameter",
success: false,
}, { status: 400 });
logger.info('Link retrieval request failed due to missing shortId', {
account_id,
url: req.url,
});
return NextResponse.json(
{
message: 'Missing shortId parameter',
success: false,
},
{ status: 400 }
);
}
const { link, return: returnValue } = await getLinkById(account_id, shortId);
if (returnValue.success && link) {
return NextResponse.json({
message: "Link retrieved successfully",
success: true,
link: sanitizeMongoDocument(link),
}, { status: 200 });
return NextResponse.json(
{
message: 'Link retrieved successfully',
success: true,
link: sanitizeMongoDocument(link),
},
{ status: 200 }
);
}
logger.info('Link retrieval request failed', { error: returnValue.status, account_id, shortId, url: req.url });
return NextResponse.json({
message: returnValue.status || "Link not found",
success: false,
}, { status: 404 });
logger.info('Link retrieval request failed', {
error: returnValue.status,
account_id,
shortId,
url: req.url,
});
return NextResponse.json(
{
message: returnValue.status || 'Link not found',
success: false,
},
{ status: 404 }
);
} catch (error) {
logger.error('Link retrieval error:', { error, url: req.url });
return NextResponse.json({
message: "Failed to retrieve link",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Failed to retrieve link',
success: false,
},
{ status: 500 }
);
}
}
@ -64,10 +87,13 @@ export async function POST(req: NextRequest) {
if (!session?.user?.accountId) {
logger.info('Link creation request failed due to unauthorized access', { url: req.url });
return NextResponse.json({
message: "Unauthorized. Please sign in first.",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized. Please sign in first.',
success: false,
},
{ status: 401 }
);
}
const account_id = session.user.accountId;
@ -81,41 +107,60 @@ export async function POST(req: NextRequest) {
}
} catch {
logger.info('Link creation request failed due to missing parameters', { url: req.url });
return NextResponse.json({
message: "Missing required parameters",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Missing required parameters',
success: false,
},
{ status: 400 }
);
}
if (!target_url || !isValidUrl(target_url)) {
logger.info('Link creation request failed due to invalid URL', { account_id, url: req.url });
return NextResponse.json({
message: "Invalid URL. Please provide a valid URL with http:// or https://",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Invalid URL. Please provide a valid URL with http:// or https://',
success: false,
},
{ status: 400 }
);
}
const { shortId, return: returnValue } = await createLink(account_id, target_url);
if(returnValue.success) {
return NextResponse.json({
message: "Link Creation succeeded",
success: true,
shortId,
}, { status: 200 });
if (returnValue.success) {
return NextResponse.json(
{
message: 'Link Creation succeeded',
success: true,
shortId,
},
{ status: 200 }
);
}
logger.error('Link creation request failed', { error: returnValue.status, account_id, url: req.url });
return NextResponse.json({
message: returnValue.status || "Link creation failed",
success: false,
}, { status: 422 });
logger.error('Link creation request failed', {
error: returnValue.status,
account_id,
url: req.url,
});
return NextResponse.json(
{
message: returnValue.status || 'Link creation failed',
success: false,
},
{ status: 422 }
);
} catch (error) {
logger.error('Link creation error:', { error, url: req.url });
return NextResponse.json({
message: "Link creation failed",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Link creation failed',
success: false,
},
{ status: 500 }
);
}
}
@ -126,10 +171,13 @@ export async function PATCH(req: NextRequest) {
if (!session?.user?.accountId) {
logger.info('Link edit request failed due to unauthorized access', { url: req.url });
return NextResponse.json({
message: "Unauthorized. Please sign in first.",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized. Please sign in first.',
success: false,
},
{ status: 401 }
);
}
const account_id = session.user.accountId;
@ -144,48 +192,71 @@ export async function PATCH(req: NextRequest) {
}
} catch {
logger.info('Link edit request failed due to missing parameters', { url: req.url });
return NextResponse.json({
message: "Missing required parameters",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Missing required parameters',
success: false,
},
{ status: 400 }
);
}
if (!shortId) {
logger.info('Link edit request failed due to missing shortId', { account_id, url: req.url });
return NextResponse.json({
message: "Missing shortId parameter",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Missing shortId parameter',
success: false,
},
{ status: 400 }
);
}
if (!target_url || !isValidUrl(target_url)) {
logger.info('Link edit request failed due to invalid URL', { account_id, url: req.url });
return NextResponse.json({
message: "Invalid URL. Please provide a valid URL with http:// or https://",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Invalid URL. Please provide a valid URL with http:// or https://',
success: false,
},
{ status: 400 }
);
}
const returnValue = await editLink(account_id, shortId, target_url);
if(returnValue.success) {
return NextResponse.json({
message: "Link updated successfully",
success: true,
}, { status: 200 });
if (returnValue.success) {
return NextResponse.json(
{
message: 'Link updated successfully',
success: true,
},
{ status: 200 }
);
}
logger.error('Link edit request failed', { error: returnValue.status, account_id, shortId, url: req.url });
return NextResponse.json({
message: returnValue.status || "Link update failed",
success: false,
}, { status: 422 });
logger.error('Link edit request failed', {
error: returnValue.status,
account_id,
shortId,
url: req.url,
});
return NextResponse.json(
{
message: returnValue.status || 'Link update failed',
success: false,
},
{ status: 422 }
);
} catch (error) {
logger.error('Link edit error:', { error, url: req.url });
return NextResponse.json({
message: "Link update failed",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Link update failed',
success: false,
},
{ status: 500 }
);
}
}
@ -196,10 +267,13 @@ export async function DELETE(req: NextRequest) {
if (!session?.user?.accountId) {
logger.info('Link removal request failed due to unauthorized access', { url: req.url });
return NextResponse.json({
message: "Unauthorized. Please sign in first.",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized. Please sign in first.',
success: false,
},
{ status: 401 }
);
}
const account_id = session.user.accountId;
@ -213,40 +287,62 @@ export async function DELETE(req: NextRequest) {
}
} catch {
logger.info('Link removal request failed due to missing parameters', { url: req.url });
return NextResponse.json({
message: "Missing required parameters",
success: false,
}, { status: 400 });
return NextResponse.json(
{
message: 'Missing required parameters',
success: false,
},
{ status: 400 }
);
}
if (!shortId) {
logger.info('Link removal request failed due to missing shortId', { account_id, url: req.url });
return NextResponse.json({
message: "Missing shortId parameter",
success: false,
}, { status: 400 });
logger.info('Link removal request failed due to missing shortId', {
account_id,
url: req.url,
});
return NextResponse.json(
{
message: 'Missing shortId parameter',
success: false,
},
{ status: 400 }
);
}
await removeAllAnalytics(account_id, shortId);
const returnValue = await removeLink(account_id, shortId);
if(returnValue.success) {
return NextResponse.json({
message: "Link removal succeeded",
success: true,
}, { status: 200 });
if (returnValue.success) {
return NextResponse.json(
{
message: 'Link removal succeeded',
success: true,
},
{ status: 200 }
);
}
logger.error('Link removal request failed', { error: returnValue.status, account_id, url: req.url });
return NextResponse.json({
message: returnValue.status || "Link removal failed",
success: false,
}, { status: 422 });
logger.error('Link removal request failed', {
error: returnValue.status,
account_id,
url: req.url,
});
return NextResponse.json(
{
message: returnValue.status || 'Link removal failed',
success: false,
},
{ status: 422 }
);
} catch (error) {
logger.error('Link removal error:', { error, url: req.url });
return NextResponse.json({
message: "Link removal failed",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Link removal failed',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -12,33 +12,49 @@ export async function GET(req: NextRequest) {
if (!session?.user?.accountId) {
logger.info('Links retrieval request failed due to unauthorized access', { url: req.url });
return NextResponse.json({
message: "Unauthorized. Please sign in first.",
success: false,
}, { status: 401 });
return NextResponse.json(
{
message: 'Unauthorized. Please sign in first.',
success: false,
},
{ status: 401 }
);
}
const account_id = session.user.accountId;
const { links, return: returnValue } = await getLinks(account_id);
if (returnValue.success) {
return NextResponse.json({
message: "Links retrieved successfully",
success: true,
links: links.map(link => sanitizeMongoDocument(link)),
}, { status: 200 });
return NextResponse.json(
{
message: 'Links retrieved successfully',
success: true,
links: links.map(link => sanitizeMongoDocument(link)),
},
{ status: 200 }
);
}
logger.info('Links retrieval request failed', { error: returnValue.status, account_id, url: req.url });
return NextResponse.json({
message: returnValue.status || "Failed to retrieve links",
success: false,
}, { status: 404 });
logger.info('Links retrieval request failed', {
error: returnValue.status,
account_id,
url: req.url,
});
return NextResponse.json(
{
message: returnValue.status || 'Failed to retrieve links',
success: false,
},
{ status: 404 }
);
} catch (error) {
logger.error('Links retrieval error:', { error, url: req.url });
return NextResponse.json({
message: "Failed to retrieve links",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Failed to retrieve links',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -5,29 +5,39 @@ export async function GET() {
try {
let stats = await getAllStats();
if (!stats || !stats.last_updated ||
(new Date().getTime() - new Date(stats.last_updated).getTime() > 5 * 60 * 1000)) { // 5min
if (
!stats ||
!stats.last_updated ||
new Date().getTime() - new Date(stats.last_updated).getTime() > 5 * 60 * 1000
) {
// 5min
await updateStats();
stats = await getAllStats();
}
if (!stats) {
return NextResponse.json({
message: "Failed to retrieve statistics",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'Failed to retrieve statistics',
success: false,
},
{ status: 500 }
);
}
return NextResponse.json({
message: "Statistics retrieved successfully",
message: 'Statistics retrieved successfully',
success: true,
stats,
});
} catch (error) {
console.error('Statistics retrieval error:', error);
return NextResponse.json({
message: "An error occurred while retrieving statistics",
success: false,
}, { status: 500 });
return NextResponse.json(
{
message: 'An error occurred while retrieving statistics',
success: false,
},
{ status: 500 }
);
}
}

View file

@ -23,7 +23,8 @@
gap: 1rem;
}
.securityButton, .adminButton {
.securityButton,
.adminButton {
padding: 0.5rem 1rem;
border-radius: 4px;
font-weight: 500;
@ -42,7 +43,8 @@
color: white;
}
.securityButton:hover, .adminButton:hover {
.securityButton:hover,
.adminButton:hover {
opacity: 0.9;
transform: translateY(-2px);
}

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import { useState, useEffect, useRef } from 'react';
import { useParams, useRouter } from 'next/navigation';
@ -40,14 +40,14 @@ export default function LinkDetailPage() {
const [countryStats, setCountryStats] = useState<StatItem[]>([]);
const [ipVersionStats, setIpVersionStats] = useState<StatItem[]>([]);
function isValidUrl(urlStr: string) : boolean {
if(urlStr.trim() === "") {
function isValidUrl(urlStr: string): boolean {
if (urlStr.trim() === '') {
return false;
}
try {
const parsedUrl = new URL(urlStr);
return parsedUrl.protocol !== "" && parsedUrl.hostname !== "";
return parsedUrl.protocol !== '' && parsedUrl.hostname !== '';
} catch {
return false;
}
@ -124,7 +124,9 @@ export default function LinkDetailPage() {
async function fetchPaginatedAnalytics() {
try {
const response = await fetch(`/api/analytics?link_id=${shortId}&page=${page}&limit=${limit}`);
const response = await fetch(
`/api/analytics?link_id=${shortId}&page=${page}&limit=${limit}`
);
if (!response.ok) {
showToast('Failed to load analytics page', 'error');
return;
@ -177,21 +179,29 @@ export default function LinkDetailPage() {
}, {});
// Convert to StatItem[] and sort by count
setBrowserStats(Object.entries(browsers)
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count));
setBrowserStats(
Object.entries(browsers)
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count)
);
setOsStats(Object.entries(oses)
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count));
setOsStats(
Object.entries(oses)
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count)
);
setCountryStats(Object.entries(countries)
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count));
setCountryStats(
Object.entries(countries)
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count)
);
setIpVersionStats(Object.entries(ipVersions)
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count));
setIpVersionStats(
Object.entries(ipVersions)
.map(([id, count]) => ({ id, count }))
.sort((a, b) => b.count - a.count)
);
} catch {
showToast('An error occurred while processing analytics data', 'error');
} finally {
@ -220,7 +230,7 @@ export default function LinkDetailPage() {
},
body: JSON.stringify({
shortId: shortId,
target_url: targetUrl
target_url: targetUrl,
}),
});
@ -233,7 +243,7 @@ export default function LinkDetailPage() {
setLink({
...link,
target_url: targetUrl,
last_modified: new Date()
last_modified: new Date(),
});
}
} else {
@ -253,7 +263,7 @@ export default function LinkDetailPage() {
},
body: JSON.stringify({
link_id: shortId,
analytics_id: analyticsToDelete
analytics_id: analyticsToDelete,
}),
});
@ -285,7 +295,7 @@ export default function LinkDetailPage() {
},
body: JSON.stringify({
link_id: shortId,
delete_all: true
delete_all: true,
}),
});
@ -323,7 +333,7 @@ export default function LinkDetailPage() {
<div className={styles.container}>
<div className={styles.header}>
<h1 className={styles.title}>Link Details</h1>
<Link href="/dashboard" className={styles.backLink}>
<Link href='/dashboard' className={styles.backLink}>
Back to Dashboard
</Link>
</div>
@ -342,8 +352,8 @@ export default function LinkDetailPage() {
<span className={styles.label}>Short URL:</span>
<a
href={`${window.location.origin}/l/${shortId}`}
target="_blank"
rel="noopener noreferrer"
target='_blank'
rel='noopener noreferrer'
className={styles.shortUrl}
>
{`${window.location.origin}/l/${shortId}`}
@ -378,10 +388,7 @@ export default function LinkDetailPage() {
<div className={styles.targetUrlHeader}>
<span className={styles.label}>Target URL:</span>
{!isEditing && (
<button
className={styles.defaultButton}
onClick={() => setIsEditing(true)}
>
<button className={styles.defaultButton} onClick={() => setIsEditing(true)}>
Edit
</button>
)}
@ -389,7 +396,7 @@ export default function LinkDetailPage() {
{isEditing ? (
<form
onSubmit={(e) => {
onSubmit={e => {
e.preventDefault();
handleEditLink();
}}
@ -397,15 +404,15 @@ export default function LinkDetailPage() {
>
<input
ref={inputRef}
type="url"
type='url'
value={targetUrl}
onChange={(e) => setTargetUrl(e.target.value)}
onChange={e => setTargetUrl(e.target.value)}
className={styles.urlInput}
placeholder="https://example.com"
placeholder='https://example.com'
/>
<div className={styles.editActions}>
<button
type="button"
type='button'
className={styles.cancelButton}
onClick={() => {
setIsEditing(false);
@ -414,10 +421,7 @@ export default function LinkDetailPage() {
>
Cancel
</button>
<button
type="submit"
className={styles.saveButton}
>
<button type='submit' className={styles.saveButton}>
Save
</button>
</div>
@ -425,8 +429,8 @@ export default function LinkDetailPage() {
) : (
<a
href={link?.target_url}
target="_blank"
rel="noopener noreferrer"
target='_blank'
rel='noopener noreferrer'
className={styles.targetUrl}
>
{link?.target_url}
@ -440,14 +444,9 @@ export default function LinkDetailPage() {
<div className={styles.analyticsSection}>
<div className={styles.analyticsHeader}>
<h2>Analytics</h2>
<span className={styles.totalClicks}>
Total Clicks: {totalAnalytics}
</span>
<span className={styles.totalClicks}>Total Clicks: {totalAnalytics}</span>
{totalAnalytics > 0 && (
<button
className={styles.deleteAllButton}
onClick={() => setShowDeleteAllModal(true)}
>
<button className={styles.deleteAllButton} onClick={() => setShowDeleteAllModal(true)}>
Delete All Analytics
</button>
)}
@ -458,38 +457,23 @@ export default function LinkDetailPage() {
<div className={styles.graphs}>
<div className={styles.graphCard}>
<h3>Browsers</h3>
<Graph
type="doughnut"
data={browserStats}
loading={isLoadingStats}
height={200}
/>
<Graph type='doughnut' data={browserStats} loading={isLoadingStats} height={200} />
</div>
<div className={styles.graphCard}>
<h3>Operating Systems</h3>
<Graph
type="doughnut"
data={osStats}
loading={isLoadingStats}
height={200}
/>
<Graph type='doughnut' data={osStats} loading={isLoadingStats} height={200} />
</div>
<div className={styles.graphCard}>
<h3>Countries</h3>
<Graph
type="doughnut"
data={countryStats}
loading={isLoadingStats}
height={200}
/>
<Graph type='doughnut' data={countryStats} loading={isLoadingStats} height={200} />
</div>
<div className={styles.graphCard}>
<h3>IP Versions</h3>
<Graph
type="doughnut"
type='doughnut'
data={ipVersionStats}
loading={isLoadingStats}
height={200}
@ -504,7 +488,7 @@ export default function LinkDetailPage() {
currentPage={page}
itemsPerPage={limit}
onPageChange={handlePageChange}
onDeleteClick={(id) => {
onDeleteClick={id => {
setAnalyticsToDelete(id);
setShowDeleteModal(true);
}}
@ -520,10 +504,10 @@ export default function LinkDetailPage() {
{/* Confirm Delete Modal */}
<ConfirmModal
isOpen={showDeleteModal}
title="Delete Analytics Entry"
message="Are you sure you want to delete this analytics entry? This action cannot be undone."
confirmLabel="Delete"
cancelLabel="Cancel"
title='Delete Analytics Entry'
message='Are you sure you want to delete this analytics entry? This action cannot be undone.'
confirmLabel='Delete'
cancelLabel='Cancel'
onConfirm={handleDeleteAnalytics}
onCancel={() => {
setShowDeleteModal(false);
@ -534,10 +518,10 @@ export default function LinkDetailPage() {
{/* Confirm Delete All Modal */}
<ConfirmModal
isOpen={showDeleteAllModal}
title="Delete All Analytics"
message="Are you sure you want to delete all analytics for this link? This action cannot be undone."
confirmLabel="Delete All"
cancelLabel="Cancel"
title='Delete All Analytics'
message='Are you sure you want to delete all analytics for this link? This action cannot be undone.'
confirmLabel='Delete All'
cancelLabel='Cancel'
onConfirm={handleDeleteAllAnalytics}
onCancel={() => setShowDeleteAllModal(false)}
/>

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import { useState, useEffect } from 'react';
import { useSession } from 'next-auth/react';
@ -16,7 +16,7 @@ export default function Dashboard() {
const [loading, setLoading] = useState(true);
useEffect(() => {
if (status === "unauthenticated") {
if (status === 'unauthenticated') {
router.push('/');
}
}, [status, router]);
@ -50,11 +50,11 @@ export default function Dashboard() {
}
};
if (status === "loading") {
if (status === 'loading') {
return <div className={styles.loading}>Loading...</div>;
}
if (status === "unauthenticated") {
if (status === 'unauthenticated') {
return null;
}
@ -63,12 +63,12 @@ export default function Dashboard() {
<header className={styles.dashboardHeader}>
<h1 className={styles.dashboardTitle}>Dashboard</h1>
<div className={styles.actionButtons}>
<Link href="/dashboard/security">
<Link href='/dashboard/security'>
<button className={styles.securityButton}>Security Settings</button>
</Link>
{session?.user?.isAdmin && (
<Link href="/admin">
<Link href='/admin'>
<button className={styles.adminButton}>Admin Dashboard</button>
</Link>
)}
@ -105,7 +105,7 @@ function CreateLinkForm({ onLinkCreated }: CreateLinkFormProps) {
const [creating, setCreating] = useState(false);
const { showToast } = useToast();
const handleSubmit = async (e: { preventDefault: () => void; }) => {
const handleSubmit = async (e: { preventDefault: () => void }) => {
e.preventDefault();
if (!url.trim()) {
@ -144,18 +144,14 @@ function CreateLinkForm({ onLinkCreated }: CreateLinkFormProps) {
<form onSubmit={handleSubmit} className={styles.createForm}>
<div className={styles.inputGroup}>
<input
type="url"
type='url'
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="Enter URL to shorten"
onChange={e => setUrl(e.target.value)}
placeholder='Enter URL to shorten'
className={styles.urlInput}
required
/>
<button
type="submit"
className={styles.createButton}
disabled={creating}
>
<button type='submit' className={styles.createButton} disabled={creating}>
{creating ? 'Creating...' : 'Create Short Link'}
</button>
</div>

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import { useState } from 'react';
import { useSession, signOut } from 'next-auth/react';
@ -23,7 +23,7 @@ export default function SecurityPage() {
const response = await fetch('/api/auth/remove', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
},
body: JSON.stringify({ account_id: session?.user?.accountId }),
});
@ -52,7 +52,7 @@ export default function SecurityPage() {
<div className={styles.container}>
<header className={styles.header}>
<h1>Security Settings</h1>
<Link href="/dashboard" className={styles.backLink}>
<Link href='/dashboard' className={styles.backLink}>
Back to Dashboard
</Link>
</header>
@ -68,8 +68,8 @@ export default function SecurityPage() {
<div className={styles.dangerInfo}>
<h3>Delete Account</h3>
<p>
This will permanently delete your account and all associated data.
This action cannot be undone.
This will permanently delete your account and all associated data. This action
cannot be undone.
</p>
</div>
<button
@ -85,9 +85,9 @@ export default function SecurityPage() {
<ConfirmModal
isOpen={isDeleteModalOpen}
title="Delete Account"
message="Are you sure you want to delete your account? This will permanently remove your account and all your data, including all shortened links. This action cannot be undone."
confirmLabel={isDeleting ? "Deleting..." : "Delete Account"}
title='Delete Account'
message='Are you sure you want to delete your account? This will permanently remove your account and all your data, including all shortened links. This action cannot be undone.'
confirmLabel={isDeleting ? 'Deleting...' : 'Delete Account'}
onConfirm={handleAccountDeletion}
onCancel={() => setIsDeleteModalOpen(false)}
/>

View file

@ -40,7 +40,7 @@
body {
background-color: var(--bg-primary);
color: var(--text-primary);
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
padding: 0;
min-height: 100vh;

View file

@ -4,10 +4,7 @@ import { getClientInfo } from '@/lib/utils';
import { saveAnalytics } from '@/lib/analyticsdb';
import logger from '@/lib/logger';
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ shortId: string }> }
) {
export async function GET(req: NextRequest, { params }: { params: Promise<{ shortId: string }> }) {
try {
const { shortId } = await params;
@ -22,11 +19,12 @@ export async function GET(
const analyticsData = {
link_id: shortId,
account_id: link.account_id,
...clientInfo
...clientInfo,
};
saveAnalytics(analyticsData)
.catch(err => logger.error('Failed to save analytics', { error: err, shortId }));
saveAnalytics(analyticsData).catch(err =>
logger.error('Failed to save analytics', { error: err, shortId })
);
return NextResponse.redirect(new URL(link.target_url));
} catch (error) {

View file

@ -1,27 +1,27 @@
import Providers from '@/components/Providers';
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Header from "@/components/header";
import Footer from "@/components/footer";
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import Header from '@/components/header';
import Footer from '@/components/footer';
import { ToastProvider } from '@/contexts/ToastContext';
import Toast from '@/components/ui/Toast';
import ResponsiveLayout from '@/components/ResponsiveLayout';
import SessionMonitor from '@/components/SessionMonitor';
import "./globals.css";
import './globals.css';
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
variable: '--font-geist-sans',
subsets: ['latin'],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
variable: '--font-geist-mono',
subsets: ['latin'],
});
export const metadata: Metadata = {
title: "µLinkShortener",
description: "Create short links and see who accessed them!",
title: 'µLinkShortener',
description: 'Create short links and see who accessed them!',
};
export default function RootLayout({
@ -30,16 +30,12 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<html lang='en'>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<Providers>
<ToastProvider>
<Header />
<div className="page-content">
{children}
</div>
<div className='page-content'>{children}</div>
<Footer />
<Toast />
<SessionMonitor />

View file

@ -3,13 +3,13 @@ import styles from './NotFound.module.css';
export default function NotFound() {
return (
<main className={styles["default-container"]}>
<section className={styles["hero-section"]}>
<h1 className={styles["hero-title"]}>Link Not Found</h1>
<p className={styles["hero-description"]}>
<main className={styles['default-container']}>
<section className={styles['hero-section']}>
<h1 className={styles['hero-title']}>Link Not Found</h1>
<p className={styles['hero-description']}>
The shortened link you&apos;re looking for doesn&apos;t exist or has been removed.
</p>
<Link href="/" className={styles["hero-cta"]}>
<Link href='/' className={styles['hero-cta']}>
Go Home
</Link>
</section>

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
@ -17,20 +17,20 @@ const sampleData = {
{ id: 'Thu', count: 35 },
{ id: 'Fri', count: 50 },
{ id: 'Sat', count: 20 },
{ id: 'Sun', count: 15 }
{ id: 'Sun', count: 15 },
],
geoData: [
{ id: 'United States', count: 120 },
{ id: 'Germany', count: 80 },
{ id: 'United Kingdom', count: 65 },
{ id: 'Canada', count: 45 },
{ id: 'France', count: 40 }
{ id: 'France', count: 40 },
],
deviceData: [
{ id: 'Desktop', count: 210 },
{ id: 'Mobile', count: 180 },
{ id: 'Tablet', count: 50 }
]
{ id: 'Tablet', count: 50 },
],
};
export default function Home() {
@ -103,7 +103,7 @@ export default function Home() {
if (data.success && data.account_id) {
const signInResult = await signIn('credentials', {
accountId: data.account_id,
redirect: false
redirect: false,
});
if (signInResult?.error) {
@ -124,45 +124,45 @@ export default function Home() {
};
return (
<main className={styles["default-container"]}>
<main className={styles['default-container']}>
{/* Hero Section */}
<section className={styles["hero-section"]}>
<h1 className={styles["hero-title"]}>µLinkShortener</h1>
<p className={styles["hero-description"]}>
<section className={styles['hero-section']}>
<h1 className={styles['hero-title']}>µLinkShortener</h1>
<p className={styles['hero-description']}>
An analytics-driven URL shortening service to track and manage your links.
</p>
<button
className={styles["hero-cta"]}
onClick={handleGetStarted}
disabled={isLoading}
>
<button className={styles['hero-cta']} onClick={handleGetStarted} disabled={isLoading}>
{isLoading ? 'Loading...' : 'Get Started'}
</button>
</section>
{/* Stats Summary */}
<section className={styles["stats-summary"]}>
<div className={styles["stats-container"]}>
<div className={styles["stats-card"]}>
<section className={styles['stats-summary']}>
<div className={styles['stats-container']}>
<div className={styles['stats-card']}>
<h3>Total Links</h3>
<p className={styles["stats-number"]}>{statsLoading ? '...' : totalLinks.toLocaleString()}</p>
<p className={styles['stats-number']}>
{statsLoading ? '...' : totalLinks.toLocaleString()}
</p>
</div>
<div className={styles["stats-card"]}>
<div className={styles['stats-card']}>
<h3>Total Clicks</h3>
<p className={styles["stats-number"]}>{statsLoading ? '...' : totalClicks.toLocaleString()}</p>
<p className={styles['stats-number']}>
{statsLoading ? '...' : totalClicks.toLocaleString()}
</p>
</div>
</div>
</section>
{/* Graphs Section */}
<section className={styles["graphs-section"]}>
<h2 className={styles["graphs-title"]}>Analytics Dashboard</h2>
<div className={styles["graphs-container"]}>
<div className={styles["graph-card"]}>
<h3 className={styles["graph-title"]}>IP Versions</h3>
<div className={styles["graph-content"]}>
<section className={styles['graphs-section']}>
<h2 className={styles['graphs-title']}>Analytics Dashboard</h2>
<div className={styles['graphs-container']}>
<div className={styles['graph-card']}>
<h3 className={styles['graph-title']}>IP Versions</h3>
<div className={styles['graph-content']}>
<Graph
type="doughnut"
type='doughnut'
data={ipVersionStats.length > 0 ? ipVersionStats : sampleData.deviceData}
loading={statsLoading}
height={250}
@ -170,11 +170,11 @@ export default function Home() {
</div>
</div>
<div className={styles["graph-card"]}>
<h3 className={styles["graph-title"]}>Operating Systems</h3>
<div className={styles["graph-content"]}>
<div className={styles['graph-card']}>
<h3 className={styles['graph-title']}>Operating Systems</h3>
<div className={styles['graph-content']}>
<Graph
type="doughnut"
type='doughnut'
data={osStats.length > 0 ? osStats : sampleData.deviceData}
loading={statsLoading}
height={250}
@ -182,11 +182,11 @@ export default function Home() {
</div>
</div>
<div className={styles["graph-card"]}>
<h3 className={styles["graph-title"]}>Countries</h3>
<div className={styles["graph-content"]}>
<div className={styles['graph-card']}>
<h3 className={styles['graph-title']}>Countries</h3>
<div className={styles['graph-content']}>
<Graph
type="doughnut"
type='doughnut'
data={countryStats.length > 0 ? countryStats : sampleData.geoData}
loading={statsLoading}
height={250}
@ -194,11 +194,11 @@ export default function Home() {
</div>
</div>
<div className={styles["graph-card"]}>
<h3 className={styles["graph-title"]}>Internet Service Providers</h3>
<div className={styles["graph-content"]}>
<div className={styles['graph-card']}>
<h3 className={styles['graph-title']}>Internet Service Providers</h3>
<div className={styles['graph-content']}>
<Graph
type="doughnut"
type='doughnut'
data={ispStats.length > 0 ? ispStats : sampleData.geoData}
loading={statsLoading}
height={250}

View file

@ -3,14 +3,16 @@ import styles from './Privacy.module.css';
export default function PrivacyPage() {
return (
<main className={styles["default-container"]}>
<main className={styles['default-container']}>
{/* Title Section */}
<section className={styles["hero-section"]}>
<h1 className={styles["hero-title"]}>Privacy Policy</h1>
<p className={styles["hero-description"]}>
We are committed to respecting user privacy while maintaining the integrity and security of our service. <br></br>This policy outlines what data we collect, why we collect it, and how it is used.
<section className={styles['hero-section']}>
<h1 className={styles['hero-title']}>Privacy Policy</h1>
<p className={styles['hero-description']}>
We are committed to respecting user privacy while maintaining the integrity and security
of our service. <br></br>This policy outlines what data we collect, why we collect it, and
how it is used.
</p>
<Link href="/" className={styles["hero-cta"]}>
<Link href='/' className={styles['hero-cta']}>
Back to Home
</Link>
</section>
@ -29,7 +31,10 @@ export default function PrivacyPage() {
<li>ISP information</li>
<li>Geographic location based on IP address</li>
</ul>
<p>We also use cookies to store your account session and preferences for your convenience.</p>
<p>
We also use cookies to store your account session and preferences for your
convenience.
</p>
</div>
<h2 className={styles['policy-section-title']}>How We Use Your Information</h2>
@ -42,33 +47,59 @@ export default function PrivacyPage() {
<li>Detect and prevent abusive usage</li>
<li>Provide analytics to link creators</li>
</ul>
<p>We do <strong>not</strong> sell or share your personal data with third parties, except where required by law.</p>
<p>
We do <strong>not</strong> sell or share your personal data with third parties, except
where required by law.
</p>
</div>
<h2 className={styles['policy-section-title']}>Third-Party Services</h2>
<div className={styles['policy-text']}>
<p>We use Cloudflare as a content delivery network (CDN) and security provider. Cloudflare may process technical data such as IP addresses, request headers, and browser metadata to deliver and protect the service. This data is handled in accordance with <Link href="https://www.cloudflare.com/privacypolicy/" className={styles.link}>Cloudflare&apos;s Privacy Policy</Link>.</p>
<p>
We use Cloudflare as a content delivery network (CDN) and security provider.
Cloudflare may process technical data such as IP addresses, request headers, and
browser metadata to deliver and protect the service. This data is handled in
accordance with{' '}
<Link href='https://www.cloudflare.com/privacypolicy/' className={styles.link}>
Cloudflare&apos;s Privacy Policy
</Link>
.
</p>
<p>We do not share user data with any other third-party services.</p>
</div>
<h2 className={styles['policy-section-title']}>Data Retention</h2>
<div className={styles['policy-text']}>
<ul>
<li><strong>Analytics and usage data</strong> are retained until explicitly deleted by the link creator.</li>
<li><strong>User accounts and associated data</strong> are retained until a deletion request is received.</li>
<li>Shortened URLs remain active until deleted by their creator or by us in accordance with our Terms of Service.</li>
<li>
<strong>Analytics and usage data</strong> are retained until explicitly deleted by
the link creator.
</li>
<li>
<strong>User accounts and associated data</strong> are retained until a deletion
request is received.
</li>
<li>
Shortened URLs remain active until deleted by their creator or by us in accordance
with our Terms of Service.
</li>
</ul>
</div>
<h2 className={styles['policy-section-title']}>Your Rights</h2>
<div className={styles['policy-text']}>
<p>You may request deletion of your account and associated data at any time by contacting us. Deletion is permanent and cannot be reversed.</p>
<p>
You may request deletion of your account and associated data at any time by contacting
us. Deletion is permanent and cannot be reversed.
</p>
</div>
<h2 className={styles['policy-section-title']}>Contact Us</h2>
<div className={styles['policy-text']}>
<p>If you have any questions about this Privacy Policy, please contact us at:</p>
<Link href="mailto:privacy.uLink@kizuren.dev" className={styles.link}>privacy.uLink@kizuren.dev</Link>
<Link href='mailto:privacy.uLink@kizuren.dev' className={styles.link}>
privacy.uLink@kizuren.dev
</Link>
</div>
</div>
</section>

View file

@ -3,14 +3,15 @@ import styles from './ToS.module.css';
export default function TermsOfServicePage() {
return (
<main className={styles["default-container"]}>
<main className={styles['default-container']}>
{/* Title Section */}
<section className={styles["hero-section"]}>
<h1 className={styles["hero-title"]}>Terms of Service</h1>
<p className={styles["hero-description"]}>
By using our URL shortening service, you agree to comply with these Terms of Service. Please read them carefully before using the platform.
<section className={styles['hero-section']}>
<h1 className={styles['hero-title']}>Terms of Service</h1>
<p className={styles['hero-description']}>
By using our URL shortening service, you agree to comply with these Terms of Service.
Please read them carefully before using the platform.
</p>
<Link href="/" className={styles["hero-cta"]}>
<Link href='/' className={styles['hero-cta']}>
Back to Home
</Link>
</section>
@ -20,29 +21,45 @@ export default function TermsOfServicePage() {
<div className={styles['policy-text-container']}>
<h2 className={styles['policy-section-title']}>Acceptance of Terms</h2>
<div className={styles['policy-text']}>
<p>By accessing or using our URL shortening service, you agree to be bound by these Terms of Service. If you do not agree to these terms, do not use the service.</p>
<p>
By accessing or using our URL shortening service, you agree to be bound by these Terms
of Service. If you do not agree to these terms, do not use the service.
</p>
</div>
<h2 className={styles['policy-section-title']}>Description of Service</h2>
<div className={styles['policy-text']}>
<p>We provide a URL shortening service with analytics and tracking functionality. The service is provided as is, without guarantees or warranties of any kind.</p>
<p>
We provide a URL shortening service with analytics and tracking functionality. The
service is provided as is, without guarantees or warranties of any kind.
</p>
</div>
<h2 className={styles['policy-section-title']}>User Responsibilities</h2>
<div className={styles['policy-text']}>
<p>By using this service, you agree that you will <strong>not</strong>:</p>
<p>
By using this service, you agree that you will <strong>not</strong>:
</p>
<ul>
<li>Use the service for any unlawful or unauthorized purpose</li>
<li>Distribute malware, phishing links, or any malicious code</li>
<li>Infringe on any third party&apos;s intellectual property or proprietary rights</li>
<li>
Infringe on any third party&apos;s intellectual property or proprietary rights
</li>
<li>Harass, spam, or abuse individuals or systems</li>
<li>Attempt to probe, scan, or compromise our infrastructure or interfere with service operation</li>
<li>
Attempt to probe, scan, or compromise our infrastructure or interfere with service
operation
</li>
</ul>
</div>
<h2 className={styles['policy-section-title']}>Content Restrictions</h2>
<div className={styles['policy-text']}>
<p>You may <strong>not</strong> use the service to create or distribute links that direct to content which:</p>
<p>
You may <strong>not</strong> use the service to create or distribute links that direct
to content which:
</p>
<ul>
<li>Contains malware, viruses, or other harmful code</li>
<li>Facilitates or promotes illegal activity</li>
@ -51,24 +68,42 @@ export default function TermsOfServicePage() {
<li>Includes adult or explicit content without compliant age verification</li>
<li>Encourages self-harm, suicide, or criminal activity</li>
</ul>
<p>We reserve the right to remove or disable any links at any time without explanation.</p>
<p>
We reserve the right to remove or disable any links at any time without explanation.
</p>
</div>
<h2 className={styles['policy-section-title']}>Service Modifications</h2>
<div className={styles['policy-text']}>
<p>We may modify, suspend, or discontinue any part of the service at any time, with or without notice. We are not liable for any loss, data deletion, or disruption caused by such changes.</p>
<p>
We may modify, suspend, or discontinue any part of the service at any time, with or
without notice. We are not liable for any loss, data deletion, or disruption caused by
such changes.
</p>
</div>
<h2 className={styles['policy-section-title']}>Termination</h2>
<div className={styles['policy-text']}>
<p>We may suspend or terminate your access to the service at any time, with or without notice, for any reason we deem appropriate. This includes, but is not limited to, violations of these Terms, behavior we consider abusive, disruptive, unlawful, or harmful to the service, to us, to other users, or to third parties. Termination is at our sole discretion and may be irreversible. We are under no obligation to preserve, return, or provide access to any data following termination.</p>
<p>Attempts to bypass suspension or re-register after termination may result in permanent blocking.</p>
<p>
We may suspend or terminate your access to the service at any time, with or without
notice, for any reason we deem appropriate. This includes, but is not limited to,
violations of these Terms, behavior we consider abusive, disruptive, unlawful, or
harmful to the service, to us, to other users, or to third parties. Termination is at
our sole discretion and may be irreversible. We are under no obligation to preserve,
return, or provide access to any data following termination.
</p>
<p>
Attempts to bypass suspension or re-register after termination may result in permanent
blocking.
</p>
</div>
<h2 className={styles['policy-section-title']}>Contact Us</h2>
<div className={styles['policy-text']}>
<p>If you have any questions about these Terms of Service, please contact us at:</p>
<Link href="mailto:terms.uLink@kizuren.dev" className={styles.link}>terms.uLink@kizuren.dev</Link>
<Link href='mailto:terms.uLink@kizuren.dev' className={styles.link}>
terms.uLink@kizuren.dev
</Link>
</div>
</div>
</section>

View file

@ -40,7 +40,11 @@
}
/* Base button styles for all buttons */
.loginBtn, .registerBtn, .loginSubmitBtn, .logoutBtn, .dashboardBtn {
.loginBtn,
.registerBtn,
.loginSubmitBtn,
.logoutBtn,
.dashboardBtn {
background-color: var(--accent);
color: var(--text-primary);
border: none;
@ -51,7 +55,10 @@
padding: clamp(4px, 1vw, 8px) clamp(8px, 2vw, 16px);
}
.loginBtn:hover, .registerBtn:hover, .loginSubmitBtn:hover, .dashboardBtn:hover {
.loginBtn:hover,
.registerBtn:hover,
.loginSubmitBtn:hover,
.dashboardBtn:hover {
background-color: var(--accent-hover);
filter: brightness(1.2);
}
@ -107,7 +114,8 @@
gap: 10px;
}
.loginBtn, .registerBtn {
.loginBtn,
.registerBtn {
padding: 8px 16px;
font-size: 0.9rem;
}

View file

@ -1,11 +1,7 @@
"use client";
'use client';
import { SessionProvider } from "next-auth/react";
import { SessionProvider } from 'next-auth/react';
export default function Providers({ children }: { children: React.ReactNode }) {
return (
<SessionProvider>
{children}
</SessionProvider>
);
return <SessionProvider>{children}</SessionProvider>;
}

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import { useEffect } from 'react';

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import { useEffect, useRef } from 'react';
import { signOut, useSession } from 'next-auth/react';

View file

@ -5,13 +5,11 @@ export default function Footer() {
return (
<footer className={styles.footer}>
<div className={styles.content}>
<div className={styles.copyright}>
© {new Date().getFullYear()} µLinkShortener
</div>
<div className={styles.copyright}>© {new Date().getFullYear()} µLinkShortener</div>
<div className={styles.links}>
<Link href="/privacy">Privacy Policy</Link>
<Link href="/tos">Terms of Service</Link>
<Link href="https://github.com/Kizuren/uLinkShortener">GitHub</Link>
<Link href='/privacy'>Privacy Policy</Link>
<Link href='/tos'>Terms of Service</Link>
<Link href='https://github.com/Kizuren/uLinkShortener'>GitHub</Link>
</div>
</div>
</footer>

View file

@ -1,16 +1,17 @@
"use client";
'use client';
import Link from 'next/link';
import { useState, useRef, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { signIn, signOut, useSession } from "next-auth/react";
import { signIn, signOut, useSession } from 'next-auth/react';
import styles from './Header.module.css';
import LoadingIcon from '@/components/ui/LoadingIcon';
import { useToast } from '@/contexts/ToastContext';
const copyAccountIdToClipboard = (accountId: string) => {
if (navigator.clipboard && accountId) {
navigator.clipboard.writeText(accountId)
navigator.clipboard
.writeText(accountId)
.then(() => {
const displayElement = document.querySelector(`.${styles.accountIdDisplay}`);
if (displayElement) {
@ -34,8 +35,8 @@ export default function Header() {
const { data: session, status } = useSession();
const { showToast } = useToast();
const isLoggedIn = status === "authenticated";
const accountId = session?.user?.accountId as string || '';
const isLoggedIn = status === 'authenticated';
const accountId = (session?.user?.accountId as string) || '';
useEffect(() => {
if (showLoginForm && inputRef.current) {
@ -52,9 +53,9 @@ export default function Header() {
if (enteredAccountId) {
setIsLoading(true);
try {
const result = await signIn("credentials", {
const result = await signIn('credentials', {
accountId: enteredAccountId,
redirect: false
redirect: false,
});
if (result?.error) {
@ -86,9 +87,9 @@ export default function Header() {
const data = await response.json();
if (data.success && data.account_id) {
await signIn("credentials", {
await signIn('credentials', {
accountId: data.account_id,
redirect: false
redirect: false,
});
router.push('/dashboard');
@ -120,47 +121,38 @@ export default function Header() {
<header className={styles.header}>
<div className={styles.container}>
<div className={styles.logo}>
<Link href="/">
<Link href='/'>
<h1>µLinkShortener</h1>
</Link>
</div>
<div className={styles.auth}>
{status === "loading" || isLoading ? (
<LoadingIcon size={24} color="var(--accent)" />
{status === 'loading' || isLoading ? (
<LoadingIcon size={24} color='var(--accent)' />
) : isLoggedIn ? (
<div className={`${styles.userInfo} ${styles.animateIn}`}>
<span
className={styles.accountIdDisplay}
onClick={() => copyAccountIdToClipboard(accountId)}
title="Click to copy account ID"
title='Click to copy account ID'
>
<span className={styles.idLabel}>Account ID: </span>
{accountId}
<span className={styles.copyMessage}>Copied!</span>
</span>
<button
className={styles.logoutBtn}
onClick={handleLogout}
>
<button className={styles.logoutBtn} onClick={handleLogout}>
Logout
</button>
<Link href="/dashboard">
<Link href='/dashboard'>
<button className={styles.dashboardBtn}>Dashboard</button>
</Link>
</div>
) : !showLoginForm ? (
<>
<button
className={styles.loginBtn}
onClick={() => setShowLoginForm(true)}
>
<button className={styles.loginBtn} onClick={() => setShowLoginForm(true)}>
Login
</button>
<button
className={styles.registerBtn}
onClick={handleRegister}
>
<button className={styles.registerBtn} onClick={handleRegister}>
Register
</button>
</>
@ -171,18 +163,18 @@ export default function Header() {
>
<input
ref={inputRef}
type="text"
placeholder="Enter Account ID"
pattern="[0-9]*"
inputMode="numeric"
type='text'
placeholder='Enter Account ID'
pattern='[0-9]*'
inputMode='numeric'
className={styles.accountInput}
required
/>
<button type="submit" className={styles.loginSubmitBtn}>
<button type='submit' className={styles.loginSubmitBtn}>
Login
</button>
<button
type="button"
type='button'
className={styles.cancelBtn}
onClick={() => setShowLoginForm(false)}
>

View file

@ -74,11 +74,21 @@
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from { transform: translateY(30px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
from {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import { useEffect, useRef } from 'react';
import styles from './ConfirmModal.module.css';
@ -17,10 +17,10 @@ export default function ConfirmModal({
isOpen,
title,
message,
confirmLabel = "Delete",
cancelLabel = "Cancel",
confirmLabel = 'Delete',
cancelLabel = 'Cancel',
onConfirm,
onCancel
onCancel,
}: ConfirmModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
@ -62,25 +62,21 @@ export default function ConfirmModal({
className={styles.modalContent}
onClick={e => e.stopPropagation()}
ref={modalRef}
role="dialog"
aria-labelledby="modal-title"
aria-modal="true"
role='dialog'
aria-labelledby='modal-title'
aria-modal='true'
>
<h3 id="modal-title" className={styles.modalTitle}>{title}</h3>
<h3 id='modal-title' className={styles.modalTitle}>
{title}
</h3>
<div className={styles.modalBody}>
<p>{message}</p>
</div>
<div className={styles.modalFooter}>
<button
className={styles.cancelButton}
onClick={onCancel}
>
<button className={styles.cancelButton} onClick={onCancel}>
{cancelLabel}
</button>
<button
className={styles.confirmButton}
onClick={onConfirm}
>
<button className={styles.confirmButton} onClick={onConfirm}>
{confirmLabel}
</button>
</div>

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import { useEffect, useState, useRef } from 'react';
import {
@ -62,7 +62,7 @@ export default function Graph({
loading = false,
height = 200,
maxItems = 8,
colors = defaultColors
colors = defaultColors,
}: GraphProps) {
const [chartLabels, setChartLabels] = useState<string[]>([]);
const [chartValues, setChartValues] = useState<number[]>([]);
@ -141,11 +141,11 @@ export default function Graph({
labels: {
color: 'white',
font: {
size: 10
size: 10,
},
boxWidth: 12,
padding: 10
}
padding: 10,
},
},
tooltip: {
backgroundColor: 'var(--card-bg)',
@ -155,10 +155,10 @@ export default function Graph({
borderWidth: 1,
padding: 8,
boxWidth: 10,
boxHeight: 10
}
boxHeight: 10,
},
},
color: 'white'
color: 'white',
};
// Only add scales for bar and line charts
@ -170,37 +170,37 @@ export default function Graph({
ticks: {
color: 'white',
font: {
size: 10
size: 10,
},
maxRotation: 45,
minRotation: 45,
autoSkip: true,
maxTicksLimit: 10
maxTicksLimit: 10,
},
grid: {
color: 'rgba(255, 255, 255, 0.1)',
border: {
display: false
}
}
display: false,
},
},
},
y: {
beginAtZero: true,
ticks: {
color: 'white',
font: {
size: 10
size: 10,
},
precision: 0
precision: 0,
},
grid: {
color: 'rgba(255, 255, 255, 0.1)',
border: {
display: false
}
}
}
}
display: false,
},
},
},
},
};
}
@ -217,7 +217,7 @@ export default function Graph({
backgroundColor: chartColors,
borderColor: chartColors,
borderWidth: 1,
borderRadius: 4
borderRadius: 4,
},
],
});
@ -233,7 +233,7 @@ export default function Graph({
borderWidth: 2,
tension: 0.1,
pointRadius: 3,
pointHoverRadius: 5
pointHoverRadius: 5,
},
],
});
@ -247,7 +247,7 @@ export default function Graph({
backgroundColor: chartColors,
borderColor: chartColors,
borderWidth: 1,
hoverOffset: 5
hoverOffset: 5,
},
],
});
@ -264,7 +264,7 @@ export default function Graph({
<Bar
data={getBarData()}
options={getOptions()}
ref={(ref) => {
ref={ref => {
if (ref) chartRef.current = ref;
}}
/>
@ -273,7 +273,7 @@ export default function Graph({
<Line
data={getLineData()}
options={getOptions()}
ref={(ref) => {
ref={ref => {
if (ref) chartRef.current = ref;
}}
/>
@ -282,7 +282,7 @@ export default function Graph({
<Doughnut
data={getDoughnutData()}
options={getOptions()}
ref={(ref) => {
ref={ref => {
if (ref) chartRef.current = ref;
}}
/>

View file

@ -1,4 +1,4 @@
"use client";
'use client';
interface LoadingIconProps {
size?: number;
@ -11,7 +11,7 @@ export default function LoadingIcon({
size = 24,
color = 'var(--accent)',
thickness = 2,
className = ''
className = '',
}: LoadingIconProps) {
return (
<div

View file

@ -17,7 +17,9 @@
cursor: pointer;
opacity: 0;
transform: translateY(-20px);
transition: opacity 0.3s, transform 0.3s;
transition:
opacity 0.3s,
transform 0.3s;
}
.toast.error {
@ -52,6 +54,10 @@
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
from {
opacity: 1;
}
to {
opacity: 0;
}
}

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import { useToast } from '@/contexts/ToastContext';
import styles from './Toast.module.css';
@ -10,7 +10,7 @@ export default function Toast() {
return (
<div className={styles.toastContainer}>
{toasts.map((toast) => (
{toasts.map(toast => (
<div
key={toast.id}
className={`${styles.toast} ${styles[toast.type]} ${styles.toastShow}`}

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
@ -15,7 +15,7 @@ interface LinkData {
interface LinkTableProps {
links: LinkData[];
accountId: string
accountId: string;
onLinkDeleted: () => void;
}
@ -38,11 +38,12 @@ export default function AdminLinkTable({ links, accountId, onLinkDeleted }: Link
}
const term = searchTerm.toLowerCase();
const filtered = links.filter(link =>
link.short_id.toLowerCase().includes(term) ||
link.target_url.toLowerCase().includes(term) ||
new Date(link.created_at).toLocaleString().toLowerCase().includes(term) ||
new Date(link.last_modified).toLocaleString().toLowerCase().includes(term)
const filtered = links.filter(
link =>
link.short_id.toLowerCase().includes(term) ||
link.target_url.toLowerCase().includes(term) ||
new Date(link.created_at).toLocaleString().toLowerCase().includes(term) ||
new Date(link.last_modified).toLocaleString().toLowerCase().includes(term)
);
setFilteredLinks(filtered);
@ -50,7 +51,8 @@ export default function AdminLinkTable({ links, accountId, onLinkDeleted }: Link
const copyToClipboard = (shortId: string) => {
const fullUrl = `${window.location.origin}/l/${shortId}`;
navigator.clipboard.writeText(fullUrl)
navigator.clipboard
.writeText(fullUrl)
.then(() => {
showToast('Link copied to clipboard!', 'success');
})
@ -114,17 +116,17 @@ export default function AdminLinkTable({ links, accountId, onLinkDeleted }: Link
<div className={styles.tableWrapper}>
<div className={styles.searchContainer}>
<input
type="text"
placeholder="Search links..."
type='text'
placeholder='Search links...'
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onChange={e => setSearchTerm(e.target.value)}
className={styles.searchInput}
/>
{searchTerm && (
<button
className={styles.clearSearchButton}
onClick={() => setSearchTerm('')}
title="Clear search"
title='Clear search'
>
</button>
@ -150,7 +152,7 @@ export default function AdminLinkTable({ links, accountId, onLinkDeleted }: Link
</tr>
</thead>
<tbody>
{filteredLinks.map((link) => (
{filteredLinks.map(link => (
<tr key={link.short_id}>
<td className={styles.shortLinkCell}>
<Link
@ -161,7 +163,7 @@ export default function AdminLinkTable({ links, accountId, onLinkDeleted }: Link
</Link>
</td>
<td className={styles.targetUrl} title={link.target_url}>
<a href={link.target_url} target="_blank" rel="noopener noreferrer">
<a href={link.target_url} target='_blank' rel='noopener noreferrer'>
{truncateUrl(link.target_url)}
</a>
</td>
@ -171,7 +173,7 @@ export default function AdminLinkTable({ links, accountId, onLinkDeleted }: Link
<button
className={styles.copyButton}
onClick={() => copyToClipboard(link.short_id)}
title="Copy full short URL to clipboard"
title='Copy full short URL to clipboard'
>
Copy
</button>
@ -179,7 +181,7 @@ export default function AdminLinkTable({ links, accountId, onLinkDeleted }: Link
className={styles.deleteButton}
onClick={() => confirmDelete(link.short_id)}
disabled={deletingId === link.short_id}
title="Delete this link"
title='Delete this link'
>
{deletingId === link.short_id ? 'Deleting...' : 'Delete'}
</button>
@ -193,8 +195,8 @@ export default function AdminLinkTable({ links, accountId, onLinkDeleted }: Link
<ConfirmModal
isOpen={isDeleteModalOpen}
title="Delete Link"
message="Are you sure you want to delete this link? This action cannot be undone."
title='Delete Link'
message='Are you sure you want to delete this link? This action cannot be undone.'
onConfirm={handleDeleteLink}
onCancel={cancelDelete}
/>

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
@ -31,11 +31,12 @@ export default function LinkTable({ links, onLinkDeleted }: LinkTableProps) {
}
const term = searchTerm.toLowerCase();
const filtered = links.filter(link =>
link.short_id.toLowerCase().includes(term) ||
link.target_url.toLowerCase().includes(term) ||
new Date(link.created_at).toLocaleString().toLowerCase().includes(term) ||
new Date(link.last_modified).toLocaleString().toLowerCase().includes(term)
const filtered = links.filter(
link =>
link.short_id.toLowerCase().includes(term) ||
link.target_url.toLowerCase().includes(term) ||
new Date(link.created_at).toLocaleString().toLowerCase().includes(term) ||
new Date(link.last_modified).toLocaleString().toLowerCase().includes(term)
);
setFilteredLinks(filtered);
@ -43,7 +44,8 @@ export default function LinkTable({ links, onLinkDeleted }: LinkTableProps) {
const copyToClipboard = (shortId: string) => {
const fullUrl = `${window.location.origin}/l/${shortId}`;
navigator.clipboard.writeText(fullUrl)
navigator.clipboard
.writeText(fullUrl)
.then(() => {
showToast('Link copied to clipboard!', 'success');
})
@ -107,17 +109,17 @@ export default function LinkTable({ links, onLinkDeleted }: LinkTableProps) {
<div className={styles.tableWrapper}>
<div className={styles.searchContainer}>
<input
type="text"
placeholder="Search links..."
type='text'
placeholder='Search links...'
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onChange={e => setSearchTerm(e.target.value)}
className={styles.searchInput}
/>
{searchTerm && (
<button
className={styles.clearSearchButton}
onClick={() => setSearchTerm('')}
title="Clear search"
title='Clear search'
>
</button>
@ -143,18 +145,15 @@ export default function LinkTable({ links, onLinkDeleted }: LinkTableProps) {
</tr>
</thead>
<tbody>
{filteredLinks.map((link) => (
{filteredLinks.map(link => (
<tr key={link.short_id}>
<td className={styles.shortLinkCell}>
<Link
href={`/dashboard/link/${link.short_id}`}
className={styles.shortLink}
>
<Link href={`/dashboard/link/${link.short_id}`} className={styles.shortLink}>
{link.short_id}
</Link>
</td>
<td className={styles.targetUrl} title={link.target_url}>
<a href={link.target_url} target="_blank" rel="noopener noreferrer">
<a href={link.target_url} target='_blank' rel='noopener noreferrer'>
{truncateUrl(link.target_url)}
</a>
</td>
@ -164,7 +163,7 @@ export default function LinkTable({ links, onLinkDeleted }: LinkTableProps) {
<button
className={styles.copyButton}
onClick={() => copyToClipboard(link.short_id)}
title="Copy full short URL to clipboard"
title='Copy full short URL to clipboard'
>
Copy
</button>
@ -172,7 +171,7 @@ export default function LinkTable({ links, onLinkDeleted }: LinkTableProps) {
className={styles.deleteButton}
onClick={() => confirmDelete(link.short_id)}
disabled={deletingId === link.short_id}
title="Delete this link"
title='Delete this link'
>
{deletingId === link.short_id ? 'Deleting...' : 'Delete'}
</button>
@ -186,8 +185,8 @@ export default function LinkTable({ links, onLinkDeleted }: LinkTableProps) {
<ConfirmModal
isOpen={isDeleteModalOpen}
title="Delete Link"
message="Are you sure you want to delete this link? This action cannot be undone."
title='Delete Link'
message='Are you sure you want to delete this link? This action cannot be undone.'
onConfirm={handleDeleteLink}
onCancel={cancelDelete}
/>

View file

@ -41,7 +41,9 @@
color: var(--text-primary);
}
.loading, .error, .noSessions {
.loading,
.error,
.noSessions {
padding: 2rem;
text-align: center;
border-radius: 4px;

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import { useState, useEffect, useRef } from 'react';
import { useSession } from 'next-auth/react';
@ -60,11 +60,12 @@ export default function SessionManager() {
}
const term = searchTerm.toLowerCase();
const filtered = sessions.filter(s =>
s.userAgent.toLowerCase().includes(term) ||
s.ipAddress.toLowerCase().includes(term) ||
new Date(s.lastActive).toLocaleString().toLowerCase().includes(term) ||
new Date(s.createdAt).toLocaleString().toLowerCase().includes(term)
const filtered = sessions.filter(
s =>
s.userAgent.toLowerCase().includes(term) ||
s.ipAddress.toLowerCase().includes(term) ||
new Date(s.lastActive).toLocaleString().toLowerCase().includes(term) ||
new Date(s.createdAt).toLocaleString().toLowerCase().includes(term)
);
setFilteredSessions(filtered);
@ -74,7 +75,7 @@ export default function SessionManager() {
try {
const sessionToRevoke = sessions.find(s => s.id === sessionId);
if (sessionToRevoke?.isCurrentSession) {
showToast("You cannot revoke your current session", "error");
showToast('You cannot revoke your current session', 'error');
return;
}
@ -83,16 +84,14 @@ export default function SessionManager() {
const response = await fetch('/api/auth/sessions/revoke', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId })
body: JSON.stringify({ sessionId }),
});
const data = await response.json();
if (response.ok && data.success) {
showToast('Session revoked successfully', 'success');
setSessions(prevSessions =>
prevSessions.filter(s => s.id !== sessionId)
);
setSessions(prevSessions => prevSessions.filter(s => s.id !== sessionId));
} else {
showToast(data.message || 'Failed to revoke session', 'error');
}
@ -123,17 +122,17 @@ export default function SessionManager() {
<div className={styles.searchContainer}>
<input
type="text"
placeholder="Search sessions..."
type='text'
placeholder='Search sessions...'
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onChange={e => setSearchTerm(e.target.value)}
className={styles.searchInput}
/>
{searchTerm && (
<button
className={styles.clearSearchButton}
onClick={() => setSearchTerm('')}
title="Clear search"
title='Clear search'
>
</button>
@ -157,7 +156,7 @@ export default function SessionManager() {
</tr>
</thead>
<tbody>
{filteredSessions.map((s) => (
{filteredSessions.map(s => (
<tr key={s.id} className={s.isCurrentSession ? styles.currentSession : ''}>
<td className={styles.deviceCell}>
{s.userAgent.split(' ').slice(0, 3).join(' ')}

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import { useState, useEffect } from 'react';
import { Analytics } from '@/types/analytics';
@ -21,7 +21,7 @@ export default function AnalyticsTable({
currentPage,
itemsPerPage,
onPageChange,
onDeleteClick
onDeleteClick,
}: AnalyticsTableProps) {
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
const [searchQuery, setSearchQuery] = useState<string>('');
@ -46,15 +46,17 @@ export default function AnalyticsTable({
item.ip_version.toLowerCase().includes(query) ||
item.country.toLowerCase().includes(query) ||
(item.ip_data?.isp && item.ip_data.isp.toLowerCase().includes(query)) ||
// Device and browser info
item.platform.toLowerCase().includes(query) ||
item.platform
.toLowerCase()
.includes(query) ||
item.browser.toLowerCase().includes(query) ||
item.version.toLowerCase().includes(query) ||
item.language.toLowerCase().includes(query) ||
// Additional details
item.user_agent.toLowerCase().includes(query) ||
item.user_agent
.toLowerCase()
.includes(query) ||
item.referrer.toLowerCase().includes(query) ||
item.remote_port.toLowerCase().includes(query) ||
item.accept?.toLowerCase().includes(query) ||
@ -100,18 +102,18 @@ export default function AnalyticsTable({
<h3 className={styles.tableTitle}>Click Details</h3>
<div className={styles.searchContainer}>
<input
type="text"
type='text'
value={searchQuery}
onChange={handleSearchChange}
placeholder="Search analytics..."
placeholder='Search analytics...'
className={styles.searchInput}
aria-label="Search analytics"
aria-label='Search analytics'
/>
{searchQuery && (
<button
onClick={clearSearch}
className={styles.clearSearchButton}
aria-label="Clear search"
aria-label='Clear search'
>
</button>
@ -151,11 +153,11 @@ export default function AnalyticsTable({
</td>
<td>
{item.country}
<div className={styles.secondaryInfo}>ISP: {item.ip_data?.isp || 'Unknown'}</div>
</td>
<td>
{item.platform}
<div className={styles.secondaryInfo}>
ISP: {item.ip_data?.isp || 'Unknown'}
</div>
</td>
<td>{item.platform}</td>
<td>
{item.browser} {item.version}
<div className={styles.secondaryInfo}>Lang: {item.language}</div>
@ -165,14 +167,14 @@ export default function AnalyticsTable({
<button
className={styles.expandButton}
onClick={() => toggleRowExpansion(id)}
aria-label={isExpanded ? "Collapse details" : "Expand details"}
aria-label={isExpanded ? 'Collapse details' : 'Expand details'}
>
{isExpanded ? '' : '+'}
</button>
<button
className={styles.deleteButton}
onClick={() => onDeleteClick(id)}
aria-label="Delete entry"
aria-label='Delete entry'
>
Delete
</button>
@ -187,9 +189,7 @@ export default function AnalyticsTable({
{displayedAnalytics.length === 0 && (
<div className={styles.noResults}>
{isSearching
? `No results found for "${searchQuery}"`
: "No analytics data available"}
{isSearching ? `No results found for "${searchQuery}"` : 'No analytics data available'}
</div>
)}

View file

@ -1,4 +1,4 @@
"use client";
'use client';
import React, { createContext, useState, useContext, ReactNode } from 'react';
@ -24,7 +24,7 @@ export function ToastProvider({ children }: { children: ReactNode }) {
const showToast = (message: string, type: ToastType = 'info') => {
const id = Math.random().toString(36).substring(2, 9);
const newToast = { id, message, type };
setToasts((prev) => [...prev, newToast]);
setToasts(prev => [...prev, newToast]);
setTimeout(() => {
hideToast(id);
@ -32,7 +32,7 @@ export function ToastProvider({ children }: { children: ReactNode }) {
};
const hideToast = (id: string) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
setToasts(prev => prev.filter(toast => toast.id !== id));
};
return (

View file

@ -28,11 +28,7 @@ export async function getIPData(ip: string): Promise<IPAddress> {
timestamp: new Date(),
};
await collection.updateOne(
{ ip_address: ip },
{ $set: ipData },
{ upsert: true }
);
await collection.updateOne({ ip_address: ip }, { $set: ipData }, { upsert: true });
return ipData;
} catch {
@ -42,7 +38,7 @@ export async function getIPData(ip: string): Promise<IPAddress> {
isp: 'Unknown',
country: 'Unknown',
timestamp: new Date(),
}
};
}
}
@ -56,15 +52,16 @@ export interface AnalyticsQueryOptions {
export async function getAllAnalytics(
account_id: string,
link_id: string,
query_options: AnalyticsQueryOptions = {}): Promise<{analytics: Analytics[]; total: number}> {
query_options: AnalyticsQueryOptions = {}
): Promise<{ analytics: Analytics[]; total: number }> {
try {
const { db } = await getMongo();
const collection = db.collection<Analytics>(Collection.analytics_collection);
const { startDate, endDate, page = 1, limit = 50 } = query_options;
const timestamp: Record<string, Date> = {};
if (startDate) timestamp["$gte"] = startDate;
if (endDate) timestamp["$lte"] = endDate;
if (startDate) timestamp['$gte'] = startDate;
if (endDate) timestamp['$lte'] = endDate;
// Overcomplicated shit
const query: Omit<Partial<Analytics>, 'timestamp'> & { timestamp?: Record<string, Date> } = {
@ -76,17 +73,17 @@ export async function getAllAnalytics(
}
const cursor = collection
.find(query)
.sort({ timestamp: -1 }) // Most recent first
.skip((page - 1) * limit)
.limit(limit);
.find(query)
.sort({ timestamp: -1 }) // Most recent first
.skip((page - 1) * limit)
.limit(limit);
const analytics = await cursor.toArray();
const total = await collection.countDocuments(query);
return { analytics, total };
} catch {
return {analytics: [], total: 0};
return { analytics: [], total: 0 };
}
}
@ -96,22 +93,28 @@ export async function saveAnalytics(analytics: Analytics): Promise<DetailedRetur
const collection = db.collection<Analytics>(Collection.analytics_collection);
await collection.insertOne(analytics);
return { success: true, status: "Analytics successfully saved" };
return { success: true, status: 'Analytics successfully saved' };
} catch {
return { success: false, status: "An exception occured" };
return { success: false, status: 'An exception occured' };
}
}
export async function removeAllAnalytics(account_id: string, link_id: string): Promise<DetailedReturn> {
export async function removeAllAnalytics(
account_id: string,
link_id: string
): Promise<DetailedReturn> {
try {
const { db } = await getMongo();
const collection = db.collection<Analytics>(Collection.analytics_collection);
const result = await collection.deleteMany({account_id: account_id, link_id: link_id});
const result = await collection.deleteMany({ account_id: account_id, link_id: link_id });
const success = result.deletedCount > 0;
return { success, status: success ? "Analytics were successfully deleted" : "No analytics found" };
return {
success,
status: success ? 'Analytics were successfully deleted' : 'No analytics found',
};
} catch {
return { success: false, status: "An exception occured" };
return { success: false, status: 'An exception occured' };
}
}
@ -119,28 +122,35 @@ export async function removeAllAnalyticsFromUser(account_id: string): Promise<De
try {
const { db } = await getMongo();
const collection = db.collection<Analytics>(Collection.analytics_collection);
const result = await collection.deleteMany({account_id: account_id});
const result = await collection.deleteMany({ account_id: account_id });
const hasRemovedAnalytics = result.deletedCount > 0;
return { success: true, status: hasRemovedAnalytics ? "All analytics were successfully removed" : "No analytics found" };
return {
success: true,
status: hasRemovedAnalytics
? 'All analytics were successfully removed'
: 'No analytics found',
};
} catch {
return { success: false, status: "An exception occured" };
return { success: false, status: 'An exception occured' };
}
}
export async function removeAnalytics(account_id: string, link_id: string, _id: string): Promise<DetailedReturn> {
export async function removeAnalytics(
account_id: string,
link_id: string,
_id: string
): Promise<DetailedReturn> {
const objectId = safeObjectId(_id);
if(!objectId) return { success: false, status: "Invalid object ID" };
if (!objectId) return { success: false, status: 'Invalid object ID' };
try {
const { db } = await getMongo();
const collection = db.collection<Analytics>(Collection.analytics_collection);
await collection.deleteOne(
{_id: objectId, account_id: account_id, link_id: link_id}
);
await collection.deleteOne({ _id: objectId, account_id: account_id, link_id: link_id });
return { success: true, status: "Analytics successfully removed" };
return { success: true, status: 'Analytics successfully removed' };
} catch {
return { success: false, status: "An exception occured" };
return { success: false, status: 'An exception occured' };
}
}

View file

@ -3,39 +3,44 @@ import { generateLinkID, isValidUrl } from './utils';
import type { Link } from '@/types/link';
import type { DetailedReturn } from '@/types/global';
export async function getLinks(account_id: string): Promise<{links: Link[], return: DetailedReturn}> {
export async function getLinks(
account_id: string
): Promise<{ links: Link[]; return: DetailedReturn }> {
try {
const { db } = await getMongo();
const collection = db.collection<Link>(Collection.links_collection);
const links = await collection.find({ account_id })
.sort({ created_at: -1 })
.toArray();
const links = await collection.find({ account_id }).sort({ created_at: -1 }).toArray();
return { links, return: { success: true, status: "Links retrieved successfully" } };
return { links, return: { success: true, status: 'Links retrieved successfully' } };
} catch {
return { links: [], return: { success: false, status: "An exception occurred" } };
return { links: [], return: { success: false, status: 'An exception occurred' } };
}
}
export async function getTargetUrl(short_id: string): Promise<{ target_url: string; account_id: string }> {
export async function getTargetUrl(
short_id: string
): Promise<{ target_url: string; account_id: string }> {
try {
const { db } = await getMongo();
const collection = db.collection<Link>(Collection.links_collection);
const found_link = await collection.findOne({short_id: short_id});
const found_link = await collection.findOne({ short_id: short_id });
if (!found_link) return { target_url: "", account_id: "" };
if (!found_link) return { target_url: '', account_id: '' };
return {
target_url: found_link.target_url,
account_id: found_link.account_id
account_id: found_link.account_id,
};
} catch {
return { target_url: "", account_id: "" };
return { target_url: '', account_id: '' };
}
}
export async function getLinkById(account_id: string, short_id: string): Promise<{link: Link | null, return: DetailedReturn}> {
export async function getLinkById(
account_id: string,
short_id: string
): Promise<{ link: Link | null; return: DetailedReturn }> {
try {
const { db } = await getMongo();
const collection = db.collection<Link>(Collection.links_collection);
@ -43,18 +48,28 @@ export async function getLinkById(account_id: string, short_id: string): Promise
const link = await collection.findOne({ short_id, account_id });
if (!link) {
return { link: null, return: { success: false, status: "Link not found or you don't have permission to view it" } };
return {
link: null,
return: {
success: false,
status: "Link not found or you don't have permission to view it",
},
};
}
return { link, return: { success: true, status: "Link retrieved successfully" } };
return { link, return: { success: true, status: 'Link retrieved successfully' } };
} catch {
return { link: null, return: { success: false, status: "An exception occurred" } };
return { link: null, return: { success: false, status: 'An exception occurred' } };
}
}
export async function createLink(account_id: string, target_url: string): Promise<{shortId: string | null, return: DetailedReturn}> {
export async function createLink(
account_id: string,
target_url: string
): Promise<{ shortId: string | null; return: DetailedReturn }> {
try {
if(!isValidUrl(target_url)) return { shortId: null, return: { success: false, status: "Invalid target URL" } };
if (!isValidUrl(target_url))
return { shortId: null, return: { success: false, status: 'Invalid target URL' } };
const { db } = await getMongo();
const collection = db.collection<Link>(Collection.links_collection);
@ -68,38 +83,44 @@ export async function createLink(account_id: string, target_url: string): Promis
} while (duplicate);
const newLink: Link = {
short_id: shortId,
target_url: target_url,
account_id: account_id,
created_at: new Date(),
last_modified: new Date()
}
short_id: shortId,
target_url: target_url,
account_id: account_id,
created_at: new Date(),
last_modified: new Date(),
};
await collection.insertOne(newLink);
return { shortId, return: { success: true, status: "Link was successfully created" } };
return { shortId, return: { success: true, status: 'Link was successfully created' } };
} catch {
return { shortId: null, return: { success: false, status: "An exception occured" } };
return { shortId: null, return: { success: false, status: 'An exception occured' } };
}
}
export async function editLink(account_id: string, short_id: string, target_url: string): Promise<DetailedReturn> {
export async function editLink(
account_id: string,
short_id: string,
target_url: string
): Promise<DetailedReturn> {
try {
const { db } = await getMongo();
const collection = db.collection<Link>(Collection.links_collection);
const result = await collection.updateOne(
{ account_id: account_id, short_id: short_id },
{ $set: {
target_url: target_url,
last_modified: new Date()
}}
{ account_id: account_id, short_id: short_id },
{
$set: {
target_url: target_url,
last_modified: new Date(),
},
}
);
const success = result.modifiedCount > 0;
return { success, status: success ? "Link was successfully updated" : "Link not found" };
return { success, status: success ? 'Link was successfully updated' : 'Link not found' };
} catch {
return { success: false, status: "An exception occured" };
return { success: false, status: 'An exception occured' };
}
}
@ -107,12 +128,12 @@ export async function removeLink(account_id: string, short_id: string): Promise<
try {
const { db } = await getMongo();
const collection = db.collection<Link>(Collection.links_collection);
const result = await collection.deleteOne({account_id: account_id, short_id: short_id});
const result = await collection.deleteOne({ account_id: account_id, short_id: short_id });
const success = result.deletedCount > 0;
return { success, status: success ? "Link was successfully removed" : "Link not found" };
return { success, status: success ? 'Link was successfully removed' : 'Link not found' };
} catch {
return { success: false, status: "An exception occured" };
return { success: false, status: 'An exception occured' };
}
}
@ -120,12 +141,15 @@ export async function removeAllLinksFromUser(account_id: string): Promise<Detail
try {
const { db } = await getMongo();
const collection = db.collection<Link>(Collection.links_collection);
const result = await collection.deleteMany({account_id: account_id});
const result = await collection.deleteMany({ account_id: account_id });
const hasRemovedLinks = result.deletedCount > 0;
// Here it doesn't matter if no links were removed
return { success: true, status: hasRemovedLinks ? "Links were successfully removed" : "No Links found" };
return {
success: true,
status: hasRemovedLinks ? 'Links were successfully removed' : 'No Links found',
};
} catch {
return { success: false, status: "An exception occured" };
return { success: false, status: 'An exception occured' };
}
}

View file

@ -2,21 +2,15 @@ import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
}),
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
new winston.transports.File({ filename: 'combined.log' }),
],
});
export default logger;

View file

@ -1,15 +1,15 @@
import { Db, MongoClient, ObjectId } from "mongodb";
import { Db, MongoClient, ObjectId } from 'mongodb';
let client: MongoClient;
let db: Db;
export const Collection = {
links_collection: "links",
analytics_collection: "analytics",
statistics_collection: "statistics",
user_collection: "users",
sessions_collection: "sessions",
ip_addresses_collection: "ip_addresses"
links_collection: 'links',
analytics_collection: 'analytics',
statistics_collection: 'statistics',
user_collection: 'users',
sessions_collection: 'sessions',
ip_addresses_collection: 'ip_addresses',
};
export async function getMongo(): Promise<{ client: MongoClient; db: Db }> {

View file

@ -8,7 +8,7 @@ export async function createSession(
accountId: string,
userAgent: string,
ipAddress: string
): Promise<{sessionId: string; return: DetailedReturn}> {
): Promise<{ sessionId: string; return: DetailedReturn }> {
try {
const { db } = await getMongo();
const collection = db.collection<SessionInfo>(Collection.sessions_collection);
@ -25,20 +25,20 @@ export async function createSession(
ipAddress,
lastActive: now,
createdAt: now,
expiresAt
expiresAt,
};
await collection.insertOne(session);
return {
sessionId,
return: { success: true, status: "Session created" }
return: { success: true, status: 'Session created' },
};
} catch (error) {
logger.error('Error creating session:', error);
return {
sessionId: '',
return: { success: false, status: "Failed to create session" }
return: { success: false, status: 'Failed to create session' },
};
}
}
@ -51,7 +51,7 @@ export async function isSessionValid(sessionId: string, accountId: string): Prom
const session = await collection.findOne({
id: sessionId,
accountId: accountId,
revoked: { $ne: true }
revoked: { $ne: true },
});
return !!session;
@ -69,7 +69,7 @@ export async function getSessions(accountId: string): Promise<SessionInfo[]> {
const sessions = await collection
.find({
accountId,
expiresAt: { $gt: new Date() }
expiresAt: { $gt: new Date() },
})
.sort({ lastActive: -1 })
.toArray();
@ -88,18 +88,18 @@ export async function revokeSession(sessionId: string, accountId: string): Promi
const result = await collection.deleteOne({
id: sessionId,
accountId
accountId,
});
return {
success: result.deletedCount > 0,
status: result.deletedCount > 0 ? "Session revoked" : "Session not found"
status: result.deletedCount > 0 ? 'Session revoked' : 'Session not found',
};
} catch (error) {
logger.error('Error revoking session:', error);
return {
success: false,
status: "Failed to revoke session"
status: 'Failed to revoke session',
};
}
}
@ -110,18 +110,18 @@ export async function removeAllSessionsByAccountId(accountId: string): Promise<D
const collection = db.collection<SessionInfo>(Collection.sessions_collection);
const result = await collection.deleteMany({
accountId
accountId,
});
return {
success: result.deletedCount > 0,
status: result.deletedCount > 0 ? "Sessions terminated" : "No Sessions found"
status: result.deletedCount > 0 ? 'Sessions terminated' : 'No Sessions found',
};
} catch (error) {
logger.error('Error terminating session:', error);
return {
success: false,
status: "Failed to terminate session"
status: 'Failed to terminate session',
};
}
}
@ -131,10 +131,7 @@ export async function updateSessionActivity(sessionId: string): Promise<void> {
const { db } = await getMongo();
const collection = db.collection<SessionInfo>(Collection.sessions_collection);
await collection.updateOne(
{ id: sessionId },
{ $set: { lastActive: new Date() } }
);
await collection.updateOne({ id: sessionId }, { $set: { lastActive: new Date() } });
} catch (error) {
logger.error('Error updating session activity:', error);
}

View file

@ -28,35 +28,41 @@ export async function updateStats(): Promise<DetailedReturn> {
const analyticsCollection = db.collection<Analytics>(Collection.analytics_collection);
const total_links = await db.collection<Link>(Collection.links_collection).countDocuments();
const total_clicks = await analyticsCollection.countDocuments();
const ipv6_count = await analyticsCollection.countDocuments({ip_version: 'IPv6'});
const ipv6_count = await analyticsCollection.countDocuments({ ip_version: 'IPv6' });
const ip_versions: StatItem[] = [
{ id: 'IPv4', count: total_clicks - ipv6_count },
{ id: 'IPv6', count: ipv6_count },
];
const os_stats_raw = await analyticsCollection.aggregate<StatItem>([
{ $group: { _id: "$platform", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $project: { id: "$_id", count: 1, _id: 0 } }
]).toArray();
const os_stats_raw = await analyticsCollection
.aggregate<StatItem>([
{ $group: { _id: '$platform', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $project: { id: '$_id', count: 1, _id: 0 } },
])
.toArray();
const os_stats = os_stats_raw.map(item => ({
id: formatOSStrings(item.id),
count: item.count
count: item.count,
}));
const country_stats = await analyticsCollection.aggregate<StatItem>([
{ $group: { _id: "$country", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $project: { id: "$_id", count: 1, _id: 0 } }
]).toArray();
const country_stats = await analyticsCollection
.aggregate<StatItem>([
{ $group: { _id: '$country', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $project: { id: '$_id', count: 1, _id: 0 } },
])
.toArray();
const isp_stats = await analyticsCollection.aggregate<StatItem>([
{ $group: { _id: "$ip_data.isp", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $project: { id: "$_id", count: 1, _id: 0 } }
]).toArray();
const isp_stats = await analyticsCollection
.aggregate<StatItem>([
{ $group: { _id: '$ip_data.isp', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $project: { id: '$_id', count: 1, _id: 0 } },
])
.toArray();
const newStats: Stats = {
total_links,
@ -65,7 +71,7 @@ export async function updateStats(): Promise<DetailedReturn> {
ip_versions,
os_stats,
country_stats,
isp_stats
isp_stats,
},
last_updated: new Date(),
};
@ -73,8 +79,8 @@ export async function updateStats(): Promise<DetailedReturn> {
const result = await statisticsCollection.replaceOne({}, newStats, { upsert: true });
const success = result.modifiedCount > 0;
return { success, status: success ? "Stats successfully updated" : "Failed to update stats" };
return { success, status: success ? 'Stats successfully updated' : 'Failed to update stats' };
} catch {
return { success: false, status: "An exception occurred" };
return { success: false, status: 'An exception occurred' };
}
}

View file

@ -20,7 +20,7 @@ export async function isUserAdmin(account_id: string): Promise<boolean> {
const { db } = await getMongo();
const collection = db.collection<User>(Collection.user_collection);
const user = await collection.findOne({account_id: account_id});
const user = await collection.findOne({ account_id: account_id });
return user?.is_admin ?? false;
} catch {
@ -39,27 +39,28 @@ export async function makeUserAdmin(account_id: string, admin: boolean): Promise
);
if (result.matchedCount === 0) {
return { success: false, status: "User not found" };
return { success: false, status: 'User not found' };
}
return {
success: result.modifiedCount > 0,
status: result.modifiedCount > 0
? `User is now ${admin ? 'an admin' : 'no longer an admin'}`
: "No changes were made"
status:
result.modifiedCount > 0
? `User is now ${admin ? 'an admin' : 'no longer an admin'}`
: 'No changes were made',
};
} catch (error) {
console.error('Error toggling admin status:', error);
return { success: false, status: "An exception occurred" };
return { success: false, status: 'An exception occurred' };
}
}
export async function existsUser(account_id: string) : Promise<boolean> {
export async function existsUser(account_id: string): Promise<boolean> {
try {
const { db } = await getMongo();
const collection = db.collection<User>(Collection.user_collection);
const user = await collection.findOne({account_id: account_id});
const user = await collection.findOne({ account_id: account_id });
return user !== null;
} catch {
@ -67,7 +68,9 @@ export async function existsUser(account_id: string) : Promise<boolean> {
}
}
export async function createUser(is_admin: boolean): Promise<{account_id: string, return: DetailedReturn}> {
export async function createUser(
is_admin: boolean
): Promise<{ account_id: string; return: DetailedReturn }> {
try {
const { db } = await getMongo();
const collection = db.collection<User>(Collection.user_collection);
@ -81,17 +84,18 @@ export async function createUser(is_admin: boolean): Promise<{account_id: string
} while (duplicate);
const newUser: User = {
account_id: account_id,
is_admin: is_admin,
created_at: new Date()
account_id: account_id,
is_admin: is_admin,
created_at: new Date(),
};
const result = await collection.insertOne(newUser);
if(!result.acknowledged) return { account_id: "", return: {success: false, status: "An error occured"} };
if (!result.acknowledged)
return { account_id: '', return: { success: false, status: 'An error occured' } };
return { account_id, return: {success: true, status: "User was successfully created"} };
return { account_id, return: { success: true, status: 'User was successfully created' } };
} catch {
return { account_id: "", return: {success: false, status: "An exception occured"} };
return { account_id: '', return: { success: false, status: 'An exception occured' } };
}
}
@ -100,15 +104,16 @@ export async function removeUser(account_id: string): Promise<DetailedReturn> {
const { db } = await getMongo();
const collection = db.collection<User>(Collection.user_collection);
const result = await collection.deleteOne({account_id: account_id});
const result = await collection.deleteOne({ account_id: account_id });
const removeAnalyticsResult = await removeAllAnalyticsFromUser(account_id);
const removeLinksResult = await removeAllLinksFromUser(account_id);
await removeAllSessionsByAccountId(account_id);
const success = result.deletedCount > 0 && removeAnalyticsResult.success && removeLinksResult.success;
const success =
result.deletedCount > 0 && removeAnalyticsResult.success && removeLinksResult.success;
return { success: success, status: success ? "User successfully deleted" : "An error occured" };
return { success: success, status: success ? 'User successfully deleted' : 'An error occured' };
} catch {
return { success: false, status: "An exception occured" };
return { success: false, status: 'An exception occured' };
}
}
@ -124,7 +129,9 @@ export async function getUserById(account_id: string): Promise<User | null> {
}
}
export async function listUsers(query_options: UserQueryOptions = {}): Promise<{users: User[] | null, total: number, return: DetailedReturn}> {
export async function listUsers(
query_options: UserQueryOptions = {}
): Promise<{ users: User[] | null; total: number; return: DetailedReturn }> {
try {
const { db } = await getMongo();
const collection = db.collection<User>(Collection.user_collection);
@ -138,21 +145,25 @@ export async function listUsers(query_options: UserQueryOptions = {}): Promise<{
if (endDate) query.created_at.$lte = endDate;
}
if (search && search.trim() !== "") {
query.account_id = { $regex: new RegExp(search, "i") };
if (search && search.trim() !== '') {
query.account_id = { $regex: new RegExp(search, 'i') };
}
const cursor = collection
.find(query)
.sort({ created_at: -1 }) // Most recent first
.skip((page - 1) * limit)
.limit(limit);
.find(query)
.sort({ created_at: -1 }) // Most recent first
.skip((page - 1) * limit)
.limit(limit);
const users = await cursor.toArray();
const total = await collection.countDocuments(query);
return { users: users, total: total, return: {success: true, status: "Users successfully fetched"} };
return {
users: users,
total: total,
return: { success: true, status: 'Users successfully fetched' },
};
} catch {
return { users: null, total: 0, return: {success: false, status: "An exception occured"} };
return { users: null, total: 0, return: { success: false, status: 'An exception occured' } };
}
}

View file

@ -1,6 +1,6 @@
import { NextRequest } from "next/server";
import { NextRequest } from 'next/server';
import { UAParser } from 'ua-parser-js';
import { getIPData } from "./analyticsdb";
import { getIPData } from './analyticsdb';
// For accounts
const letterBytes = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
@ -33,23 +33,23 @@ export function generateAuthToken(): string {
}
// For Links
export function isValidUrl(urlStr: string) : boolean {
if(urlStr.trim() === "") {
export function isValidUrl(urlStr: string): boolean {
if (urlStr.trim() === '') {
return false;
}
try {
const parsedUrl = new URL(urlStr);
return parsedUrl.protocol !== "" && parsedUrl.hostname !== "";
return parsedUrl.protocol !== '' && parsedUrl.hostname !== '';
} catch {
return false;
}
}
// For Clients
const defaultValue = "Unknown"
function valueOrDefault(value: string | null) : string {
return !value || value?.trim() === "" ? defaultValue : value;
const defaultValue = 'Unknown';
function valueOrDefault(value: string | null): string {
return !value || value?.trim() === '' ? defaultValue : value;
}
export async function getClientInfo(req: NextRequest) {
@ -109,9 +109,9 @@ export async function getClientInfo(req: NextRequest) {
// For stats
export function formatOSStrings(os_string: string): string {
os_string = os_string.trim();
os_string = os_string.replaceAll("\"", ""); // Windows usually reports ""Windows"""
os_string = os_string.replaceAll("CPU ", ""); // iOS usually reports "CPU ....."
os_string = os_string.replaceAll(" like Mac OS X", ""); // iOS usually reports at its end " like Mac OS X"
os_string = os_string.replaceAll('"', ''); // Windows usually reports ""Windows"""
os_string = os_string.replaceAll('CPU ', ''); // iOS usually reports "CPU ....."
os_string = os_string.replaceAll(' like Mac OS X', ''); // iOS usually reports at its end " like Mac OS X"
return os_string;
}

View file

@ -7,15 +7,16 @@ export async function proxy(request: NextRequest) {
const response = NextResponse.next();
try {
if (path === '/dashboard' ||
path === '/admin' ||
path.startsWith('/api/link/') ||
path.startsWith('/dashboard/') ||
path.startsWith('/admin/')) {
if (
path === '/dashboard' ||
path === '/admin' ||
path.startsWith('/api/link/') ||
path.startsWith('/dashboard/') ||
path.startsWith('/admin/')
) {
const token = await getToken({
req: request,
secret: process.env.NEXTAUTH_SECRET || 'fallback-secret-for-testing'
secret: process.env.NEXTAUTH_SECRET || 'fallback-secret-for-testing',
});
// Not authenticated
@ -50,6 +51,6 @@ export const config = {
'/admin',
'/admin/:path*',
'/api/link/:path*',
'/api/auth/sessions/:path*'
]
'/api/auth/sessions/:path*',
],
};

View file

@ -1,30 +1,30 @@
import { ObjectId } from "mongodb";
import { ObjectId } from 'mongodb';
export interface Analytics {
_id?: ObjectId
link_id: string;
account_id: string;
ip_address: string;
user_agent: string;
platform: string;
browser: string;
version: string;
language: string;
referrer: string;
timestamp: Date;
remote_port: string;
accept: string;
accept_language: string;
accept_encoding: string;
country: string;
ip_data: IPAddress;
ip_version: string;
_id?: ObjectId;
link_id: string;
account_id: string;
ip_address: string;
user_agent: string;
platform: string;
browser: string;
version: string;
language: string;
referrer: string;
timestamp: Date;
remote_port: string;
accept: string;
accept_language: string;
accept_encoding: string;
country: string;
ip_data: IPAddress;
ip_version: string;
}
export interface IPAddress {
ip_address: string;
ip_version: string;
isp: string;
country: string;
timestamp: Date;
ip_address: string;
ip_version: string;
isp: string;
country: string;
timestamp: Date;
}

8
src/types/auth.d.ts vendored
View file

@ -1,11 +1,11 @@
import "next-auth";
import 'next-auth';
declare module "next-auth" {
declare module 'next-auth' {
interface Session {
user: {
accountId: string;
isAdmin: boolean;
} & DefaultSession["user"];
} & DefaultSession['user'];
}
interface User {
@ -14,7 +14,7 @@ declare module "next-auth" {
}
}
declare module "next-auth/jwt" {
declare module 'next-auth/jwt' {
interface JWT {
accountId: string;
isAdmin: boolean;

10
src/types/link.d.ts vendored
View file

@ -1,7 +1,7 @@
export interface Link {
short_id: string;
target_url: string;
account_id: string;
created_at: Date;
last_modified: Date;
short_id: string;
target_url: string;
account_id: string;
created_at: Date;
last_modified: Date;
}

View file

@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@ -23,9 +19,7 @@
}
],
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"]
}
},
"include": [
@ -35,7 +29,5 @@
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}