Ekisa02/security-header-scanner

GitHub: Ekisa02/security-header-scanner

Stars: 1 | Forks: 0

# Web App Template (tRPC + Manus Auth + Database) This template gives you a React 19 + Tailwind 4 + Express 4 + tRPC 11 stack with Manus OAuth already wired. Procedures are your contracts, types flow end to end, and authentication "just works". ## Quick Facts - **tRPC-first:** define procedures in `server/routers.ts`, consume them with `trpc.*` hooks. - **Superjson out of the box:** return Drizzle rows directly—`Date` stays a `Date`. - **Auth baked in:** `/api/oauth/callback` handles Manus OAuth, `protectedProcedure` injects `ctx.user`. - **Gateway-ready:** all RPC traffic is under `/api/trpc`, making it easy to route at the edge. ## Build Loop (Four Touch Points) 1. Update schema in `drizzle/schema.ts`, then run `pnpm db:push`. 2. Add database helpers in `server/db.ts` (return raw results). 3. Add or extend procedures in `server/routers.ts`, then wire the UI with `trpc.*.useQuery/useMutation`. 4. Build frontend experience according to `Frontend Workflow` 5. Cover your changes with Vitest specs inside `server/*.test.ts` (see `server/auth.logout.test.ts`) and run `pnpm test`. That's it—no manual REST routes, no Axios client, no shared contract files. ## Key Files server/auth.logout.test.ts → Reference sample vitest test file drizzle/schema.ts → Database tables & types server/db.ts → Query helpers (reuse across procedures) server/routers.ts → tRPC procedures (auth + features) client/src/App.tsx → Routes wiring & layout shells client/src/lib/trpc.ts → tRPC client binding client/src/pages/ → Feature UI that calls trpc hooks Framework plumbing (OAuth, context, Vite bridge) lives under `server/_core`. ## File Structure client/ public/ ← Small configuration files ONLY (favicon.ico, robots.txt). DO NOT put images/media here. src/ pages/ ← Page-level components components/ ← Reusable UI & shadcn/ui contexts/ ← React contexts hooks/ ← Custom hooks lib/trpc.ts ← tRPC client App.tsx ← Routes & layout main.tsx ← Providers index.css ← global style drizzle/ ← Schema & migrations server/ db.ts ← Query helpers routers.ts ← tRPC procedures storage/ ← S3 helpers shared/ ← Shared constants & types Only touch the files under "←" markers. Anything under `server/_core` or other tooling directories is framework-level—avoid editing unless you are extending the infrastructure. ### ⚠️ Handling Images & Media **DO NOT** store images, videos, or large assets in `client/public/` or `client/src/assets/`. Local media files will cause deployment timeouts. **Required workflow:** 1. Upload assets using the CLI: `manus-upload-file --webdev path/to/image.png` 2. Use the returned storage path directly in your code: `` 3. Store the original local file in `/home/ubuntu/webdev-static-assets/` (outside the project directory) Only small configuration files like `favicon.ico`, `robots.txt`, and `manifest.json` belong in `client/public/`. Files in `client/public` are available at the root of your site—reference them with absolute paths (`/robots.txt`, etc.) from HTML templates, JSX, or meta tags. ## Authentication Flow - Manus OAuth completes at `/api/oauth/callback` and drops a session cookie. - Each request to `/api/trpc` builds context via `server/_core/context.ts`, making the current user available as `ctx.user`. - Wrap protected logic in `protectedProcedure`; public access uses `publicProcedure`. - Frontend reads auth state with `trpc.auth.me.useQuery()` and invokes `trpc.auth.logout.useMutation()`—no cookie plumbing required. ## Environment Variables Available pre-defined system envs: - `DATABASE_URL`: MySQL/TiDB connection string - `JWT_SECRET`: Session cookie signing secret - `VITE_APP_ID`: Manus OAuth application ID - `OAUTH_SERVER_URL`: Manus OAuth backend base URL - `VITE_OAUTH_PORTAL_URL`: Manus login portal URL (frontend) - `OWNER_OPEN_ID`, `OWNER_NAME`: Owner's info - `BUILT_IN_FORGE_API_URL`: Manus built-in apis (includes llm, storage, data_api, notification, etc...) - `BUILT_IN_FORGE_API_KEY`: Bearer token used by Manus built-in apis (server-side) - `VITE_FRONTEND_FORGE_API_KEY`: Bearer token for frontend access to Manus built-in apis - `VITE_FRONTEND_FORGE_API_URL`: Manus built-in apis URL for frontend Do not edit these directly in code or commit `.env` files. The envs above are system envs, when use env in website code, refer `server/_core/env.ts` for available list. ## Frontend Workflow 1. Choose a design style before you write any frontend code according to Design Guide (color, font, shadow, art style). Remember to edit `client/src/index.css` for global theming and add needed font using google font cdn in `client/index.html`. 2. Design the layout and navigation structure based on app purpose. Establish navigation in App.tsx accordingly: - **Personal tools & internal dashboards** (finance trackers, task managers, admin panels, personal finance apps, analytics): Use DashboardLayout with sidebar navigation for consistent experience. - **Public-facing products** (marketing sites, e-commerce, communities): Design custom navigation (top nav, contextual nav) and landing page to attract users. 3. Start by updating `client/src/pages/Home.tsx` (the landing page shell) using shadcn/ui components to introduce links, CTAs, or feature entry points. 4. Create or update additional components under `client/src/pages/FeatureName.tsx`, continuing to leverage shadcn/ui + Tailwind for consistent styling. 5. Register the route (or navigation entry) in `client/src/App.tsx`. 6. Read data with `const { data, isLoading } = trpc.feature.useQuery(params);`. 7. Mutate data with `trpc.feature.useMutation()`. Use optimistic updates for list operations, toggles, and profile edits. For critical operations (payments, auth), use `invalidate` with loading states. 8. Use `useAuth()` for current user state, login URL from `getLoginUrl()`, and avoid direct cookie handling. 9. Handle loading/empty/error states in the UI—tRPC already surfaces typed responses and errors. ## Frontend Development Guidelines **tRPC & Data Management:** - Use `trpc.*.useQuery/useMutation` for all backend calls—never introduce Axios/fetch wrappers. - **Use optimistic updates for instant feedback**: ideal for adding/editing/deleting list items, toggling states, updating profiles. Use `onMutate` to update cache, `onError` to rollback (The onMutate/onError/onSettled pattern). For critical operations (payments, auth), prefer `invalidate` with explicit loading states. - When using `invalidate` as fallback: call `trpc.useUtils().feature.invalidate()` in mutation's `onSuccess`. - Auth state comes from `useAuth()`; do not manipulate cookies manually. **UI & Styling:** - Prefer shadcn/ui components for interactions to keep a modern, consistent look; import from `@/components/ui/*` (e.g., `button`, `card`, `dialog`). - Compose Tailwind utilities with component variants for layout and states; avoid excessive custom CSS. Use built-in `variant`, `size`, etc. where available. - Preserve design tokens: keep the `@layer base` rules in `client/src/index.css`. Utilities like `border-border` and `font-sans` depend on them. - Consistent design language: use spacing, radius, shadows, and typography via tokens. Extract shared UI into `components/` for reuse instead of copy‑paste. - Accessibility and responsiveness: keep visible focus rings and ensure keyboard reachability; design mobile‑first with thoughtful breakpoints. - Theming: Choose dark/light theme to start with for ThemeProvider according to your design style (dark or light bg), then manage colors pallette with CSS variables in `client/src/index.css` instead of hard‑coding to keep global consistency. - Micro‑interactions and empty states: add motion, empty states, and icons tastefully to improve quality without distracting from content. - Navigation: For internal tools/admin panels, use persistent sidebar. For public-facing apps, design navigation based on content structure (top nav, side nav, or contextual)—ensure clear escape routes from all pages. - Placeholder UI elements: When adding structural placeholders (nav items, table actions) for not-yet-implemented features, show toast on click ("Feature coming soon"). Inform user which elements are placeholders when presenting work. **React Best Practices:** - Never call setState/navigation in render phase → wrap in `useEffect` **Customized Defaults:** This template customizes some Tailwind/shadcn defaults for simplified usage: - `.container` is customized to auto-center and add responsive padding (see `index.css`). Use directly without `mx-auto`/`px-*`. For custom widths, use `max-w-*` with `mx-auto px-4`. - `.flex` is customized to have `min-width:0` and `min-height:0` by default - `button` variant `outline` uses transparent background (not `bg-background`). Add bg color class manually if needed. ## 🎨 Design Guide When generating frontend UI, avoid generic patterns that lack visual distinction: - Avoid generic full-page centered layouts—prefer asymmetric/sidebar/grid structures for landing pages and dashboards - Avoid applying dashboard/sidebar patterns to public-facing apps (forums, communities, e-commerce)—reserve those for internal tools - When user provides vague requirements, make creative design decisions (choose specific color palette, typography, layout approach) - Prioritize visual diversity: combine different design systems (e.g., one color scheme + different typography + another layout principle) - For landing pages: prefer asymmetric layouts, specific color values (not just "blue"), and textured backgrounds over flat colors - For dashboards: use defined spacing systems, soft shadows over borders, and accent colors for hierarchy ## Animation Guide Bake motion taste in from the first line of code. Snappy, physically intuitive interactions are not a polish pass — they are part of the initial build. - Decide whether to animate at all: keyboard-initiated actions (command palettes, shortcuts) must be instant — never animate them. High-frequency interactions (hover, list nav) should be minimal. Reserve richer motion for occasional events (modals, drawers, toasts) and rare delight moments (onboarding). - Keep UI animations under 300ms. A 180ms dropdown feels significantly better than a 400ms one. Typical ranges: button press 100–160ms, tooltips 125–200ms, dropdowns 150–250ms, modals/drawers 200–500ms. - Use strong custom easings, not the weak CSS defaults. Default to a snappy ease-out for entering/exiting UI: `--ease-out: cubic-bezier(0.23, 1, 0.32, 1);`. For moving/morphing use `--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);`. NEVER use `ease-in` for UI animations — it feels sluggish. - Buttons must feel responsive: add `transform: scale(0.97)` on `:active` with a ~160ms ease-out transition so the UI confirms it heard the user. - Never animate from `scale(0)` — nothing in the real world appears from nothing. Start from `scale(0.95)` combined with `opacity: 0`. - Origin-aware popovers/dropdowns: scale in from the trigger point (e.g. `transform-origin: var(--radix-popover-content-transform-origin)`). Modals are the exception and stay centered. - Prefer CSS transitions over @keyframes for dynamic UI state. Transitions can be interrupted and reversed smoothly mid-flight; keyframes restart from zero and feel broken when interrupted. - Only animate `transform` and `opacity` for motion — they run on the GPU and skip layout/paint. Avoid animating `width`, `height`, `padding`, `margin`, `top/left` unless absolutely necessary. - Stagger grouped entrances by 30–80ms per item to create a cascading reveal instead of a wall of motion. - Asymmetric timing for deliberate actions: hold-to-confirm should be slow and linear on press (e.g. 2s linear), but release/cancel should snap back fast (~200ms ease-out). - Respect `prefers-reduced-motion`: gate non-essential motion behind `@media (prefers-reduced-motion: no-preference)`. ## Feature Checklist - [ ] Tables updated in `drizzle/schema.ts`, migrations pushed (`pnpm db:push`) - [ ] Query helper added in `server/db.ts` (returns raw Drizzle rows) - [ ] Procedure created in `server/routers.ts` (choose `public` vs `protected`) - [ ] UI calls the procedure via `trpc.*.useQuery/useMutation` - [ ] Success + error paths verified in the browser ## Pre-built Components Before implementing UI features, check if these components already exist: Dashboard & Layout: - `client/src/components/DashboardLayout.tsx` - Full dashboard layout with sidebar navigation, auth handling, and user profile. Use this for any admin panel or dashboard-style app instead of building from scratch. - `client/src/components/DashboardLayoutSkeleton.tsx` - Loading skeleton for dashboard during auth checks Chat & Messaging: Maps: - `client/src/components/Map.tsx` - Google Maps integration with proxy authentication. Provides MapView component with onMapReady callback for initializing Google Maps services (Places, Geocoder, Directions, Drawing, etc.). All map functionality works directly in the browser. When implementing features that match these categories, MUST evaluate the component first to decide whether to use or customize it. ## Internal Tools & Admin Panels For certain app types, this template provides DashboardLayout—a standardized sidebar pattern. **Use DashboardLayout for:** - Admin/management dashboards - Personal productivity apps (task managers, note-taking) - Analytics/monitoring tools **Do NOT use for:** - Public content platforms (forums, blogs, social networks) - E-commerce storefronts - Marketing/landing sites **Layout & Navigation** - Use `DashboardLayout` component from `client/src/components/DashboardLayout.tsx` and remove any page-level headers to avoid duplication. - When use DashboardLayout, read its content before making changes and preserve its core structure by default. **Role-based Access Control** When building apps with distinct access levels (e.g., e-commerce with public home, user account, admin panel): - The `user` table includes a `role` field (enum: `admin` | `user`) for identity separation - Use `ctx.user.role` in procedures to gate admin-only operations - Wrap admin-only backend logic in `adminProcedure` - Frontend can conditionally render navigation/routes based on `useAuth().user?.role` Example procedure pattern: adminOnlyProcedure: protectedProcedure.use(({ ctx, next }) => { if (ctx.user.role !== 'admin') throw new TRPCError({ code: 'FORBIDDEN' }); return next({ ctx }); }), **Managing Admins** - To promote a user to admin, update the `role` field directly in the database via the system UI or SQL - If you need additional roles beyond `admin`/`user`, extend the enum in `drizzle/schema.ts` and push the migration ## LLM Integration Use the preconfigured LLM helpers. Credentials are injected from the platform (no manual setup required). import { invokeLLM } from "./server/_core/llm"; /** * Simple chat completion * type Role = "system" | "user" | "assistant" | "tool" | "function"; * type TextContent = { * type: "text"; * text: string; * }; * * type ImageContent = { * type: "image_url"; * image_url: { * url: string; * detail?: "auto" | "low" | "high"; * }; * }; * * type FileContent = { * type: "file_url"; * file_url: { * url: string; * mime_type?: "audio/mpeg" | "audio/wav" | "application/pdf" | "audio/mp4" | "video/mp4" ; * }; * }; * * export type Message = { * role: Role; * content: string | Array * }; * * Supported parameters: * messages: Array<{ * role: 'system' | 'user' | 'assistant' | 'tool', * content: string | { tool_call: { name: string, arguments: string } } * }> * tool_choice?: 'none' | 'auto' | 'required' | { type: 'function', function: { name: string } } * tools?: Tool[] */ const response = await invokeLLM({ messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: "Hello, world!" }, ], }); Tips - Always call llm functions from server-side code (e.g., inside tRPC procedures), to avoid exposing your API key. - You don't need to manually set the model; the helper uses a sensible default. - LLM responses often contain markdown. Use `{content}` (imported from `streamdown`) to render markdown content with proper formatting and streaming support. ### Structured Responses (JSON Schema) Ask the model to return structured JSON via `response_format`: import { invokeLLM } from "./server/_core/llm"; const structured = await invokeLLM({ messages: [ { role: "system", content: "You are a helpful assistant designed to output JSON." }, { role: "user", content: "Extract the name and age from the following text: \"My name is Alice and I am 30 years old.\"" }, ], response_format: { type: "json_schema", json_schema: { name: "person_info", strict: true, schema: { type: "object", properties: { name: { type: "string", description: "The name of the person" }, age: { type: "integer", description: "The age of the person" }, }, required: ["name", "age"], additionalProperties: false, }, }, }, }); // The model responds with JSON content matching the schema. // Access via `structured.choices[0].message.content` and JSON.parse if needed. The helpers mirror the Python SDK semantics but produce JavaScript-first code, keeping credentials inside the server and ensuring every environment has access to the same token. ## Voice Transcription Integration Use the preconfigured voice transcription helper that converts speech to text using Whisper API, no manual setup required. Example usage: import { transcribeAudio } from "./server/_core/voiceTranscription"; const result = await transcribeAudio({ audioUrl: "https://storage.example.com/audio/recording.mp3", language: "en", // Optional: helps improve accuracy prompt: "Transcribe meeting notes" // Optional: context hint }); // Returns native Whisper API response // result.text - Full transcription // result.language - Detected language (ISO-639-1) // result.segments - Timestamped segments with metadata Tips - Accepts URL to pre-uploaded audio file - 16MB file size limit enforced during transcription, size flag to be set by frontend - Supported formats: webm, mp3, wav, ogg, m4a - Returns native Whisper API response with rich metadata - Frontend should handle audio capture, storage upload, and size validation ## Image Generation Integration Use the preconfigured image generation helper that connects to the internal ImageService, no manual setup required. Example usage: import { generateImage } from "./server/_core/imageGeneration.ts"; const { url: imageUrl } = await generateImage({ prompt: "A serene landscape with mountains" }); // For editing: const { url: imageUrl } = await generateImage({ prompt: "Add a rainbow to this landscape", originalImages: [{ url: "https://example.com/original.jpg", mimeType: "image/jpeg" }] }); Tips - Always call from server-side code (e.g., inside tRPC procedures) to avoid exposing API keys - Image generation can take 5-20 seconds, implement proper loading states - Implement proper error handling as image generation can fail ## ☁️ File Storage Use the preconfigured storage helpers in `server/storage.ts`. Credentials are injected from the platform (no manual setup required). Files are stored securely and served via the built-in `/manus-storage/` path — no manual URL management needed. import { storagePut } from "./server/storage"; // Upload bytes to storage const fileKey = `${userId}-files/${fileName}.png` const { key, url } = await storagePut( fileKey, fileBuffer, // Buffer | Uint8Array | string "image/png" ); // url = "/manus-storage/{key}" — use directly in frontend code // key = unique storage key — save in database Tips - Save the `key` or `url` in your database; use storage for the actual file bytes. This applies to all files including images, documents, and media. - For file uploads, have the client POST to your server, then call `storagePut` from your backend. - The returned `url` (e.g. `/manus-storage/...`) is automatically served via signed redirect — no manual URL signing needed. - To delete a file, drop its `key` from your DB and any UI references — the key is the only way to reach the object, so an unreferenced file is effectively gone. Do not implement a helper to remove the underlying object; the template's storage layer does not expose a delete endpoint. ## 🗺️ Maps Integration **CRITICAL: The Manus proxy provides FULL access to ALL Google Maps features** - including advanced drawing, heatmaps, Street View, all layers, Places API, etc. Do ask users for Google Map API keys - authentication is automatic. **Default: Use Frontend SDK** - Import MapView from `client/src/components/Map.tsx` and initialize ANY Google Maps service (geocoding, directions, places, drawing, visualization, geometry, etc.) in the onMapReady callback. **Use Backend API only when:** - Persisting data (save routes/locations to database) - Bulk operations (1000+ addresses) - Server-side needs (caching, scheduled jobs, hiding business logic) **Implementation:** - Frontend: See `client/src/components/Map.tsx` for component usage - ALL Google Maps JavaScript API features work - Backend: Create tRPC procedures using `makeRequest` from `server/_core/map.ts` NEVER use external map libraries or request API keys from users - the Manus proxy handles everything automatically with no feature limitations. ## ☁️ Data API When you need external data, use the omni_search with search_type = 'api' to see there's any built-in api available in Manus API Hub access. You only have to connect other api if there's no suitable built-in api available. ## Owner Notifications This template already ships with a `notifyOwner({ title, content })` helper (`server/_core/notification.ts`) and a protected tRPC mutation at `trpc.system.notifyOwner`. Use it whenever backend logic needs to push an operational update to the Manus project owner—common triggers are new form submissions, survey feedback, or workflow results. 1. On the server, call `await notifyOwner({ title, content })` or reuse the provided `system.notifyOwner` mutation from jobs/webhooks (`trpc.system.notifyOwner.useMutation()` on the client). 2. Handle the boolean return (`true` on success, `false` if the upstream service is temporarily unavailable) to decide whether you need a fallback channel. Keep this channel for owner-facing alerts; end-user messaging should flow through your app-specific systems. ## ⏱ Datetime & Timezone Persistence: Store all business timestamps as UTC-based Unix timestamps (milliseconds since epoch) at the database and API layer. Do not store client-local, timezone-dependent, or string-based timestamps unless explicitly required as separate fields. Frontend display: In React components, always convert UTC timestamps to the user’s local timezone for display e.g. new Date(utcTimestamp).toLocaleString(). Keep all internal state and API interactions in UTC timestamps to avoid drift and confusion. ## Tips - Keep router files under ~150 lines—split into `server/routers/.ts` once they grow. - Show loading states at component level (spinners, skeletons) rather than blocking entire pages—keeps the app feeling responsive. ## Core File References Note: All TODO comments are remarks for the agent (you), not for the user. `package.json` { "name": "{{project_name}}", "version": "1.0.0", "type": "module", "license": "MIT", "scripts": { "dev": "NODE_ENV=development tsx watch server/_core/index.ts", "build": "vite build && esbuild server/_core/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist", "start": "NODE_ENV=production node dist/index.js", "check": "tsc --noEmit", "format": "prettier --write .", "test": "vitest run", "db:push": "drizzle-kit generate && drizzle-kit migrate" }, "dependencies": { "@aws-sdk/client-s3": "^3.693.0", "@aws-sdk/s3-request-presigner": "^3.693.0", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-aspect-ratio": "^1.1.7", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-menubar": "^1.1.16", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "@trpc/client": "^11.6.0", "@trpc/react-query": "^11.6.0", "@trpc/server": "^11.6.0", "axios": "^1.12.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "cookie": "^1.0.2", "date-fns": "^4.1.0", "dotenv": "^17.2.2", "drizzle-orm": "^0.44.5", "embla-carousel-react": "^8.6.0", "express": "^4.21.2", "framer-motion": "^12.23.22", "input-otp": "^1.4.2", "jose": "6.1.0", "lucide-react": "^0.453.0", "mysql2": "^3.15.0", "nanoid": "^5.1.5", "next-themes": "^0.4.6", "react": "^19.2.1", "react-day-picker": "^9.11.1", "react-dom": "^19.2.1", "react-hook-form": "^7.64.0", "react-resizable-panels": "^3.0.6", "recharts": "^2.15.2", "sonner": "^2.0.7", "streamdown": "^1.4.0", "superjson": "^1.13.3", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", "vaul": "^1.1.2", "wouter": "^3.3.5", "zod": "^4.1.12" }, "devDependencies": { "@builder.io/vite-plugin-jsx-loc": "^0.1.1", "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.1.3", "@types/express": "4.17.21", "@types/google.maps": "^3.58.1", "@types/node": "^24.7.0", "@types/react": "^19.2.1", "@types/react-dom": "^19.2.1", "@vitejs/plugin-react": "^5.0.4", "add": "^2.0.6", "autoprefixer": "^10.4.20", "drizzle-kit": "^0.31.4", "esbuild": "^0.25.0", "pnpm": "^10.15.1", "postcss": "^8.4.47", "prettier": "^3.6.2", "tailwindcss": "^4.1.14", "tsx": "^4.19.1", "tw-animate-css": "^1.4.0", "typescript": "5.9.3", "vite": "^7.1.7", "vite-plugin-manus-runtime": "^0.0.57", "vitest": "^2.1.4" }, "packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af", "pnpm": { "patchedDependencies": { "wouter@3.7.1": "patches/wouter@3.7.1.patch" }, "overrides": { "tailwindcss>nanoid": "3.3.7" } } } `drizzle/schema.ts` import { int, mysqlEnum, mysqlTable, text, timestamp, varchar } from "drizzle-orm/mysql-core"; /** * Core user table backing auth flow. * Extend this file with additional tables as your product grows. * Columns use camelCase to match both database fields and generated types. */ export const users = mysqlTable("users", { /** * Surrogate primary key. Auto-incremented numeric value managed by the database. * Use this for relations between tables. */ id: int("id").autoincrement().primaryKey(), /** Manus OAuth identifier (openId) returned from the OAuth callback. Unique per user. */ openId: varchar("openId", { length: 64 }).notNull().unique(), name: text("name"), email: varchar("email", { length: 320 }), loginMethod: varchar("loginMethod", { length: 64 }), role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(), createdAt: timestamp("createdAt").defaultNow().notNull(), updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(), }); export type User = typeof users.$inferSelect; export type InsertUser = typeof users.$inferInsert; // TODO: Add your tables here `server/db.ts` import { eq } from "drizzle-orm"; import { drizzle } from "drizzle-orm/mysql2"; import { InsertUser, users } from "../drizzle/schema"; import { ENV } from './_core/env'; let _db: ReturnType | null = null; // Lazily create the drizzle instance so local tooling can run without a DB. export async function getDb() { if (!_db && process.env.DATABASE_URL) { try { _db = drizzle(process.env.DATABASE_URL); } catch (error) { console.warn("[Database] Failed to connect:", error); _db = null; } } return _db; } export async function upsertUser(user: InsertUser): Promise { if (!user.openId) { throw new Error("User openId is required for upsert"); } const db = await getDb(); if (!db) { console.warn("[Database] Cannot upsert user: database not available"); return; } try { const values: InsertUser = { openId: user.openId, }; const updateSet: Record = {}; const textFields = ["name", "email", "loginMethod"] as const; type TextField = (typeof textFields)[number]; const assignNullable = (field: TextField) => { const value = user[field]; if (value === undefined) return; const normalized = value ?? null; values[field] = normalized; updateSet[field] = normalized; }; textFields.forEach(assignNullable); if (user.lastSignedIn !== undefined) { values.lastSignedIn = user.lastSignedIn; updateSet.lastSignedIn = user.lastSignedIn; } if (user.role !== undefined) { values.role = user.role; updateSet.role = user.role; } else if (user.openId === ENV.ownerOpenId) { values.role = 'admin'; updateSet.role = 'admin'; } if (!values.lastSignedIn) { values.lastSignedIn = new Date(); } if (Object.keys(updateSet).length === 0) { updateSet.lastSignedIn = new Date(); } await db.insert(users).values(values).onDuplicateKeyUpdate({ set: updateSet, }); } catch (error) { console.error("[Database] Failed to upsert user:", error); throw error; } } export async function getUserByOpenId(openId: string) { const db = await getDb(); if (!db) { console.warn("[Database] Cannot get user: database not available"); return undefined; } const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1); return result.length > 0 ? result[0] : undefined; } // TODO: add feature queries here as your schema grows. `server/routers.ts` import { COOKIE_NAME } from "@shared/const"; import { getSessionCookieOptions } from "./_core/cookies"; import { systemRouter } from "./_core/systemRouter"; import { publicProcedure, router } from "./_core/trpc"; export const appRouter = router({ // if you need to use socket.io, read and register route in server/_core/index.ts, all api should start with '/api/' so that the gateway can route correctly system: systemRouter, auth: router({ me: publicProcedure.query(opts => opts.ctx.user), logout: publicProcedure.mutation(({ ctx }) => { const cookieOptions = getSessionCookieOptions(ctx.req); ctx.res.clearCookie(COOKIE_NAME, { ...cookieOptions, maxAge: -1 }); return { success: true, } as const; }), }), // TODO: add feature routers here, e.g. // todo: router({ // list: protectedProcedure.query(({ ctx }) => // db.getUserTodos(ctx.user.id) // ), // }), }); export type AppRouter = typeof appRouter; `client/src/App.tsx` import { Toaster } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import NotFound from "@/pages/NotFound"; import { Route, Switch } from "wouter"; import ErrorBoundary from "./components/ErrorBoundary"; import { ThemeProvider } from "./contexts/ThemeContext"; import Home from "./pages/Home"; function Router() { // make sure to consider if you need authentication for certain routes return ( {/* Final fallback route */} ); } // NOTE: About Theme // - First choose a default theme according to your design style (dark or light bg), than change color palette in index.css // to keep consistent foreground/background color across components // - If you want to make theme switchable, pass `switchable` ThemeProvider and use `useTheme` hook function App() { return ( ); } export default App; `client/src/lib/trpc.ts` import { createTRPCReact } from "@trpc/react-query"; import type { AppRouter } from "../../../server/routers"; export const trpc = createTRPCReact(); `client/src/pages/Home.tsx` import { useAuth } from "@/_core/hooks/useAuth"; import { Button } from "@/components/ui/button"; import { Loader2 } from "lucide-react"; import { getLoginUrl } from "@/const"; import { Streamdown } from 'streamdown'; /** * All content in this page are only for example, replace with your own feature implementation * When building pages, remember your instructions in Frontend Workflow, Frontend Best Practices, Design Guide and Common Pitfalls */ export default function Home() { // The userAuth hooks provides authentication state // To implement login/logout functionality, simply call logout() or redirect to getLoginUrl() let { user, loading, error, isAuthenticated, logout } = useAuth(); // If theme is switchable in App.tsx, we can implement theme toggling like this: // const { theme, toggleTheme } = useTheme(); return (
{/* Example: lucide-react for icons */} Example Page {/* Example: Streamdown for markdown rendering */} Any **markdown** content
); } `server/auth.logout.test.ts` import { describe, expect, it } from "vitest"; import { appRouter } from "./routers"; import { COOKIE_NAME } from "../shared/const"; import type { TrpcContext } from "./_core/context"; type CookieCall = { name: string; options: Record; }; type AuthenticatedUser = NonNullable; function createAuthContext(): { ctx: TrpcContext; clearedCookies: CookieCall[] } { const clearedCookies: CookieCall[] = []; const user: AuthenticatedUser = { id: 1, openId: "sample-user", email: "sample@example.com", name: "Sample User", loginMethod: "manus", role: "user", createdAt: new Date(), updatedAt: new Date(), lastSignedIn: new Date(), }; const ctx: TrpcContext = { user, req: { protocol: "https", headers: {}, } as TrpcContext["req"], res: { clearCookie: (name: string, options: Record) => { clearedCookies.push({ name, options }); }, } as TrpcContext["res"], }; return { ctx, clearedCookies }; } describe("auth.logout", () => { it("clears the session cookie and reports success", async () => { const { ctx, clearedCookies } = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.auth.logout(); expect(result).toEqual({ success: true }); expect(clearedCookies).toHaveLength(1); expect(clearedCookies[0]?.name).toBe(COOKIE_NAME); expect(clearedCookies[0]?.options).toMatchObject({ maxAge: -1, secure: true, sameSite: "none", httpOnly: true, path: "/", }); }); }); ## Common Pitfalls ### Infinite loading loops from unstable references **Anti-pattern:** Creating new objects/arrays in render that are used as query inputs // ❌ Bad: New Date() creates new reference every render → infinite queries const { data } = trpc.items.getByDate.useQuery({ date: new Date(), // ← New object every render! }); // ❌ Bad: Array/object literals in query input const { data } = trpc.items.getByIds.useQuery({ ids: [1, 2, 3], // ← New array reference every render! }); **Correct approach:** Stabilize references with useState/useMemo // ✅ Good: Initialize once with useState const [date] = useState(() => new Date()); const { data } = trpc.items.getByDate.useQuery({ date }); // ✅ Good: Memoize complex inputs const ids = useMemo(() => [1, 2, 3], []); const { data } = trpc.items.getByIds.useQuery({ ids }); **Why this happens:** TRPC queries trigger when input references change. Objects/arrays created in render have new references each time, causing infinite re-fetches. ### Storing file bytes in database columns **Anti-pattern:** Adding BLOB/BYTEA columns to store file content // ❌ Bad: Database bloat and slow queries export const files = sqliteTable('files', { content: blob('content'), // Never store file bytes }); **Correct approach:** Store S3 reference only, upload file bytes to S3 // ✅ Good: Store metadata + S3 reference export const files = sqliteTable('files', { url: text('url').notNull(), // Url to reference the file in s3 fileKey: text('file_key').notNull(), // also save file_key for clarity // optional, save other metadata if needed // filename: text('filename'), // mimeType: text('mime_type'), }); Use `storagePut()` to upload files (see S3 File Storage section). ### Navigation dead-ends in subpages **Problem:** Creating nested routes without escape routes—no header nav, no sidebar, no back button. **Root cause:** Implementing individual pages before establishing global layout structure. **Solution:** Define layout wrapper in App.tsx first, then build pages inside it. For admin tools use DashboardLayout; for detail pages add back button with `router.back()`. ### Invisible text from theme/color mismatches **Root cause:** Semantic colors (`bg-background`, `text-foreground`) are CSS variables that resolve based on ThemeProvider's active theme. Mismatches cause invisible text. **Two critical rules:** 1. **Match theme to CSS variables:** If `defaultTheme="dark"` in App.tsx, ensure `.dark {}` in index.css has dark background + light foreground values 2. **Always pair bg with text:** When using `bg-{semantic}`, MUST also use `text-{semantic}-foreground` (not automatic - text inherits from parent otherwise) **Quick reference:** // ✅ Theme + CSS alignment {/* Must match .dark in index.css */}
...
// ✅ Required class pairs
...
...
...
### Nested anchor tags in Link components **Problem:** Wrapping `` tags inside another `` or wouter's `` creates nested anchors and runtime errors. **Solution:** Pass children directly to Link—it already renders an `` internally. // ❌ Bad: ... or ... // ✅ Good: ... or just ... ### Empty `Select.Item` values **Rule:** Every `` must have a non-empty `value` prop—never `""`, `undefined`, or omitted. ## Manus OAuth Best Practices **Key Rule:** Always use `window.location.origin` for redirect URLs—never hardcode domains or use `req.host`. Frontend and backend run on separate servers, so the frontend must pass its origin explicitly. **Unsupported browsers:** Safari Private Browsing, Firefox Strict ETP, Brave Aggressive Shields, or any browser blocking cookies. **Anti-patterns:** // ❌ Never construct URLs from env vars or patterns const url = `https://${projectName}.manus.space/callback`; const url = `https://${process.env.APP_SUBDOMAIN}.example.com/verify`; **Correct approach:** This template already implements the pattern correctly: - `client/src/const.ts`: `getLoginUrl(returnPath?)` encodes origin + returnPath in state - `server/_core/oauth.ts`: `parseState()` extracts origin from state for redirects **For invite/magic links:** When backend generates URLs, frontend must pass origin in the request: // Frontend const createInvite = trpc.invites.create.useMutation(); await createInvite.mutateAsync({ eventId: "123", origin: window.location.origin }); // Backend - use input.origin to build the URL const inviteUrl = `${input.origin}/events/${eventId}/join?token=${token}`;
标签:自动化攻击