feat: enhance admin UI with data grid, search and sorting features

This commit is contained in:
Ender 2025-10-24 15:38:32 +02:00
parent 99c0d95ef2
commit a6e86eb976
7 changed files with 590 additions and 60 deletions

View File

@ -14,6 +14,7 @@
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.4", "@mui/icons-material": "^7.3.4",
"@mui/material": "^7.3.4", "@mui/material": "^7.3.4",
"@mui/x-data-grid": "^8.15.0",
"@tiptap/extension-image": "^3.7.2", "@tiptap/extension-image": "^3.7.2",
"@tiptap/extension-link": "^3.7.2", "@tiptap/extension-link": "^3.7.2",
"@tiptap/extension-placeholder": "^3.7.2", "@tiptap/extension-placeholder": "^3.7.2",

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { Box, Button, Stack, Typography } from '@mui/material'; import { Box, Button, Stack, Typography, Paper, TextField, MenuItem } from '@mui/material';
type MediaItem = { type MediaItem = {
key: string; key: string;
@ -11,6 +11,8 @@ export default function MediaLibrary({ onInsert, onSetFeature, showSetFeature }:
const [items, setItems] = useState<MediaItem[]>([]); const [items, setItems] = useState<MediaItem[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [query, setQuery] = useState('');
const [sortBy, setSortBy] = useState<'date_desc' | 'date_asc' | 'name_asc' | 'size_desc'>('date_desc');
const load = async () => { const load = async () => {
try { try {
@ -46,37 +48,85 @@ export default function MediaLibrary({ onInsert, onSetFeature, showSetFeature }:
} }
}; };
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
let arr = items.filter(it => it.key.toLowerCase().includes(q));
arr.sort((a, b) => {
if (sortBy === 'date_desc') return (new Date(b.lastModified || 0).getTime()) - (new Date(a.lastModified || 0).getTime());
if (sortBy === 'date_asc') return (new Date(a.lastModified || 0).getTime()) - (new Date(b.lastModified || 0).getTime());
if (sortBy === 'size_desc') return (b.size - a.size);
// name_asc
const an = a.key.split('/').slice(-1)[0].toLowerCase();
const bn = b.key.split('/').slice(-1)[0].toLowerCase();
return an.localeCompare(bn);
});
return arr;
}, [items, query, sortBy]);
const fmtSize = (n: number) => {
if (n >= 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + ' MB';
if (n >= 1024) return (n / 1024).toFixed(1) + ' KB';
return n + ' B';
};
const fmtDate = (s: string | null) => s ? new Date(s).toLocaleString() : '';
const copyUrl = async (url: string) => {
try {
await navigator.clipboard.writeText(url);
} catch {}
};
return ( return (
<Box> <Paper sx={{ p: 2 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}> <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}>
<Typography variant="subtitle1">Media Library</Typography> <Typography variant="h6">Media Library</Typography>
<Button size="small" onClick={load} disabled={loading}>Refresh</Button> <Stack direction="row" spacing={1}>
<TextField
size="small"
placeholder="Search by name…"
value={query}
onChange={e => setQuery(e.target.value)}
/>
<TextField size="small" select value={sortBy} onChange={e => setSortBy(e.target.value as any)}>
<MenuItem value="date_desc">Newest</MenuItem>
<MenuItem value="date_asc">Oldest</MenuItem>
<MenuItem value="name_asc">Name</MenuItem>
<MenuItem value="size_desc">Largest</MenuItem>
</TextField>
<Button size="small" onClick={load} disabled={loading}>Refresh</Button>
</Stack>
</Stack> </Stack>
{error && <Typography color="error" sx={{ mb: 1 }}>{error}</Typography>} {error && <Typography color="error" sx={{ mb: 1 }}>{error}</Typography>}
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 1 }}> <Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: 1.5 }}>
{items.map((it) => { {filtered.map((it) => {
const url = `/api/media/obj?key=${encodeURIComponent(it.key)}`; const url = `/api/media/obj?key=${encodeURIComponent(it.key)}`;
const name = it.key.split('/').slice(-1)[0]; const name = it.key.split('/').slice(-1)[0];
return ( return (
<Box key={it.key} sx={{ border: '1px solid #eee', borderRadius: 1, p: 1 }}> <Paper key={it.key} sx={{ p: 1.25, display: 'flex', flexDirection: 'column', height: 260 }}>
<Box sx={{ width: '100%', height: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', mb: 1, overflow: 'hidden', background: '#fafafa' }}> <Box sx={{ width: '100%', flex: '0 0 140px', display: 'flex', alignItems: 'center', justifyContent: 'center', mb: 1, overflow: 'hidden', background: '#fafafa', borderRadius: 1 }}>
<img src={url} alt={name} style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} /> <a href={url} target="_blank" rel="noreferrer">
<img src={url} alt={name} style={{ maxWidth: '100%', maxHeight: '140px', objectFit: 'contain', display: 'block' }} />
</a>
</Box> </Box>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }} title={name}>{name}</Typography> <Typography variant="caption" noWrap sx={{ display: 'block' }} title={name}>{name}</Typography>
<Stack direction="row" spacing={1}> <Typography variant="caption" sx={{ display: 'block', color: 'text.secondary' }}>{fmtSize(it.size)} · {fmtDate(it.lastModified)}</Typography>
<Button size="small" variant="outlined" onClick={() => onInsert(url)}>Insert</Button> <Box sx={{ flexGrow: 1 }} />
<Stack direction="row" spacing={1} sx={{ mt: 1, flexWrap: 'wrap' }}>
<Button size="small" variant="outlined" sx={{ flex: '1 1 48%' }} onClick={() => onInsert(url)}>Insert</Button>
{showSetFeature && onSetFeature && ( {showSetFeature && onSetFeature && (
<Button size="small" variant="outlined" onClick={() => onSetFeature(url)}>Set as Feature</Button> <Button size="small" variant="outlined" sx={{ flex: '1 1 48%' }} onClick={() => onSetFeature(url)}>Set Feature</Button>
)} )}
<Button size="small" color="error" onClick={() => del(it.key)}>Delete</Button> <Button size="small" variant="text" sx={{ flex: '1 1 48%' }} onClick={() => copyUrl(url)}>Copy URL</Button>
<Button size="small" color="error" variant="text" sx={{ flex: '1 1 48%' }} onClick={() => { if (confirm('Delete this image?')) del(it.key) }}>Delete</Button>
</Stack> </Stack>
</Box> </Paper>
); );
})} })}
{items.length === 0 && !loading && ( {filtered.length === 0 && !loading && (
<Typography variant="body2">No images yet.</Typography> <Typography variant="body2">No images found.</Typography>
)} )}
</Box> </Box>
</Box> </Paper>
); );
} }

