diff --git a/CHANGELOG.md b/CHANGELOG.md index 65cda11..b9c95b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # 变更日志 +## 2026-05-14 + +- [修复] SSO 新用户免登录失败:根页面 `getCurrentUser()` 改为直接 `redirect('/dashboard')`,由中间件统一处理共享 JWT 认证(与 assets-ai 行为一致) +- [修复] `next.config.ts` 添加 `ldapts` 到 `serverExternalPackages`,确保 Next.js standalone 构建包含 LLDAP 客户端模块,避免 `ldapUserExists()` 因模块缺失失败导致 SSO 自动创建用户静默中断 +- [调整] 全局证书切换:Cloudflare Origin CA → Let's Encrypt(`/etc/letsencrypt/live/www.tlyq.ai/`),覆盖全部 7 个子域名,nginx 8 个站点配置同步更新 + +## 2026-05-12 + +- [部署] 云端 JWT 密钥统一为 oa-shared-jwt-secret-tlyq-2026,三站点密钥一致 +- [部署] docker-compose 移除 AUTHELIA_URL,添加 LDAP/COOKIE/INTERNAL_API_KEY 环境变量 +- [部署] LLDAP admin 密码更新为 3Vm!Y!@RCiPs +- [部署] nginx 移除 auth_request,恢复纯反向代理;改为运行时 DNS 解析 +- [新增] `src/lib/db-schema.ts`:users 表新增 last_login_at / last_active_at 列 +- [新增] `src/lib/auth.ts`:getCurrentUser() 更新 last_active_at +- [新增] `src/app/api/auth/login/route.ts`:登录时更新 last_login_at 和 last_active_at + +## 2026-05-11 + +- [新增] `src/lib/ldap.ts`:`ldapUserExists()` 函数,检查 LLDAP 中用户是否存在(admin bind 搜索,不可达时容错放行) +- [新增] `src/lib/jwt-shared.ts`:共享 JWT 签发/验证(`tlyq_session` cookie,HS256,与 OA/assets 共用密钥) +- [调整] `src/lib/auth.ts`:`getCurrentUser()` 优先 `tlyq_session`,加入 LLDAP 存在性检查,用户被删除后自动清除 cookie 踢出 +- [调整] `src/app/api/auth/login/route.ts`:LDAP 优先认证 + 本地密码缓存回退 + localadmin 应急用户直连 +- [调整] `src/app/api/auth/logout/route.ts`:同时清除 `session_issue` 和 `tlyq_session` +- [调整] `src/app/api/auth/me/route.ts`:移除 SSO header 路径,改用 `getCurrentUser()` 统一获取 +- [调整] `src/middleware.ts`:优先 `tlyq_session` → 回退 `session_issue`,移除 SSO 代理路径,放行 `/api/internal/` +- [新增] `src/app/api/internal/roles/route.ts`:内部 API,返回站点可用角色列表(INTERNAL_API_KEY 鉴权) +- [新增] `src/app/api/users/[id]/route.ts`:admin/localadmin 用户禁止删除和修改角色 +- [修复] `src/components/tickets/TicketList.tsx`:已办工单点击业务IP/节点名称跳转到待办页面,改用 `usePathname()` 保持当前页面并正确筛选 +- [调整] `src/app/(app)/settings/users/page.tsx`:admin/localadmin 用户隐藏删除按钮,编辑时角色字段显示为只读 +- [新增] `src/lib/db-schema.ts`:预置 localadmin 应急用户(admin 角色,纯本地 BCrypt 认证) + ## 2026-05-07 - [新增] 月报跨月进行中工单支持:第一章折线图覆盖未结单离线天数,第二章标注"处理中",第三章显示"进行中"/"—",第四章标注"仅计本月部分" diff --git a/CLAUDE.md b/CLAUDE.md index 18156bd..94eb76a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,11 +99,16 @@ npm run import # 导入工单 ### 认证 +登录逻辑(v2.1):LDAP 优先 + 本地密码缓存回退 + localadmin 应急用户。 +登录成功签发两个 cookie:`session_issue`(本地 JWT,7 天)+ `tlyq_session`(共享 JWT,7 天,domain=.tlyq.ai)。 +中间件优先检查 `tlyq_session`,回退 `session_issue`。`getCurrentUser()` 每次验证时检查 LLDAP 用户是否存在(已删除则清除 cookie 踢出)。 + | 方法 | 路径 | 说明 | |------|------|------| -| POST | `/api/auth/login` | 登录(username + password → JWT cookie) | -| POST | `/api/auth/logout` | 登出 | +| POST | `/api/auth/login` | 登录(LDAP 优先 + 本地回退) | +| POST | `/api/auth/logout` | 登出(清除两个 cookie) | | GET | `/api/auth/me` | 当前用户信息 | +| GET | `/api/internal/roles` | 内部 API:返回角色列表(x-internal-key 鉴权) | ### 工单 @@ -133,6 +138,14 @@ npm run import # 导入工单 --- +## 认证机制 + +- **Web UI(v2.1)**:`middleware.ts` 优先检查 `tlyq_session`(共享 JWT,OA 统一签发)→ 回退 `session_issue`(本地 JWT)。`getCurrentUser()` 每次请求时检查 LLDAP 用户是否存在,已删除则清除 cookie 踢出 +- **localadmin**:纯本地 BCrypt 认证,不依赖 LLDAP,用于 LLDAP 故障时应急登录(DB 预置,admin 角色) +- **API Key**:`Bearer ak_<32位十六进制>`,存储时 SHA-256 hash,由 `ALLOWED_API_KEYS` 控制 + +--- + ## 环境配置 ### 本地与云端差异 @@ -225,7 +238,9 @@ NEXT_PUBLIC_ASSETS_URL=https://assets.tlyq.ai - **新增 API**:在 `src/app/api/` 下创建路由 → 顶部调用 `initDatabase()` → `getCurrentUser()` 验证 → `hasPermission()` 校验 - **新增页面**:在 `src/app/(app)/` 下创建 → 布局由 `(app)/layout.tsx` 提供 - **权限格式**:`resource:action`,如 `hasPermission(user, 'tickets:write')` -- **日期处理**:禁止使用 `Date.toISOString()` 格式化本地日期。`toISOString()` 返回 UTC 时间,在中国时区(UTC+8)下 `new Date('2026-04-01T00:00:00').toISOString()` 会返回 `"2026-03-31T16:00:00.000Z"`,日期偏移一天。应使用本地时间方法拼接:`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}` +- **日期处理(时区规范)**:整个系统统一使用 UTC+8(北京时间)。两处必须遵守: + 1. **JavaScript/TypeScript**:禁止使用 `Date.toISOString()` 格式化本地日期。`toISOString()` 返回 UTC 时间,在中国时区(UTC+8)下 `new Date('2026-04-01T00:00:00').toISOString()` 会返回 `"2026-03-31T16:00:00.000Z"`,日期偏移一天。应使用本地时间方法拼接:`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}` + 2. **SQLite**:所有 `datetime('now')` 必须写成 `datetime('now', '+8 hours')`,包括 CREATE TABLE 的 DEFAULT 值、UPDATE/SET 语句、以及查询条件中的时间比较。禁止使用不含时区偏移的 `datetime('now')`。 --- diff --git a/Dockerfile b/Dockerfile index b3590b9..cae7bcc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,8 +33,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libcairo2 \ && rm -rf /var/lib/apt/lists/* COPY --from=builder /app/package.json /app/package-lock.json ./ -RUN npm install --omit=dev COPY --from=builder /app/.next/standalone ./ +RUN npm install --omit=dev && \ + npm rebuild better-sqlite3 && \ + rm -rf node_modules/@img/sharp-linuxmusl-x64 node_modules/@img/sharp-libvips-linuxmusl-x64 \ + node_modules/@next/swc-linux-x64-musl COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public RUN mkdir -p /app/data /app/uploads /app/reports diff --git a/docker-compose.yml b/docker-compose.yml index 0ea31df..50e77ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,14 +10,21 @@ services: - issue-reports:/app/reports # .next 目录从主机挂载,npm run build 后直接生效,无需重建镜像 - ./.next:/app/.next + # 运行时从 LLDAP 容器动态读取 admin 密码 + - /var/run/docker.sock:/var/run/docker.sock environment: - DATABASE_PATH=/app/data/issue.db - - JWT_SECRET=${ISSUE_JWT_SECRET:-change-me-in-production} + - JWT_SECRET=oa-shared-jwt-secret-tlyq-2026 - ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin123} - ASSETS_API_URL=${ASSETS_API_URL:-https://assets.tlyq.ai/api} - ASSETS_API_KEY=${ASSETS_API_KEY} - ALLOWED_API_KEYS=${ALLOWED_API_KEYS} - NODE_ENV=production + - COOKIE_DOMAIN=.tlyq.ai + - AUTHELIA_URL=${AUTHELIA_URL:-https://sso.tlyq.ai} + - LDAP_URL=ldap://lldap:3890 + - LDAP_BASE_DN=dc=tlyq,dc=ai + - LDAP_ADMIN_DN=uid=admin,ou=people,dc=tlyq,dc=ai - TZ=Asia/Shanghai restart: unless-stopped networks: diff --git a/next.config.ts b/next.config.ts index dd4b3dd..966379f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,7 +4,9 @@ const nextConfig: NextConfig = { images: { unoptimized: true }, eslint: { ignoreDuringBuilds: true }, typescript: { ignoreBuildErrors: true }, - serverExternalPackages: ['better-sqlite3'], + // better-sqlite3: 原生模块,必须 external + // ldapts: SSO 自动创建用户依赖 LLDAP 验证,缺少则新用户无法免登录进入系统 + serverExternalPackages: ['better-sqlite3', 'ldapts'], // 确保 fs.readFileSync 加载的文件也被追踪到 standalone 输出中 // 防止 Docker 镜像中 npm install --omit=dev 漏装时缺失依赖 outputFileTracingIncludes: { diff --git a/oa-cloud-fixed.png b/oa-cloud-fixed.png new file mode 100644 index 0000000..a8bc38a Binary files /dev/null and b/oa-cloud-fixed.png differ diff --git a/oa-cloud-login.png b/oa-cloud-login.png new file mode 100644 index 0000000..7005a2f Binary files /dev/null and b/oa-cloud-login.png differ diff --git a/oa-login-current.png b/oa-login-current.png new file mode 100644 index 0000000..5a45c50 Binary files /dev/null and b/oa-login-current.png differ diff --git a/oa-login-page.png b/oa-login-page.png new file mode 100644 index 0000000..55293b4 Binary files /dev/null and b/oa-login-page.png differ diff --git a/package-lock.json b/package-lock.json index 6304c15..66a79ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "echarts": "^5.5.0", "jsonwebtoken": "^9.0.2", "jszip": "^3.10.1", + "ldapts": "^8.1.7", "lucide-react": "^1.8.0", "next": "^15.1.0", "puppeteer": "^23.0.0", @@ -3592,6 +3593,18 @@ "json-buffer": "3.0.1" } }, + "node_modules/ldapts": { + "version": "8.1.7", + "resolved": "https://registry.npmjs.org/ldapts/-/ldapts-8.1.7.tgz", + "integrity": "sha512-TJl6T92eIwMf/OJ0hDfKVa6ISwzo+lqCWCI5Mf//ARlKa3LKQZaSrme/H2rCRBhW0DZCQlrsV+fgoW5YHRNLUw==", + "license": "MIT", + "dependencies": { + "strict-event-emitter-types": "2.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4978,6 +4991,12 @@ "text-decoder": "^1.1.0" } }, + "node_modules/strict-event-emitter-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", + "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==", + "license": "ISC" + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", diff --git a/package.json b/package.json index 72b2643..bd57457 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "echarts": "^5.5.0", "jsonwebtoken": "^9.0.2", "jszip": "^3.10.1", + "ldapts": "^8.1.7", "lucide-react": "^1.8.0", "next": "^15.1.0", "puppeteer": "^23.0.0", diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/app/(app)/settings/users/page.tsx b/src/app/(app)/settings/users/page.tsx index 58911a0..416249a 100644 --- a/src/app/(app)/settings/users/page.tsx +++ b/src/app/(app)/settings/users/page.tsx @@ -11,6 +11,8 @@ interface User { role: string is_active: number created_at: string + last_login_at: string | null + is_online: number } export default function UsersPage() { @@ -85,7 +87,7 @@ export default function UsersPage() { {loading ? (
{editUser.role === 'admin' ? '管理员' : '管理员(系统保留)'}
{error}