├── pnpm-workspace.yaml ├── docs └── screens │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ └── 6.png ├── apps ├── web │ ├── public │ │ ├── favicon.ico │ │ └── apple-touch-icon.png │ ├── src │ │ ├── app │ │ │ ├── profile │ │ │ │ ├── items │ │ │ │ │ └── page.js │ │ │ │ ├── shop │ │ │ │ │ └── page.js │ │ │ │ ├── rewards │ │ │ │ │ └── page.js │ │ │ │ ├── wallet │ │ │ │ │ └── page.js │ │ │ │ ├── page.js │ │ │ │ ├── badges │ │ │ │ │ └── page.js │ │ │ │ ├── layout.js │ │ │ │ ├── topics │ │ │ │ │ └── page.js │ │ │ │ └── settings │ │ │ │ │ └── components │ │ │ │ │ └── UsernameChangeDialog.js │ │ │ ├── dashboard │ │ │ │ ├── shop │ │ │ │ │ └── page.js │ │ │ │ ├── badges │ │ │ │ │ └── page.js │ │ │ │ ├── ledger │ │ │ │ │ └── page.js │ │ │ │ ├── settings │ │ │ │ │ └── components │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── SecuritySettings.js │ │ │ │ └── layout.js │ │ │ ├── topic │ │ │ │ └── [id] │ │ │ │ │ ├── loading.js │ │ │ │ │ ├── not-found.js │ │ │ │ │ └── page.js │ │ │ ├── reference │ │ │ │ └── page.js │ │ │ ├── (home) │ │ │ │ ├── layout.js │ │ │ │ ├── trending │ │ │ │ │ └── page.js │ │ │ │ ├── featured │ │ │ │ │ └── page.js │ │ │ │ ├── page.js │ │ │ │ └── categories │ │ │ │ │ └── [id] │ │ │ │ │ └── page.js │ │ │ ├── not-found.js │ │ │ └── create │ │ │ │ └── page.js │ │ ├── components │ │ │ ├── layout │ │ │ │ ├── Sidebar │ │ │ │ │ ├── index.js │ │ │ │ │ └── Sidebar.jsx │ │ │ │ └── Footer.jsx │ │ │ ├── topic │ │ │ │ ├── TopicList │ │ │ │ │ ├── index.js │ │ │ │ │ └── TopicListClient.js │ │ │ │ └── ReplySection.js │ │ │ ├── user │ │ │ │ ├── UserActivityTabs │ │ │ │ │ └── index.js │ │ │ │ ├── ReportUserButton.jsx │ │ │ │ ├── UserProfileClient.jsx │ │ │ │ ├── FollowButton.jsx │ │ │ │ └── UserContentGuard.jsx │ │ │ ├── ui │ │ │ │ ├── spinner.jsx │ │ │ │ ├── sonner.jsx │ │ │ │ ├── label.jsx │ │ │ │ ├── separator.jsx │ │ │ │ ├── textarea.jsx │ │ │ │ ├── checkbox.jsx │ │ │ │ ├── input.jsx │ │ │ │ ├── avatar.jsx │ │ │ │ ├── switch.jsx │ │ │ │ ├── popover.jsx │ │ │ │ ├── badge.jsx │ │ │ │ ├── scroll-area.jsx │ │ │ │ ├── alert.jsx │ │ │ │ ├── card.jsx │ │ │ │ ├── tooltip.jsx │ │ │ │ ├── tabs.jsx │ │ │ │ ├── accordion.jsx │ │ │ │ ├── slider.jsx │ │ │ │ ├── button.jsx │ │ │ │ └── table.jsx │ │ │ ├── auth │ │ │ │ ├── LoginDialog │ │ │ │ │ ├── FormMessage.js │ │ │ │ │ ├── OAuthSection.js │ │ │ │ │ ├── ModeSwitcher.js │ │ │ │ │ └── LoginForm.js │ │ │ │ ├── RequireAdmin.jsx │ │ │ │ └── RequireAuth.jsx │ │ │ └── common │ │ │ │ ├── MarkdownRender │ │ │ │ └── plugins │ │ │ │ │ └── remark-media.js │ │ │ │ ├── ScalarAPI │ │ │ │ └── index.jsx │ │ │ │ ├── CopyButton.jsx │ │ │ │ ├── Time.jsx │ │ │ │ ├── FormDialog.jsx │ │ │ │ ├── StickySidebar.jsx │ │ │ │ └── AlertDialog.jsx │ │ ├── lib │ │ │ ├── utils.js │ │ │ ├── server │ │ │ │ ├── ledger.js │ │ │ │ └── api.js │ │ │ └── api-url.js │ │ ├── extensions │ │ │ ├── shop │ │ │ │ ├── hooks │ │ │ │ │ ├── index.js │ │ │ │ │ ├── useUserItems.js │ │ │ │ │ ├── useItemActions.js │ │ │ │ │ └── useShopItems.js │ │ │ │ ├── components │ │ │ │ │ ├── user │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── ShopItemGrid.jsx │ │ │ │ │ │ ├── ItemInventoryGrid.jsx │ │ │ │ │ │ └── ShopItemCard.jsx │ │ │ │ │ └── shared │ │ │ │ │ │ ├── ItemTypeBadge.jsx │ │ │ │ │ │ ├── ItemTypeIcon.jsx │ │ │ │ │ │ └── ItemTypeSelector.jsx │ │ │ │ ├── utils │ │ │ │ │ └── itemTypes.js │ │ │ │ ├── api │ │ │ │ │ └── index.js │ │ │ │ └── pages │ │ │ │ │ └── user │ │ │ │ │ └── UserItemsPage.jsx │ │ │ ├── rewards │ │ │ │ ├── pages │ │ │ │ │ └── index.js │ │ │ │ ├── api │ │ │ │ │ └── index.js │ │ │ │ └── components │ │ │ │ │ └── AutoCheckIn.jsx │ │ │ ├── ledger │ │ │ │ ├── components │ │ │ │ │ ├── user │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── BalanceCard.jsx │ │ │ │ │ │ ├── CheckInStatus.jsx │ │ │ │ │ │ └── BalanceOverview.jsx │ │ │ │ │ ├── common │ │ │ │ │ │ ├── TransactionTypeBadge.jsx │ │ │ │ │ │ └── CreditsBadge.jsx │ │ │ │ │ └── admin │ │ │ │ │ │ └── LedgerOverview.jsx │ │ │ │ ├── utils │ │ │ │ │ ├── currency.js │ │ │ │ │ └── formatters.js │ │ │ │ ├── api │ │ │ │ │ └── index.js │ │ │ │ └── pages │ │ │ │ │ └── admin │ │ │ │ │ └── LedgerAdminPage.jsx │ │ │ └── badges │ │ │ │ ├── api │ │ │ │ └── index.js │ │ │ │ └── components │ │ │ │ ├── admin │ │ │ │ └── BadgeTable.jsx │ │ │ │ └── BadgesList.jsx │ │ ├── config │ │ │ └── theme.config.js │ │ ├── proxy.js │ │ ├── hooks │ │ │ └── useClipboard.js │ │ └── contexts │ │ │ ├── SettingsContext.jsx │ │ │ └── ThemeContext.jsx │ ├── jsconfig.json │ ├── postcss.config.mjs │ ├── .env.example │ ├── next.config.mjs │ ├── components.json │ ├── .gitignore │ ├── ecosystem.config.cjs │ ├── README.md │ ├── .dockerignore │ └── package.json └── api │ ├── src │ ├── db │ │ └── index.js │ ├── utils │ │ ├── env.js │ │ ├── index.js │ │ └── settings.js │ ├── plugins │ │ ├── multipart.js │ │ ├── formbody.js │ │ ├── sensible.js │ │ ├── redis.js │ │ ├── oauth.js │ │ ├── event-bus.js │ │ ├── static.js │ │ ├── emailVerification.js │ │ ├── security.js │ │ ├── rateLimit.js │ │ └── cleanup.js │ ├── extensions │ │ ├── ledger │ │ │ └── index.js │ │ ├── shop │ │ │ └── index.js │ │ ├── badges │ │ │ ├── index.js │ │ │ ├── listeners.js │ │ │ └── enricher.js │ │ └── rewards │ │ │ ├── index.js │ │ │ └── routes │ │ │ └── checkin.js │ ├── scripts │ │ └── init │ │ │ └── rewards.js │ ├── app.js │ ├── templates │ │ └── email │ │ │ └── index.js │ ├── server.js │ ├── services │ │ └── userEnricher.js │ └── routes │ │ └── dashboard │ │ └── index.js │ ├── drizzle │ ├── 0003_add_user_items_metadata.sql │ └── meta │ │ └── _journal.json │ ├── drizzle.config.js │ ├── ecosystem.config.cjs │ ├── .env.example │ ├── README.md │ ├── .gitignore │ ├── .dockerignore │ ├── package.json │ └── Dockerfile ├── scripts └── init-db.sql ├── turbo.json ├── .gitignore ├── package.json ├── LICENSE └── docker ├── .env.docker.example ├── docker-compose.lowmem.yml ├── docker-compose.prod.yml └── .dockerignore /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /docs/screens/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiprojecthub/nodebbs/HEAD/docs/screens/1.png -------------------------------------------------------------------------------- /docs/screens/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiprojecthub/nodebbs/HEAD/docs/screens/2.png -------------------------------------------------------------------------------- /docs/screens/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiprojecthub/nodebbs/HEAD/docs/screens/3.png -------------------------------------------------------------------------------- /docs/screens/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiprojecthub/nodebbs/HEAD/docs/screens/4.png -------------------------------------------------------------------------------- /docs/screens/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiprojecthub/nodebbs/HEAD/docs/screens/5.png -------------------------------------------------------------------------------- /docs/screens/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiprojecthub/nodebbs/HEAD/docs/screens/6.png -------------------------------------------------------------------------------- /apps/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiprojecthub/nodebbs/HEAD/apps/web/public/favicon.ico -------------------------------------------------------------------------------- /apps/web/src/app/profile/items/page.js: -------------------------------------------------------------------------------- 1 | export { default } from '@/extensions/shop/pages/user/UserItemsPage'; 2 | -------------------------------------------------------------------------------- /apps/web/src/app/profile/shop/page.js: -------------------------------------------------------------------------------- 1 | export { default } from '@/extensions/shop/pages/user/UserShopPage'; 2 | -------------------------------------------------------------------------------- /apps/web/src/app/dashboard/shop/page.js: -------------------------------------------------------------------------------- 1 | export { default } from '@/extensions/shop/pages/admin/AdminShopPage'; 2 | -------------------------------------------------------------------------------- /apps/web/src/app/profile/rewards/page.js: -------------------------------------------------------------------------------- 1 | export { default } from '@/extensions/ledger/pages/user/UserWalletPage'; 2 | -------------------------------------------------------------------------------- /apps/web/src/app/profile/wallet/page.js: -------------------------------------------------------------------------------- 1 | export { default } from '@/extensions/ledger/pages/user/UserWalletPage'; 2 | -------------------------------------------------------------------------------- /apps/web/src/app/dashboard/badges/page.js: -------------------------------------------------------------------------------- 1 | export { default } from '@/extensions/badges/pages/admin/AdminBadgesPage'; 2 | -------------------------------------------------------------------------------- /apps/web/src/app/dashboard/ledger/page.js: -------------------------------------------------------------------------------- 1 | export { default } from '@/extensions/ledger/pages/admin/LedgerAdminPage'; 2 | -------------------------------------------------------------------------------- /apps/web/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /apps/web/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiprojecthub/nodebbs/HEAD/apps/web/public/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/api/src/db/index.js: -------------------------------------------------------------------------------- 1 | import { drizzle } from 'drizzle-orm/node-postgres'; 2 | 3 | const db = drizzle(process.env.DATABASE_URL); 4 | 5 | export default db; 6 | -------------------------------------------------------------------------------- /apps/api/drizzle/0003_add_user_items_metadata.sql: -------------------------------------------------------------------------------- 1 | DO $$ BEGIN 2 | ALTER TABLE "user_items" ADD COLUMN "metadata" text; 3 | EXCEPTION 4 | WHEN duplicate_column THEN null; 5 | END $$; 6 | -------------------------------------------------------------------------------- /apps/web/src/components/layout/Sidebar/index.js: -------------------------------------------------------------------------------- 1 | export { default as Sidebar } from './Sidebar'; 2 | export { SidebarUI, CategoryList, StatsPanel, formatNumber } from './SidebarUI'; 3 | -------------------------------------------------------------------------------- /apps/web/src/lib/utils.js: -------------------------------------------------------------------------------- 1 | import { clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/src/app/profile/page.js: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | export default function ProfilePage() { 4 | // 重定向到我的话题页面 5 | redirect('/profile/topics'); 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/.env.example: -------------------------------------------------------------------------------- 1 | # 应用名,用于显示pm2服务等场景 2 | APP_NAME=nodebbs 3 | 4 | # 应用端口 5 | PORT=3100 6 | 7 | # 运行时 API 地址 (关键配置) 8 | # 本地开发时,Next.js 服务器需要知道转发给谁 9 | SERVER_API_URL=http://localhost:7100 10 | -------------------------------------------------------------------------------- /apps/api/src/utils/env.js: -------------------------------------------------------------------------------- 1 | export const NODE_ENV = process.env.NODE_ENV || 'development'; 2 | 3 | console.log('当前环境: ', NODE_ENV); 4 | 5 | export const isDev = NODE_ENV === 'development'; 6 | export const isProd = NODE_ENV === 'production'; 7 | -------------------------------------------------------------------------------- /apps/api/src/utils/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | export const dirname = (url) => { 5 | const filename = fileURLToPath(url); 6 | const dirname = path.dirname(filename); 7 | return dirname; 8 | }; -------------------------------------------------------------------------------- /apps/web/src/extensions/shop/hooks/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Shop extension hooks 3 | */ 4 | 5 | export { useShopItems } from './useShopItems'; 6 | export { useUserItems } from './useUserItems'; 7 | export { useItemActions } from './useItemActions'; 8 | -------------------------------------------------------------------------------- /apps/web/src/components/topic/TopicList/index.js: -------------------------------------------------------------------------------- 1 | export { default as TopicList } from './TopicList'; 2 | export { default as TopicListClient } from './TopicListClient'; 3 | export { TopicListUI, TopicItem, TopicListHeader, EmptyState } from './TopicListUI'; 4 | -------------------------------------------------------------------------------- /apps/web/src/extensions/rewards/pages/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Credits system pages 4 | */ 5 | 6 | // Admin pages 7 | export { default as AdminRewardsPage } from './admin/AdminRewardsPage'; 8 | 9 | // User pages 10 | // (Currently empty or migrated to ledger) 11 | -------------------------------------------------------------------------------- /apps/web/src/extensions/ledger/components/user/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * User-facing components for ledger system 3 | */ 4 | 5 | export { BalanceCard } from './BalanceCard'; 6 | export { BalanceOverview } from './BalanceOverview'; 7 | export { CheckInStatus } from './CheckInStatus'; 8 | -------------------------------------------------------------------------------- /apps/api/drizzle.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'drizzle-kit'; 2 | 3 | export default defineConfig({ 4 | out: './drizzle', 5 | schema: './src/db/schema.js', 6 | dialect: 'postgresql', 7 | dbCredentials: { 8 | url: process.env.DATABASE_URL, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /apps/api/drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1765547640840, 9 | "tag": "0003_handy_lightspeed", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /apps/api/src/plugins/multipart.js: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import multipart from '@fastify/multipart'; 3 | 4 | export default fp(async function (fastify, opts) { 5 | fastify.register(multipart, { 6 | limits: { 7 | fileSize: 5 * 1024 * 1024, // 5MB 8 | }, 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /scripts/init-db.sql: -------------------------------------------------------------------------------- 1 | -- PostgreSQL 初始化脚本 2 | -- 这个脚本会在数据库首次创建时执行 3 | 4 | -- 创建扩展(如果需要) 5 | -- CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 6 | -- CREATE EXTENSION IF NOT EXISTS "pg_trgm"; 7 | 8 | -- 设置时区 9 | SET timezone = 'Asia/Shanghai'; 10 | 11 | -- 数据库已由 POSTGRES_DB 环境变量创建 12 | -- 这里可以添加其他初始化 SQL 13 | 14 | \echo '数据库初始化完成' 15 | -------------------------------------------------------------------------------- /apps/web/src/components/user/UserActivityTabs/index.js: -------------------------------------------------------------------------------- 1 | export { default as UserActivityTabs } from './UserActivityTabs'; 2 | export { default as UserActivityTabsServer } from './UserActivityTabsServer'; 3 | export { 4 | TopicsList, 5 | PostsList, 6 | TopicItem, 7 | PostItem, 8 | EmptyState, 9 | } from './UserActivityTabsUI'; 10 | -------------------------------------------------------------------------------- /apps/web/src/app/topic/[id]/loading.js: -------------------------------------------------------------------------------- 1 | import { Loading } from '@/components/common/Loading'; 2 | 3 | export default function LoadingPage() { 4 | // 你可以在这里放骨架屏 5 | return ( 6 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/components/layout/Sidebar/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { usePathname } from 'next/navigation'; 4 | import { SidebarUI } from './SidebarUI'; 5 | 6 | export default function Sidebar({ categories, stats }) { 7 | const pathname = usePathname(); 8 | 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /apps/api/src/plugins/formbody.js: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import formBody from '@fastify/formbody'; 3 | 4 | /** 5 | * 此插件增加了对 application/x-www-form-urlencoded 内容类型的支持 6 | * 这对于处理 Apple Sign In 回调 (form_post 模式) 是必需的 7 | */ 8 | async function formBodyPlugin(fastify) { 9 | await fastify.register(formBody); 10 | } 11 | 12 | export default fp(formBodyPlugin); 13 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.com/schema.json", 3 | "ui": "stream", 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "inputs": ["$TURBO_DEFAULT$", ".env*"], 8 | "outputs": [".next/**", "!.next/cache/**"] 9 | }, 10 | "dev": { 11 | "cache": false, 12 | "persistent": true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/api/src/plugins/sensible.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import fp from 'fastify-plugin' 4 | import sensible from '@fastify/sensible' 5 | 6 | /** 7 | * This plugins adds some utilities to handle http errors 8 | * 9 | * @see https://github.com/fastify/fastify-sensible 10 | */ 11 | export default fp(async function (fastify, opts) { 12 | fastify.register(sensible, { 13 | errorHandler: false 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/spinner.jsx: -------------------------------------------------------------------------------- 1 | import { Loader2Icon } from "lucide-react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Spinner({ 6 | className, 7 | ...props 8 | }) { 9 | return ( 10 | 15 | ); 16 | } 17 | 18 | export { Spinner } 19 | -------------------------------------------------------------------------------- /apps/web/src/extensions/shop/components/user/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * User-facing components for shop system 3 | */ 4 | 5 | export { ShopItemCard } from './ShopItemCard'; 6 | export { ShopItemGrid } from './ShopItemGrid'; 7 | export { ItemInventoryCard } from './ItemInventoryCard'; 8 | export { ItemInventoryGrid } from './ItemInventoryGrid'; 9 | export { PurchaseDialog } from './PurchaseDialog'; 10 | export { BadgeUnlockDialog } from './BadgeUnlockDialog'; 11 | -------------------------------------------------------------------------------- /apps/web/src/app/reference/page.js: -------------------------------------------------------------------------------- 1 | import ScalarAPI from '@/components/common/ScalarAPI'; 2 | export default function ApiReference() { 3 | const url = `/docs/json`; 4 | const config = { 5 | url, 6 | theme: 'alternate', 7 | customCss: `.scalar-app .h-dvh{height: calc(100dvh - 58px); top: 58px}`, 8 | defaultHttpClient: { 9 | targetKey: 'node', 10 | clientKey: 'fetch', 11 | }, 12 | }; 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /apps/api/ecosystem.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: `${process.env.APP_NAME}-api`, 5 | script: './src/app.js', 6 | // instances: 'max', // 使用最大可用CPU核心数 7 | instances: 1, // 使用最大可用CPU核心数 8 | // exec_mode: 'cluster', // 使用集群模式 可以是 “cluster” 或 “fork”,默认 fork 9 | env: { 10 | // NODE_ENV: 'production', // pm2 启动优先使用这个值 11 | LOG_LEVEL: 'info', 12 | // PORT: 7100, 13 | }, 14 | }, 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "**", // 匹配所有域名 8 | }, 9 | { 10 | protocol: "http", 11 | hostname: "**", 12 | }, 13 | ], 14 | }, 15 | // 启用 standalone 输出模式,用于 Docker 部署 16 | output: 'standalone', 17 | // 代理逻辑已迁移至 src/proxy.js,以支持 Docker 运行时环境变量动态注入 18 | }; 19 | 20 | export default nextConfig; 21 | -------------------------------------------------------------------------------- /apps/api/src/extensions/ledger/index.js: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import { LedgerService } from './services/ledgerService.js'; 3 | import ledgerRoutes from './routes.js'; 4 | 5 | async function ledgerPlugin(fastify, options) { 6 | const service = new LedgerService(fastify); 7 | fastify.decorate('ledger', service); 8 | 9 | fastify.register(ledgerRoutes, { prefix: '/api/ledger' }); 10 | } 11 | 12 | export default fp(ledgerPlugin, { 13 | name: 'ledger-plugin', 14 | dependencies: [] 15 | }); 16 | -------------------------------------------------------------------------------- /apps/api/src/extensions/shop/index.js: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import shopRoutes from './routes/index.js'; 3 | import registerShopEnricher from './enricher.js'; 4 | 5 | /** 6 | * 商城插件 7 | * 处理商城系统逻辑和路由。 8 | */ 9 | async function shopPlugin(fastify, options) { 10 | // 注册路由 11 | fastify.register(shopRoutes, { prefix: '/api/shop' }); 12 | 13 | // 注册增强器 14 | registerShopEnricher(fastify); 15 | } 16 | 17 | export default fp(shopPlugin, { 18 | name: 'shop-plugin', 19 | // dependencies: ['credits-plugin'] // 可选:如果紧密依赖积分系统 20 | }); 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env* 10 | !.env*.example 11 | /.dockerignore 12 | 13 | # Testing 14 | coverage 15 | 16 | # Turbo 17 | .turbo 18 | 19 | # Vercel 20 | .vercel 21 | .vscode 22 | .agent 23 | .claude 24 | 25 | # Build Outputs 26 | .next/ 27 | out/ 28 | build 29 | dist 30 | 31 | 32 | # Debug 33 | npm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | 37 | # Misc 38 | .DS_Store 39 | *.pem 40 | -------------------------------------------------------------------------------- /apps/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": false, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "iconLibrary": "lucide", 14 | "aliases": { 15 | "components": "@/components", 16 | "utils": "@/lib/utils", 17 | "ui": "@/components/ui", 18 | "lib": "@/lib", 19 | "hooks": "@/hooks" 20 | }, 21 | "registries": {} 22 | } 23 | -------------------------------------------------------------------------------- /apps/web/src/extensions/shop/components/shared/ItemTypeBadge.jsx: -------------------------------------------------------------------------------- 1 | import { Badge } from '@/components/ui/badge'; 2 | import { getItemTypeLabel } from '../../utils/itemTypes'; 3 | 4 | /** 5 | * Display item type badge 6 | * @param {Object} props 7 | * @param {string} props.type - Item type 8 | * @param {string} props.variant - Badge variant 9 | */ 10 | export function ItemTypeBadge({ type, variant = 'default' }) { 11 | const label = getItemTypeLabel(type); 12 | 13 | return ( 14 | 15 | {label} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/sonner.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner } from "sonner"; 5 | 6 | const Toaster = ({ 7 | ...props 8 | }) => { 9 | const { theme = "system" } = useTheme() 10 | 11 | return ( 12 | 23 | ); 24 | } 25 | 26 | export { Toaster } 27 | -------------------------------------------------------------------------------- /apps/web/src/components/auth/LoginDialog/FormMessage.js: -------------------------------------------------------------------------------- 1 | export function FormMessage({ error, success }) { 2 | if (!error && !success) return null; 3 | 4 | if (error) { 5 | return ( 6 |
7 | {error} 8 |
9 | ); 10 | } 11 | 12 | if (success) { 13 | return ( 14 |
15 | {success} 16 |
17 | ); 18 | } 19 | 20 | return null; 21 | } 22 | -------------------------------------------------------------------------------- /apps/api/src/extensions/badges/index.js: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import badgeRoutes from './routes/index.js'; 3 | import badgeListeners from './listeners.js'; 4 | import registerBadgeEnricher from './enricher.js'; 5 | 6 | /** 7 | * Badges Feature 8 | * Handles achievement badges and user honors. 9 | */ 10 | async function badgesFeature(fastify, options) { 11 | fastify.register(badgeRoutes, { prefix: '/api/badges' }); 12 | fastify.register(badgeListeners); 13 | 14 | // Register User Enricher 15 | registerBadgeEnricher(fastify); 16 | } 17 | 18 | export default fp(badgesFeature, { 19 | name: 'badges-feature' 20 | }); 21 | -------------------------------------------------------------------------------- /apps/web/src/extensions/ledger/components/common/TransactionTypeBadge.jsx: -------------------------------------------------------------------------------- 1 | import { Badge } from '@/components/ui/badge'; 2 | import { getTransactionTypeLabel, getTransactionTypeColor } from '../../utils/transactionTypes'; 3 | 4 | /** 5 | * Display transaction type badge 6 | * @param {Object} props 7 | * @param {string} props.type - Transaction type key 8 | */ 9 | export function TransactionTypeBadge({ type }) { 10 | const label = getTransactionTypeLabel(type); 11 | const variant = getTransactionTypeColor(type); 12 | 13 | return ( 14 | 15 | {label} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /apps/api/src/plugins/redis.js: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin' 2 | import fastifyRedis from '@fastify/redis' 3 | 4 | export default fp(async (fastify) => { 5 | fastify.register(fastifyRedis, { 6 | url: process.env.REDIS_URL, 7 | // 可选:配置 ioredis 原生选项 8 | connectTimeout: 5000, 9 | maxRetriesPerRequest: 3, 10 | }) 11 | 12 | // 可选:在 ready 阶段进行连接测试 13 | fastify.addHook('onReady', async () => { 14 | try { 15 | await fastify.redis.ping() 16 | fastify.log.info('✅ Redis connected successfully') 17 | } catch (err) { 18 | fastify.log.error('❌ Redis connection failed:', err) 19 | } 20 | }) 21 | }, { 22 | name: 'redis' 23 | }) 24 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/label.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }) { 12 | return ( 13 | 20 | ); 21 | } 22 | 23 | export { Label } 24 | -------------------------------------------------------------------------------- /apps/web/src/app/dashboard/settings/components/index.js: -------------------------------------------------------------------------------- 1 | export { GeneralSettings } from './GeneralSettings'; 2 | export { FeatureSettings } from './FeatureSettings'; 3 | export { OAuthSettings } from './OAuthProviderCard'; 4 | export { EmailSettings } from './EmailProviderCard'; 5 | export { RateLimitSettings } from './RateLimitSettings'; 6 | 7 | // Feature Settings 子组件 8 | export { RegistrationSettings } from './RegistrationSettings'; 9 | export { SecuritySettings } from './SecuritySettings'; 10 | export { AuthenticationSettings } from './AuthenticationSettings'; 11 | export { UserManagementSettings } from './UserManagementSettings'; 12 | export { SpamProtectionSettings } from './SpamProtectionSettings'; 13 | -------------------------------------------------------------------------------- /apps/web/src/extensions/shop/components/shared/ItemTypeIcon.jsx: -------------------------------------------------------------------------------- 1 | import { Package, Award, Sparkles } from 'lucide-react'; 2 | import { ITEM_TYPES } from '../../utils/itemTypes'; 3 | 4 | /** 5 | * Display appropriate icon for item type 6 | * @param {Object} props 7 | * @param {string} props.type - Item type 8 | * @param {string} props.className - Additional CSS classes 9 | */ 10 | export function ItemTypeIcon({ type, className = 'h-5 w-5' }) { 11 | switch (type) { 12 | case ITEM_TYPES.AVATAR_FRAME: 13 | return ; 14 | case ITEM_TYPES.BADGE: 15 | return ; 16 | default: 17 | return ; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | !.env.example 36 | 37 | # vercel 38 | .vercel 39 | .claude 40 | .kiro 41 | .vscode 42 | 43 | # typescript 44 | *.tsbuildinfo 45 | next-env.d.ts 46 | -------------------------------------------------------------------------------- /apps/web/ecosystem.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | // name: "next-dash", 5 | name: `${process.env.APP_NAME}-dash`, 6 | script: 'node_modules/next/dist/bin/next', 7 | // script: '.next/standalone/server.js', // 为后续docker部署准备, 需要同步静态文件 cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone/public 8 | args: `start -p ${process.env.PORT}`, // 指定端口 9 | exec_mode: 'cluster', // 启用集群模式 10 | // instances: "max", // 使用所有 CPU 核心 11 | instances: 1, // 使用所有 CPU 核心 12 | autorestart: true, // 崩溃自动重启 13 | watch: false, // 禁用文件监听(生产环境建议关闭) 14 | env: { 15 | NODE_ENV: 'production', 16 | }, 17 | }, 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /apps/web/src/lib/server/ledger.js: -------------------------------------------------------------------------------- 1 | import { request } from '@/lib/server/api'; 2 | 3 | /** 4 | * 检查特定货币是否已启用 (Server Side) 5 | * 使用后端公开接口 /api/ledger/active-currencies 6 | * @param {string} currencyCode - 货币代码 (默认 'credits') 7 | * @returns {Promise} 8 | */ 9 | export async function isCurrencyActive(currencyCode = 'credits') { 10 | try { 11 | const activeCurrencies = await request('/ledger/active-currencies'); 12 | if (!Array.isArray(activeCurrencies)) { 13 | return false; 14 | } 15 | return activeCurrencies.some(c => c.code === currencyCode); 16 | } catch (error) { 17 | console.error(`[Server] Failed to check status for currency ${currencyCode}`, error); 18 | return false; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/src/lib/api-url.js: -------------------------------------------------------------------------------- 1 | function isServer() { 2 | return typeof window === 'undefined'; 3 | } 4 | 5 | function normalizeHost(host) { 6 | return host.replace(/\/+$/, ''); 7 | } 8 | 9 | export function getApiHost() { 10 | if (isServer()) { 11 | // 服务端:优先使用运行时变量 SERVER_API_URL 12 | const host = process.env.SERVER_API_URL; 13 | if (!host) { 14 | throw new Error('SERVER_API_URL is not defined (server-side)'); 15 | } 16 | return normalizeHost(host); 17 | } 18 | 19 | // 客户端:返回空字符串,使请求变为相对路径 (e.g., /api/users) 20 | // 这会触发 Next.js 的 rewrites 规则,将其代理到 SERVER_API_URL 21 | return ''; 22 | } 23 | 24 | export function getApiPath() { 25 | return '/api'; 26 | } 27 | 28 | export function getApiBaseUrl() { 29 | return `${getApiHost()}${getApiPath()}`; 30 | } -------------------------------------------------------------------------------- /apps/web/src/components/ui/separator.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }) { 14 | return ( 15 | 24 | ); 25 | } 26 | 27 | export { Separator } 28 | -------------------------------------------------------------------------------- /apps/web/src/extensions/ledger/utils/currency.js: -------------------------------------------------------------------------------- 1 | import { ledgerApi } from '../api'; 2 | 3 | /** 4 | * 检查特定货币是否已启用 5 | * @param {string} currencyCode - 货币代码 (默认 'credits') 6 | * @returns {Promise} 7 | */ 8 | export async function isCurrencyActive(currencyCode = 'credits') { 9 | try { 10 | // 使用公开接口检查货币状态 11 | const currencies = await ledgerApi.getActiveCurrencies(); 12 | 13 | if (!Array.isArray(currencies)) { 14 | return false; 15 | } 16 | 17 | const currency = currencies.find(c => c.code === currencyCode); 18 | return !!(currency && currency.isActive); 19 | } catch (error) { 20 | console.error(`Failed to check status for currency ${currencyCode}`, error); 21 | return false; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/api/.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | 3 | # 应用名,用于显示pm2服务等场景 4 | APP_NAME=nodebbs 5 | 6 | # 主机/端口 7 | HOST=0.0.0.0 8 | PORT=7100 9 | 10 | # 数据库 11 | DATABASE_URL=postgres://postgres@0.0.0.0:5432/nodebbs 12 | 13 | # 缓存配置 14 | REDIS_URL=redis://default:password@redis:6379/0 15 | 16 | # 用户信息缓存配置(秒) 17 | # 用于缓存用户角色、封禁状态等信息,减少数据库查询 18 | # 建议值:开发环境 30-60,生产环境 120-300 19 | USER_CACHE_TTL=30 20 | 21 | # JWT 使用 `openssl rand -base64 32` 生成 22 | JWT_SECRET=change-this-to-a-secure-random-string-in-production 23 | JWT_ACCESS_TOKEN_EXPIRES_IN=1y 24 | 25 | CORS_ORIGIN=* 26 | 27 | # 前端 URL,用于发送验证邮件和构建 Oauth 回调 URL 28 | APP_URL=http://localhost:3100 29 | 30 | # COOKIE 配置(备用,通常不用设置) 31 | # COOKIE_SECRET=change-this-to-a-secure-random-string-in-production 32 | # COOKIE_SECURE=true 33 | # COOKIE_SAMESITE=Lax 34 | # COOKIE_DOMAIN=.example.com -------------------------------------------------------------------------------- /apps/web/src/app/topic/[id]/not-found.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { Button } from '@/components/ui/button'; 3 | 4 | export default function NotFound() { 5 | return ( 6 |
7 |
8 |
9 | 话题不存在 10 |
11 |

12 | 该话题可能已被删除或不存在 13 |

14 |
15 | 16 | 19 | 20 |
21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/textarea.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ 6 | className, 7 | ...props 8 | }) { 9 | return ( 10 |