View File

@ -1,5 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { Box, Button, Chip, Stack, Typography } from '@mui/material'; import { Box, Button, Chip, Stack, Typography, TextField } from '@mui/material';
import { DataGrid } from '@mui/x-data-grid';
import type { GridColDef, GridPaginationModel, GridSortModel } from '@mui/x-data-grid';
export type PostSummary = { export type PostSummary = {
id: string; id: string;
@ -9,52 +11,108 @@ export type PostSummary = {
}; };
export default function PostsList({ onSelect, onNew }: { onSelect: (id: string) => void; onNew?: () => void }) { export default function PostsList({ onSelect, onNew }: { onSelect: (id: string) => void; onNew?: () => void }) {
const [items, setItems] = useState<PostSummary[]>([]); const [rows, setRows] = useState<PostSummary[]>([]);
const [rowCount, setRowCount] = useState(0);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [query, setQuery] = useState('');
const [effectiveQuery, setEffectiveQuery] = useState('');
const [paginationModel, setPaginationModel] = useState<GridPaginationModel>({ page: 0, pageSize: 20 });
const [sortModel, setSortModel] = useState<GridSortModel>([{ field: 'updatedAt', sort: 'desc' }]);
useEffect(() => {
const handle = setTimeout(() => setEffectiveQuery(query), 300);
return () => clearTimeout(handle);
}, [query]);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
setLoading(true); setLoading(true);
setError(''); setError('');
const res = await fetch('/api/posts'); const page = paginationModel.page + 1; // server is 1-based
const pageSize = paginationModel.pageSize;
const sort = sortModel[0] ? `${sortModel[0].field}:${sortModel[0].sort}` : 'updatedAt:desc';
const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize), sort });
if (effectiveQuery.trim()) params.set('q', effectiveQuery.trim());
const res = await fetch(`/api/posts?${params.toString()}`);
if (!res.ok) throw new Error('Failed to load posts'); if (!res.ok) throw new Error('Failed to load posts');
const data = await res.json(); const data = await res.json();
const rows = (data.items || []) as Array<{ id: string; title?: string; status: string; updatedAt: string }>; setRows(data.items || []);
setItems(rows); setRowCount(data.total || 0);
} catch (e: any) { } catch (e: any) {
setError(e?.message || 'Failed to load posts'); setError(e?.message || 'Failed to load posts');
} finally { } finally {
setLoading(false); setLoading(false);
} }
})(); })();
}, []); }, [paginationModel, sortModel, effectiveQuery]);
const columns = useMemo<GridColDef[]>(() => [
{
field: 'title', headerName: 'Title', flex: 1, minWidth: 160,
renderCell: (p) => {
const title = (((p.row as any).title as string | null) || '').trim();
return <span>{title || 'Untitled'}</span>;
}
},
{
field: 'status', headerName: 'Status', width: 140,
renderCell: (p) => {
const value = ((p.row as any).status as string) ?? '';
const status = value.toLowerCase();
const color: any = status === 'published' ? 'success' : status === 'ready_for_publish' ? 'secondary' : status === 'archived' ? 'warning' : status === 'editing' ? 'primary' : 'default';
return <Chip label={value} color={color} size="small" />
}
},
{
field: 'updatedAt', headerName: 'Updated', width: 180,
renderCell: (p) => {
const v = (p.row as any).updatedAt as string;
return <span>{new Date(v).toLocaleString()}</span>;
},
sortable: true,
},
{
field: 'id', headerName: 'ID', width: 120,
renderCell: (p) => <span>{((p.row as any).id as string).slice(0, 8)}</span>,
sortable: false,
},
{
field: 'actions', headerName: 'Actions', width: 120, sortable: false, filterable: false,
renderCell: (p) => (
<Button size="small" variant="outlined" onClick={() => onSelect((p.row as any).id)}>Open</Button>
)
}
], [onSelect]);
return ( return (
<Box> <Box>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}> <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}>
<Typography variant="h5">Posts</Typography> <Typography variant="h5">Posts</Typography>
<Button variant="contained" onClick={onNew}>New Post</Button> <Stack direction="row" spacing={1}>
</Stack> <TextField size="small" placeholder="Search…" value={query} onChange={e => setQuery(e.target.value)} />
{loading && <Typography>Loading</Typography>} <Button variant="contained" onClick={onNew}>New Post</Button>
{error && <Typography color="error">{error}</Typography>} </Stack>
{!loading && items.length === 0 && (
<Typography variant="body2">No posts yet. Click New Post to create one.</Typography>
)}
<Stack spacing={1}>
{items.map((p) => (
<Stack key={p.id} direction="row" spacing={2} sx={{ border: '1px solid #eee', p: 1, borderRadius: 1, alignItems: 'center', justifyContent: 'space-between' }}>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<Typography variant="subtitle1">{(p.title && p.title.trim()) || 'Untitled'}</Typography>
<Chip label={p.status} size="small" />
<Typography variant="caption" sx={{ color: 'text.secondary' }}>{new Date(p.updatedAt).toLocaleString()}</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>ID: {p.id.slice(0, 8)}</Typography>
</Stack>
<Button size="small" variant="outlined" onClick={() => onSelect(p.id)}>Open</Button>
</Stack>
))}
</Stack> </Stack>
{error && <Typography color="error" sx={{ mb: 1 }}>{error}</Typography>}
<div style={{ width: '100%' }}>
<DataGrid<PostSummary>
rows={rows}
columns={columns}
loading={loading}
rowCount={rowCount}
paginationMode="server"
sortingMode="server"
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
sortModel={sortModel}
onSortModelChange={setSortModel}
disableRowSelectionOnClick
pageSizeOptions={[10, 20, 50, 100]}
autoHeight
/>
</div>
</Box> </Box>
); );
} }

