Skip to content

Conversation

@leoaiagent
Copy link

@leoaiagent leoaiagent commented Aug 18, 2025

Summary by CodeRabbit

  • New Features

    • Email/password login with localized error messages.
    • Registration page and flow with auto-login and success toasts.
    • Server-driven logout with success toast and redirect.
    • New admin pages for managing users and links (initial/basic versions).
  • Changes

    • Dashboard access now gated by logged-in user; unauthorized users are redirected to login.
    • Links list defaults to “newest” and pulls only your links; pagination and totals updated.
    • Analytics counters and realtime chart always render with safe numeric defaults.
    • Unauthorized API responses trigger redirect to the login page.
  • Bug Fixes

    • More robust handling of missing/invalid stats to prevent blank or dimmed displays.

@coderabbitai
Copy link

coderabbitai bot commented Aug 18, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
Frontend Auth Flow
app/components/login/index.vue, app/components/register/index.vue, app/pages/dashboard/register.vue, app/components/dashboard/Logout.vue, app/middleware/auth.global.ts
Implement email/password login, registration with auto-login, and server-driven logout; store user/token locally; client middleware now gates /dashboard via SinkUser; adds register page layout.
Links UI Updates
app/components/dashboard/links/Editor.vue, app/components/dashboard/links/Index.vue
Editor switches to explicit Zod schema with config-based slug regex/default; Index uses /api/link/me with new response shape, default sort 'newest', updated pagination/completion logic.
Analytics Components
app/components/dashboard/analysis/Counters.vue, app/components/dashboard/realtime/Chart.vue
Normalize numeric metrics with defaults; remove conditional blur/opacity and always render headers/values.
UI Utils Import Path
app/components/spark-ui/*.vue, app/components/ui/drawer/*.vue
Update cn import from '@/lib/utils' to '@/utils'; no behavioral changes.
API Utility
app/utils/api.ts
Cache token from localStorage, add logging; on 401, clear token and redirect to /dashboard/login; maintain toast/error propagation.
Schemas
schemas/link.ts, schemas/user.ts
LinkSchema: add userId, use non-secure nanoid, export Link type; add comprehensive User/Register/Login schemas and types.
Server Auth Endpoints
server/api/auth/login.post.ts, server/api/auth/register.post.ts, server/api/auth/logout.post.ts, server/api/verify.ts, server/utils/auth.ts, server/middleware/2.auth.ts
Add JWT auth helpers; implement login/register/logout routes with cookies/JWT; verify endpoint now authenticates and returns user; middleware verifies JWT or site token, sets context.user.
Link Endpoints (User-bound)
server/api/link/create.post.ts, server/api/link/upsert.post.ts, server/api/link/edit.put.ts, server/api/link/delete.post.ts, server/api/link/me.get.ts, server/api/link/search.get.ts
Inject authenticated userId into validation; persist userId in KV metadata; enforce ownership on delete; add per-user list endpoint; minor import fix in search.
Logs/Stats Access Control
server/api/logs/events.ts, server/api/logs/locations.ts, server/api/stats/counters.get.ts, server/api/stats/metrics.get.ts, server/api/stats/views.get.ts
Make query builders async; add admin checks; for non-admins, restrict queries to owned link IDs via KV-derived filters; ensure non-empty default responses.
Admin Pages & APIs
app/pages/dashboard/admin/links.vue, app/pages/dashboard/admin/users.vue, server/api/admin/links.get.ts, server/api/admin/users.get.ts
Add admin UI for links/users with placeholder delete/filter logic; add admin-only API routes returning empty datasets with access checks.
Middleware Redirect
server/middleware/1.redirect.ts
Read redirectStatusCode once from runtime config and reuse; behavior otherwise unchanged.
Wrangler Config
wrangler.jsonc
Update compatibility_date, KV namespace id, and enable observability logs.

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
Loading
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 }
Loading
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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

A hop, a skip, I sign you in,
With carrots, keys, and JWT spin.
New links now know their bunny friend,
Stats obey the paths we send.
Admin burrows, tidy and bright,
KV crumbs gleam in the night.
Thump! Logged out—sweet, secure delight. 🥕✨

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 Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@leoaiagent leoaiagent marked this pull request as draft August 18, 2025 04:33
Copy link

@coderabbitai coderabbitai bot left a 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’s metadata doesn’t include the key’s expiration. You should preserve the original key’s expiration from the list entry (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 raw readBody.

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 editing

The 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 exist

The 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.optional which 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 defaults

Reading redirectStatusCode once is solid. Minor improvement: coerce and validate numeric config (e.g., redirectStatusCode, possibly linkCacheTtl) up front to avoid relying on unary + at call sites and to prevent accidental NaN if 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)
  : 302

And then use safeRedirectStatus where you redirect (see next comment).


40-40: Guard redirect status code with a 3xx allowlist and fallback to 302

The only definition of redirectStatusCode is in nuxt.config.ts (default '301'), so +redirectStatusCode yields 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) – default redirectStatusCode: '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 safeRedirectStatus earlier, 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 shortTime is 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 :class

If 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 !keys check after KV.list

KV.list always returns an array in keys. The !keys guard is redundant and can be dropped; list_complete is sufficient to break the loop.

-      if (!keys || list_complete) {
+      if (list_complete) {
         break
       }

15-23: Defensive loop guard for KV.list cursor

If Cloudflare KV ever returns a non-advancing cursor (rare), the loop could spin. Guarding with if (!cursor) break after setting finalCursor = cursor adds 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 API

Most 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 token

Sending 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 intentional

The 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 PII

Great 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 messages

Email + min(8) password is fine. If desired, provide localized zod error messages for better UX.


31-42: Type the API response and set autocomplete hints

Optionally 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, add autocomplete: 'current-password'; for email add autocomplete: 'email'.


73-78: Use NuxtLink instead of anchor to avoid full page reload

Preserve 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.

getCookie is imported but never used.

-import { getCookie, setCookie } from 'h3'
+import { setCookie } from 'h3'

9-17: Add lightweight rate limiting to logout endpoint

It’s recommended to introduce minimal per-IP rate limiting on POST /auth/logout to 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 Lazy prefix.

-    <Register />
+    <LazyRegister />
server/api/auth/login.post.ts (1)

48-55: Optional: Add Zod validation for KV-loaded user data

For defense-in-depth, validate the shape of userData before using its fields. You can reuse the existing UserSchema from schemas/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 using immediate: true on the watcher.

You can remove onMounted by triggering the watcher immediately.

 watch([time, filters], getRealtimeStats, {
-  deep: true,
+  deep: true,
+  immediate: true,
 })
 
-onMounted(async () => {
-  getRealtimeStats()
-})
+// onMounted no longer needed due to immediate watcher
server/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 userId

As 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 contexts

Accessing 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 presence

Relying 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 exist

Prevents 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 import

AlertCircle isn’t used. Removing it trims bundle size.

Apply this diff:

-import { AlertCircle } from 'lucide-vue-next'

10-16: Name the component explicitly

PascalCase 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

previewMode isn’t used.

Apply this diff:

-const { previewMode } = useRuntimeConfig().public

41-41: Type the form parameter from the Zod schema

Leverage 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.message may be undefined (H3 errors often carry data.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 reload

Prefer 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 drift

If schemas/user.ts defines a RegisterSchema, 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 storage

Client-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 comment

The Chinese comment // 获取当前登录用户 should be in English for consistency with the rest of the codebase.

-  // 获取当前登录用户
+  // Get the currently authenticated user
server/api/stats/metrics.get.ts (3)

18-22: Simplify admin check logic

The 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 comments

The 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 data

Also applies to: 24-24


31-43: Refactor serial KV.get calls to parallel Promise.all

The 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 UX

While 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 comment

The Chinese comment should be in English for consistency.

-  // 获取当前登录用户
+  // Get the currently authenticated user

68-76: Consider using appropriate HTTP status codes for errors

The 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 logic

The 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 getUserFromToken and checkAdminAccess are 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 and function'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 result

Or 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.

eventHandler and createError are 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_IN constant is defined but never used in the code.

-const JWT_EXPIRES_IN = '7d' // 7天有效期

@@ -0,0 +1,115 @@
<script setup>
Copy link

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.

Suggested change
<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'
Copy link

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/utils imports detected.
  • All components (e.g. app/components/ui/drawer/DrawerContent.vue and ~80 others) import { cn } from @/utils, but no file under that alias currently exports cn.
    • Ensure you have a utils entrypoint (e.g. app/utils/index.ts) that defines and exports cn.
    • 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 in tsconfig.json/jsconfig.json (or handled by your framework) so @/utils resolves 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'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Comment on lines +42 to +44
const linkFilter = or(userLinks.map(id => eq('index1', id)))
// 使用安全的方式合并过滤器,不依赖length属性
filter = Array.isArray(filter) && filter.length > 0 ? and(filter, linkFilter) : linkFilter
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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).

Comment on lines 65 to 72
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 }]
})
Copy link

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.

Comment on lines +54 to +56
const linkFilter = or(userLinks.map(id => eq('index1', id)))
// 使用安全的方式合并过滤器,不依赖length属性
filter = Array.isArray(filter) && filter.length > 0 ? and(filter, linkFilter) : linkFilter
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines 76 to 83
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 }]
})
Copy link

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 })
  }
}

Comment on lines +19 to 53
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',
})
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Copy link

@coderabbitai coderabbitai bot left a 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...

Comment on lines +40 to +43
// 如果是H3错误,直接抛出
if (error instanceof Error && 'status' in error && error.status === 401) {
throw error;
}
Copy link

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 -20

Length 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',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +12 to +16
export async function hashPassword(password: string): Promise<string> {
const hash = createHash('sha256')
hash.update(password)
return hash.digest('hex')
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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)
}

Comment on lines +25 to +40
// 生成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}`
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Comment on lines +42 to +64
// 验证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',
})
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
// 验证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.

Comment on lines +68 to +69
const token = getCookie(event, 'auth_token') ||
getHeader(event, 'Authorization')?.replace(/^Bearer\s+/, '')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant