Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 77b6a3e0d5 | |||
| 4fb551106a | |||
| 7ee9be8e11 | |||
| f4b1f61bc2 | |||
| 7a7624866f | |||
| e5948cd346 | |||
| 929b838e0f | |||
| 1f8b539b68 | |||
| ce72892dc8 | |||
| 45b149ad58 | |||
| 1564d2dd30 | |||
| 1ea483016f | |||
| 4c6aeb3e6c | |||
| 13abfc1e52 | |||
| 13d0d7707a | |||
| 6ae9f49b4c | |||
| ea23f6264c | |||
| d0ba581184 | |||
| 5c956c195b | |||
| 4578116a30 | |||
| 45177e9fad | |||
| 2ca790abf9 | |||
| 2bce9a59c6 | |||
| 180f7a9baa | |||
| 019436507e | |||
| 63298d6827 | |||
| 3a7c67bb88 | |||
| 5d88ac783b | |||
| c98adb3051 | |||
| c48e16a977 | |||
| 661788ae75 | |||
| ecf0d9ff03 | |||
| de1db459c2 | |||
| 0c496c6ba6 | |||
| 8dff555db4 | |||
| a2d1840e47 | |||
| 73e0f1312c | |||
| 8ef7c75948 | |||
| fab72906c9 | |||
| 5d71f3b527 | |||
| 60dbd1be9d | |||
| bb0a09d627 | |||
| b106d91f8a | |||
| 8b5af6e172 | |||
| aff645bc5d | |||
| e8dd85ee65 | |||
| eeee62c4e8 | |||
| f7069fe07d | |||
| e4c2095004 | |||
| 95c10eca77 | |||
| 25857abf26 | |||
| 352b3c1b3c | |||
| ec1a4f7478 | |||
| 7192cda20f | |||
| aae03b6b0d | |||
| 48b69793ce | |||
| 4b47d3bda7 | |||
| fe4f7909e3 | |||
| 579d63d39d | |||
| d7f27eaf1e | |||
| 99e5622234 | |||
| e84bef07b4 | |||
| 3a9b5fb000 | |||
| 32022c1437 | |||
| bd3241504f | |||
| 8f6ff38a32 | |||
| be15494ecd |
@@ -1,66 +0,0 @@
|
|||||||
name: API Docker Image
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [main, develop]
|
|
||||||
paths:
|
|
||||||
- "apps/api/**"
|
|
||||||
- ".github/workflows/api-docker-image.yml"
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- "apps/api/**"
|
|
||||||
- ".github/workflows/api-docker-image.yml"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: api-docker-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-publish:
|
|
||||||
name: Build API Docker Image
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Check Dockerfile
|
|
||||||
id: dockerfile
|
|
||||||
run: |
|
|
||||||
if [ -f apps/api/Dockerfile ]; then
|
|
||||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Setup Docker Buildx
|
|
||||||
if: steps.dockerfile.outputs.exists == 'true'
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
if: steps.dockerfile.outputs.exists == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build (PR/manual) or Build and Push (main)
|
|
||||||
if: steps.dockerfile.outputs.exists == 'true'
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: ./apps/api
|
|
||||||
file: ./apps/api/Dockerfile
|
|
||||||
push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
|
||||||
tags: |
|
|
||||||
ghcr.io/${{ github.repository }}/api:${{ github.sha }}
|
|
||||||
ghcr.io/${{ github.repository }}/api:latest
|
|
||||||
|
|
||||||
- name: Skip notice
|
|
||||||
if: steps.dockerfile.outputs.exists != 'true'
|
|
||||||
run: echo "apps/api/Dockerfile not found, skip docker build."
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
name: Deploy Admin
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- "apps/admin/**"
|
|
||||||
- "packages/shared-types/**"
|
|
||||||
- "packages/ui/**"
|
|
||||||
- ".github/workflows/deploy-admin.yml"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: deploy-admin-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build Admin
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: 9.15.2
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
cache: pnpm
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build workspace
|
|
||||||
run: pnpm run build
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
name: Deploy Admin (Template)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Trigger deployment webhook
|
|
||||||
env:
|
|
||||||
ADMIN_DEPLOY_WEBHOOK_URL: ${{ secrets.ADMIN_DEPLOY_WEBHOOK_URL }}
|
|
||||||
run: |
|
|
||||||
if [ -z "$ADMIN_DEPLOY_WEBHOOK_URL" ]; then
|
|
||||||
echo "ADMIN_DEPLOY_WEBHOOK_URL is not configured. Skipping deploy."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
curl -X POST "$ADMIN_DEPLOY_WEBHOOK_URL"
|
|
||||||
echo "Admin deployment webhook triggered."
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
name: Deploy Web
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- "apps/web/**"
|
|
||||||
- "packages/shared-types/**"
|
|
||||||
- "packages/ui/**"
|
|
||||||
- ".github/workflows/deploy-web.yml"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: deploy-web-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build Web
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: 9.15.2
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
cache: pnpm
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build workspace
|
|
||||||
run: pnpm run build
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
name: Deploy Web (Template)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Trigger deployment webhook
|
|
||||||
env:
|
|
||||||
WEB_DEPLOY_WEBHOOK_URL: ${{ secrets.WEB_DEPLOY_WEBHOOK_URL }}
|
|
||||||
run: |
|
|
||||||
if [ -z "$WEB_DEPLOY_WEBHOOK_URL" ]; then
|
|
||||||
echo "WEB_DEPLOY_WEBHOOK_URL is not configured. Skipping deploy."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
curl -X POST "$WEB_DEPLOY_WEBHOOK_URL"
|
|
||||||
echo "Web deployment webhook triggered."
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
name: PR Quality
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
|
||||||
branches: [main, develop]
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: pr-quality-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
quality:
|
|
||||||
name: Lint, Typecheck, Test, Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: 9.15.2
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
cache: pnpm
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Lint
|
|
||||||
run: pnpm run lint
|
|
||||||
|
|
||||||
- name: Typecheck
|
|
||||||
run: pnpm run typecheck
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: pnpm run test
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: pnpm run build
|
|
||||||
@@ -1,6 +1,2 @@
|
|||||||
develop.md
|
develop.md
|
||||||
node_modules/
|
|
||||||
.turbo/
|
|
||||||
.idea/
|
.idea/
|
||||||
.eslintcache
|
|
||||||
/.husky/_
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
pnpm lint:staged
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pnpm typecheck
|
|
||||||
pnpm test
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
"*.{js,mjs,cjs,ts,tsx}": ["eslint --fix", "prettier --write"],
|
|
||||||
"*.{json,md,yml,yaml}": ["prettier --write"]
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
node_modules
|
|
||||||
.turbo
|
|
||||||
.idea
|
|
||||||
dist
|
|
||||||
build
|
|
||||||
coverage
|
|
||||||
*.png
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": false,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"printWidth": 100
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
# 贡献指南(Contributing)
|
|
||||||
|
|
||||||
本文档定义 TodoList 仓库的协作规范,所有贡献者提交代码前请先阅读。
|
|
||||||
|
|
||||||
## 1. 分支模型
|
|
||||||
|
|
||||||
- 长期分支:
|
|
||||||
- `main`:生产稳定分支
|
|
||||||
- `develop`:开发集成分支
|
|
||||||
- 功能分支:
|
|
||||||
- 命名:`feature/<phase>-<name>`
|
|
||||||
- 示例:`feature/p1-code-quality-hooks`
|
|
||||||
- 其他分支:
|
|
||||||
- `release/<version>`
|
|
||||||
- `hotfix/<issue-id>-<short-desc>`
|
|
||||||
|
|
||||||
## 2. 提交流程
|
|
||||||
|
|
||||||
1. 从目标基线分支切出功能分支。
|
|
||||||
2. 每完成一个小功能,提交一个最小 commit。
|
|
||||||
3. 完成后推送分支并创建 PR。
|
|
||||||
4. 通过 Code Review 后再合并到目标分支。
|
|
||||||
|
|
||||||
## 3. Commit 规范
|
|
||||||
|
|
||||||
- 使用 Conventional Commits:
|
|
||||||
- `feat(scope): ...`
|
|
||||||
- `fix(scope): ...`
|
|
||||||
- `chore(scope): ...`
|
|
||||||
- `docs(scope): ...`
|
|
||||||
- `test(scope): ...`
|
|
||||||
- `ci(scope): ...`
|
|
||||||
- 要求:
|
|
||||||
- commit 粒度最小化,不要把多个不相关改动塞进一个提交。
|
|
||||||
- commit 必须可回滚、可解释。
|
|
||||||
- 默认使用 GPG 签名提交:`git commit -S`。
|
|
||||||
|
|
||||||
## 4. PR 规范
|
|
||||||
|
|
||||||
- PR 标题简明描述变更目标。
|
|
||||||
- PR 描述至少包含:
|
|
||||||
- 变更概述
|
|
||||||
- 具体改动
|
|
||||||
- 测试结果
|
|
||||||
- 风险评估
|
|
||||||
- 回滚方案
|
|
||||||
- 一个 PR 只解决一类问题,避免“超大 PR”。
|
|
||||||
|
|
||||||
## 5. 代码质量检查
|
|
||||||
|
|
||||||
提交前建议至少执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm install
|
|
||||||
pnpm run lint
|
|
||||||
pnpm run typecheck
|
|
||||||
pnpm run test
|
|
||||||
```
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
- `pre-commit` 会自动执行 `lint-staged`。
|
|
||||||
- `pre-push` 会自动执行 `typecheck + test`。
|
|
||||||
|
|
||||||
## 6. 变更边界要求
|
|
||||||
|
|
||||||
- 不要提交无关文件(例如本地 IDE 缓存、临时导出文件)。
|
|
||||||
- 不要随意修改与当前任务无关的历史代码。
|
|
||||||
- 如发现仓库出现非本人预期改动,先暂停并和维护者确认。
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/todolist?schema=public"
|
|
||||||
AUTH_ACCESS_SECRET="dev-access-secret"
|
|
||||||
AUTH_ACCESS_EXPIRES_IN_SECONDS="900"
|
|
||||||
AUTH_REFRESH_EXPIRES_IN_SECONDS="2592000"
|
|
||||||
AUTH_EMAIL_CODE_TTL_SECONDS="300"
|
|
||||||
AUTH_TOTP_ISSUER="TodoList"
|
|
||||||
OAUTH_GITHUB_CLIENT_ID="github-client-id"
|
|
||||||
OAUTH_GITHUB_CLIENT_SECRET="github-client-secret"
|
|
||||||
OAUTH_GITHUB_CALLBACK_URL="http://localhost:3000/auth/oauth/github/callback"
|
|
||||||
OAUTH_QQ_CLIENT_ID="qq-client-id"
|
|
||||||
OAUTH_QQ_CLIENT_SECRET="qq-client-secret"
|
|
||||||
OAUTH_QQ_CALLBACK_URL="http://localhost:3000/auth/oauth/qq/callback"
|
|
||||||
OAUTH_QQ_AUTH_URL="https://graph.qq.com/oauth2.0/authorize"
|
|
||||||
OAUTH_QQ_TOKEN_URL="https://graph.qq.com/oauth2.0/token"
|
|
||||||
OAUTH_WECHAT_CLIENT_ID="wechat-client-id"
|
|
||||||
OAUTH_WECHAT_CLIENT_SECRET="wechat-client-secret"
|
|
||||||
OAUTH_WECHAT_CALLBACK_URL="http://localhost:3000/auth/oauth/wechat/callback"
|
|
||||||
OAUTH_WECHAT_AUTH_URL="https://open.weixin.qq.com/connect/qrconnect"
|
|
||||||
OAUTH_WECHAT_TOKEN_URL="https://api.weixin.qq.com/sns/oauth2/access_token"
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
node_modules
|
|
||||||
# 环境变量文件不纳入版本控制
|
|
||||||
.env
|
|
||||||
|
|
||||||
/generated/prisma
|
|
||||||
dist
|
|
||||||
prisma.config.js
|
|
||||||
prisma.config.js.map
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@todolist/api",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "TodoList API service",
|
|
||||||
"scripts": {
|
|
||||||
"prisma:generate": "prisma generate",
|
|
||||||
"prisma:format": "prisma format",
|
|
||||||
"prisma:validate": "prisma validate",
|
|
||||||
"start": "node dist/main.js",
|
|
||||||
"start:dev": "ts-node-dev --respawn --transpile-only src/main.ts",
|
|
||||||
"build": "tsc -p tsconfig.build.json",
|
|
||||||
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
||||||
"test": "echo api tests pending"
|
|
||||||
},
|
|
||||||
"license": "GPL-3.0-or-later",
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^25.5.2",
|
|
||||||
"@types/passport-github2": "^1.2.9",
|
|
||||||
"@types/passport-oauth2": "^1.8.0",
|
|
||||||
"dotenv": "^16.6.1",
|
|
||||||
"prisma": "^7.6.0",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"ts-node-dev": "^2.0.0",
|
|
||||||
"typescript": "^5.9.3"
|
|
||||||
},
|
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@nestjs/common": "^11.1.18",
|
|
||||||
"@nestjs/config": "^4.0.3",
|
|
||||||
"@nestjs/core": "^11.1.18",
|
|
||||||
"@nestjs/jwt": "^11.0.2",
|
|
||||||
"@nestjs/platform-express": "^11.1.18",
|
|
||||||
"@otplib/preset-default": "^12.0.1",
|
|
||||||
"@prisma/client": "^7.6.0",
|
|
||||||
"class-transformer": "^0.5.1",
|
|
||||||
"class-validator": "^0.15.1",
|
|
||||||
"otplib": "^13.4.0",
|
|
||||||
"reflect-metadata": "^0.2.2",
|
|
||||||
"rxjs": "^7.8.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
// Prisma CLI 配置(TodoList)
|
|
||||||
import "dotenv/config";
|
|
||||||
import { defineConfig } from "prisma/config";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
schema: "prisma/schema.prisma",
|
|
||||||
migrations: {
|
|
||||||
path: "prisma/migrations"
|
|
||||||
},
|
|
||||||
datasource: {
|
|
||||||
url: process.env["DATABASE_URL"]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,403 +0,0 @@
|
|||||||
// Prisma 数据模型定义(TodoList)
|
|
||||||
|
|
||||||
generator client {
|
|
||||||
provider = "prisma-client"
|
|
||||||
output = "../generated/prisma"
|
|
||||||
}
|
|
||||||
|
|
||||||
datasource db {
|
|
||||||
provider = "postgresql"
|
|
||||||
}
|
|
||||||
|
|
||||||
enum UserStatus {
|
|
||||||
ACTIVE
|
|
||||||
DISABLED
|
|
||||||
BANNED
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AuthProvider {
|
|
||||||
EMAIL
|
|
||||||
GITHUB
|
|
||||||
QQ
|
|
||||||
WECHAT
|
|
||||||
}
|
|
||||||
|
|
||||||
enum TaskPriority {
|
|
||||||
LOW
|
|
||||||
MEDIUM
|
|
||||||
HIGH
|
|
||||||
URGENT
|
|
||||||
}
|
|
||||||
|
|
||||||
enum TaskStatus {
|
|
||||||
TODO
|
|
||||||
IN_PROGRESS
|
|
||||||
DONE
|
|
||||||
ARCHIVED
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AttachmentType {
|
|
||||||
IMAGE
|
|
||||||
VIDEO
|
|
||||||
FILE
|
|
||||||
LINK
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AiChannel {
|
|
||||||
USER_KEY
|
|
||||||
ASTRBOT
|
|
||||||
PUBLIC_POOL
|
|
||||||
}
|
|
||||||
|
|
||||||
enum NotificationChannel {
|
|
||||||
EMAIL
|
|
||||||
WEB_PUSH
|
|
||||||
}
|
|
||||||
|
|
||||||
enum NotificationStatus {
|
|
||||||
PENDING
|
|
||||||
SENT
|
|
||||||
FAILED
|
|
||||||
CANCELED
|
|
||||||
}
|
|
||||||
|
|
||||||
model User {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
email String @unique
|
|
||||||
nickname String?
|
|
||||||
avatarUrl String?
|
|
||||||
status UserStatus @default(ACTIVE)
|
|
||||||
defaultStorageQuotaMb Int @default(100)
|
|
||||||
usedStorageBytes BigInt @default(0)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
identities AuthIdentity[]
|
|
||||||
refreshTokens RefreshToken[]
|
|
||||||
security UserSecurity?
|
|
||||||
tasks Task[]
|
|
||||||
tags Tag[]
|
|
||||||
attachments Attachment[]
|
|
||||||
taskActivityLogs TaskActivityLog[]
|
|
||||||
syncOperations SyncOperation[]
|
|
||||||
syncCursors SyncCursor[]
|
|
||||||
taskTombstones TaskTombstone[]
|
|
||||||
aiProviderBindings AiProviderBinding[]
|
|
||||||
aiUsageLogs AiUsageLog[]
|
|
||||||
notificationRules NotificationRule[]
|
|
||||||
notificationJobs NotificationJob[]
|
|
||||||
createdAdminTokens AdminToken[]
|
|
||||||
auditLogs AuditLog[]
|
|
||||||
|
|
||||||
@@map("users")
|
|
||||||
}
|
|
||||||
|
|
||||||
model AuthIdentity {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
userId String
|
|
||||||
provider AuthProvider
|
|
||||||
providerUserId String
|
|
||||||
email String?
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@unique([provider, providerUserId])
|
|
||||||
@@index([userId])
|
|
||||||
@@map("auth_identities")
|
|
||||||
}
|
|
||||||
|
|
||||||
model UserSecurity {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
userId String @unique
|
|
||||||
twoFactorEnabled Boolean @default(false)
|
|
||||||
twoFactorSecret String?
|
|
||||||
recoveryCodes String[] @default([])
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@map("user_security")
|
|
||||||
}
|
|
||||||
|
|
||||||
model RefreshToken {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
userId String
|
|
||||||
tokenHash String @unique
|
|
||||||
deviceId String?
|
|
||||||
expiresAt DateTime
|
|
||||||
revokedAt DateTime?
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@index([userId, expiresAt])
|
|
||||||
@@map("refresh_tokens")
|
|
||||||
}
|
|
||||||
|
|
||||||
model Task {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
userId String
|
|
||||||
title String
|
|
||||||
contentJson Json?
|
|
||||||
contentText String?
|
|
||||||
priority TaskPriority @default(MEDIUM)
|
|
||||||
status TaskStatus @default(TODO)
|
|
||||||
ddl DateTime?
|
|
||||||
completedAt DateTime?
|
|
||||||
version Int @default(1)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
taskTags TaskTag[]
|
|
||||||
attachments Attachment[]
|
|
||||||
activityLogs TaskActivityLog[]
|
|
||||||
notificationJobs NotificationJob[]
|
|
||||||
notificationRules NotificationRule[]
|
|
||||||
|
|
||||||
@@index([userId, status])
|
|
||||||
@@index([userId, ddl])
|
|
||||||
@@map("tasks")
|
|
||||||
}
|
|
||||||
|
|
||||||
model Tag {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
userId String
|
|
||||||
name String
|
|
||||||
color String?
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
taskTags TaskTag[]
|
|
||||||
|
|
||||||
@@unique([userId, name])
|
|
||||||
@@index([userId])
|
|
||||||
@@map("tags")
|
|
||||||
}
|
|
||||||
|
|
||||||
model TaskTag {
|
|
||||||
taskId String
|
|
||||||
tagId String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
|
||||||
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@id([taskId, tagId])
|
|
||||||
@@index([tagId])
|
|
||||||
@@map("task_tags")
|
|
||||||
}
|
|
||||||
|
|
||||||
model Attachment {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
userId String
|
|
||||||
taskId String?
|
|
||||||
type AttachmentType
|
|
||||||
url String
|
|
||||||
mimeType String?
|
|
||||||
fileName String?
|
|
||||||
fileSize Int
|
|
||||||
width Int?
|
|
||||||
height Int?
|
|
||||||
durationMs Int?
|
|
||||||
checksum String?
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
task Task? @relation(fields: [taskId], references: [id], onDelete: SetNull)
|
|
||||||
|
|
||||||
@@index([userId])
|
|
||||||
@@index([taskId])
|
|
||||||
@@map("attachments")
|
|
||||||
}
|
|
||||||
|
|
||||||
model TaskActivityLog {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
userId String
|
|
||||||
taskId String
|
|
||||||
action String
|
|
||||||
payload Json?
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@index([taskId, createdAt])
|
|
||||||
@@index([userId, createdAt])
|
|
||||||
@@map("task_activity_logs")
|
|
||||||
}
|
|
||||||
|
|
||||||
model SyncOperation {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
opId String @unique
|
|
||||||
userId String
|
|
||||||
deviceId String
|
|
||||||
entityType String
|
|
||||||
entityId String
|
|
||||||
action String
|
|
||||||
payload Json?
|
|
||||||
clientTs DateTime
|
|
||||||
serverTs DateTime @default(now())
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@index([userId, deviceId, serverTs])
|
|
||||||
@@index([userId, entityType, entityId])
|
|
||||||
@@map("sync_operations")
|
|
||||||
}
|
|
||||||
|
|
||||||
model SyncCursor {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
userId String
|
|
||||||
deviceId String
|
|
||||||
lastPulledAt DateTime?
|
|
||||||
lastOperationServerTs DateTime?
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@unique([userId, deviceId])
|
|
||||||
@@map("sync_cursors")
|
|
||||||
}
|
|
||||||
|
|
||||||
model TaskTombstone {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
taskId String @unique
|
|
||||||
userId String
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
deleteOpId String?
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@index([userId, deletedAt])
|
|
||||||
@@map("task_tombstones")
|
|
||||||
}
|
|
||||||
|
|
||||||
model AiProviderBinding {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
userId String
|
|
||||||
channel AiChannel
|
|
||||||
providerName String
|
|
||||||
model String?
|
|
||||||
encryptedApiKey String?
|
|
||||||
endpoint String?
|
|
||||||
isDefault Boolean @default(false)
|
|
||||||
isEnabled Boolean @default(true)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@index([userId, isEnabled])
|
|
||||||
@@map("ai_provider_bindings")
|
|
||||||
}
|
|
||||||
|
|
||||||
model AiPublicPoolConfig {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
enabled Boolean @default(false)
|
|
||||||
providerName String?
|
|
||||||
model String?
|
|
||||||
encryptedApiKey String?
|
|
||||||
endpoint String?
|
|
||||||
rpmLimit Int @default(60)
|
|
||||||
dailyTokenLimit Int @default(0)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
@@map("ai_public_pool_config")
|
|
||||||
}
|
|
||||||
|
|
||||||
model AiUsageLog {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
userId String?
|
|
||||||
channel AiChannel
|
|
||||||
providerName String?
|
|
||||||
model String?
|
|
||||||
promptTokens Int @default(0)
|
|
||||||
completionTokens Int @default(0)
|
|
||||||
totalTokens Int @default(0)
|
|
||||||
latencyMs Int?
|
|
||||||
success Boolean @default(true)
|
|
||||||
errorCode String?
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
||||||
|
|
||||||
@@index([userId, createdAt])
|
|
||||||
@@index([channel, createdAt])
|
|
||||||
@@map("ai_usage_logs")
|
|
||||||
}
|
|
||||||
|
|
||||||
model NotificationRule {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
userId String
|
|
||||||
taskId String?
|
|
||||||
channel NotificationChannel @default(EMAIL)
|
|
||||||
advanceMinutes Int @default(60)
|
|
||||||
enabled Boolean @default(true)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
task Task? @relation(fields: [taskId], references: [id], onDelete: SetNull)
|
|
||||||
jobs NotificationJob[]
|
|
||||||
|
|
||||||
@@index([userId, enabled])
|
|
||||||
@@index([taskId])
|
|
||||||
@@map("notification_rules")
|
|
||||||
}
|
|
||||||
|
|
||||||
model NotificationJob {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
userId String
|
|
||||||
taskId String?
|
|
||||||
ruleId String?
|
|
||||||
channel NotificationChannel
|
|
||||||
scheduledAt DateTime
|
|
||||||
sentAt DateTime?
|
|
||||||
status NotificationStatus @default(PENDING)
|
|
||||||
retryCount Int @default(0)
|
|
||||||
errorMessage String?
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
task Task? @relation(fields: [taskId], references: [id], onDelete: SetNull)
|
|
||||||
rule NotificationRule? @relation(fields: [ruleId], references: [id], onDelete: SetNull)
|
|
||||||
|
|
||||||
@@index([status, scheduledAt])
|
|
||||||
@@index([userId, createdAt])
|
|
||||||
@@map("notification_jobs")
|
|
||||||
}
|
|
||||||
|
|
||||||
model SystemSetting {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
key String @unique
|
|
||||||
value Json
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
@@map("system_settings")
|
|
||||||
}
|
|
||||||
|
|
||||||
model AdminToken {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
tokenHash String @unique
|
|
||||||
name String
|
|
||||||
expiresAt DateTime
|
|
||||||
lastUsedAt DateTime?
|
|
||||||
revokedAt DateTime?
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
createdByUserId String?
|
|
||||||
createdByUser User? @relation(fields: [createdByUserId], references: [id], onDelete: SetNull)
|
|
||||||
|
|
||||||
@@index([expiresAt])
|
|
||||||
@@map("admin_tokens")
|
|
||||||
}
|
|
||||||
|
|
||||||
model AuditLog {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
actorUserId String?
|
|
||||||
action String
|
|
||||||
targetType String
|
|
||||||
targetId String?
|
|
||||||
meta Json?
|
|
||||||
ip String?
|
|
||||||
userAgent String?
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
actorUser User? @relation(fields: [actorUserId], references: [id], onDelete: SetNull)
|
|
||||||
|
|
||||||
@@index([action, createdAt])
|
|
||||||
@@index([actorUserId, createdAt])
|
|
||||||
@@map("audit_logs")
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Module } from "@nestjs/common";
|
|
||||||
import { ConfigModule } from "@nestjs/config";
|
|
||||||
import { AuthModule } from "./auth/auth.module";
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
ConfigModule.forRoot({
|
|
||||||
isGlobal: true,
|
|
||||||
envFilePath: ".env"
|
|
||||||
}),
|
|
||||||
AuthModule
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class AppModule {}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import { Body, Controller, Get, Post, Req, UseGuards } from "@nestjs/common";
|
|
||||||
import { AuthGuard } from "@nestjs/passport";
|
|
||||||
import { AuthService } from "./auth.service";
|
|
||||||
import { EmailLoginDto } from "./dto/email-login.dto";
|
|
||||||
import { RefreshTokenDto } from "./dto/refresh-token.dto";
|
|
||||||
import { SendEmailCodeDto } from "./dto/send-email-code.dto";
|
|
||||||
import { TwoFactorEnrollDto } from "./dto/two-factor-enroll.dto";
|
|
||||||
import { TwoFactorVerifyDto } from "./dto/two-factor-verify.dto";
|
|
||||||
|
|
||||||
@Controller("auth")
|
|
||||||
export class AuthController {
|
|
||||||
constructor(private readonly authService: AuthService) {}
|
|
||||||
|
|
||||||
@Post("email/send-code")
|
|
||||||
async sendEmailCode(
|
|
||||||
@Body() body: SendEmailCodeDto
|
|
||||||
): Promise<{ success: boolean; expiresInSeconds: number; debugCode: string }> {
|
|
||||||
return this.authService.sendEmailCode(body.email);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post("email/login")
|
|
||||||
async loginWithEmailCode(@Body() body: EmailLoginDto): Promise<{
|
|
||||||
accessToken: string;
|
|
||||||
tokenType: "Bearer";
|
|
||||||
expiresInSeconds: number;
|
|
||||||
refreshToken: string;
|
|
||||||
refreshExpiresInSeconds: number;
|
|
||||||
user: { id: string; email: string };
|
|
||||||
}> {
|
|
||||||
return this.authService.loginWithEmailCode(body.email, body.code);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post("token/refresh")
|
|
||||||
async refreshTokens(@Body() body: RefreshTokenDto): Promise<{
|
|
||||||
accessToken: string;
|
|
||||||
tokenType: "Bearer";
|
|
||||||
expiresInSeconds: number;
|
|
||||||
refreshToken: string;
|
|
||||||
refreshExpiresInSeconds: number;
|
|
||||||
user: { id: string; email: string };
|
|
||||||
}> {
|
|
||||||
return this.authService.refreshTokens(body.refreshToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post("token/revoke")
|
|
||||||
async revokeRefreshToken(@Body() body: RefreshTokenDto): Promise<{ success: boolean }> {
|
|
||||||
return this.authService.revokeRefreshToken(body.refreshToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post("2fa/enroll")
|
|
||||||
async enrollTwoFactor(@Body() body: TwoFactorEnrollDto): Promise<{
|
|
||||||
userId: string;
|
|
||||||
secret: string;
|
|
||||||
otpauthUrl: string;
|
|
||||||
enabled: boolean;
|
|
||||||
}> {
|
|
||||||
return this.authService.enrollTwoFactor(body.email);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post("2fa/verify")
|
|
||||||
async verifyTwoFactor(
|
|
||||||
@Body() body: TwoFactorVerifyDto
|
|
||||||
): Promise<{ success: boolean; enabled: boolean }> {
|
|
||||||
return this.authService.verifyTwoFactor(body.email, body.token);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get("oauth/github")
|
|
||||||
@UseGuards(AuthGuard("github"))
|
|
||||||
githubLogin(): void {}
|
|
||||||
|
|
||||||
@Get("oauth/github/callback")
|
|
||||||
@UseGuards(AuthGuard("github"))
|
|
||||||
githubCallback(@Req() req: { user: unknown }): {
|
|
||||||
success: boolean;
|
|
||||||
provider: "github";
|
|
||||||
profile: unknown;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
provider: "github",
|
|
||||||
profile: req.user
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get("oauth/qq")
|
|
||||||
@UseGuards(AuthGuard("qq"))
|
|
||||||
qqLogin(): void {}
|
|
||||||
|
|
||||||
@Get("oauth/qq/callback")
|
|
||||||
@UseGuards(AuthGuard("qq"))
|
|
||||||
qqCallback(@Req() req: { user: unknown }): {
|
|
||||||
success: boolean;
|
|
||||||
provider: "qq";
|
|
||||||
profile: unknown;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
provider: "qq",
|
|
||||||
profile: req.user
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get("oauth/wechat")
|
|
||||||
@UseGuards(AuthGuard("wechat"))
|
|
||||||
wechatLogin(): void {}
|
|
||||||
|
|
||||||
@Get("oauth/wechat/callback")
|
|
||||||
@UseGuards(AuthGuard("wechat"))
|
|
||||||
wechatCallback(@Req() req: { user: unknown }): {
|
|
||||||
success: boolean;
|
|
||||||
provider: "wechat";
|
|
||||||
profile: unknown;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
provider: "wechat",
|
|
||||||
profile: req.user
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Module } from "@nestjs/common";
|
|
||||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
|
||||||
import { JwtModule } from "@nestjs/jwt";
|
|
||||||
import { PassportModule } from "@nestjs/passport";
|
|
||||||
import { AuthController } from "./auth.controller";
|
|
||||||
import { AuthService } from "./auth.service";
|
|
||||||
import { GithubStrategy } from "./strategies/github.strategy";
|
|
||||||
import { QqStrategy } from "./strategies/qq.strategy";
|
|
||||||
import { WechatStrategy } from "./strategies/wechat.strategy";
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
ConfigModule,
|
|
||||||
PassportModule.register({ session: false }),
|
|
||||||
JwtModule.registerAsync({
|
|
||||||
inject: [ConfigService],
|
|
||||||
useFactory: (configService: ConfigService) => {
|
|
||||||
const expiresInSeconds = Number(configService.get("AUTH_ACCESS_EXPIRES_IN_SECONDS") ?? 900);
|
|
||||||
|
|
||||||
return {
|
|
||||||
secret: configService.get<string>("AUTH_ACCESS_SECRET") ?? "dev-access-secret",
|
|
||||||
signOptions: {
|
|
||||||
expiresIn: expiresInSeconds
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
controllers: [AuthController],
|
|
||||||
providers: [AuthService, GithubStrategy, QqStrategy, WechatStrategy]
|
|
||||||
})
|
|
||||||
export class AuthModule {}
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { JwtService } from "@nestjs/jwt";
|
|
||||||
import { randomUUID } from "node:crypto";
|
|
||||||
import { authenticator } from "@otplib/preset-default";
|
|
||||||
|
|
||||||
type EmailCodeEntry = {
|
|
||||||
code: string;
|
|
||||||
expiresAt: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AuthUser = {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type RefreshTokenEntry = {
|
|
||||||
userId: string;
|
|
||||||
expiresAt: number;
|
|
||||||
revokedAt?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TwoFactorEntry = {
|
|
||||||
secret: string;
|
|
||||||
enabled: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AuthTokenResult = {
|
|
||||||
accessToken: string;
|
|
||||||
tokenType: "Bearer";
|
|
||||||
expiresInSeconds: number;
|
|
||||||
refreshToken: string;
|
|
||||||
refreshExpiresInSeconds: number;
|
|
||||||
user: AuthUser;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AuthService {
|
|
||||||
private readonly emailCodeStore = new Map<string, EmailCodeEntry>();
|
|
||||||
private readonly userStoreByEmail = new Map<string, AuthUser>();
|
|
||||||
private readonly userStoreById = new Map<string, AuthUser>();
|
|
||||||
private readonly refreshTokenStore = new Map<string, RefreshTokenEntry>();
|
|
||||||
private readonly twoFactorStore = new Map<string, TwoFactorEntry>();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly configService: ConfigService,
|
|
||||||
private readonly jwtService: JwtService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async sendEmailCode(
|
|
||||||
email: string
|
|
||||||
): Promise<{ success: boolean; expiresInSeconds: number; debugCode: string }> {
|
|
||||||
const ttlSeconds = Number(this.configService.get("AUTH_EMAIL_CODE_TTL_SECONDS") ?? 300);
|
|
||||||
const code = this.generateCode();
|
|
||||||
const expiresAt = Date.now() + ttlSeconds * 1000;
|
|
||||||
|
|
||||||
this.emailCodeStore.set(email.toLowerCase(), { code, expiresAt });
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
expiresInSeconds: ttlSeconds,
|
|
||||||
debugCode: code
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async loginWithEmailCode(email: string, code: string): Promise<AuthTokenResult> {
|
|
||||||
const lowerEmail = email.toLowerCase();
|
|
||||||
const codeEntry = this.emailCodeStore.get(lowerEmail);
|
|
||||||
|
|
||||||
if (!codeEntry) {
|
|
||||||
throw new UnauthorizedException("验证码不存在或已失效");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (codeEntry.expiresAt < Date.now()) {
|
|
||||||
this.emailCodeStore.delete(lowerEmail);
|
|
||||||
throw new UnauthorizedException("验证码已过期");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (codeEntry.code !== code) {
|
|
||||||
throw new UnauthorizedException("验证码错误");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emailCodeStore.delete(lowerEmail);
|
|
||||||
|
|
||||||
const user = this.getOrCreateUser(lowerEmail);
|
|
||||||
return this.issueTokens(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshTokens(refreshToken: string): Promise<AuthTokenResult> {
|
|
||||||
const entry = this.refreshTokenStore.get(refreshToken);
|
|
||||||
if (!entry) {
|
|
||||||
throw new UnauthorizedException("刷新令牌不存在");
|
|
||||||
}
|
|
||||||
if (entry.revokedAt) {
|
|
||||||
throw new UnauthorizedException("刷新令牌已注销");
|
|
||||||
}
|
|
||||||
if (entry.expiresAt < Date.now()) {
|
|
||||||
this.refreshTokenStore.delete(refreshToken);
|
|
||||||
throw new UnauthorizedException("刷新令牌已过期");
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = this.userStoreById.get(entry.userId);
|
|
||||||
if (!user) {
|
|
||||||
throw new UnauthorizedException("用户不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.revokedAt = Date.now();
|
|
||||||
return this.issueTokens(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
async revokeRefreshToken(refreshToken: string): Promise<{ success: boolean }> {
|
|
||||||
const entry = this.refreshTokenStore.get(refreshToken);
|
|
||||||
if (!entry) {
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.revokedAt = Date.now();
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
async enrollTwoFactor(
|
|
||||||
email: string
|
|
||||||
): Promise<{ userId: string; secret: string; otpauthUrl: string; enabled: boolean }> {
|
|
||||||
const user = this.getOrCreateUser(email.toLowerCase());
|
|
||||||
const secret = authenticator.generateSecret();
|
|
||||||
const issuer = this.configService.get<string>("AUTH_TOTP_ISSUER") ?? "TodoList";
|
|
||||||
const otpauthUrl = authenticator.keyuri(user.email, issuer, secret);
|
|
||||||
|
|
||||||
this.twoFactorStore.set(user.id, {
|
|
||||||
secret,
|
|
||||||
enabled: false
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
userId: user.id,
|
|
||||||
secret,
|
|
||||||
otpauthUrl,
|
|
||||||
enabled: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyTwoFactor(
|
|
||||||
email: string,
|
|
||||||
token: string
|
|
||||||
): Promise<{ success: boolean; enabled: boolean }> {
|
|
||||||
const user = this.getOrCreateUser(email.toLowerCase());
|
|
||||||
const entry = this.twoFactorStore.get(user.id);
|
|
||||||
if (!entry) {
|
|
||||||
throw new UnauthorizedException("尚未启用两步验证");
|
|
||||||
}
|
|
||||||
|
|
||||||
const valid = authenticator.check(token, entry.secret);
|
|
||||||
if (!valid) {
|
|
||||||
throw new UnauthorizedException("两步验证码错误");
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.enabled = true;
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
enabled: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private getOrCreateUser(email: string): AuthUser {
|
|
||||||
const existingUser = this.userStoreByEmail.get(email);
|
|
||||||
if (existingUser) {
|
|
||||||
return existingUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUser = {
|
|
||||||
id: randomUUID(),
|
|
||||||
email
|
|
||||||
};
|
|
||||||
this.userStoreByEmail.set(email, newUser);
|
|
||||||
this.userStoreById.set(newUser.id, newUser);
|
|
||||||
|
|
||||||
return newUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateCode(): string {
|
|
||||||
return String(Math.floor(100000 + Math.random() * 900000));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async issueTokens(user: AuthUser): Promise<AuthTokenResult> {
|
|
||||||
const accessExpiresInSeconds = Number(
|
|
||||||
this.configService.get("AUTH_ACCESS_EXPIRES_IN_SECONDS") ?? 900
|
|
||||||
);
|
|
||||||
const refreshExpiresInSeconds = Number(
|
|
||||||
this.configService.get("AUTH_REFRESH_EXPIRES_IN_SECONDS") ?? 2592000
|
|
||||||
);
|
|
||||||
const accessToken = await this.jwtService.signAsync({
|
|
||||||
sub: user.id,
|
|
||||||
email: user.email
|
|
||||||
});
|
|
||||||
const refreshToken = `${randomUUID()}${randomUUID()}`;
|
|
||||||
|
|
||||||
this.refreshTokenStore.set(refreshToken, {
|
|
||||||
userId: user.id,
|
|
||||||
expiresAt: Date.now() + refreshExpiresInSeconds * 1000
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
accessToken,
|
|
||||||
tokenType: "Bearer",
|
|
||||||
expiresInSeconds: accessExpiresInSeconds,
|
|
||||||
refreshToken,
|
|
||||||
refreshExpiresInSeconds,
|
|
||||||
user
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { IsEmail, IsString, Length, Matches } from "class-validator";
|
|
||||||
|
|
||||||
export class EmailLoginDto {
|
|
||||||
@IsEmail()
|
|
||||||
email!: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@Length(6, 6)
|
|
||||||
@Matches(/^\d{6}$/)
|
|
||||||
code!: string;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { IsString, MinLength } from "class-validator";
|
|
||||||
|
|
||||||
export class RefreshTokenDto {
|
|
||||||
@IsString()
|
|
||||||
@MinLength(20)
|
|
||||||
refreshToken!: string;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { IsEmail } from "class-validator";
|
|
||||||
|
|
||||||
export class SendEmailCodeDto {
|
|
||||||
@IsEmail()
|
|
||||||
email!: string;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { IsEmail } from "class-validator";
|
|
||||||
|
|
||||||
export class TwoFactorEnrollDto {
|
|
||||||
@IsEmail()
|
|
||||||
email!: string;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { IsEmail, IsString, Length, Matches } from "class-validator";
|
|
||||||
|
|
||||||
export class TwoFactorVerifyDto {
|
|
||||||
@IsEmail()
|
|
||||||
email!: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@Length(6, 6)
|
|
||||||
@Matches(/^\d{6}$/)
|
|
||||||
token!: string;
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { PassportStrategy } from "@nestjs/passport";
|
|
||||||
import { Profile, Strategy } from "passport-github2";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class GithubStrategy extends PassportStrategy(Strategy, "github") {
|
|
||||||
constructor(configService: ConfigService) {
|
|
||||||
super({
|
|
||||||
clientID: configService.get<string>("OAUTH_GITHUB_CLIENT_ID") ?? "github-client-id",
|
|
||||||
clientSecret:
|
|
||||||
configService.get<string>("OAUTH_GITHUB_CLIENT_SECRET") ?? "github-client-secret",
|
|
||||||
callbackURL:
|
|
||||||
configService.get<string>("OAUTH_GITHUB_CALLBACK_URL") ??
|
|
||||||
"http://localhost:3000/auth/oauth/github/callback",
|
|
||||||
scope: ["user:email"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async validate(
|
|
||||||
accessToken: string,
|
|
||||||
refreshToken: string,
|
|
||||||
profile: Profile
|
|
||||||
): Promise<{ provider: "github"; accessToken: string; refreshToken: string; profile: Profile }> {
|
|
||||||
return {
|
|
||||||
provider: "github",
|
|
||||||
accessToken,
|
|
||||||
refreshToken,
|
|
||||||
profile
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { PassportStrategy } from "@nestjs/passport";
|
|
||||||
import { Strategy } from "passport-oauth2";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class QqStrategy extends PassportStrategy(Strategy, "qq") {
|
|
||||||
constructor(configService: ConfigService) {
|
|
||||||
super({
|
|
||||||
authorizationURL:
|
|
||||||
configService.get<string>("OAUTH_QQ_AUTH_URL") ?? "https://graph.qq.com/oauth2.0/authorize",
|
|
||||||
tokenURL:
|
|
||||||
configService.get<string>("OAUTH_QQ_TOKEN_URL") ?? "https://graph.qq.com/oauth2.0/token",
|
|
||||||
clientID: configService.get<string>("OAUTH_QQ_CLIENT_ID") ?? "qq-client-id",
|
|
||||||
clientSecret: configService.get<string>("OAUTH_QQ_CLIENT_SECRET") ?? "qq-client-secret",
|
|
||||||
callbackURL:
|
|
||||||
configService.get<string>("OAUTH_QQ_CALLBACK_URL") ??
|
|
||||||
"http://localhost:3000/auth/oauth/qq/callback",
|
|
||||||
scope: ["get_user_info"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async validate(
|
|
||||||
accessToken: string,
|
|
||||||
refreshToken: string
|
|
||||||
): Promise<{ provider: "qq"; accessToken: string; refreshToken: string }> {
|
|
||||||
return {
|
|
||||||
provider: "qq",
|
|
||||||
accessToken,
|
|
||||||
refreshToken
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { PassportStrategy } from "@nestjs/passport";
|
|
||||||
import { Strategy } from "passport-oauth2";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class WechatStrategy extends PassportStrategy(Strategy, "wechat") {
|
|
||||||
constructor(configService: ConfigService) {
|
|
||||||
super({
|
|
||||||
authorizationURL:
|
|
||||||
configService.get<string>("OAUTH_WECHAT_AUTH_URL") ??
|
|
||||||
"https://open.weixin.qq.com/connect/qrconnect",
|
|
||||||
tokenURL:
|
|
||||||
configService.get<string>("OAUTH_WECHAT_TOKEN_URL") ??
|
|
||||||
"https://api.weixin.qq.com/sns/oauth2/access_token",
|
|
||||||
clientID: configService.get<string>("OAUTH_WECHAT_CLIENT_ID") ?? "wechat-client-id",
|
|
||||||
clientSecret:
|
|
||||||
configService.get<string>("OAUTH_WECHAT_CLIENT_SECRET") ?? "wechat-client-secret",
|
|
||||||
callbackURL:
|
|
||||||
configService.get<string>("OAUTH_WECHAT_CALLBACK_URL") ??
|
|
||||||
"http://localhost:3000/auth/oauth/wechat/callback",
|
|
||||||
scope: ["snsapi_login"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async validate(
|
|
||||||
accessToken: string,
|
|
||||||
refreshToken: string
|
|
||||||
): Promise<{ provider: "wechat"; accessToken: string; refreshToken: string }> {
|
|
||||||
return {
|
|
||||||
provider: "wechat",
|
|
||||||
accessToken,
|
|
||||||
refreshToken
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import "reflect-metadata";
|
|
||||||
import { ValidationPipe } from "@nestjs/common";
|
|
||||||
import { NestFactory } from "@nestjs/core";
|
|
||||||
import { AppModule } from "./app.module";
|
|
||||||
|
|
||||||
async function bootstrap(): Promise<void> {
|
|
||||||
const app = await NestFactory.create(AppModule);
|
|
||||||
app.useGlobalPipes(
|
|
||||||
new ValidationPipe({
|
|
||||||
transform: true,
|
|
||||||
whitelist: true,
|
|
||||||
forbidNonWhitelisted: true
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
await app.listen(3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
void bootstrap();
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"exclude": ["node_modules", "dist", "**/*.spec.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
|
||||||
"extends": "../../packages/tsconfig/nest-app.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"rootDir": "src",
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.ts"],
|
|
||||||
"exclude": ["dist", "node_modules"]
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
# ADR-XXXX:<决策标题>
|
|
||||||
|
|
||||||
- 状态:Proposed | Accepted | Deprecated | Superseded
|
|
||||||
- 日期:YYYY-MM-DD
|
|
||||||
- 决策人:<团队/人员>
|
|
||||||
- 关联需求:<Issue/PR/文档链接>
|
|
||||||
|
|
||||||
## 背景
|
|
||||||
|
|
||||||
描述当前问题、约束条件,以及为什么现在必须做出这项决策。
|
|
||||||
|
|
||||||
## 决策驱动因素
|
|
||||||
|
|
||||||
- <驱动因素 1>
|
|
||||||
- <驱动因素 2>
|
|
||||||
- <驱动因素 3>
|
|
||||||
|
|
||||||
## 可选方案
|
|
||||||
|
|
||||||
1. <方案 A>
|
|
||||||
2. <方案 B>
|
|
||||||
3. <方案 C>
|
|
||||||
|
|
||||||
## 最终决策
|
|
||||||
|
|
||||||
选择方案:**<方案 X>**
|
|
||||||
|
|
||||||
说明选择该方案的理由,以及未选择其他方案的原因。
|
|
||||||
|
|
||||||
## 影响评估
|
|
||||||
|
|
||||||
### 正向影响
|
|
||||||
|
|
||||||
- <收益 1>
|
|
||||||
- <收益 2>
|
|
||||||
|
|
||||||
### 负向影响 / 取舍
|
|
||||||
|
|
||||||
- <代价 1>
|
|
||||||
- <代价 2>
|
|
||||||
|
|
||||||
## 实施计划
|
|
||||||
|
|
||||||
1. <步骤 1>
|
|
||||||
2. <步骤 2>
|
|
||||||
3. <步骤 3>
|
|
||||||
|
|
||||||
## 回滚方案
|
|
||||||
|
|
||||||
说明当风险发生时,如何撤销或回退这项决策。
|
|
||||||
|
|
||||||
## 验证清单
|
|
||||||
|
|
||||||
- [ ] 单元测试
|
|
||||||
- [ ] 集成测试
|
|
||||||
- [ ] 性能检查
|
|
||||||
- [ ] 安全检查
|
|
||||||
|
|
||||||
## 参考资料
|
|
||||||
|
|
||||||
- <参考资料 1>
|
|
||||||
- <参考资料 2>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import js from "@eslint/js";
|
|
||||||
import globals from "globals";
|
|
||||||
|
|
||||||
export default [
|
|
||||||
{
|
|
||||||
ignores: ["**/node_modules/**", "**/.turbo/**", "**/dist/**", "**/build/**"]
|
|
||||||
},
|
|
||||||
js.configs.recommended,
|
|
||||||
{
|
|
||||||
files: ["**/*.{js,mjs}"],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: "latest",
|
|
||||||
sourceType: "module",
|
|
||||||
globals: {
|
|
||||||
...globals.node
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ["**/*.cjs"],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: "latest",
|
|
||||||
sourceType: "commonjs",
|
|
||||||
globals: {
|
|
||||||
...globals.node
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "todolist",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "turbo run test",
|
|
||||||
"dev": "turbo run dev --parallel",
|
|
||||||
"build": "turbo run build",
|
|
||||||
"lint": "turbo run lint && eslint .",
|
|
||||||
"typecheck": "turbo run typecheck",
|
|
||||||
"format": "prettier --write .",
|
|
||||||
"lint:staged": "lint-staged",
|
|
||||||
"prepare": "husky"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "GPL-3.0-or-later",
|
|
||||||
"private": true,
|
|
||||||
"packageManager": "pnpm@9.15.2",
|
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/js": "^10.0.1",
|
|
||||||
"eslint": "^10.2.0",
|
|
||||||
"globals": "^17.4.0",
|
|
||||||
"husky": "^9.1.7",
|
|
||||||
"lint-staged": "^16.4.0",
|
|
||||||
"prettier": "^3.8.1",
|
|
||||||
"turbo": "^2.9.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/** @type {import('eslint').Linter.Config} */
|
|
||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es2022: true,
|
|
||||||
node: true
|
|
||||||
},
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: "latest",
|
|
||||||
sourceType: "module"
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"no-console": "warn",
|
|
||||||
"no-debugger": "error"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@todolist/eslint-config",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Shared ESLint config presets for TodoList",
|
|
||||||
"main": "base.cjs",
|
|
||||||
"license": "GPL-3.0-or-later",
|
|
||||||
"private": true,
|
|
||||||
"files": [
|
|
||||||
"base.cjs"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2022",
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "Bundler",
|
|
||||||
"strict": true,
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"forceConsistentCasingInFileNames": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
|
||||||
"extends": "./base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "CommonJS",
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"lib": ["ES2022"],
|
|
||||||
"types": ["node"],
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"sourceMap": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@todolist/tsconfig",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Shared TypeScript config presets for TodoList",
|
|
||||||
"license": "GPL-3.0-or-later",
|
|
||||||
"private": true,
|
|
||||||
"files": [
|
|
||||||
"base.json",
|
|
||||||
"react-app.json",
|
|
||||||
"nest-app.json"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
|
||||||
"extends": "./base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"types": ["vite/client"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-4667
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
|||||||
packages:
|
|
||||||
- "apps/*"
|
|
||||||
- "packages/*"
|
|
||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://turbo.build/schema.json",
|
|
||||||
"tasks": {
|
|
||||||
"dev": {
|
|
||||||
"cache": false,
|
|
||||||
"persistent": true
|
|
||||||
},
|
|
||||||
"build": {
|
|
||||||
"dependsOn": ["^build"],
|
|
||||||
"outputs": ["dist/**", "build/**"]
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"dependsOn": ["^lint"],
|
|
||||||
"outputs": []
|
|
||||||
},
|
|
||||||
"test": {
|
|
||||||
"dependsOn": ["^test"],
|
|
||||||
"outputs": ["coverage/**"]
|
|
||||||
},
|
|
||||||
"typecheck": {
|
|
||||||
"dependsOn": ["^typecheck"],
|
|
||||||
"outputs": []
|
|
||||||
},
|
|
||||||
"format": {
|
|
||||||
"outputs": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user