View File

@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { Box, Button, Stack, Typography } from '@mui/material'; import { Box, Button, Stack, Typography, Paper } from '@mui/material';
export default function Recorder({ postId, initialClips, onInsertAtCursor, onTranscript }: { postId?: string; initialClips?: Array<{ id: string; bucket: string; key: string; mime: string; transcript?: string }>; onInsertAtCursor?: (html: string) => void; onTranscript?: (t: string) => void }) { export default function Recorder({ postId, initialClips, onInsertAtCursor, onTranscript }: { postId?: string; initialClips?: Array<{ id: string; bucket: string; key: string; mime: string; transcript?: string }>; onInsertAtCursor?: (html: string) => void; onTranscript?: (t: string) => void }) {
const mediaRecorderRef = useRef<MediaRecorder | null>(null); const mediaRecorderRef = useRef<MediaRecorder | null>(null);
@ -183,9 +183,9 @@ export default function Recorder({ postId, initialClips, onInsertAtCursor, onTra
}, [clips]); }, [clips]);
return ( return (
<Box> <Paper sx={{ p: 2 }}>
<Typography variant="h6" sx={{ mb: 1 }}>Audio Recorder</Typography> <Typography variant="h6" sx={{ mb: 1 }}>Audio Recorder</Typography>
<Stack direction="row" spacing={2} sx={{ mb: 2, flexWrap: 'wrap' }}> <Stack direction="row" spacing={1.5} sx={{ mb: 2, flexWrap: 'wrap' }}>
<Button variant="contained" disabled={recording} onClick={startRecording}>Start</Button> <Button variant="contained" disabled={recording} onClick={startRecording}>Start</Button>
<Button variant="outlined" disabled={!recording} onClick={stopRecording}>Stop</Button> <Button variant="outlined" disabled={!recording} onClick={stopRecording}>Stop</Button>
<Button variant="text" disabled={clips.every(c => !c.transcript)} onClick={applyTranscriptsToDraft}>Apply transcripts to draft</Button> <Button variant="text" disabled={clips.every(c => !c.transcript)} onClick={applyTranscriptsToDraft}>Apply transcripts to draft</Button>
@ -195,15 +195,15 @@ export default function Recorder({ postId, initialClips, onInsertAtCursor, onTra
{clips.length === 0 && ( {clips.length === 0 && (
<Typography variant="body2">No recordings yet.</Typography> <Typography variant="body2">No recordings yet.</Typography>
)} )}
<Stack spacing={2} sx={{ mt: 1 }}> <Stack spacing={1.5} sx={{ mt: 1 }}>
{clips.map((c, idx) => ( {clips.map((c, idx) => (
<Box key={c.id} sx={{ border: '1px solid #ddd', borderRadius: 2, p: 1 }}> <Paper key={c.id} sx={{ p: 1.5 }}>
<Stack direction="row" spacing={1} sx={{ justifyContent: 'space-between', alignItems: 'center', mb: 1 }}> <Stack direction="row" spacing={1} sx={{ justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="subtitle2">Clip {idx + 1}</Typography> <Typography variant="subtitle2">Clip {idx + 1}</Typography>
<Stack direction="row" spacing={1}> <Stack direction="row" spacing={1}>
<Button size="small" variant="outlined" disabled={idx === 0} onClick={() => moveClip(idx, idx - 1)}>Up</Button> <Button size="small" variant="outlined" disabled={idx === 0} onClick={() => moveClip(idx, idx - 1)}>Up</Button>
<Button size="small" variant="outlined" disabled={idx === clips.length - 1} onClick={() => moveClip(idx, idx + 1)}>Down</Button> <Button size="small" variant="outlined" disabled={idx === clips.length - 1} onClick={() => moveClip(idx, idx + 1)}>Down</Button>
<Button size="small" variant="outlined" color="error" onClick={() => removeClip(idx)}>Remove</Button> <Button size="small" color="error" variant="text" onClick={() => removeClip(idx)}>Remove</Button>
</Stack> </Stack>
</Stack> </Stack>
<audio controls src={c.url} /> <audio controls src={c.url} />
@ -224,9 +224,9 @@ export default function Recorder({ postId, initialClips, onInsertAtCursor, onTra
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>{c.transcript}</Typography> <Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>{c.transcript}</Typography>
</Box> </Box>
)} )}
</Box> </Paper>
))} ))}
</Stack> </Stack>
</Box> </Paper>
); );
} }

