-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Socialabc #183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Socialabc #183
Conversation
WalkthroughAdds JWT-based auth with login/register/logout endpoints and client flows. Binds links to users and introduces per-user link listing. Tightens access control across logs/stats via admin checks and user scoping. Updates UI: login/register pages, link editor/schema, lists, analytics counters chart behavior. Refactors utilities and config, adds admin pages/APIs. Changes
Sequence Diagram(s)sequenceDiagram
participant U as User
participant UI as Login/Register UI
participant API as /api/auth/*
participant KV as Cloudflare KV
participant JWT as Token Utils
participant Router as Client Router
U->>UI: Submit email/password
UI->>API: POST /api/auth/login
API->>KV: Lookup user by email/id
API->>JWT: Generate token
API-->>UI: { user, token } + auth_cookie
UI->>Router: navigate /dashboard
sequenceDiagram
participant C as Client
participant MW as server/middleware/2.auth
participant H as Handler (/api/link/create)
participant Auth as getUserFromToken
participant KV as Cloudflare KV
C->>H: Request with Bearer token
H->>Auth: Validate & load user
Auth->>KV: Get user by id
Auth-->>H: user { id, role }
H->>KV: Put link with userId metadata
H-->>C: { success, link, shortLink }
sequenceDiagram
participant C as Client
participant API as /api/stats/counters.get
participant Auth as checkAdminAccess/getUserFromToken
participant KV as Cloudflare KV
participant WAE as Analytics Engine
C->>API: GET stats
API->>Auth: Check admin / get user links
Auth->>KV: Read user:<id>:links -> slugs -> link IDs
API->>WAE: Query filtered by link IDs
WAE-->>API: Aggregates
API-->>C: visits/visitors/referers
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 47
🔭 Outside diff range comments (8)
server/api/link/search.get.ts (1)
44-51: Fix TTL preservation: use the key’s expiration instead of metadata.expiration
getWithMetadata’smetadatadoesn’t include the key’s expiration. You should preserve the original key’s expiration from thelistentry (key.expiration) when rewriting the value+metadata, otherwise TTL may be unintentionally dropped.Apply this diff:
- await KV.put(key.name, JSON.stringify(link), { - expiration: metadata?.expiration, + await KV.put(key.name, JSON.stringify(link), { + // Preserve original TTL from the list item + expiration: key.expiration, metadata: { ...metadata, url: link.url, comment: link.comment, }, })server/api/link/delete.post.ts (1)
16-17: Validate the request body (slug) with Zod instead of rawreadBody.This enforces a non-empty string and yields consistent 4xx on invalid input.
- const { slug } = await readBody(event) + const { slug } = await readValidatedBody( + event, + z.object({ + slug: z.string().trim().min(1), + }).parse + )server/api/stats/views.get.ts (1)
16-19: Harden query validation: restrict unit and sanitize clientTimezone to prevent SQL injection and undefined formats.
- unit currently accepts any string; if not in unitMap, it resolves to undefined, breaking the query.
- clientTimezone is interpolated into SQL; sanitize to an allowlist pattern.
Apply this diff:
-const ViewsQuerySchema = QuerySchema.extend({ - unit: z.string(), - clientTimezone: z.string().default('Etc/UTC'), -}) +const ViewsQuerySchema = QuerySchema.extend({ + unit: z.enum(['minute', 'hour', 'day']).default('day'), + // Basic IANA TZ pattern: letters, slash, underscore, plus/minus + clientTimezone: z.string().regex(/^[A-Za-z_\/+\-]{1,64}$/).default('Etc/UTC'), +})server/api/link/upsert.post.ts (2)
31-35: Enforce ownership on existing slugs to prevent data leakage/privilege escalation.Returning an existing link without checking userId leaks another user’s link metadata and allows discovery of slugs.
Apply this diff:
- if (existingLink) { - // If link exists, return it along with the short link - const shortLink = `${getRequestProtocol(event)}://${getRequestHost(event)}/${link.slug}` - return { link: existingLink, shortLink, status: 'existing' } - } + if (existingLink) { + if ((existingLink as any).userId !== user.id) { + throw createError({ status: 403, statusText: 'Forbidden: slug belongs to another user' }) + } + // If link exists (and is owned by the caller), return it + const shortLink = `${getRequestProtocol(event)}://${getRequestHost(event)}/${link.slug}` + return { link: existingLink, shortLink, status: 'existing' } + }And add createError to the h3 import:
-import { eventHandler, readValidatedBody, getRequestProtocol, getRequestHost, setResponseStatus } from 'h3' +import { eventHandler, readValidatedBody, getRequestProtocol, getRequestHost, setResponseStatus, createError } from 'h3'
40-52: Keep the per-user index consistent when creating via upsert.When creating a new link, this endpoint doesn’t append the slug to user::links, which breaks per-user stats scoping for links created here.
Apply this diff:
await KV.put(`link:${link.slug}`, JSON.stringify(link), { expiration, metadata: { expiration, url: link.url, comment: link.comment, userId: link.userId }, }) + + // Update per-user index + try { + const userLinksIndex = await KV.get(`user:${user.id}:links`, { type: 'json' }) || [] + if (Array.isArray(userLinksIndex) && !userLinksIndex.includes(link.slug)) { + userLinksIndex.push(link.slug) + await KV.put(`user:${user.id}:links`, JSON.stringify(userLinksIndex)) + } + } catch (err) { + console.error('Failed to update user links index in upsert:', err) + } setResponseStatus(event, 201)server/api/link/edit.put.ts (2)
30-38: Add ownership verification before editingThe code doesn't verify that the authenticated user owns the link they're trying to edit. This is a critical security issue that could allow users to modify other users' links.
Add ownership verification after fetching the existing link:
const existingLink: z.infer<typeof LinkSchema> | null = await KV.get(`link:${link.slug}`, { type: 'json' }) if (existingLink) { + // Verify the user owns this link + if (existingLink.userId !== user.id) { + throw createError({ + status: 403, + statusText: 'You do not have permission to edit this link', + }) + } const newLink = { ...existingLink, ...link, id: existingLink.id, // don't update id createdAt: existingLink.createdAt, // don't update createdAt updatedAt: Math.floor(Date.now() / 1000),
52-53: Handle the case when link doesn't existThe function doesn't return anything when the link doesn't exist, which will result in an undefined response. This should explicitly return an error.
const shortLink = `${getRequestProtocol(event)}://${getRequestHost(event)}/${newLink.slug}` return { link: newLink, shortLink } } + + throw createError({ + status: 404, + statusText: 'Link not found', + }) })app/components/dashboard/links/Editor.vue (1)
103-103: Fix incorrect spread operation on optional object.The spread operator is being used on
formData.optionalwhich could be an object, not an array. This will include all optional fields in the link object.- ...(formData.optional || []), + ...(formData.optional || {}),
♻️ Duplicate comments (1)
server/api/link/delete.post.ts (1)
16-17: Return a 400 when no slug is provided (now handled by Zod).With the proposed Zod change, invalid/missing slug will automatically produce a 4xx. If you keep
readBody, add an explicit 400.
🧹 Nitpick comments (59)
server/middleware/1.redirect.ts (2)
8-8: Good consolidation of runtime config; consider coercing numeric config once with safe defaultsReading
redirectStatusCodeonce is solid. Minor improvement: coerce and validate numeric config (e.g.,redirectStatusCode, possiblylinkCacheTtl) up front to avoid relying on unary+at call sites and to prevent accidentalNaNif unset/misconfigured. Also consider consistently using the same redirect status for the home redirect.You could add after this line:
// Optional: coerce and guard const safeRedirectStatus = [301, 302, 307, 308].includes(Number(redirectStatusCode)) ? Number(redirectStatusCode) : 302And then use
safeRedirectStatuswhere you redirect (see next comment).
40-40: Guard redirect status code with a 3xx allowlist and fallback to 302The only definition of
redirectStatusCodeis innuxt.config.ts(default'301'), so+redirectStatusCodeyields a valid 3xx by default. However, since the runtime config can be overridden, it’s safer to guard against misconfiguration:• server/middleware/1.redirect.ts (line 40) – current use of
+redirectStatusCode
• nuxt.config.ts (line 24) – defaultredirectStatusCode: '301'Apply this diff:
- return sendRedirect(event, target, +redirectStatusCode) + return sendRedirect( + event, + target, + [301, 302, 307, 308].includes(+redirectStatusCode) + ? +redirectStatusCode + : 302, + )If you’ve already computed a
safeRedirectStatusearlier, update accordingly:- return sendRedirect(event, target, +redirectStatusCode) + return sendRedirect(event, target, safeRedirectStatus)app/components/spark-ui/Notification.vue (1)
37-37: Potential unresolved reference: shortTime
shortTime(props.time)is used but not defined/imported in this SFC. If you’re not relying on auto-imports or a global, add an import.If
shortTimeis exported from your utils entry, you can co-locate the import:-import { cn } from '@/utils' +import { cn, shortTime } from '@/utils'app/components/ui/drawer/DrawerContent.vue (1)
19-22: Nit: Potential duplicate class binding via forwarded props and explicit :classIf forwarded already contains a class prop, combining v-bind="forwarded" with a separate :class merges classes twice. It’s harmless but can lead to duplicates. Consider omitting class from the forwarded set before binding.
server/api/link/search.get.ts (3)
62-65: Redundant!keyscheck after KV.list
KV.listalways returns an array inkeys. The!keysguard is redundant and can be dropped;list_completeis sufficient to break the loop.- if (!keys || list_complete) { + if (list_complete) { break }
15-23: Defensive loop guard for KV.list cursorIf Cloudflare KV ever returns a non-advancing cursor (rare), the loop could spin. Guarding with
if (!cursor) breakafter settingfinalCursor = cursoradds safety.const { keys, list_complete, cursor } = await KV.list({ prefix: `link:`, limit: 1000, cursor: finalCursor, }) finalCursor = cursor + if (!cursor || list_complete) { + // Let the normal break condition handle it, but this avoids + // potential non-advancing cursor edge-cases. + }
66-66: Align response shape with the rest of the APIMost endpoints in this PR return
{ data, total }. This route returns a bare array. If public consumers aren’t depending on the old shape, consider returning{ data: list, total: list.length }for consistency.app/utils/api.ts (1)
10-11: Avoid sending an Authorization header when there’s no tokenSending
Authorization: Bearer(empty token) can cause unexpected 401s or noisy server logs. Prefer conditionally setting the header only if a token exists.Minimal change within current structure:
- Authorization: `Bearer ${token}`, + // Only attach when token exists + Authorization: token ? `Bearer ${token}` : undefined,If your fetch wrapper doesn’t drop undefined headers, consider building the headers object conditionally before calling
$fetch.app/components/dashboard/analysis/Counters.vue (1)
53-76: Confirm removal of dimming/blur is intentionalThe previous conditional styling communicated “no data/loading” states. If that was removed by design, all good. If not, consider reintroducing a subtle skeleton/opacity toggle tied to the fetch lifecycle.
wrangler.jsonc (1)
30-34: Observability logs enabled — mind cost and PIIGreat for debugging. Make sure server logs don’t include tokens or PII, and consider sampling/log levels in production to control volume and cost.
app/components/login/index.vue (4)
12-15: Schema looks good; consider user-friendly messagesEmail + min(8) password is fine. If desired, provide localized zod error messages for better UX.
31-42: Type the API response and setautocompletehintsOptionally type the login API response and add browser autocomplete for better UX.
Example:
type LoginResponse = { token: string; user: Record<string, unknown> } const response = await useAPI<LoginResponse>('/api/auth/login', { method: 'POST', body: form })And in
loginFieldConfig.password.inputProps, addautocomplete: 'current-password'; for email addautocomplete: 'email'.
73-78: Use NuxtLink instead of anchor to avoid full page reloadPreserve SPA navigation and prefetch behavior.
- <a href="/dashboard/register" class="text-primary hover:underline"> - {{ $t('login.register_link') }} - </a> + <NuxtLink to="/dashboard/register" class="text-primary hover:underline"> + {{ $t('login.register_link') }} + </NuxtLink>
70-72: Ensure the submit button has type="submit"Some form wrappers default buttons to
type="button". Being explicit avoids accidental non-submit behavior.- <Button class="w-full"> + <Button type="submit" class="w-full"> {{ $t('login.submit') }} </Button>server/api/auth/logout.post.ts (2)
1-1: Remove unused import.
getCookieis imported but never used.-import { getCookie, setCookie } from 'h3' +import { setCookie } from 'h3'
9-17: Add lightweight rate limiting to logout endpointIt’s recommended to introduce minimal per-IP rate limiting on
POST /auth/logoutto align with our API guidelines. I didn’t find any existing rate-limit utility in the codebase—if one exists, please reuse it; otherwise, add a simple in-memory or shared middleware limiter.• File to update:
- server/api/auth/logout.post.ts
app/pages/dashboard/register.vue (1)
3-3: Lazy-load the Register component to reduce initial bundle size.Nuxt auto-imports components and supports lazy loading with the
Lazyprefix.- <Register /> + <LazyRegister />server/api/auth/login.post.ts (1)
48-55: Optional: Add Zod validation for KV-loaded user dataFor defense-in-depth, validate the shape of
userDatabefore using its fields. You can reuse the existingUserSchemafromschemas/user.ts.Possible changes in
server/api/auth/login.post.ts(around lines 48–55, 57, 66, and 81–86):• Import the schema:
import { UserSchema } from '../../../schemas/user'• Validate right after fetching from KV:
- const userData = await KV.get(`user:id:${userId}`, { type: 'json' }) + const rawUserData = await KV.get(`user:id:${userId}`, { type: 'json' }) + const userData = UserSchema.parse(rawUserData)This ensures
userData.isActive,userData.role,userData.password, etc., have the expected types/structure before use.app/components/dashboard/realtime/Chart.vue (2)
11-24: Handle API errors to avoid unhandled rejections and keep UI stable.Wrap the fetch/assign in try/catch; fall back to zero and log once.
async function getRealtimeStats() { - const { data } = await useAPI('/api/stats/counters', { - query: { - startAt: time.value.startAt, - endAt: time.value.endAt, - ...filters.value, - }, - }) - - // 确保visits字段有默认值0 - stats.value = { - visits: Number(data?.[0]?.visits) || 0 - } + try { + const { data } = await useAPI('/api/stats/counters', { + query: { + startAt: time.value.startAt, + endAt: time.value.endAt, + ...filters.value, + }, + }) + // 确保visits字段有默认值0 + stats.value = { + visits: Number(data?.[0]?.visits) || 0, + } + } catch (err) { + stats.value = { visits: 0 } + console.error('Failed to fetch realtime stats', err) + } }
26-33: Simplify initial fetch by usingimmediate: trueon the watcher.You can remove
onMountedby triggering the watcher immediately.watch([time, filters], getRealtimeStats, { - deep: true, + deep: true, + immediate: true, }) -onMounted(async () => { - getRealtimeStats() -}) +// onMounted no longer needed due to immediate watcherserver/api/link/delete.post.ts (1)
31-42: LGTM — deletion and index maintenance are sound with safe guards.
- Good: Array guard on index, try/catch isolation.
- Consider returning a small JSON
{ success: true }for client clarity.schemas/link.ts (2)
15-15: Disallow empty userIdAs written, an empty string passes. Enforce a minimum length to avoid invalid/anonymous linkage downstream.
Apply this diff:
- userId: z.string().trim().max(26), + userId: z.string().trim().min(1).max(26),
2-9: Module-level useAppConfig/useRuntimeConfig may break in atypical contextsAccessing runtime/app config at module import time can fail in non-Nuxt execution contexts (e.g., isolated unit tests) and makes defaults static at import-time. Consider lazy evaluation within default closures to ensure availability and correct values per request/runtime.
If you want to make config usage lazy with minimal change:
-const { slugRegex } = useAppConfig() - -const slugDefaultLength = +useRuntimeConfig().public.slugDefaultLength - -export const nanoid = (length: number = slugDefaultLength) => customAlphabet('23456789abcdefghjkmnpqrstuvwxyz', length) +export const nanoid = (length?: number) => { + const { slugRegex: _unused } = useAppConfig() // touch to keep behavior consistent if needed + const slugDefaultLength = +useRuntimeConfig().public.slugDefaultLength + return customAlphabet('23456789abcdefghjkmnpqrstuvwxyz', length ?? slugDefaultLength) +} @@ - slug: z.string().trim().max(2048).regex(new RegExp(slugRegex)).default(nanoid()), + slug: z.string().trim().max(2048) + .regex(new RegExp(useAppConfig().slugRegex)) + .default(nanoid()),Would you like me to add a small test harness to verify these imports resolve correctly in your test setup (e.g., Vitest/Nuxt test utils)?
Also applies to: 11-16
app/middleware/auth.global.ts (2)
5-13: Gate on both user and token presenceRelying only on SinkUser can allow a stale session to slip through UI gating. Check for token presence as well.
Apply this diff:
- if (to.path.startsWith('/dashboard') && + if (to.path.startsWith('/dashboard') && to.path !== '/dashboard/login' && to.path !== '/dashboard/register') { - // 检查是否有用户信息 - const user = window.localStorage.getItem('SinkUser') - if (!user) { + // 检查是否有用户信息与令牌 + const user = window.localStorage.getItem('SinkUser') + const token = window.localStorage.getItem('SinkSiteToken') + if (!user || !token) { return navigateTo('/dashboard/login') } }
16-25: Redirect from login/register only when both user and token existPrevents redirect loops when token is missing but stale user remains.
Apply this diff:
- if ((to.path === '/dashboard/login' || to.path === '/dashboard/register')) { + if ((to.path === '/dashboard/login' || to.path === '/dashboard/register')) { try { - const user = window.localStorage.getItem('SinkUser') - if (user) { + const user = window.localStorage.getItem('SinkUser') + const token = window.localStorage.getItem('SinkSiteToken') + if (user && token) { // 尝试验证用户会话是否有效 // 在实际应用中,这里可能需要调用验证API return navigateTo('/dashboard') } }app/components/register/index.vue (8)
2-2: Remove unused importAlertCircle isn’t used. Removing it trims bundle size.
Apply this diff:
-import { AlertCircle } from 'lucide-vue-next'
10-16: Name the component explicitlyPascalCase component naming is required. Add an explicit name to satisfy the guideline even if the file resides under a lowercase folder.
Apply this diff:
const { t } = useI18n() +defineOptions({ name: 'Register' }) + const RegisterSchema = z.object({
39-39: Remove unused variable
previewModeisn’t used.Apply this diff:
-const { previewMode } = useRuntimeConfig().public
41-41: Type the form parameter from the Zod schemaLeverage the schema to type the submit payload.
Apply this diff:
-async function onSubmit(form) { +async function onSubmit(form: z.infer<typeof RegisterSchema>) {
77-82: Harden error message extraction
e.messagemay be undefined (H3 errors often carrydata.message). Provide safe fallbacks to avoid showing “undefined”.Apply this diff:
catch (e) { console.error(e) toast.error(t('register.failed'), { - description: e.message, + description: (e && (e as any).data?.message) || (e as any)?.message || String(e), }) }
107-110: Use NuxtLink to avoid full page reloadPrefer client-side navigation to keep SPA behavior.
Apply this diff:
- <a href="/dashboard/login" class="text-primary hover:underline"> - {{ $t('register.login_link') }} - </a> + <NuxtLink to="/dashboard/login" class="text-primary hover:underline"> + {{ $t('register.login_link') }} + </NuxtLink>
12-17: Consider reusing a shared RegisterSchema to avoid driftIf
schemas/user.tsdefines aRegisterSchema, import and reuse it to keep server/client validation in sync and reduce duplication.I can update this component to import the shared schema and adjust the field config accordingly if that schema exists.
Also applies to: 41-76
59-65: Security note: localStorage token storageClient-side storage of long-lived tokens is susceptible to XSS. If feasible, move tokens to HttpOnly cookies and keep only minimal, non-sensitive user info in localStorage. If staying with localStorage, ensure strict CSP and sanitize all rendered HTML.
Do you want me to draft a cookie-based auth flow (Set-Cookie from login endpoint, server-side verification, and middleware adjusted accordingly)?
Also applies to: 68-75
server/api/stats/views.get.ts (1)
40-50: Fetch user links in parallel to reduce latency (KV round-trips are currently sequential).The for-await loop serializes KV.get calls and will be slow for users with many links.
Apply this diff:
- const userLinks = [] - for (const slug of userLinksIndex) { - try { - const link = await KV.get(`link:${slug}`, { type: 'json' }) - if (link && link.id) { - userLinks.push(link.id) - } - } catch (error) { - console.error(`Error fetching link ${slug}:`, error) - } - } + const userLinks = [] + const results = await Promise.allSettled( + userLinksIndex.map(slug => KV.get(`link:${slug}`, { type: 'json' })) + ) + for (let i = 0; i < results.length; i++) { + const r = results[i] + if (r.status === 'fulfilled') { + const link = r.value as any + if (link && link.id) userLinks.push(link.id) + } else { + console.error(`Error fetching link ${userLinksIndex[i]}:`, r.reason) + } + }server/middleware/2.auth.ts (1)
7-11: Prefer pathname and a centralized allowlist for unauthenticated routes.event.path can be environment-dependent; using getRequestURL(event).pathname is safer. Also consider including logout if it should be public.
Apply this diff:
-import { eventHandler, getHeader, createError } from 'h3' +import { eventHandler, getHeader, createError, getRequestURL } from 'h3' @@ - // 跳过认证的路径 - if (event.path.startsWith('/api/_') || - event.path.startsWith('/api/auth/login') || - event.path.startsWith('/api/auth/register') || - event.path.startsWith('/api/verify')) { + // 跳过认证的路径 + const { pathname } = getRequestURL(event) + if (pathname.startsWith('/api/_') || + pathname.startsWith('/api/auth/login') || + pathname.startsWith('/api/auth/register') || + pathname.startsWith('/api/auth/logout') || + pathname.startsWith('/api/verify')) { return }server/api/link/create.post.ts (2)
85-91: Avoid duplicate slugs in the per-user index.Current push() can create duplicates after retries. Deduplicate before writing.
Apply this diff:
- if (Array.isArray(userLinksIndex)) { - userLinksIndex.push(link.slug) - await KV.put(`user:${user.id}:links`, JSON.stringify(userLinksIndex)) - console.log('更新后的用户链接索引:', userLinksIndex) - } + if (Array.isArray(userLinksIndex)) { + if (!userLinksIndex.includes(link.slug)) { + userLinksIndex.push(link.slug) + await KV.put(`user:${user.id}:links`, JSON.stringify(userLinksIndex)) + console.log('更新后的用户链接索引:', userLinksIndex) + } + }
32-47: Reduce verbose console logging or gate it by environment.There are many console.log statements including user identifiers; consider using a debug logger or gating logs under a development flag to reduce noise and PII exposure in production logs.
I can provide a small wrapper around console that no-ops in production if you’d like.
Also applies to: 57-66, 96-105
server/api/stats/counters.get.ts (1)
28-38: Parallelize KV lookups for user links to improve performance.Sequential KV.get calls add unnecessary latency.
Apply this diff:
- const userLinks = [] - for (const slug of userLinksIndex) { - try { - const link = await KV.get(`link:${slug}`, { type: 'json' }) - if (link && link.id) { - userLinks.push(link.id) - } - } catch (error) { - console.error(`Error fetching link ${slug}:`, error) - } - } + const userLinks = [] + const results = await Promise.allSettled( + userLinksIndex.map(slug => KV.get(`link:${slug}`, { type: 'json' })) + ) + results.forEach((r, i) => { + if (r.status === 'fulfilled') { + const link = r.value as any + if (link && link.id) userLinks.push(link.id) + } else { + console.error(`Error fetching link ${userLinksIndex[i]}:`, r.reason) + } + })server/api/link/edit.put.ts (1)
9-10: Consider internationalizing the commentThe Chinese comment
// 获取当前登录用户should be in English for consistency with the rest of the codebase.- // 获取当前登录用户 + // Get the currently authenticated userserver/api/stats/metrics.get.ts (3)
18-22: Simplify admin check logicThe current pattern of catching and assigning null could be simplified using optional chaining or a try-catch pattern that's more explicit about the intent.
try { - // 尝试检查管理员权限 - const admin = await checkAdminAccess(event).catch(() => null) + // Check admin permissions + let isAdmin = false + try { + await checkAdminAccess(event) + isAdmin = true + } catch { + // Not an admin, continue with user-scoped query + } - if (!admin) { + if (!isAdmin) {
19-20: Consider internationalizing commentsThe Chinese comments should be in English for consistency with the rest of the codebase.
- // 尝试检查管理员权限 + // Check admin permissions- // 非管理员,只能查看自己的链接数据 + // Non-admin users can only view their own link dataAlso applies to: 24-24
31-43: Refactor serial KV.get calls to parallel Promise.allThe current loop awaits each KV.get sequentially, which can become a bottleneck for large link arrays. Since Cloudflare KV doesn’t support multi-get out of the box, we can instead fire all fetches in parallel—and optionally throttle or chunk them—to improve throughput.
Suggested refactor:
// Before: sequential fetches const userLinks: string[] = [] for (const slug of userLinksIndex) { try { const link = await KV.get(`link:${slug}`, { type: 'json' }) if (link?.id) userLinks.push(link.id) } catch (error) { console.error(`Error fetching link ${slug}:`, error) } } // After: parallel fetches with Promise.all const linkPromises = userLinksIndex.map(async (slug) => { try { const link = await KV.get(`link:${slug}`, { type: 'json' }) return link?.id } catch (error) { console.error(`Error fetching link ${slug}:`, error) return undefined } }) let userLinks = await Promise.all(linkPromises) // Filter out any undefined or null ids userLinks = userLinks.filter((id): id is string => !!id)Optional enhancements:
- Use a concurrency limiter (e.g., p-limit) if you anticipate very large lists.
- Chunk the slugs into smaller batches to avoid saturating KV or the Worker’s CPU.
app/components/dashboard/Logout.vue (1)
22-27: Consider adding error toast for better UXWhile the error is logged and the user is still redirected, consider showing an error toast to inform the user that logout may not have completed successfully on the server side.
} catch (e) { console.error(e) - // 即使API调用失败,也清除本地状态并跳转 + // Clear local state and redirect even if API call fails localStorage.removeItem('SinkUser') + toast.error(t('logout.error'), { + description: t('logout.error_message'), + }) navigateTo('/dashboard/login') }server/api/link/me.get.ts (2)
10-11: Consider internationalizing the commentThe Chinese comment should be in English for consistency.
- // 获取当前登录用户 + // Get the currently authenticated user
68-76: Consider using appropriate HTTP status codes for errorsThe current implementation always returns a 200 OK status even when there's an error. Consider throwing proper HTTP errors instead.
} catch (error) { console.error('Error fetching user links:', error) - return { - success: false, - message: 'Failed to fetch user links', - data: [], - total: 0 - } + throw createError({ + status: 500, + statusText: 'Failed to fetch user links', + }) }app/pages/dashboard/admin/links.vue (1)
125-127: Simplify Badge variant logicThe conditional class binding for Badge variant could be simplified using a computed property or a more readable ternary operator.
- <Badge variant="secondary" :class="link.expiration && link.expiration < Date.now() / 1000 ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'">{{ - link.expiration ? new Date(link.expiration * 1000).toLocaleDateString() : t('common.never') - }}</Badge> + <Badge + :variant="link.expiration && link.expiration < Date.now() / 1000 ? 'destructive' : 'success'" + > + {{ link.expiration ? new Date(link.expiration * 1000).toLocaleDateString() : t('common.never') }} + </Badge>server/api/auth/register.post.ts (1)
41-57: Duplicate checks are racy under concurrency; consider a write-first or reservation pattern.Between GET-check and PUT, two concurrent requests can pass checks and create duplicates. With KV lacking CAS, mitigations include:
- Write a short-lived “reservation” (e.g., user:email: -> pending:) via a Durable Object or DO-backed lock.
- Alternatively, write the index key first if not exists via DO, or move user storage to D1/DO for strong consistency.
If keeping KV, add post-write verification and cleanup on conflict.
Would you like me to propose a simple Durable Object-based reservation/lock flow compatible with your setup?
app/components/dashboard/links/Index.vue (1)
39-45: Deduplicate appended links by id to avoid duplicates on cursor retries.If the same page is fetched twice (e.g., transient error, cursor reuse), the current concat can duplicate entries. Use a Map to keep latest by id.
- // 添加新链接到列表中 - links.value = links.value.concat(data.data || []).filter(Boolean) + // 添加新链接到列表中,按 id 去重 + const incoming = (data?.data || []).filter(Boolean) + const byId = new Map<string, any>(links.value.map(l => [l.id, l])) + for (const l of incoming) byId.set(l.id, l) + links.value = Array.from(byId.values())server/api/logs/locations.ts (2)
26-36: Parallelize KV reads for user links to reduce latency.Fetching links sequentially can be slow with many links. Use Promise.all with per-item try/catch.
- const userLinks = [] - for (const slug of userLinksIndex) { - try { - const link = await KV.get(`link:${slug}`, { type: 'json' }) - if (link && link.id) { - userLinks.push(link.id) - } - } catch (error) { - console.error(`Error fetching link ${slug}:`, error) - } - } + const userLinks = (await Promise.all(userLinksIndex.map(async (slug: string) => { + try { + const link = await KV.get(`link:${slug}`, { type: 'json' }) + return link?.id as string | undefined + } catch (error) { + console.error(`Error fetching link ${slug}:`, error) + return undefined + } + }))).filter(Boolean) as string[]
65-77: Consider adding rate limiting to this analytics endpoint.These queries can be expensive; Adding a lightweight rate limiter (per-IP or per-user) will protect the system without impacting normal usage.
I can propose a reusable server/utils/rate-limit.ts and wire it here.
app/pages/dashboard/admin/users.vue (2)
48-53: Localize the breadcrumb title.Use i18n rather than a hardcoded English string.
- <DashboardBreadcrumb title="Users Management" /> + <DashboardBreadcrumb :title="$t('admin.users.title')" />
25-39: Implement real delete via API or clearly mark as disabled.Currently, delete updates UI only and may confuse admins. Either wire an admin delete endpoint with proper checks or disable the button until implemented.
I can scaffold server/api/admin/users/[id].delete.ts (admin-only, Zod-validated) and hook it up here.
schemas/user.ts (1)
7-8: Normalize email (and optionally username) to lowercase at the schema layer.This avoids case-related duplicates and keeps persistence consistent with KV indexes.
- email: z.string().trim().email().max(255), + email: z.string().trim().email().max(255).transform(v => v.toLowerCase()), - username: z.string().trim().min(3).max(50), + username: z.string().trim().min(3).max(50),Consider also enforcing a username character set via regex to match backend expectations.
server/api/logs/events.ts (4)
4-6: Consolidate duplicate imports from the same module.Both
getUserFromTokenandcheckAdminAccessare imported from the same module but on separate lines.-import { getUserFromToken } from '../../utils/auth' -import { checkAdminAccess } from '../../utils/auth' +import { getUserFromToken, checkAdminAccess } from '../../utils/auth'
43-45: Simplify filter merging logic.The current filter merging logic with explicit array checks can be simplified using the
andfunction's built-in handling of arguments.- const linkFilter = or(userLinks.map(id => eq('index1', id))) - // 使用安全的方式合并过滤器,不依赖length属性 - filter = Array.isArray(filter) && filter.length > 0 ? and(filter, linkFilter) : linkFilter + const linkFilter = or(...userLinks.map(id => eq('index1', id))) + // Merge filters - and() handles empty arrays gracefully + filter = filter ? and(filter, linkFilter) : linkFilter
97-113: Consider returning an empty array instead of a dummy object.Returning a single dummy object with default values when there's no data could be misleading. Consider returning an empty array or explicitly documenting why a dummy object is needed.
If the client expects at least one object:
- // 如果结果为空数组,返回一个包含默认值的数组 - return result.length > 0 ? result : [{ - ip: '', - id: '', - timestamp: 0, - url: '', - slug: '', - referer: '', - userAgent: '', - device: '', - os: '', - browser: '', - country: '', - region: '', - city: '', - language: '', - isBot: false - }] + // Return empty array when no results - let client handle empty state + return resultOr add a comment explaining the business requirement:
// 如果结果为空数组,返回一个包含默认值的数组 + // Note: Client expects at least one object for UI initialization return result.length > 0 ? result : [{
31-39: Add error handling for concurrent KV operations.The sequential KV fetches in the loop could benefit from parallel processing for better performance, especially when dealing with many links.
- // 获取所有链接对象以获取link.id - const userLinks = [] - for (const slug of userLinksIndex) { - try { - const link = await KV.get(`link:${slug}`, { type: 'json' }) - if (link && link.id) { - userLinks.push(link.id) - } - } catch (error) { - console.error(`Error fetching link ${slug}:`, error) - } - } + // 获取所有链接对象以获取link.id - parallel fetch for better performance + const linkPromises = userLinksIndex.map(async (slug) => { + try { + const link = await KV.get(`link:${slug}`, { type: 'json' }) + return link?.id || null + } catch (error) { + console.error(`Error fetching link ${slug}:`, error) + return null + } + }) + const userLinks = (await Promise.all(linkPromises)).filter(id => id !== null)server/api/verify.ts (1)
2-3: Remove redundant imports.
eventHandlerandcreateErrorare already available from h3 auto-imports in Nuxt/Nitro context.import { getUserFromToken } from '../utils/auth' -import { eventHandler, createError } from 'h3' import { useRuntimeConfig } from '#imports'server/utils/auth.ts (1)
9-9: Remove unused constant.The
JWT_EXPIRES_INconstant is defined but never used in the code.-const JWT_EXPIRES_IN = '7d' // 7天有效期
| @@ -0,0 +1,115 @@ | |||
| <script setup> | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use TypeScript in new Vue SFCs
Repository guideline: “Use TypeScript for all new code.” Switch the script to TS.
Apply this diff:
-<script setup>
+<script setup lang="ts">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <script setup> | |
| <script setup lang="ts"> |
🤖 Prompt for AI Agents
In app/components/register/index.vue around line 1, the component uses a plain
<script setup> block but the repo requires TypeScript for new files; change the
block to <script setup lang="ts"> and update any local variables, props, emits
and refs to use proper TypeScript types (use
defineProps<Type>()/defineEmits<Type>() or typed refs/reactive state) and import
any needed types from Vue to satisfy the compiler; ensure the file compiles with
strict TS by fixing any implicit any types and adjusting imports/exports
accordingly.
| import type { DialogContentEmits, DialogContentProps } from 'radix-vue' | ||
| import type { HtmlHTMLAttributes } from 'vue' | ||
| import { cn } from '@/lib/utils' | ||
| import { cn } from '@/utils' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Verify alias and exports for '@/utils' and remove any lingering '@/lib/utils' imports
To avoid runtime import errors after the path migration, please verify the alias and ensure a named export for cn exists. Also check for any stale '@/lib/utils' imports.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "1) Searching for lingering '@/lib/utils' imports (should be none):"
rg -nP --type=ts --type=tsx --type=vue -C2 '\bfrom\s+[\'"]@/lib/utils[\'"]' || echo "OK: No matches."
echo
echo "2) Verifying that '@/utils' exports a named 'cn' (direct or re-export):"
rg -nP --type=ts --type=tsx --type=vue -C1 '\bexport\s+(?:const|function)\s+cn\b|\bexport\s*\{\s*[^}]*\bcn\b[^}]*\}' || {
echo "WARNING: Could not find an explicit export for 'cn'. Ensure it is exported from '@/utils' (e.g., app/utils/index.ts)."
}
echo
echo "3) Listing all importers of '@/utils' to confirm consistent usage:"
rg -nP --type=ts --type=tsx --type=vue -C1 '\bfrom\s+[\'"]@/utils[\'"]' || echo "No imports found (unexpected if migration occurred)."
echo
echo "4) Checking TS path alias for '@' (tsconfig/jsconfig):"
rg -nP -C2 '"paths"\s*:\s*\{[^}]*"@/\*"\s*:\s*\[[^\]]+\]' tsconfig*.json jsconfig*.json || echo "NOTE: No explicit '@' path alias found in tsconfig/jsconfig (Nuxt may handle this implicitly)."Length of output: 278
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "1) Checking for lingering '@/lib/utils' imports:"
rg -nP --type=ts --type=tsx --type=vue '\bfrom\s+[\'"]@/lib/utils[\'"]' -C2 || echo "OK: No matches."
echo
echo "2) Verifying that '@/utils' exports a named 'cn':"
rg -nP --type=ts --type=tsx --type=vue '\bexport\s+const\s+cn\b' -C2 || true
rg -nP --type=ts --type=tsx --type=vue '\bexport\s+function\s+cn\b' -C2 || true
rg -nP --type=ts --type=tsx --type=vue '\bexport\s*{[^}]*\bcn\b[^}]*}' -C2 || echo "WARNING: No 'cn' export found in '@/utils'."
echo
echo "3) Listing all '@/utils' import sites:"
rg -nP --type=ts --type=tsx --type=vue '\bfrom\s+[\'"]@/utils[\'"]' -C2 || echo "No '@/utils' imports found."
echo
echo "4) Inspecting TS/JS path aliases for '@':"
grep -R '"@/' -n tsconfig.json tsconfig.*.json jsconfig.json jsconfig.*.json || echo "NOTE: No '@/' path alias found in tsconfig/jsconfig."Length of output: 250
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "1) Searching for lingering '@/lib/utils' imports:"
rg -nP --type=ts --type=tsx --type=vue "from\s+['\"]@/lib/utils['\"]" -C2 || echo "OK: No '@/lib/utils' imports found."
echo
echo "2) Verifying that '@/utils' exports a named 'cn':"
rg -nP --type=ts --type=tsx --type=vue "export\s+(const|function)\s+cn\b" -C2 || true
rg -nP --type=ts --type=tsx --type=vue "export\s*{[^}]*\bcn\b[^}]*}" -C2 || echo "WARNING: 'cn' not exported from '@/utils'."
echo
echo "3) Listing all '@/utils' import sites:"
rg -nP --type=ts --type=tsx --type=vue "from\s+['\"]@/utils['\"]" -C2 || echo "No '@/utils' imports found."
echo
echo "4) Checking TS/JS path alias for '@/*' in tsconfig/jsconfig:"
grep -R '"@/\*"' -n tsconfig*.json jsconfig*.json || echo "NOTE: No '@/*' path alias found in tsconfig/jsconfig."Length of output: 1428
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "1) DrawerContent.vue import snippet:"
sed -n '1,20p' app/components/ui/drawer/DrawerContent.vue || echo " - File not found: DrawerContent.vue"
echo
echo "2) Searching for lingering '@/lib/utils' imports:"
rg -n "from\s+['\"]@/lib/utils['\"]" -g "*.*" || echo " - No '@/lib/utils' imports found."
echo
echo "3) Searching for '@/utils' imports:"
rg -n "from\s+['\"]@/utils['\"]" -g "*.*" || echo " - No '@/utils' imports found."
echo
echo "4) Locating all 'utils' files:"
fd --type f utils || echo " - No 'utils' files found."
echo
echo "5) Checking exports of 'cn' in each utils file:"
for file in $(fd --type f utils); do
echo "File: $file"
rg -n "export.*\bcn\b" "$file" || echo " -> 'cn' not exported here"
done
echo
echo "6) Checking TS/JS path alias for '@/*' in config files:"
grep -R '"@/\*"' -n tsconfig*.json jsconfig*.json || echo " - No '@/*' alias in tsconfig/jsconfig"Length of output: 12822
Verify @/utils alias and cn export; remove any @/lib/utils imports
- No lingering
@/lib/utilsimports detected. - All components (e.g.
app/components/ui/drawer/DrawerContent.vueand ~80 others) import{ cn }from@/utils, but no file under that alias currently exportscn.
• Ensure you have autilsentrypoint (e.g.app/utils/index.ts) that defines and exportscn.
• If you’ve moved or renamed this file, update the path or re-export accordingly. - Confirm your TS/JS path alias for
@/*(e.g."@/*": ["app/*"]) is declared intsconfig.json/jsconfig.json(or handled by your framework) so@/utilsresolves correctly.
🤖 Prompt for AI Agents
In app/components/ui/drawer/DrawerContent.vue around line 4, the import "import
{ cn } from '@/utils'" assumes a utils entrypoint exporting cn but none exists;
add or restore a utils entrypoint (e.g. create app/utils/index.ts) that defines
and exports a cn helper (or re-export it from its new location), update any
imports if the file was moved/renamed, and ensure your project path alias "@/*"
(e.g. "@/*": ["app/*"]) is declared in tsconfig.json/jsconfig.json so "@/utils"
resolves correctly.
| @@ -1,6 +1,6 @@ | |||
| <script lang="ts" setup> | |||
| import type { HtmlHTMLAttributes } from 'vue' | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in Vue type: use HTMLAttributes (not HtmlHTMLAttributes)
As with other Drawer components, switch to HTMLAttributes.
Suggested fix:
-import type { HtmlHTMLAttributes } from 'vue'
+import type { HTMLAttributes } from 'vue'
@@
-const props = defineProps<{
- class?: HtmlHTMLAttributes['class']
-}>()
+const props = defineProps<{
+ class?: HTMLAttributes['class']
+}>()Also applies to: 5-7
🤖 Prompt for AI Agents
In app/components/ui/drawer/DrawerHeader.vue around lines 2 and 5-7, the
imported Vue type is misspelled as HtmlHTMLAttributes; replace it with
HTMLAttributes everywhere it's used (update the import statement and any type
annotations/props using HtmlHTMLAttributes to use HTMLAttributes instead) to
match other Drawer components.
| @@ -1,7 +1,7 @@ | |||
| <script lang="ts" setup> | |||
| import type { DialogOverlayProps } from 'radix-vue' | |||
| import type { HtmlHTMLAttributes } from 'vue' | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in Vue type: use HTMLAttributes (not HtmlHTMLAttributes)
HtmlHTMLAttributes isn’t exported by Vue; this will fail type-checking.
Apply this correction:
-import type { HtmlHTMLAttributes } from 'vue'
+import type { HTMLAttributes } from 'vue'
@@
-const props = defineProps<DialogOverlayProps & { class?: HtmlHTMLAttributes['class'] }>()
+const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes['class'] }>()Also applies to: 8-8
🤖 Prompt for AI Agents
In app/components/ui/drawer/DrawerOverlay.vue around lines 3 and 8, the import
and any uses mistakenly reference the non-existent type HtmlHTMLAttributes;
replace all occurrences with Vue's correct HTMLAttributes type (i.e., change the
import to import type { HTMLAttributes } from 'vue' and update any type
annotations or props that use HtmlHTMLAttributes to use HTMLAttributes instead).
| @@ -1,7 +1,7 @@ | |||
| <script lang="ts" setup> | |||
| import type { DrawerTitleProps } from 'vaul-vue' | |||
| import type { HtmlHTMLAttributes } from 'vue' | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in Vue type: use HTMLAttributes (not HtmlHTMLAttributes)
Vue exports HTMLAttributes (React has HtmlHTMLAttributes). This will break type-checking.
Apply this fix:
-import type { HtmlHTMLAttributes } from 'vue'
+import type { HTMLAttributes } from 'vue'
@@
-const props = defineProps<DrawerTitleProps & { class?: HtmlHTMLAttributes['class'] }>()
+const props = defineProps<DrawerTitleProps & { class?: HTMLAttributes['class'] }>()Also applies to: 8-8
🤖 Prompt for AI Agents
In app/components/ui/drawer/DrawerTitle.vue around lines 3 and 8, the imported
Vue prop type is misspelled as HtmlHTMLAttributes (React naming); change the
import to HTMLAttributes and update any usage references to HTMLAttributes so
TypeScript uses Vue's correct type export.
| const linkFilter = or(userLinks.map(id => eq('index1', id))) | ||
| // 使用安全的方式合并过滤器,不依赖length属性 | ||
| filter = Array.isArray(filter) && filter.length > 0 ? and(filter, linkFilter) : linkFilter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix sql-bricks OR usage: spread conditions as varargs.
As in views.get.ts, passing an array into or(...) won’t generate the intended condition list.
Apply this diff:
- const linkFilter = or(userLinks.map(id => eq('index1', id)))
+ const linkFilter = or(...userLinks.map(id => eq('index1', id)))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const linkFilter = or(userLinks.map(id => eq('index1', id))) | |
| // 使用安全的方式合并过滤器,不依赖length属性 | |
| filter = Array.isArray(filter) && filter.length > 0 ? and(filter, linkFilter) : linkFilter | |
| const linkFilter = or(...userLinks.map(id => eq('index1', id))) | |
| // 使用安全的方式合并过滤器,不依赖length属性 | |
| filter = Array.isArray(filter) && filter.length > 0 ? and(filter, linkFilter) : linkFilter |
🤖 Prompt for AI Agents
In server/api/stats/counters.get.ts around lines 42 to 44, the call
or(userLinks.map(...)) passes an array into sql-bricks' or instead of varargs,
so the generated SQL is wrong; change it to spread the mapped conditions (use
or(...userLinks.map(id => eq('index1', id)))) and keep the existing safe merge
logic for filter (i.e., when filter is an array and non-empty, combine with
and(filter, linkFilter), otherwise set filter = linkFilter).
| export default eventHandler(async (event) => { | ||
| const query = await getValidatedQuery(event, QuerySchema.parse) | ||
| const sql = query2sql(query, event) | ||
| return useWAE(event, sql) | ||
| const sql = await query2sql(query, event) | ||
|
|
||
| const result = await useWAE(event, sql) as { data: any[] } | ||
| // 确保即使没有数据也返回一个包含0值的对象数组 | ||
| return result?.data && result.data.length > 0 ? result.data : [{ visits: 0, visitors: 0, referers: 0 }] | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add API rate limiting per coding guidelines.
This endpoint lacks rate limiting.
Apply this diff:
export default eventHandler(async (event) => {
+ await rateLimit(event, { key: 'stats:counters', limit: 60, window: 60 })
const query = await getValidatedQuery(event, QuerySchema.parse)
const sql = await query2sql(query, event)And add the import at the top with the other imports:
+import { rateLimit } from '../../utils/rate-limit'🤖 Prompt for AI Agents
In server/api/stats/counters.get.ts around lines 65 to 72, the endpoint is
missing the required API rate limiting; add the provided import for the
rate-limit helper alongside the other imports at the top of the file, then call
the rate limiter at the start of the handler (before any heavy work or DB calls)
to enforce the configured limits and return a 429 response when exceeded; ensure
the limiter invocation uses the incoming event to identify the client (IP/key)
and that normal execution continues only when the limiter allows the request.
| const linkFilter = or(userLinks.map(id => eq('index1', id))) | ||
| // 使用安全的方式合并过滤器,不依赖length属性 | ||
| filter = Array.isArray(filter) && filter.length > 0 ? and(filter, linkFilter) : linkFilter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix sql-bricks OR usage: pass varargs, not an array (current filter likely ineffective).
sql-bricks' or(...) expects conditions as varargs. Passing an array results in a single operand array node, which won’t generate the intended OR chain.
Apply this diff:
- const linkFilter = or(userLinks.map(id => eq('index1', id)))
+ const linkFilter = or(...userLinks.map(id => eq('index1', id)))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const linkFilter = or(userLinks.map(id => eq('index1', id))) | |
| // 使用安全的方式合并过滤器,不依赖length属性 | |
| filter = Array.isArray(filter) && filter.length > 0 ? and(filter, linkFilter) : linkFilter | |
| const linkFilter = or(...userLinks.map(id => eq('index1', id))) | |
| // 使用安全的方式合并过滤器,不依赖length属性 | |
| filter = Array.isArray(filter) && filter.length > 0 ? and(filter, linkFilter) : linkFilter |
🤖 Prompt for AI Agents
In server/api/stats/views.get.ts around lines 54-56, the code builds linkFilter
with or(userLinks.map(...)) which passes a single array to sql-bricks' or and
produces an array node instead of an OR chain; change the call to pass varargs
by spreading the array (use or(...userLinks.map(id => eq('index1', id)))) so
each condition becomes a separate argument; keep the subsequent safe merge logic
(and(filter, linkFilter) when filter exists) unchanged.
| export default eventHandler(async (event) => { | ||
| const query = await getValidatedQuery(event, ViewsQuerySchema.parse) | ||
| const sql = query2sql(query, event) | ||
| return useWAE(event, sql) | ||
| const sql = await query2sql(query, event) | ||
|
|
||
| const result = await useWAE(event, sql) as { data: any[] } | ||
| // 确保即使没有数据也返回一个包含0值的对象数组 | ||
| return result?.data && result.data.length > 0 ? result.data : [{ time: '', visits: 0, visitors: 0 }] | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add API rate limiting per coding guidelines.
Endpoints under server/api should implement rate limiting. This route lacks it.
Apply this diff to use a simple KV-backed token bucket (assuming you’ll add utils/rate-limit):
export default eventHandler(async (event) => {
+ await rateLimit(event, { key: 'stats:views', limit: 60, window: 60 }) // 60 req/min per client
const query = await getValidatedQuery(event, ViewsQuerySchema.parse)
const sql = await query2sql(query, event)And add the import near the other imports:
+import { rateLimit } from '../../utils/rate-limit'If you don’t have a rate limiter yet, here’s a minimal utility you can drop in at server/utils/rate-limit.ts:
// server/utils/rate-limit.ts
import type { H3Event } from 'h3'
type Options = { key: string; limit: number; window: number } // window in seconds
export async function rateLimit(event: H3Event, { key, limit, window }: Options) {
const ip = event.node?.req?.headers['cf-connecting-ip'] || event.node?.req?.socket?.remoteAddress || 'unknown'
const bucketKey = `rl:${key}:${ip}`
const { KV } = event.context.cloudflare.env
const now = Math.floor(Date.now() / 1000)
const record = (await KV.get(bucketKey, { type: 'json' })) as { reset: number; count: number } | null
if (record && record.reset > now) {
if (record.count >= limit) {
throw createError({ status: 429, statusText: 'Too Many Requests' })
}
await KV.put(bucketKey, JSON.stringify({ reset: record.reset, count: record.count + 1 }), { expiration: record.reset })
} else {
await KV.put(bucketKey, JSON.stringify({ reset: now + window, count: 1 }), { expirationTtl: window })
}
}| if (event.path.startsWith('/api/')) { | ||
| // 尝试JWT令牌认证 | ||
| if (token) { | ||
| try { | ||
| // 验证JWT令牌 | ||
| const user = await verifyToken(token) | ||
| // 将用户信息添加到请求上下文中 | ||
| event.context.user = user | ||
| return | ||
| } catch (error) { | ||
| // JWT验证失败,继续尝试siteToken验证 | ||
| if (token !== runtimeConfig.siteToken) { | ||
| throw createError({ | ||
| status: 401, | ||
| statusText: 'Unauthorized: Invalid token', | ||
| }) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // 使用原始的siteToken验证 | ||
| if (!token || token !== runtimeConfig.siteToken) { | ||
| throw createError({ | ||
| status: 401, | ||
| statusText: 'Unauthorized', | ||
| }) | ||
| } | ||
|
|
||
| if (token && token.length < 8) { | ||
| throw createError({ | ||
| status: 401, | ||
| statusText: 'Token is too short', | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Return early on accepted siteToken (or reorder checks); current flow can throw “Token is too short” after accepting the token.
After passing the siteToken equality check, the code still runs the length check and may reject a valid but short site token.
Apply this diff to make the flow unambiguous:
if (event.path.startsWith('/api/')) {
// 尝试JWT令牌认证
if (token) {
try {
// 验证JWT令牌
const user = await verifyToken(token)
// 将用户信息添加到请求上下文中
event.context.user = user
return
} catch (error) {
- // JWT验证失败,继续尝试siteToken验证
- if (token !== runtimeConfig.siteToken) {
- throw createError({
- status: 401,
- statusText: 'Unauthorized: Invalid token',
- })
- }
+ // JWT验证失败,继续尝试siteToken验证
+ if (token === runtimeConfig.siteToken) {
+ if (token.length < 8) {
+ throw createError({ status: 401, statusText: 'Token is too short' })
+ }
+ return
+ }
+ throw createError({ status: 401, statusText: 'Unauthorized: Invalid token' })
}
}
- // 使用原始的siteToken验证
- if (!token || token !== runtimeConfig.siteToken) {
- throw createError({
- status: 401,
- statusText: 'Unauthorized',
- })
- }
-
- if (token && token.length < 8) {
- throw createError({
- status: 401,
- statusText: 'Token is too short',
- })
- }
+ // 使用原始的siteToken验证(不带 Bearer)
+ if (!token || token !== runtimeConfig.siteToken) {
+ throw createError({ status: 401, statusText: 'Unauthorized' })
+ }
+ if (token.length < 8) {
+ throw createError({ status: 401, statusText: 'Token is too short' })
+ }
+ return
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (event.path.startsWith('/api/')) { | |
| // 尝试JWT令牌认证 | |
| if (token) { | |
| try { | |
| // 验证JWT令牌 | |
| const user = await verifyToken(token) | |
| // 将用户信息添加到请求上下文中 | |
| event.context.user = user | |
| return | |
| } catch (error) { | |
| // JWT验证失败,继续尝试siteToken验证 | |
| if (token !== runtimeConfig.siteToken) { | |
| throw createError({ | |
| status: 401, | |
| statusText: 'Unauthorized: Invalid token', | |
| }) | |
| } | |
| } | |
| } | |
| // 使用原始的siteToken验证 | |
| if (!token || token !== runtimeConfig.siteToken) { | |
| throw createError({ | |
| status: 401, | |
| statusText: 'Unauthorized', | |
| }) | |
| } | |
| if (token && token.length < 8) { | |
| throw createError({ | |
| status: 401, | |
| statusText: 'Token is too short', | |
| }) | |
| } | |
| } | |
| if (event.path.startsWith('/api/')) { | |
| // 尝试JWT令牌认证 | |
| if (token) { | |
| try { | |
| // 验证JWT令牌 | |
| const user = await verifyToken(token) | |
| // 将用户信息添加到请求上下文中 | |
| event.context.user = user | |
| return | |
| } catch (error) { | |
| // JWT验证失败,继续尝试siteToken验证 | |
| if (token === runtimeConfig.siteToken) { | |
| if (token.length < 8) { | |
| throw createError({ status: 401, statusText: 'Token is too short' }) | |
| } | |
| return | |
| } | |
| throw createError({ status: 401, statusText: 'Unauthorized: Invalid token' }) | |
| } | |
| } | |
| // 使用原始的siteToken验证(不带 Bearer) | |
| if (!token || token !== runtimeConfig.siteToken) { | |
| throw createError({ status: 401, statusText: 'Unauthorized' }) | |
| } | |
| if (token.length < 8) { | |
| throw createError({ status: 401, statusText: 'Token is too short' }) | |
| } | |
| return | |
| } |
🤖 Prompt for AI Agents
In server/middleware/2.auth.ts around lines 19 to 53, the logic accepts a valid
runtimeConfig.siteToken but still proceeds to a length check that can throw
"Token is too short"; change the control flow so that once the siteToken is
accepted you return immediately (or reorder checks) to skip the length
validation for siteToken. Concretely: after confirming token ===
runtimeConfig.siteToken, short-circuit out (return) before any further checks,
and keep the length validation only for non-siteToken (JWT) paths.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review continued from previous batch...
| // 如果是H3错误,直接抛出 | ||
| if (error instanceof Error && 'status' in error && error.status === 401) { | ||
| throw error; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Improve error type checking for better reliability.
The current error checking approach is fragile and may not work correctly with all error types. Consider using a more robust check.
- // 如果是H3错误,直接抛出
- if (error instanceof Error && 'status' in error && error.status === 401) {
- throw error;
- }
+ // 如果是H3错误,直接抛出
+ if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 401) {
+ throw error;
+ }🏁 Script executed:
#!/bin/bash
# Check how H3 errors are typically structured in the codebase
rg -A 3 -B 3 "createError\(" server/ --type ts | head -20Length of output: 927
🏁 Script executed:
#!/bin/bash
# Verify how createError is imported
echo "== createError import =="
rg -nP "import\s+{[^}]*createError[^}]*}\s+from\s+['\"][^'\"]+['\"]" -C2 server --type ts || echo "<none>"
# Search for any usage of statusCode on error objects
echo -e "\n== statusCode usage =="
rg -n "statusCode" -nC3 server || echo "<none>"
# Check for H3-provided type guards
echo -e "\n== H3 error type guards =="
rg -n "isH3Error" -nC3 server || echo "<no isH3Error>"
rg -n "isHttpError" -nC3 server || echo "<no isHttpError>"Length of output: 3832
Use H3’s type guard and correct the status property
The errors thrown by createError in H3 carry a .status field (not .statusCode), so switching to statusCode will never match. For a more robust check, import and use H3’s built-in guard:
--- server/api/verify.ts
+++ server/api/verify.ts
@@ Lines 40-43
- // 如果是H3错误,直接抛出
- if (error instanceof Error && 'status' in error && error.status === 401) {
- throw error;
- }
+ import { isHttpError } from 'h3';
+
+ // Rethrow 401 errors created by H3
+ if (isHttpError(error) && error.status === 401) {
+ throw error;
+ }If your H3 version doesn’t export isHttpError, you can instead assert the shape directly:
--- server/api/verify.ts
+++ server/api/verify.ts
@@ Lines 40-43
- if (error instanceof Error && 'status' in error && error.status === 401) {
- throw error;
- }
+ import type { H3Error } from 'h3';
+
+ // Rethrow 401 errors created by H3
+ if ((error as unknown as H3Error).status === 401) {
+ throw error;
+ }Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In server/api/verify.ts around lines 40–43, replace the current runtime check
that uses instanceof Error and property presence with H3’s type guard: import
and use isHttpError from 'h3' and check isHttpError(error) and error.status ===
401 before rethrowing; if your H3 version lacks isHttpError, replace the
condition with a safe shape assertion that confirms error is an object with a
numeric status property and status === 401, then rethrow.
| throw createError({ | ||
| status: 401, | ||
| statusText: 'User is not authenticated', | ||
| message: error instanceof Error ? error.message : 'Authentication failed', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid exposing internal error messages to clients.
Exposing raw error messages could leak sensitive information. Consider using a generic message.
- message: error instanceof Error ? error.message : 'Authentication failed',
+ message: 'Authentication failed',📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| message: error instanceof Error ? error.message : 'Authentication failed', | |
| message: 'Authentication failed', |
🤖 Prompt for AI Agents
In server/api/verify.ts around line 49 the response uses error instanceof Error
? error.message : 'Authentication failed', which can leak internal details to
clients; change the response to always return a generic message like
'Authentication failed' (or a brief auth failure reason) and move the real error
details into a server-side log (e.g., logger.error with error.stack or error) so
the client never receives internal error text.
|
|
||
| // JWT相关配置 | ||
| import { useRuntimeConfig } from '#imports' | ||
| const JWT_SECRET = useRuntimeConfig().jwtSecret || 'default-secret-key-change-in-production' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security concern: Hardcoded fallback secret in production.
Using a hardcoded fallback secret is a serious security vulnerability. The application should fail to start if JWT_SECRET is not properly configured.
-const JWT_SECRET = useRuntimeConfig().jwtSecret || 'default-secret-key-change-in-production'
+const config = useRuntimeConfig()
+if (!config.jwtSecret) {
+ throw new Error('JWT_SECRET must be configured in production')
+}
+const JWT_SECRET = config.jwtSecret📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const JWT_SECRET = useRuntimeConfig().jwtSecret || 'default-secret-key-change-in-production' | |
| // server/utils/auth.ts | |
| const config = useRuntimeConfig() | |
| if (!config.jwtSecret) { | |
| throw new Error('JWT_SECRET must be configured in production') | |
| } | |
| const JWT_SECRET = config.jwtSecret |
🤖 Prompt for AI Agents
In server/utils/auth.ts around line 8, the code currently falls back to a
hardcoded JWT secret which is a security risk; change it to require a configured
secret by reading useRuntimeConfig().jwtSecret and immediately throw an explicit
error (or exit process) if it's missing or empty so the app fails to start;
remove any hardcoded default, update startup/error message to state the
environment variable/key required, and ensure documentation/README and
deployment manifests are updated to provide the secret.
| export async function hashPassword(password: string): Promise<string> { | ||
| const hash = createHash('sha256') | ||
| hash.update(password) | ||
| return hash.digest('hex') | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use proper cryptographic hashing for passwords.
SHA-256 is not suitable for password hashing as it's too fast and vulnerable to brute force attacks. Use a proper password hashing algorithm like bcrypt, scrypt, or Argon2.
-// 加密密码
-export async function hashPassword(password: string): Promise<string> {
- const hash = createHash('sha256')
- hash.update(password)
- return hash.digest('hex')
-}
+// 加密密码
+import bcrypt from 'bcryptjs'
+
+export async function hashPassword(password: string): Promise<string> {
+ const saltRounds = 10
+ return await bcrypt.hash(password, saltRounds)
+}Also update the verifyPassword function:
-// 验证密码
-export async function verifyPassword(plainPassword: string, hashedPassword: string): Promise<boolean> {
- const hash = createHash('sha256')
- hash.update(plainPassword)
- return hash.digest('hex') === hashedPassword
-}
+// 验证密码
+export async function verifyPassword(plainPassword: string, hashedPassword: string): Promise<boolean> {
+ return await bcrypt.compare(plainPassword, hashedPassword)
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function hashPassword(password: string): Promise<string> { | |
| const hash = createHash('sha256') | |
| hash.update(password) | |
| return hash.digest('hex') | |
| } | |
| // 加密密码 | |
| import bcrypt from 'bcryptjs' | |
| export async function hashPassword(password: string): Promise<string> { | |
| const saltRounds = 10 | |
| return await bcrypt.hash(password, saltRounds) | |
| } | |
| // 验证密码 | |
| export async function verifyPassword( | |
| plainPassword: string, | |
| hashedPassword: string | |
| ): Promise<boolean> { | |
| return await bcrypt.compare(plainPassword, hashedPassword) | |
| } |
| // 生成JWT令牌 (简化版实现,实际项目中应使用更安全的JWT库) | ||
| export async function generateToken(userId: string, role: string): Promise<string> { | ||
| const payload = { | ||
| id: userId, | ||
| role, | ||
| exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, // 7天 | ||
| } | ||
|
|
||
| // 这里使用简化的JWT实现,实际项目中应使用如jsonwebtoken库 | ||
| const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url') | ||
| const signature = createHash('sha256') | ||
| .update(encodedPayload + JWT_SECRET) | ||
| .digest('base64url') | ||
|
|
||
| return `${encodedPayload}.${signature}` | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace custom JWT implementation with a proper JWT library.
The custom JWT implementation is insecure and doesn't follow JWT standards. It lacks proper header, algorithm specification, and secure signature generation.
Install a proper JWT library like jsonwebtoken or jose:
-// 生成JWT令牌 (简化版实现,实际项目中应使用更安全的JWT库)
-export async function generateToken(userId: string, role: string): Promise<string> {
- const payload = {
- id: userId,
- role,
- exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, // 7天
- }
-
- // 这里使用简化的JWT实现,实际项目中应使用如jsonwebtoken库
- const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')
- const signature = createHash('sha256')
- .update(encodedPayload + JWT_SECRET)
- .digest('base64url')
-
- return `${encodedPayload}.${signature}`
-}
+// 生成JWT令牌
+import jwt from 'jsonwebtoken'
+
+export async function generateToken(userId: string, role: string): Promise<string> {
+ const payload = {
+ id: userId,
+ role,
+ }
+
+ return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' })
+}🤖 Prompt for AI Agents
In server/utils/auth.ts around lines 25 to 40, replace the insecure custom JWT
construction with a standard library: install and import a maintained JWT
package (e.g., jsonwebtoken or jose), then use its sign function to produce
tokens (pass a proper header/algorithm via options or let the library set it),
supply the payload { id: userId, role } and set expiration with the library's
expiresIn option, and sign using the JWT_SECRET environment variable; remove the
manual base64 and hash steps and ensure you handle errors/await correctly if
using an async signer.
| // 验证JWT令牌 | ||
| export async function verifyToken(token: string): Promise<{ id: string; role: string }> { | ||
| try { | ||
| const [encodedPayload, signature] = token.split('.') | ||
| const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString()) | ||
|
|
||
| // 验证签名 | ||
| const expectedSignature = createHash('sha256') | ||
| .update(encodedPayload + JWT_SECRET) | ||
| .digest('base64url') | ||
|
|
||
| if (signature !== expectedSignature || payload.exp < Date.now() / 1000) { | ||
| throw new Error('Invalid token') | ||
| } | ||
|
|
||
| return { id: payload.id, role: payload.role } | ||
| } catch (error) { | ||
| throw createError({ | ||
| status: 401, | ||
| statusText: 'Invalid or expired token', | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace custom JWT verification with proper JWT library.
The custom JWT verification is insecure and should be replaced with a proper JWT library for security.
-// 验证JWT令牌
-export async function verifyToken(token: string): Promise<{ id: string; role: string }> {
- try {
- const [encodedPayload, signature] = token.split('.')
- const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString())
-
- // 验证签名
- const expectedSignature = createHash('sha256')
- .update(encodedPayload + JWT_SECRET)
- .digest('base64url')
-
- if (signature !== expectedSignature || payload.exp < Date.now() / 1000) {
- throw new Error('Invalid token')
- }
-
- return { id: payload.id, role: payload.role }
- } catch (error) {
- throw createError({
- status: 401,
- statusText: 'Invalid or expired token',
- })
- }
-}
+// 验证JWT令牌
+import jwt from 'jsonwebtoken'
+
+export async function verifyToken(token: string): Promise<{ id: string; role: string }> {
+ try {
+ const payload = jwt.verify(token, JWT_SECRET) as { id: string; role: string }
+ return { id: payload.id, role: payload.role }
+ } catch (error) {
+ throw createError({
+ status: 401,
+ statusText: 'Invalid or expired token',
+ })
+ }
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 验证JWT令牌 | |
| export async function verifyToken(token: string): Promise<{ id: string; role: string }> { | |
| try { | |
| const [encodedPayload, signature] = token.split('.') | |
| const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString()) | |
| // 验证签名 | |
| const expectedSignature = createHash('sha256') | |
| .update(encodedPayload + JWT_SECRET) | |
| .digest('base64url') | |
| if (signature !== expectedSignature || payload.exp < Date.now() / 1000) { | |
| throw new Error('Invalid token') | |
| } | |
| return { id: payload.id, role: payload.role } | |
| } catch (error) { | |
| throw createError({ | |
| status: 401, | |
| statusText: 'Invalid or expired token', | |
| }) | |
| } | |
| } | |
| // 验证JWT令牌 | |
| import jwt from 'jsonwebtoken' | |
| export async function verifyToken(token: string): Promise<{ id: string; role: string }> { | |
| try { | |
| const payload = jwt.verify(token, JWT_SECRET) as { id: string; role: string } | |
| return { id: payload.id, role: payload.role } | |
| } catch (error) { | |
| throw createError({ | |
| status: 401, | |
| statusText: 'Invalid or expired token', | |
| }) | |
| } | |
| } |
🤖 Prompt for AI Agents
In server/utils/auth.ts around lines 42 to 64, the current custom JWT parsing
and signature check is insecure; replace it with a standard JWT library (e.g.,
jsonwebtoken or jose) to perform token verification and expiration checking.
Update verifyToken to call the library's verify method with the JWT_SECRET (from
env) and proper algorithms, catch verification errors and convert them to the
same 401 createError response, and return the decoded payload's id and role
after successful verification; ensure you remove the manual base64 and hash
logic and rely on the library's secure verification flow.
| const token = getCookie(event, 'auth_token') || | ||
| getHeader(event, 'Authorization')?.replace(/^Bearer\s+/, '') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add missing import for getHeader.
The getHeader function is used but not imported.
-import { createError, getCookie, setCookie } from 'h3'
+import { createError, getCookie, setCookie, getHeader } from 'h3'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const token = getCookie(event, 'auth_token') || | |
| getHeader(event, 'Authorization')?.replace(/^Bearer\s+/, '') | |
| import { createError, getCookie, setCookie, getHeader } from 'h3' |
🤖 Prompt for AI Agents
In server/utils/auth.ts around lines 68-69, the code uses getHeader(event,
'Authorization') but getHeader is not imported; add getHeader to the existing h3
import (or the appropriate module used for getCookie) at the top of the file so
getHeader is available, then run a quick typecheck to ensure no unused-import or
naming conflicts.
Summary by CodeRabbit
New Features
Changes
Bug Fixes