├── env.d.ts ├── src ├── vite-env.d.ts ├── lib │ ├── utils.ts │ └── theme-sync.ts ├── i18n │ ├── locales │ │ ├── zh-CN │ │ │ ├── auth.json │ │ │ ├── navigation.json │ │ │ ├── validation.json │ │ │ ├── dashboard.json │ │ │ ├── reports.json │ │ │ └── subscription.json │ │ └── en │ │ │ ├── auth.json │ │ │ ├── navigation.json │ │ │ ├── validation.json │ │ │ ├── dashboard.json │ │ │ └── reports.json │ ├── config.ts │ └── types.ts ├── components │ ├── imports │ │ ├── types.ts │ │ └── steps │ │ │ ├── CompleteStep.tsx │ │ │ ├── FileValidationStep.tsx │ │ │ └── FileUploadStep.tsx │ ├── ui │ │ ├── skeleton.tsx │ │ ├── badge.tsx │ │ ├── textarea.tsx │ │ ├── toggle.tsx │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── button.tsx │ │ ├── separator.tsx │ │ ├── progress.tsx │ │ ├── toaster.tsx │ │ ├── search-input.tsx │ │ ├── switch.tsx │ │ ├── tooltip.tsx │ │ ├── popover.tsx │ │ ├── confirm-dialog.tsx │ │ ├── scroll-area.tsx │ │ ├── alert.tsx │ │ ├── tabs.tsx │ │ ├── card.tsx │ │ ├── LanguageSwitcher.tsx │ │ └── variants.ts │ ├── subscription │ │ ├── form │ │ │ ├── types.ts │ │ │ ├── FormField.tsx │ │ │ ├── validation.ts │ │ │ └── AmountInput.tsx │ │ ├── payment │ │ │ ├── PaymentListState.tsx │ │ │ ├── PaymentHistoryHeader.tsx │ │ │ └── usePaymentOperations.ts │ │ └── CurrencySelector.tsx │ ├── ThemeProvider.tsx │ ├── charts │ │ ├── YearlyTrendChartNew.tsx │ │ └── ExpenseTrendChartNew.tsx │ ├── ModeToggle.tsx │ └── dashboard │ │ ├── StatCard.tsx │ │ ├── RecentlyPaid.tsx │ │ ├── CategoryBreakdown.tsx │ │ └── UpcomingRenewals.tsx ├── pages │ ├── NotificationHistoryPage.tsx │ └── LoginPage.tsx ├── main.tsx ├── hooks │ ├── use-mobile.tsx │ └── use-confirmation.ts ├── services │ ├── authApi.ts │ └── schedulerApi.ts ├── utils │ ├── error-handler.tsx │ ├── error-utils.tsx │ ├── logger.ts │ ├── error-utils.ts │ └── currency.ts ├── config │ ├── api.ts │ └── constants.ts ├── App.tsx ├── types │ └── index.ts └── store │ ├── authStore.ts │ ├── notificationStore.ts │ └── useMonthlyExpenses.ts ├── docs ├── images │ ├── reports.png │ ├── dashboard.png │ ├── reports-dark.png │ ├── subscriptions.png │ ├── monthly-expense.png │ └── subscriptions-payments.png ├── STRUCTURE.md └── DEPLOYMENT.zh-CN.md ├── postcss.config.js ├── server ├── middleware │ ├── auth.js │ ├── requireLogin.js │ └── session.js ├── tests │ └── jest.config.js ├── db │ ├── migrate.js │ └── init.js ├── routes │ ├── analytics.js │ ├── templates.js │ ├── scheduler.js │ ├── monthlyCategorySummary.js │ ├── subscriptionRenewalScheduler.js │ ├── subscriptionManagement.js │ ├── settings.js │ ├── paymentHistory.js │ ├── notifications.js │ ├── categoriesAndPaymentMethods.js │ ├── subscriptions.js │ └── exchangeRates.js ├── package.json ├── scripts │ ├── run-migration.js │ └── rotate-admin-password.js ├── controllers │ ├── subscriptionRenewalSchedulerController.js │ ├── categoriesController.js │ ├── paymentMethodsController.js │ └── schedulerController.js ├── utils │ └── logger.js ├── start.sh ├── services │ ├── userService.js │ ├── emailService.js │ └── subscriptionRenewalScheduler.js └── config │ ├── notification.js │ └── database.js ├── .gitignore ├── .cursorindexingignore ├── tsconfig.json ├── .gitattributes ├── components.json ├── tsconfig.node.json ├── eslint.config.js ├── tsconfig.app.json ├── docker-compose.yml ├── vite.env.js ├── LICENSE ├── vite.config.ts ├── .env.production.example ├── .env.development.example ├── .github └── workflows │ └── docker-build.yml ├── Dockerfile ├── .dockerignore ├── .specstory ├── history │ └── 2025-07-17_06-42Z-解释选中的代码.md └── .what-is-this.md ├── package.json └── tailwind.config.js /env.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /docs/images/reports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huhusmang/Subscription-Management/HEAD/docs/images/reports.png -------------------------------------------------------------------------------- /docs/images/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huhusmang/Subscription-Management/HEAD/docs/images/dashboard.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /server/middleware/auth.js: -------------------------------------------------------------------------------- 1 | // Deprecated: API key auth removed in favor of session-based auth. 2 | module.exports = {}; 3 | -------------------------------------------------------------------------------- /docs/images/reports-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huhusmang/Subscription-Management/HEAD/docs/images/reports-dark.png -------------------------------------------------------------------------------- /docs/images/subscriptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huhusmang/Subscription-Management/HEAD/docs/images/subscriptions.png -------------------------------------------------------------------------------- /docs/images/monthly-expense.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huhusmang/Subscription-Management/HEAD/docs/images/monthly-expense.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | 4 | .env 5 | .env.development 6 | 7 | server/db/database.sqlite 8 | 9 | .specstory 10 | -------------------------------------------------------------------------------- /.cursorindexingignore: -------------------------------------------------------------------------------- 1 | 2 | # Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references 3 | .specstory/** 4 | -------------------------------------------------------------------------------- /docs/images/subscriptions-payments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huhusmang/Subscription-Management/HEAD/docs/images/subscriptions-payments.png -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/i18n/locales/zh-CN/auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "登录", 3 | "description": "登录以访问仪表板", 4 | "username": "用户名", 5 | "password": "密码", 6 | "submit": "登录", 7 | "signingIn": "正在登录...", 8 | "invalidCredentials": "用户名或密码不正确" 9 | } 10 | -------------------------------------------------------------------------------- /src/i18n/locales/zh-CN/navigation.json: -------------------------------------------------------------------------------- 1 | { 2 | "home": "首页", 3 | "subscriptions": "订阅管理", 4 | "reports": "报表", 5 | "settings": "设置", 6 | "dashboard": "仪表板", 7 | "expenseReports": "支出报表", 8 | "notifications": "通知历史", 9 | "logout": "登出" 10 | } -------------------------------------------------------------------------------- /server/middleware/requireLogin.js: -------------------------------------------------------------------------------- 1 | function requireLogin(req, res, next) { 2 | if (req.session && req.session.user) { 3 | return next(); 4 | } 5 | return res.status(401).json({ message: 'Authentication required' }); 6 | } 7 | 8 | module.exports = { requireLogin }; 9 | 10 | -------------------------------------------------------------------------------- /src/i18n/locales/en/auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Login", 3 | "description": "Sign in to access the dashboard", 4 | "username": "Username", 5 | "password": "Password", 6 | "submit": "Login", 7 | "signingIn": "Signing in...", 8 | "invalidCredentials": "Invalid username or password" 9 | } 10 | -------------------------------------------------------------------------------- /src/i18n/locales/en/navigation.json: -------------------------------------------------------------------------------- 1 | { 2 | "home": "Home", 3 | "subscriptions": "Subscriptions", 4 | "reports": "Reports", 5 | "settings": "Settings", 6 | "dashboard": "Dashboard", 7 | "expenseReports": "Expense Reports", 8 | "notifications": "Notification History", 9 | "logout": "Logout" 10 | } -------------------------------------------------------------------------------- /src/components/imports/types.ts: -------------------------------------------------------------------------------- 1 | import { Subscription } from "@/store/subscriptionStore" 2 | 3 | // Import data type - excludes auto-calculated fields 4 | export type SubscriptionImportData = Omit 5 | 6 | export enum ImportStep { 7 | Upload, 8 | Validate, 9 | Review, 10 | Complete 11 | } -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /src/pages/NotificationHistoryPage.tsx: -------------------------------------------------------------------------------- 1 | import { NotificationHistory } from '@/components/notification/NotificationHistory'; 2 | 3 | export function NotificationHistoryPage() { 4 | // Single user system - no need for user ID parameter 5 | // The backend will handle the single user context 6 | return ( 7 | 8 | ); 9 | } 10 | 11 | export default NotificationHistoryPage; 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | }, 12 | "noImplicitAny": false, 13 | "noUnusedParameters": false, 14 | "skipLibCheck": true, 15 | "allowJs": true, 16 | "noUnusedLocals": false, 17 | "strictNullChecks": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Ensure shell scripts always use LF line endings regardless of platform 2 | *.sh text eol=lf 3 | 4 | # Ensure other text files use appropriate line endings 5 | *.js text 6 | *.ts text 7 | *.json text 8 | *.md text 9 | *.yml text 10 | *.yaml text 11 | *.html text 12 | *.css text 13 | *.vue text 14 | 15 | # Binary files 16 | *.png binary 17 | *.jpg binary 18 | *.jpeg binary 19 | *.gif binary 20 | *.ico binary 21 | *.woff binary 22 | *.woff2 binary 23 | *.ttf binary 24 | *.eot binary 25 | -------------------------------------------------------------------------------- /server/tests/jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | testEnvironment: 'node', 5 | rootDir: path.resolve(__dirname, '..'), 6 | testMatch: ['/tests/**/*.test.js'], 7 | clearMocks: true, 8 | collectCoverageFrom: [ 9 | 'routes/**/*.js', 10 | 'services/**/*.js', 11 | 'config/**/*.js', 12 | 'db/**/*.js', 13 | ], 14 | coveragePathIgnorePatterns: [ 15 | '/node_modules/', 16 | '/tests/', 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /src/i18n/locales/zh-CN/validation.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": "此字段为必填项", 3 | "email": "请输入有效的电子邮件地址", 4 | "minLength": "至少需要 {{min}} 个字符", 5 | "maxLength": "最多允许 {{max}} 个字符", 6 | "number": "请输入有效的数字", 7 | "invalidFormat": "格式无效", 8 | "positiveNumber": "请输入正数", 9 | "nonNegativeNumber": "请输入非负数(0或更大)", 10 | "decimalPlaces": "最多允许 {{max}} 位小数", 11 | "futureDate": "日期必须是未来日期", 12 | "pastDate": "日期必须是过去日期", 13 | "invalidUrl": "请输入有效的URL", 14 | "passwordMismatch": "密码不匹配", 15 | "invalidPhone": "请输入有效的电话号码" 16 | } -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | import { badgeVariants } from "./variants" 6 | 7 | export interface BadgeProps 8 | extends React.HTMLAttributes, 9 | VariantProps {} 10 | 11 | function Badge({ className, variant, ...props }: BadgeProps) { 12 | return ( 13 |
14 | ) 15 | } 16 | 17 | export { Badge } -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { BrowserRouter } from 'react-router-dom' 4 | import './index.css' 5 | import './i18n/config' // Import i18n configuration 6 | import App from './App.tsx' 7 | 8 | createRoot(document.getElementById('root')!).render( 9 | 10 | 16 | 17 | 18 | , 19 | ) -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /src/components/subscription/form/types.ts: -------------------------------------------------------------------------------- 1 | // Form validation types 2 | export type FormErrors = { 3 | [key: string]: string 4 | } 5 | 6 | // Form data type - excludes auto-calculated fields and optional display fields 7 | export type SubscriptionFormData = { 8 | name: string 9 | plan: string 10 | billingCycle: "monthly" | "quarterly" | "yearly" | "semiannual" 11 | amount: number 12 | currency: string 13 | paymentMethodId: number 14 | startDate: string 15 | // User-editable next billing date (defaults from startDate + billingCycle) 16 | nextBillingDate: string 17 | 18 | status: "active" | "trial" | "cancelled" 19 | categoryId: number 20 | renewalType: "auto" | "manual" 21 | notes: string 22 | website: string 23 | } -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Textarea = React.forwardRef< 6 | HTMLTextAreaElement, 7 | React.ComponentProps<"textarea"> 8 | >(({ className, ...props }, ref) => { 9 | return ( 10 |