View File

@ -4,7 +4,11 @@ import type { ReactNode } from 'react';
export default function AdminLayout({ title, onLogout, children }: { title?: string; onLogout?: () => void; children: ReactNode }) { export default function AdminLayout({ title, onLogout, children }: { title?: string; onLogout?: () => void; children: ReactNode }) {
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}> <Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
<AppBar position="static"> <AppBar position="sticky" elevation={0} sx={{
backgroundColor: 'rgba(255,255,255,0.8)',
backdropFilter: 'blur(8px)',
borderBottom: '1px solid rgba(0,0,0,0.06)'
}}>
<Toolbar> <Toolbar>
<Typography variant="h6" sx={{ flexGrow: 1 }}> <Typography variant="h6" sx={{ flexGrow: 1 }}>
{title || 'VoxBlog Admin'} {title || 'VoxBlog Admin'}

View File

@ -2,19 +2,53 @@ import express from 'express';
import crypto from 'crypto'; import crypto from 'crypto';
import { db } from './db'; import { db } from './db';
import { posts, audioClips } from './db/schema'; import { posts, audioClips } from './db/schema';
import { desc, eq } from 'drizzle-orm'; import { desc, asc, eq, and, or, like, sql } from 'drizzle-orm';
const router = express.Router(); const router = express.Router();
// List posts (minimal info) // List posts (minimal info)
router.get('/', async (_req, res) => { router.get('/', async (req, res) => {
try { try {
const page = Math.max(parseInt((req.query.page as string) || '1', 10), 1);
const pageSizeRaw = Math.max(parseInt((req.query.pageSize as string) || '20', 10), 1);
const pageSize = Math.min(pageSizeRaw, 100);
const q = ((req.query.q as string) || '').trim();
const sort = ((req.query.sort as string) || 'updatedAt:desc').toLowerCase();
const [sortField, sortDir] = sort.split(':');
const whereConds = [] as any[];
if (q) {
const likeQ = `%${q}%`;
whereConds.push(or(like(posts.title, likeQ), like(posts.contentHtml, likeQ)));
}
const whereExpr = whereConds.length > 0 ? and(...whereConds) : undefined;
// total count
const countRows = await db
.select({ cnt: sql<number>`COUNT(*)` })
.from(posts)
.where(whereExpr as any);
const total = (countRows?.[0]?.cnt as unknown as number) || 0;
// sorting
let orderByExpr: any = desc(posts.updatedAt);
const dir = sortDir === 'asc' ? 'asc' : 'desc';
if (sortField === 'title') orderByExpr = dir === 'asc' ? asc(posts.title) : desc(posts.title);
else if (sortField === 'status') orderByExpr = dir === 'asc' ? asc(posts.status) : desc(posts.status);
else orderByExpr = dir === 'asc' ? asc(posts.updatedAt) : desc(posts.updatedAt);
const offset = (page - 1) * pageSize;
const rows = await db const rows = await db
.select({ id: posts.id, title: posts.title, status: posts.status, updatedAt: posts.updatedAt }) .select({ id: posts.id, title: posts.title, status: posts.status, updatedAt: posts.updatedAt })
.from(posts) .from(posts)
.orderBy(desc(posts.updatedAt)) .where(whereExpr as any)
.limit(200); .orderBy(orderByExpr)
return res.json({ items: rows }); .limit(pageSize)
.offset(offset);
return res.json({ items: rows, total, page, pageSize });
} catch (err) { } catch (err) {
console.error('List posts error:', err); console.error('List posts error:', err);
return res.status(500).json({ error: 'Failed to list posts' }); return res.status(500).json({ error: 'Failed to list posts' });

View File

@ -22,6 +22,9 @@ importers:
'@mui/material': '@mui/material':
specifier: ^7.3.4 specifier: ^7.3.4
version: 7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@mui/x-data-grid':
specifier: ^8.15.0
version: 8.15.0(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@tiptap/extension-image': '@tiptap/extension-image':
specifier: ^3.7.2 specifier: ^3.7.2
version: 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) version: 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))
@ -86,6 +89,12 @@ importers:
'@aws-sdk/client-s3': '@aws-sdk/client-s3':
specifier: ^3.916.0 specifier: ^3.916.0
version: 3.916.0 version: 3.916.0
'@aws-sdk/s3-request-presigner':
specifier: ^3.916.0
version: 3.916.0
'@types/jsonwebtoken':
specifier: ^9.0.10
version: 9.0.10
accepts: accepts:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0 version: 2.0.0
@ -116,6 +125,9 @@ importers:
dotenv: dotenv:
specifier: ^17.2.3 specifier: ^17.2.3
version: 17.2.3 version: 17.2.3
drizzle-orm:
specifier: ^0.44.7
version: 0.44.7(mysql2@3.15.3)
encodeurl: encodeurl:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0 version: 2.0.0
@ -137,6 +149,9 @@ importers:
http-errors: http-errors:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0 version: 2.0.0
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.2
merge-descriptors: merge-descriptors:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0 version: 2.0.0
@ -146,6 +161,9 @@ importers:
multer: multer:
specifier: ^2.0.2 specifier: ^2.0.2
version: 2.0.2 version: 2.0.2
mysql2:
specifier: ^3.15.3
version: 3.15.3
on-finished: on-finished:
specifier: ^2.4.1 specifier: ^2.4.1
version: 2.4.1 version: 2.4.1
@ -371,6 +389,10 @@ packages:
resolution: {integrity: sha512-KlmHhRbn1qdwXUdsdrJ7S/MAkkC1jLpQ11n+XvxUUUCGAJd1gjC7AjxPZUM7ieQ2zcb8bfEzIU7al+Q3ZT0u7Q==} resolution: {integrity: sha512-KlmHhRbn1qdwXUdsdrJ7S/MAkkC1jLpQ11n+XvxUUUCGAJd1gjC7AjxPZUM7ieQ2zcb8bfEzIU7al+Q3ZT0u7Q==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@aws-sdk/s3-request-presigner@3.916.0':
resolution: {integrity: sha512-XkHIhRISbdQHZ08Tq5Zt6A4n49mjVvcZd/PeY1SPCv47rzLnxdfxVSuEFynAg3Lgw/f/iHdv3lok2PAesCvi4A==}
engines: {node: '>=18.0.0'}
'@aws-sdk/signature-v4-multi-region@3.916.0': '@aws-sdk/signature-v4-multi-region@3.916.0':
resolution: {integrity: sha512-fuzUMo6xU7e0NBzBA6TQ4FUf1gqNbg4woBSvYfxRRsIfKmSMn9/elXXn4sAE5UKvlwVQmYnb6p7dpVRPyFvnQA==} resolution: {integrity: sha512-fuzUMo6xU7e0NBzBA6TQ4FUf1gqNbg4woBSvYfxRRsIfKmSMn9/elXXn4sAE5UKvlwVQmYnb6p7dpVRPyFvnQA==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@ -391,6 +413,10 @@ packages:
resolution: {integrity: sha512-bAgUQwvixdsiGNcuZSDAOWbyHlnPtg8G8TyHD6DTfTmKTHUW6tAn+af/ZYJPXEzXhhpwgJqi58vWnsiDhmr7NQ==} resolution: {integrity: sha512-bAgUQwvixdsiGNcuZSDAOWbyHlnPtg8G8TyHD6DTfTmKTHUW6tAn+af/ZYJPXEzXhhpwgJqi58vWnsiDhmr7NQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@aws-sdk/util-format-url@3.914.0':
resolution: {integrity: sha512-QpdkoQjvPaYyzZwgk41vFyHQM5s0DsrsbQ8IoPUggQt4HaJUvmL1ShwMcSldbgdzwiRMqXUK8q7jrqUvkYkY6w==}
engines: {node: '>=18.0.0'}
'@aws-sdk/util-locate-window@3.893.0': '@aws-sdk/util-locate-window@3.893.0':
resolution: {integrity: sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==} resolution: {integrity: sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@ -914,6 +940,35 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@mui/x-data-grid@8.15.0':
resolution: {integrity: sha512-JNPG2WSYJVKbUAbDpLCbWmIY25k9hyfUjAVnzDREbJMwPL+/5B9pIK0ikRQEXc0wRKY2T59SeR/Um2FZjBeeWQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.9.0
'@emotion/styled': ^11.8.1
'@mui/material': ^5.15.14 || ^6.0.0 || ^7.0.0
'@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@mui/x-internals@8.14.0':
resolution: {integrity: sha512-esYyl61nuuFXiN631TWuPh2tqdoyTdBI/4UXgwH3rytF8jiWvy6prPBPRHEH1nvW3fgw9FoBI48FlOO+yEI8xg==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
'@mui/x-virtualizer@0.2.5':
resolution: {integrity: sha512-kCo/i9YfNavbupqZGO1649CHwIABrwUDHVZh+GvGierHhIglUc9MHxYKsPhuojOg6izWa2HP+klt3nq2n/arOw==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
'@noble/hashes@1.8.0': '@noble/hashes@1.8.0':
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
engines: {node: ^14.21.3 || >=16} engines: {node: ^14.21.3 || >=16}
@ -1475,6 +1530,9 @@ packages:
'@types/json-schema@7.0.15': '@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/jsonwebtoken@9.0.10':
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
'@types/linkify-it@5.0.0': '@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
@ -1490,6 +1548,9 @@ packages:
'@types/morgan@1.9.10': '@types/morgan@1.9.10':
resolution: {integrity: sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==} resolution: {integrity: sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==}
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/multer@2.0.0': '@types/multer@2.0.0':
resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==} resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==}
@ -1676,6 +1737,10 @@ packages:
asynckit@0.4.0: asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
aws-ssl-profiles@1.1.2:
resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==}
engines: {node: '>= 6.0.0'}
babel-plugin-macros@3.1.0: babel-plugin-macros@3.1.0:
resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
engines: {node: '>=10', npm: '>=6'} engines: {node: '>=10', npm: '>=6'}
@ -1720,6 +1785,9 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
buffer-from@1.1.2: buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@ -1926,6 +1994,10 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
depd@2.0.0: depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -1952,6 +2024,98 @@ packages:
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
engines: {node: '>=12'} engines: {node: '>=12'}
drizzle-orm@0.44.7:
resolution: {integrity: sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==}
peerDependencies:
'@aws-sdk/client-rds-data': '>=3'
'@cloudflare/workers-types': '>=4'
'@electric-sql/pglite': '>=0.2.0'
'@libsql/client': '>=0.10.0'
'@libsql/client-wasm': '>=0.10.0'
'@neondatabase/serverless': '>=0.10.0'
'@op-engineering/op-sqlite': '>=2'
'@opentelemetry/api': ^1.4.1
'@planetscale/database': '>=1.13'
'@prisma/client': '*'
'@tidbcloud/serverless': '*'
'@types/better-sqlite3': '*'
'@types/pg': '*'
'@types/sql.js': '*'
'@upstash/redis': '>=1.34.7'
'@vercel/postgres': '>=0.8.0'
'@xata.io/client': '*'
better-sqlite3: '>=7'
bun-types: '*'
expo-sqlite: '>=14.0.0'
gel: '>=2'
knex: '*'
kysely: '*'
mysql2: '>=2'
pg: '>=8'
postgres: '>=3'
prisma: '*'
sql.js: '>=1'
sqlite3: '>=5'
peerDependenciesMeta:
'@aws-sdk/client-rds-data':
optional: true
'@cloudflare/workers-types':
optional: true
'@electric-sql/pglite':
optional: true
'@libsql/client':
optional: true
'@libsql/client-wasm':
optional: true
'@neondatabase/serverless':
optional: true
'@op-engineering/op-sqlite':
optional: true
'@opentelemetry/api':
optional: true
'@planetscale/database':
optional: true
'@prisma/client':
optional: true
'@tidbcloud/serverless':
optional: true
'@types/better-sqlite3':
optional: true
'@types/pg':
optional: true
'@types/sql.js':
optional: true
'@upstash/redis':
optional: true
'@vercel/postgres':
optional: true
'@xata.io/client':
optional: true
better-sqlite3:
optional: true
bun-types:
optional: true
expo-sqlite:
optional: true
gel:
optional: true
knex:
optional: true
kysely:
optional: true
mysql2:
optional: true
pg:
optional: true
postgres:
optional: true
prisma:
optional: true
sql.js:
optional: true
sqlite3:
optional: true
dunder-proto@1.0.1: dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -1959,6 +2123,9 @@ packages:
dynamic-dedupe@0.3.0: dynamic-dedupe@0.3.0:
resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==}
ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
ee-first@1.1.1: ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
@ -2228,6 +2395,9 @@ packages:
function-bind@1.1.2: function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
generate-function@2.3.1:
resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
gensync@1.0.0-beta.2: gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@ -2407,6 +2577,9 @@ packages:
is-promise@4.0.0: is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
is-property@1.0.2:
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
is-stream@2.0.1: is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -2491,6 +2664,16 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
hasBin: true hasBin: true
jsonwebtoken@9.0.2:
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
engines: {node: '>=12', npm: '>=6'}
jwa@1.4.2:
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
jws@3.2.2:
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
keygrip@1.1.0: keygrip@1.1.0:
resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -2522,13 +2705,37 @@ packages:
lodash.flattendeep@4.4.0: lodash.flattendeep@4.4.0:
resolution: {integrity: sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==} resolution: {integrity: sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==}
lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
lodash.isinteger@4.0.4:
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
lodash.isnumber@3.0.3:
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
lodash.isstring@4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
lodash.merge@4.6.2: lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
log-symbols@4.1.0: log-symbols@4.1.0:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'} engines: {node: '>=10'}
long@5.3.2:
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
loose-envify@1.4.0: loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true hasBin: true
@ -2536,6 +2743,14 @@ packages:
lru-cache@5.1.1: lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lru-cache@7.18.3:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
engines: {node: '>=12'}
lru.min@1.1.2:
resolution: {integrity: sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==}
engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'}
make-dir@3.1.0: make-dir@3.1.0:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -2654,6 +2869,14 @@ packages:
resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==}
engines: {node: '>= 10.16.0'} engines: {node: '>= 10.16.0'}
mysql2@3.15.3:
resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==}
engines: {node: '>= 8.0'}
named-placeholders@1.1.3:
resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==}
engines: {node: '>=12.0.0'}
nanoid@3.3.11: nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -2946,6 +3169,9 @@ packages:
require-main-filename@2.0.0: require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
resolve-from@4.0.0: resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -3013,6 +3239,9 @@ packages:
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
seq-queue@0.0.5:
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
serialize-javascript@6.0.2: serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
@ -3079,6 +3308,10 @@ packages:
sprintf-js@1.0.3: sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
sqlstring@2.3.3:
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
engines: {node: '>= 0.6'}
statuses@2.0.1: statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -3830,6 +4063,17 @@ snapshots:
'@smithy/types': 4.8.0 '@smithy/types': 4.8.0
tslib: 2.8.1 tslib: 2.8.1
'@aws-sdk/s3-request-presigner@3.916.0':
dependencies:
'@aws-sdk/signature-v4-multi-region': 3.916.0
'@aws-sdk/types': 3.914.0
'@aws-sdk/util-format-url': 3.914.0
'@smithy/middleware-endpoint': 4.3.5
'@smithy/protocol-http': 5.3.3
'@smithy/smithy-client': 4.9.1
'@smithy/types': 4.8.0
tslib: 2.8.1
'@aws-sdk/signature-v4-multi-region@3.916.0': '@aws-sdk/signature-v4-multi-region@3.916.0':
dependencies: dependencies:
'@aws-sdk/middleware-sdk-s3': 3.916.0 '@aws-sdk/middleware-sdk-s3': 3.916.0
@ -3868,6 +4112,13 @@ snapshots:
'@smithy/util-endpoints': 3.2.3 '@smithy/util-endpoints': 3.2.3
tslib: 2.8.1 tslib: 2.8.1
'@aws-sdk/util-format-url@3.914.0':
dependencies:
'@aws-sdk/types': 3.914.0
'@smithy/querystring-builder': 4.2.3
'@smithy/types': 4.8.0
tslib: 2.8.1
'@aws-sdk/util-locate-window@3.893.0': '@aws-sdk/util-locate-window@3.893.0':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@ -4397,6 +4648,45 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.2.2 '@types/react': 19.2.2
'@mui/x-data-grid@8.15.0(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@babel/runtime': 7.28.4
'@mui/material': 7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@mui/system': 7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0)
'@mui/utils': 7.3.3(@types/react@19.2.2)(react@19.2.0)
'@mui/x-internals': 8.14.0(@types/react@19.2.2)(react@19.2.0)
'@mui/x-virtualizer': 0.2.5(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
clsx: 2.1.1
prop-types: 15.8.1
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
use-sync-external-store: 1.6.0(react@19.2.0)
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0)
transitivePeerDependencies:
- '@types/react'
'@mui/x-internals@8.14.0(@types/react@19.2.2)(react@19.2.0)':
dependencies:
'@babel/runtime': 7.28.4
'@mui/utils': 7.3.3(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
reselect: 5.1.1
use-sync-external-store: 1.6.0(react@19.2.0)
transitivePeerDependencies:
- '@types/react'
'@mui/x-virtualizer@0.2.5(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@babel/runtime': 7.28.4
'@mui/utils': 7.3.3(@types/react@19.2.2)(react@19.2.0)
'@mui/x-internals': 8.14.0(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
transitivePeerDependencies:
- '@types/react'
'@noble/hashes@1.8.0': {} '@noble/hashes@1.8.0': {}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
@ -5071,6 +5361,11 @@ snapshots:
'@types/json-schema@7.0.15': {} '@types/json-schema@7.0.15': {}
'@types/jsonwebtoken@9.0.10':
dependencies:
'@types/ms': 2.1.0
'@types/node': 24.9.1
'@types/linkify-it@5.0.0': {} '@types/linkify-it@5.0.0': {}
'@types/markdown-it@14.1.2': '@types/markdown-it@14.1.2':
@ -5086,6 +5381,8 @@ snapshots:
dependencies: dependencies:
'@types/node': 24.9.1 '@types/node': 24.9.1
'@types/ms@2.1.0': {}
'@types/multer@2.0.0': '@types/multer@2.0.0':
dependencies: dependencies:
'@types/express': 5.0.3 '@types/express': 5.0.3
@ -5304,6 +5601,8 @@ snapshots:
asynckit@0.4.0: {} asynckit@0.4.0: {}
aws-ssl-profiles@1.1.2: {}
babel-plugin-macros@3.1.0: babel-plugin-macros@3.1.0:
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
@ -5359,6 +5658,8 @@ snapshots:
node-releases: 2.0.26 node-releases: 2.0.26
update-browserslist-db: 1.1.3(browserslist@4.26.3) update-browserslist-db: 1.1.3(browserslist@4.26.3)
buffer-equal-constant-time@1.0.1: {}
buffer-from@1.1.2: {} buffer-from@1.1.2: {}
busboy@1.6.0: busboy@1.6.0:
@ -5546,6 +5847,8 @@ snapshots:
delayed-stream@1.0.0: {} delayed-stream@1.0.0: {}
denque@2.1.0: {}
depd@2.0.0: {} depd@2.0.0: {}
dezalgo@1.0.4: dezalgo@1.0.4:
@ -5568,6 +5871,10 @@ snapshots:
dotenv@17.2.3: {} dotenv@17.2.3: {}
drizzle-orm@0.44.7(mysql2@3.15.3):
optionalDependencies:
mysql2: 3.15.3
dunder-proto@1.0.1: dunder-proto@1.0.1:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@ -5578,6 +5885,10 @@ snapshots:
dependencies: dependencies:
xtend: 4.0.2 xtend: 4.0.2
ecdsa-sig-formatter@1.0.11:
dependencies:
safe-buffer: 5.2.1
ee-first@1.1.1: {} ee-first@1.1.1: {}
ejs@3.1.10: ejs@3.1.10:
@ -5956,6 +6267,10 @@ snapshots:
function-bind@1.1.2: {} function-bind@1.1.2: {}
generate-function@2.3.1:
dependencies:
is-property: 1.0.2
gensync@1.0.0-beta.2: {} gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {} get-caller-file@2.0.5: {}
@ -6122,6 +6437,8 @@ snapshots:
is-promise@4.0.0: {} is-promise@4.0.0: {}
is-property@1.0.2: {}
is-stream@2.0.1: {} is-stream@2.0.1: {}
is-typedarray@1.0.0: {} is-typedarray@1.0.0: {}
@ -6205,6 +6522,30 @@ snapshots:
json5@2.2.3: {} json5@2.2.3: {}
jsonwebtoken@9.0.2:
dependencies:
jws: 3.2.2
lodash.includes: 4.3.0
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
lodash.isnumber: 3.0.3
lodash.isplainobject: 4.0.6
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
semver: 7.7.3
jwa@1.4.2:
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
jws@3.2.2:
dependencies:
jwa: 1.4.2
safe-buffer: 5.2.1
keygrip@1.1.0: keygrip@1.1.0:
dependencies: dependencies:
tsscmp: 1.0.6 tsscmp: 1.0.6
@ -6236,13 +6577,29 @@ snapshots:
lodash.flattendeep@4.4.0: {} lodash.flattendeep@4.4.0: {}
lodash.includes@4.3.0: {}
lodash.isboolean@3.0.3: {}
lodash.isinteger@4.0.4: {}
lodash.isnumber@3.0.3: {}
lodash.isplainobject@4.0.6: {}
lodash.isstring@4.0.1: {}
lodash.merge@4.6.2: {} lodash.merge@4.6.2: {}
lodash.once@4.1.1: {}
log-symbols@4.1.0: log-symbols@4.1.0:
dependencies: dependencies:
chalk: 4.1.2 chalk: 4.1.2
is-unicode-supported: 0.1.0 is-unicode-supported: 0.1.0
long@5.3.2: {}
loose-envify@1.4.0: loose-envify@1.4.0:
dependencies: dependencies:
js-tokens: 4.0.0 js-tokens: 4.0.0
@ -6251,6 +6608,10 @@ snapshots:
dependencies: dependencies:
yallist: 3.1.1 yallist: 3.1.1
lru-cache@7.18.3: {}
lru.min@1.1.2: {}
make-dir@3.1.0: make-dir@3.1.0:
dependencies: dependencies:
semver: 6.3.1 semver: 6.3.1
@ -6381,6 +6742,22 @@ snapshots:
type-is: 1.6.18 type-is: 1.6.18
xtend: 4.0.2 xtend: 4.0.2
mysql2@3.15.3:
dependencies:
aws-ssl-profiles: 1.1.2
denque: 2.1.0
generate-function: 2.3.1
iconv-lite: 0.7.0
long: 5.3.2
lru.min: 1.1.2
named-placeholders: 1.1.3
seq-queue: 0.0.5
sqlstring: 2.3.3
named-placeholders@1.1.3:
dependencies:
lru-cache: 7.18.3
nanoid@3.3.11: {} nanoid@3.3.11: {}
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
@ -6717,6 +7094,8 @@ snapshots:
require-main-filename@2.0.0: {} require-main-filename@2.0.0: {}
reselect@5.1.1: {}
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
resolve-from@5.0.0: {} resolve-from@5.0.0: {}
@ -6809,6 +7188,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
seq-queue@0.0.5: {}
serialize-javascript@6.0.2: serialize-javascript@6.0.2:
dependencies: dependencies:
randombytes: 2.1.0 randombytes: 2.1.0
@ -6886,6 +7267,8 @@ snapshots:
sprintf-js@1.0.3: {} sprintf-js@1.0.3: {}
sqlstring@2.3.3: {}
statuses@2.0.1: {} statuses@2.0.1: {}
statuses@2.0.2: {} statuses@2.0.2: {}