Compare commits
1 Commits
v2026.07.0
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
676d093568 |
|
|
@ -6,3 +6,4 @@ data/*.db
|
|||
*.log
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
tsconfig.tsbuildinfo
|
||||
|
|
|
|||
14
CLAUDE.md
14
CLAUDE.md
|
|
@ -261,6 +261,13 @@ monitor-ai/
|
|||
| GET | `/api/status-history` | 登录 | 状态变更历史(分页,可按 service_id 筛选) |
|
||||
| GET | `/api/status` | 登录 | 所有服务实时状态汇总 |
|
||||
|
||||
### 内部 API(x-internal-key 鉴权)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/internal/users` | 返回用户列表 |
|
||||
| POST | `/api/internal/users` | OA 同步用户角色 |
|
||||
|
||||
### 管理
|
||||
|
||||
| 方法 | 路径 | 权限 | 说明 |
|
||||
|
|
@ -359,11 +366,12 @@ NODE_TLS_REJECT_UNAUTHORIZED=0
|
|||
|
||||
| 库 | 用途 | 导入路径 |
|
||||
|------|------|------|
|
||||
| `@shared/lib/auth/jwt` | JWT 签名/验证(零依赖,Node crypto) | `import { signJwt, verifyJwt } from '@shared/lib/auth/jwt'` |
|
||||
| `@shared/lib/auth/jwt-v2` | JWT 签名/验证(HS256,含 iss) | `import { signJwtV2, verifyJwtV2 } from '@shared/lib/auth/jwt-v2'` |
|
||||
| `@shared/lib/auth/jwt` | JWT 签名/验证(V1 兼容,无 iss) | `import { signJwt, verifyJwt } from '@shared/lib/auth/jwt'` |
|
||||
| `@shared/lib/auth/middleware-v2` | 路由守卫工厂(V2:单 cookie 模型) | `import { createMiddlewareV2 } from '@shared/lib/auth/middleware-v2'` |
|
||||
| `@shared/lib/auth/middleware` | 路由守卫工厂(V1,已废弃) | `import { createMiddleware } from '@shared/lib/auth/middleware'` |
|
||||
| `@shared/lib/auth/oidc` | OIDC PKCE 流程 | `import { discoverOidcConfig, buildAuthorizeUrl } from '@shared/lib/auth/oidc'` |
|
||||
| `@shared/lib/auth/ldap` | LDAP 认证 | `import { ldapAuth } from '@shared/lib/auth/ldap'` |
|
||||
| `@shared/lib/auth/middleware` | 路由守卫工厂 | `import { createMiddleware } from '@shared/lib/auth/middleware'` |
|
||||
| `@shared/lib/auth/user-sync` | OIDC 用户同步 | `import { syncOidcUser } from '@shared/lib/auth/user-sync'` |
|
||||
| `@shared/lib/alert/alert-manager` | 告警决策引擎 | `import { AlertManager } from '@shared/lib/alert/alert-manager'` |
|
||||
| `@shared/lib/alert/health-checker` | 健康检查引擎 | `import { HealthChecker, HttpChecker, DockerChecker } from '@shared/lib/alert/health-checker'` |
|
||||
| `@shared/lib/audit/write-audit-log` | 审计日志 | `import { writeAuditLog } from '@shared/lib/audit/write-audit-log'` |
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ function getSiteUrl(): string {
|
|||
return process.env.NEXT_PUBLIC_SITE_URL || 'https://monitor.tlyq.ai'
|
||||
}
|
||||
|
||||
/** 清除 tlyq_session + session cookie → 302 跳转 /login */
|
||||
/** 清除 tlyq_session + session + oidc_id_token cookie → 302 跳转 /login */
|
||||
function logoutResponse(): NextResponse {
|
||||
const response = NextResponse.redirect(new URL('/login', getSiteUrl()))
|
||||
response.cookies.set('tlyq_session', '', {
|
||||
|
|
@ -18,8 +18,13 @@ function logoutResponse(): NextResponse {
|
|||
sameSite: 'lax', domain: cookieDomain, path: '/', maxAge: 0,
|
||||
})
|
||||
response.cookies.set('session', '', { path: '/', maxAge: 0 })
|
||||
response.cookies.set('oidc_id_token', '', {
|
||||
httpOnly: true, secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax', domain: cookieDomain, path: '/', maxAge: 0,
|
||||
})
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
export async function GET() { return logoutResponse() }
|
||||
export async function POST() { return logoutResponse() }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// GET /api/internal/users — 供 OA 查询 monitor 用户列表
|
||||
import { NextResponse } from 'next/server'
|
||||
// POST /api/internal/users — 供 OA 同步用户角色
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { execFileSync } from 'child_process'
|
||||
|
||||
const INTERNAL_KEY = process.env.INTERNAL_API_KEY || 'oa-internal-key-tlyq-2026'
|
||||
|
|
@ -20,3 +21,31 @@ export async function GET(request: Request) {
|
|||
return NextResponse.json({ users: [] })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const key = request.headers.get('x-internal-key')
|
||||
if (key !== INTERNAL_KEY) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
|
||||
const body = await request.json()
|
||||
const { username, displayName, role } = body
|
||||
if (!username) {
|
||||
return NextResponse.json({ error: 'username 必填' }, { status: 400 })
|
||||
}
|
||||
const VALID_ROLES = ['admin', 'editor', 'viewer']
|
||||
const safeRole = VALID_ROLES.includes(role) ? role : 'viewer'
|
||||
|
||||
try {
|
||||
execFileSync('sqlite3', [DB_PATH], {
|
||||
input: `INSERT INTO users (username, display_name, role, is_active, created_at, updated_at)
|
||||
VALUES ('${username.replace(/'/g, "''")}', '${(displayName || username).replace(/'/g, "''")}', '${safeRole}', 1,
|
||||
datetime('now', '+8 hours'), datetime('now', '+8 hours'))
|
||||
ON CONFLICT(username) DO UPDATE SET role = '${safeRole}',
|
||||
display_name = '${(displayName || username).replace(/'/g, "''")}',
|
||||
updated_at = datetime('now', '+8 hours');`,
|
||||
timeout: 3000,
|
||||
})
|
||||
return NextResponse.json({ success: true })
|
||||
} catch {
|
||||
return NextResponse.json({ error: '同步失败' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,15 @@ import { writeAuditLog } from '@/lib/audit'
|
|||
import { hasPermission } from '@/lib/permissions'
|
||||
import { getRole } from '@/lib/get-role'
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(request: NextRequest) {
|
||||
const token = request.cookies.get('tlyq_session')?.value
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
const payload = verifyJwt(token, authConfig.jwtSecret)
|
||||
if (!payload) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
if (!hasPermission(getRole(payload), 'services:view')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
const services = dbQuery(`SELECT * FROM services WHERE enabled = 1 ORDER BY display_order, id`)
|
||||
// 解析 JSON checks 字段
|
||||
const result = services.map(s => ({ ...s, checks: JSON.parse(String(s.checks || '[]')) }))
|
||||
|
|
|
|||
|
|
@ -1,8 +1,20 @@
|
|||
// GET /api/status-history — 状态变更列表
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { verifyJwt } from '@shared/lib/auth/jwt'
|
||||
import { authConfig } from '@/lib/auth-config'
|
||||
import { dbQuery } from '@/lib/db'
|
||||
import { hasPermission } from '@/lib/permissions'
|
||||
import { getRole } from '@/lib/get-role'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const token = request.cookies.get('tlyq_session')?.value
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
const payload = verifyJwt(token, authConfig.jwtSecret)
|
||||
if (!payload) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
if (!hasPermission(getRole(payload), 'status_history:view')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const page = Number(searchParams.get('page')) || 1
|
||||
const limit = 20
|
||||
|
|
|
|||
|
|
@ -1,8 +1,19 @@
|
|||
// GET /api/status — 所有服务实时状态摘要
|
||||
import { NextResponse } from 'next/server'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { verifyJwt } from '@shared/lib/auth/jwt'
|
||||
import { authConfig } from '@/lib/auth-config'
|
||||
import { dbQuery } from '@/lib/db'
|
||||
import { hasPermission } from '@/lib/permissions'
|
||||
import { getRole } from '@/lib/get-role'
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(request: NextRequest) {
|
||||
const token = request.cookies.get('tlyq_session')?.value
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
const payload = verifyJwt(token, authConfig.jwtSecret)
|
||||
if (!payload) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
if (!hasPermission(getRole(payload), 'services:view')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
const services = dbQuery('SELECT id, name, category, alert_level, current_status, status_since FROM services WHERE enabled = 1 ORDER BY display_order, id')
|
||||
const counts = { normal: 0, abnormal: 0, unknown: 0 }
|
||||
services.forEach((s: Record<string, unknown>) => { const st = String(s.current_status); if (st === 'normal') counts.normal++; else if (st === 'abnormal') counts.abnormal++; else counts.unknown++ })
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue