Compare commits
5 Commits
v2026.07.0
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
676d093568 | |
|
|
eb31132fa0 | |
|
|
90d65d3fb2 | |
|
|
b3b10cb162 | |
|
|
67e8979362 |
|
|
@ -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'` |
|
||||
|
|
|
|||
|
|
@ -4,11 +4,18 @@ services:
|
|||
build: .
|
||||
container_name: monitor-ai
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "fetch('http://localhost:3000/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
ports:
|
||||
- "6181:3000"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./data:/app/data
|
||||
- ./.next:/app/.next
|
||||
env_file: .env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ npx tsx scripts/monitor-worker.ts &
|
|||
WORKER_PID=$! || true
|
||||
|
||||
# 启动 Next.js standalone server
|
||||
node server.js &
|
||||
HOSTNAME=0.0.0.0 node server.js &
|
||||
NEXT_PID=$!
|
||||
|
||||
echo "[entrypoint] Worker PID: $WORKER_PID, Next.js PID: $NEXT_PID"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { NextConfig } from 'next'
|
|||
|
||||
const config: NextConfig = {
|
||||
output: 'standalone',
|
||||
transpilePackages: ['lucide-react'],
|
||||
transpilePackages: ['lucide-react', 'ldapts'],
|
||||
}
|
||||
|
||||
export default config
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
// src/app/admin/layout.tsx — V2 admin 路径鉴权(Layer 2:独立验签 + DB role 检查)
|
||||
// V2 middleware 不检查 role,由 layout/API 层自行鉴权
|
||||
import { cookies } from 'next/headers'
|
||||
import { verifyJwtV2, extractIss } from '@shared/lib/auth/jwt-v2'
|
||||
import { authConfig } from '@/lib/auth-config'
|
||||
import { dbQueryParams } from '@/lib/db'
|
||||
|
||||
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
const cookieStore = await cookies()
|
||||
const token = cookieStore.get('tlyq_session')?.value
|
||||
|
||||
if (!token) {
|
||||
return <Forbidden />
|
||||
}
|
||||
|
||||
// 提取 iss → 自引用验签
|
||||
const iss = extractIss(token)
|
||||
if (!iss) {
|
||||
return <Forbidden />
|
||||
}
|
||||
|
||||
const jwtSecret = authConfig.jwtSecret
|
||||
const payload = verifyJwtV2(token, jwtSecret, iss)
|
||||
if (!payload) {
|
||||
return <Forbidden />
|
||||
}
|
||||
|
||||
// 从本地 DB 查询 role
|
||||
const rows = dbQueryParams<{ role: string }>(
|
||||
'SELECT role FROM users WHERE username = ? AND is_active = 1',
|
||||
[payload.username]
|
||||
)
|
||||
const role = rows[0]?.role || 'viewer'
|
||||
|
||||
if (role !== 'admin') {
|
||||
return <Forbidden />
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
function Forbidden() {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '60vh' }}>
|
||||
<p style={{ color: '#999', fontSize: 18 }}>Forbidden</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -4,12 +4,13 @@ import { verifyJwt } from '@shared/lib/auth/jwt'
|
|||
import { authConfig } from '@/lib/auth-config'
|
||||
import { dbQueryParams, dbExec } 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 || !hasPermission(String(payload.role || 'viewer'), 'audit:view')) {
|
||||
if (!payload || !hasPermission(getRole(payload), 'audit:view')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +49,7 @@ export async function DELETE(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 || !hasPermission(String(payload.role || 'viewer'), 'audit:view')) {
|
||||
if (!payload || !hasPermission(getRole(payload), 'audit:view')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ import { authConfig } from '@/lib/auth-config'
|
|||
import { dbQueryParams, dbExec } from '@/lib/db'
|
||||
import { writeAuditLog } from '@/lib/audit'
|
||||
import { hasPermission, PERMISSIONS } 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 || !hasPermission(String(payload.role || 'viewer'), 'roles:manage')) {
|
||||
if (!payload || !hasPermission(getRole(payload), 'roles:manage')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ export async function PUT(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 || !hasPermission(String(payload.role || 'viewer'), 'roles:manage')) {
|
||||
if (!payload || !hasPermission(getRole(payload), 'roles:manage')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ import { authConfig } from '@/lib/auth-config'
|
|||
import { dbQueryParams, dbExec } from '@/lib/db'
|
||||
import { writeAuditLog } from '@/lib/audit'
|
||||
import { hasPermission } from '@/lib/permissions'
|
||||
import { getRole } from '@/lib/get-role'
|
||||
|
||||
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
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 || !hasPermission(String(payload.role || 'viewer'), 'users:manage')) {
|
||||
if (!payload || !hasPermission(getRole(payload), 'users:manage')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import { verifyJwt } from '@shared/lib/auth/jwt'
|
|||
import { authConfig } from '@/lib/auth-config'
|
||||
import { dbQueryParams } 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 || !hasPermission(String(payload.role || 'viewer'), 'users:view')) {
|
||||
if (!payload || !hasPermission(getRole(payload), 'users:view')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ import { NextRequest, NextResponse } from 'next/server'
|
|||
import { verifyJwt } from '@shared/lib/auth/jwt'
|
||||
import { authConfig } from '@/lib/auth-config'
|
||||
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 || !hasPermission(String(payload.role || 'viewer'), 'dashboard:view')) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
if (!payload || !hasPermission(getRole(payload), 'dashboard:view')) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
|
||||
return NextResponse.json({ status: 'running', uptime: process.uptime() })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ import { authConfig } from '@/lib/auth-config'
|
|||
import { dbQuery, dbExec, escapeSql } from '@/lib/db'
|
||||
import { writeAuditLog } from '@/lib/audit'
|
||||
import { hasPermission } from '@/lib/permissions'
|
||||
import { getRole } from '@/lib/get-role'
|
||||
|
||||
async function checkAdmin(request: NextRequest) {
|
||||
const token = request.cookies.get('tlyq_session')?.value
|
||||
if (!token) return null
|
||||
const payload = verifyJwt(token, authConfig.jwtSecret)
|
||||
if (!payload || !hasPermission(String(payload.role || 'viewer'), 'alerts:manage')) return null
|
||||
if (!payload || !hasPermission(getRole(payload), 'alerts:manage')) return null
|
||||
return payload
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ import { authConfig } from '@/lib/auth-config'
|
|||
import { dbQuery } from '@/lib/db'
|
||||
import { WeChatPusher } from '@shared/lib/wechat/wechat-pusher'
|
||||
import { hasPermission } from '@/lib/permissions'
|
||||
import { getRole } from '@/lib/get-role'
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
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 || !hasPermission(String(payload.role || 'viewer'), 'alerts:manage')) {
|
||||
if (!payload || !hasPermission(getRole(payload), 'alerts:manage')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
const { id } = await params
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ import { authConfig } from '@/lib/auth-config'
|
|||
import { dbQuery, dbExec, escapeSql } from '@/lib/db'
|
||||
import { writeAuditLog } from '@/lib/audit'
|
||||
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 || !hasPermission(String(payload.role || 'viewer'), 'alerts:manage')) {
|
||||
if (!payload || !hasPermission(getRole(payload), 'alerts:manage')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
const channels = dbQuery('SELECT id, name, channel_type, webhook_url, enabled, level_critical, level_warning, level_info, quiet_enabled, quiet_start, quiet_end, quiet_bypass_critical, cooldown_minutes, created_at, updated_at FROM alert_channels ORDER BY id')
|
||||
|
|
@ -21,7 +22,7 @@ export async function POST(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 || !hasPermission(String(payload.role || 'viewer'), 'alerts:manage')) {
|
||||
if (!payload || !hasPermission(getRole(payload), 'alerts:manage')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
const body = await request.json()
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ 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 || !hasPermission(String(payload.role || 'viewer'), 'alerts:view')) {
|
||||
if (!payload || !hasPermission(getRole(payload), 'alerts:view')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
const { searchParams } = new URL(request.url)
|
||||
|
|
|
|||
|
|
@ -1,107 +1,53 @@
|
|||
// GET /api/auth/callback — OIDC callback 处理
|
||||
// GET /api/auth/callback — OIDC callback(V2:使用 shared handleOidcCallback 工厂)
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { exchangeCodeForToken, getUserinfo } from '@shared/lib/auth/oidc'
|
||||
import { signJwt } from '@shared/lib/auth/jwt'
|
||||
import { syncOidcUser } from '@shared/lib/auth/user-sync'
|
||||
import { handleOidcCallback } from '@shared/lib/auth/handle-callback'
|
||||
import { authConfig } from '@/lib/auth-config'
|
||||
import { dbQueryParams, dbExec } from '@/lib/db'
|
||||
import { writeAuditLog } from '@/lib/audit'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const code = searchParams.get('code')
|
||||
const state = searchParams.get('state')
|
||||
const error = searchParams.get('error')
|
||||
return handleOidcCallback(request, {
|
||||
oidc: {
|
||||
autheliaUrl: authConfig.autheliaUrl,
|
||||
clientId: authConfig.oidcClientId,
|
||||
clientSecret: authConfig.oidcClientSecret,
|
||||
redirectUri: authConfig.oidcRedirectUri,
|
||||
},
|
||||
jwtSecret: authConfig.jwtSecret,
|
||||
cookieDomain: authConfig.cookieDomain,
|
||||
|
||||
// 使用 OIDC_REDIRECT_URI 构造公共 URL(LESSONS-LEARNED #16)
|
||||
const baseUrl = authConfig.oidcRedirectUri?.replace(/\/api\/auth\/callback.*/, '') || 'http://localhost:6181'
|
||||
|
||||
if (error) {
|
||||
return NextResponse.redirect(new URL(`/login?error=${encodeURIComponent(error)}`, baseUrl))
|
||||
}
|
||||
|
||||
if (!code || !state) {
|
||||
return NextResponse.redirect(new URL('/login?error=missing_params', baseUrl))
|
||||
}
|
||||
|
||||
// 验证 state
|
||||
const savedState = request.cookies.get('oidc_state')?.value
|
||||
if (!savedState || savedState !== state) {
|
||||
return NextResponse.redirect(new URL('/login?error=state_mismatch', baseUrl))
|
||||
}
|
||||
|
||||
// 取出 code_verifier
|
||||
const codeVerifier = request.cookies.get('oidc_code_verifier')?.value
|
||||
if (!codeVerifier) {
|
||||
return NextResponse.redirect(new URL('/login?error=missing_verifier', baseUrl))
|
||||
}
|
||||
|
||||
// 换取 token
|
||||
const tokenResult = await exchangeCodeForToken(
|
||||
{ autheliaUrl: authConfig.autheliaUrl, clientId: authConfig.oidcClientId, clientSecret: authConfig.oidcClientSecret, redirectUri: authConfig.oidcRedirectUri },
|
||||
code, codeVerifier,
|
||||
)
|
||||
|
||||
if (!tokenResult.success) {
|
||||
return NextResponse.redirect(new URL(`/login?error=${encodeURIComponent(tokenResult.error || 'token_exchange_failed')}`, baseUrl))
|
||||
}
|
||||
|
||||
// 验证 nonce
|
||||
const savedNonce = request.cookies.get('oidc_nonce')?.value
|
||||
if (savedNonce && tokenResult.nonce && savedNonce !== tokenResult.nonce) {
|
||||
return NextResponse.redirect(new URL('/login?error=nonce_mismatch', baseUrl))
|
||||
}
|
||||
|
||||
// 获取 userinfo(带错误处理)
|
||||
let userinfo
|
||||
try {
|
||||
userinfo = await getUserinfo(authConfig.autheliaUrl, tokenResult.accessToken)
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'userinfo_fetch_failed'
|
||||
return NextResponse.redirect(new URL(`/login?error=${encodeURIComponent(msg)}`, baseUrl))
|
||||
}
|
||||
|
||||
// 用户同步(使用参数化查询)
|
||||
const user = syncOidcUser({
|
||||
getUser: (username) => {
|
||||
const rows = dbQueryParams<{ id: number; role: string }>('SELECT id, role FROM users WHERE username = ?', [username])
|
||||
const rows = dbQueryParams<{ id: number; role: string }>(
|
||||
'SELECT id, role FROM users WHERE username = ? AND is_active = 1', [username]
|
||||
)
|
||||
return rows[0] ?? null
|
||||
},
|
||||
|
||||
createUser: (username, displayName, email) => {
|
||||
dbExec('INSERT INTO users (username, display_name, email, role) VALUES (?, ?, ?, ?)', [username, displayName, email, 'viewer'])
|
||||
// 使用同一连接查询 last_insert_rowid(通过合并为单条 SQL)
|
||||
dbExec(
|
||||
'INSERT INTO users (username, display_name, email, role) VALUES (?, ?, ?, ?)',
|
||||
[username, displayName, email, 'viewer']
|
||||
)
|
||||
const row = dbQueryParams<{ id: number }>('SELECT last_insert_rowid() AS id', [])
|
||||
return { id: row[0]?.id ?? 0, role: 'viewer' }
|
||||
},
|
||||
|
||||
updateUser: (username, displayName, email) => {
|
||||
dbExec('UPDATE users SET display_name = ?, email = ?, updated_at = datetime(\'now\', \'+8 hours\') WHERE username = ?', [displayName, email, username])
|
||||
dbExec(
|
||||
"UPDATE users SET display_name = ?, email = ?, updated_at = datetime('now', '+8 hours') WHERE username = ?",
|
||||
[displayName, email, username]
|
||||
)
|
||||
},
|
||||
}, userinfo)
|
||||
|
||||
// 签发 JWT
|
||||
const token = signJwt({
|
||||
secret: authConfig.jwtSecret,
|
||||
payload: { username: userinfo.preferred_username, displayName: userinfo.name, role: user.role },
|
||||
onAuditLog: (userId, username, req) => {
|
||||
writeAuditLog({
|
||||
userId,
|
||||
username,
|
||||
action: 'login',
|
||||
entityType: 'auth',
|
||||
details: { method: 'oidc' },
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const response = NextResponse.redirect(new URL('/dashboard', baseUrl))
|
||||
|
||||
// 签发 tlyq_session cookie
|
||||
response.cookies.set('tlyq_session', token, {
|
||||
httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', domain: process.env.NODE_ENV === 'production' ? authConfig.cookieDomain : undefined, path: '/', maxAge: 604800,
|
||||
})
|
||||
|
||||
// 清理 OIDC 临时 cookie
|
||||
response.cookies.delete('oidc_code_verifier')
|
||||
response.cookies.delete('oidc_state')
|
||||
response.cookies.delete('oidc_nonce')
|
||||
|
||||
// 审计日志
|
||||
writeAuditLog({
|
||||
userId: user.id, username: userinfo.preferred_username, action: 'login',
|
||||
entityType: 'auth', details: { method: 'oidc', isNew: user.isNew },
|
||||
ipAddress: request.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,16 @@
|
|||
// GET /api/auth/login/oidc — OIDC SSO 重定向
|
||||
import { NextResponse } from 'next/server'
|
||||
import { generatePkce, generateState, buildAuthorizeUrl } from '@shared/lib/auth/oidc'
|
||||
// GET /api/auth/login/oidc — OIDC SSO 重定向(V2:使用 shared handleOidcLogin 工厂)
|
||||
import { handleOidcLogin } from '@shared/lib/auth/handle-login'
|
||||
import { authConfig } from '@/lib/auth-config'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const url = new URL(request.url)
|
||||
const switchUser = url.searchParams.get('switch') === '1'
|
||||
|
||||
const { codeVerifier, codeChallenge } = generatePkce()
|
||||
const state = generateState()
|
||||
const nonce = generateState()
|
||||
|
||||
const authorizeUrl = buildAuthorizeUrl(
|
||||
{ autheliaUrl: authConfig.autheliaUrl, clientId: authConfig.oidcClientId, clientSecret: authConfig.oidcClientSecret, redirectUri: authConfig.oidcRedirectUri },
|
||||
{ codeChallenge, state, nonce, ...(switchUser && { prompt: 'login' }) },
|
||||
)
|
||||
|
||||
const response = NextResponse.redirect(authorizeUrl)
|
||||
|
||||
// 切换账号时清除旧 session cookie
|
||||
if (switchUser) {
|
||||
response.cookies.set('tlyq_session', '', { maxAge: 0, path: '/' })
|
||||
response.cookies.set('session', '', { maxAge: 0, path: '/' })
|
||||
}
|
||||
|
||||
// 存储 PKCE 参数到 httpOnly cookie(5 分钟过期)
|
||||
const cookieOpts = { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' as const, maxAge: 300, path: '/' }
|
||||
response.cookies.set('oidc_code_verifier', codeVerifier, cookieOpts)
|
||||
response.cookies.set('oidc_state', state, cookieOpts)
|
||||
response.cookies.set('oidc_nonce', nonce, cookieOpts)
|
||||
|
||||
return response
|
||||
return handleOidcLogin({
|
||||
autheliaUrl: authConfig.autheliaUrl,
|
||||
clientId: authConfig.oidcClientId,
|
||||
clientSecret: authConfig.oidcClientSecret,
|
||||
redirectUri: authConfig.oidcRedirectUri,
|
||||
switchUser,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// POST /api/auth/login — LDAP 本地登录(回退通道)
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { signJwt } from '@shared/lib/auth/jwt'
|
||||
import { signJwtV2 } from '@shared/lib/auth/jwt-v2'
|
||||
import { ldapAuth } from '@shared/lib/auth/ldap'
|
||||
import { authConfig } from '@/lib/auth-config'
|
||||
import { dbQuery, dbQueryParams } from '@/lib/db'
|
||||
|
|
@ -51,7 +51,7 @@ export async function POST(request: NextRequest) {
|
|||
resetRateLimit(rateLimitKey)
|
||||
const displayName = localadminUser.display_name || 'localadmin'
|
||||
const role = localadminUser.role || 'admin'
|
||||
const token = signJwt({ secret: authConfig.jwtSecret, payload: { username: 'localadmin', role, displayName } })
|
||||
const token = signJwtV2({ secret: authConfig.jwtSecret, iss: 'monitor.tlyq.ai', payload: { username: 'localadmin', displayName } })
|
||||
|
||||
writeAuditLog({
|
||||
username, action: 'login', entityType: 'auth',
|
||||
|
|
@ -95,7 +95,7 @@ export async function POST(request: NextRequest) {
|
|||
ipAddress: ip,
|
||||
})
|
||||
|
||||
const token = signJwt({ secret: authConfig.jwtSecret, payload: { username, role, displayName: result.displayName || username } })
|
||||
const token = signJwtV2({ secret: authConfig.jwtSecret, iss: 'monitor.tlyq.ai', payload: { username, displayName: result.displayName || username } })
|
||||
const response = NextResponse.json({
|
||||
user: { username, role, displayName: result.displayName || username },
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,30 @@
|
|||
// POST /api/auth/logout — 退出登录
|
||||
// GET + POST /api/auth/logout — 退出登录(清除 cookie + 302 跳转 /login)
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authConfig } from '@/lib/auth-config'
|
||||
|
||||
export async function POST() {
|
||||
// 清除 tlyq_session cookie
|
||||
const logoutUrl = `${authConfig.autheliaUrl}/api/oidc/end_session`
|
||||
const response = NextResponse.redirect(logoutUrl)
|
||||
response.cookies.set('tlyq_session', '', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', domain: authConfig.cookieDomain, path: '/', maxAge: 0 })
|
||||
const cookieDomain = process.env.COOKIE_DOMAIN || '.tlyq.ai'
|
||||
|
||||
/** 从 OIDC_REDIRECT_URI 提取 site URL(不可信请求头,见 LESSONS-LEARNED #51) */
|
||||
function getSiteUrl(): string {
|
||||
const redirectUri = process.env.OIDC_REDIRECT_URI || ''
|
||||
try { const u = new URL(redirectUri); return `${u.protocol}//${u.host}` } catch { /* fallthrough */ }
|
||||
return process.env.NEXT_PUBLIC_SITE_URL || 'https://monitor.tlyq.ai'
|
||||
}
|
||||
|
||||
/** 清除 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', '', {
|
||||
httpOnly: true, secure: process.env.NODE_ENV === 'production',
|
||||
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,7 +1,8 @@
|
|||
// GET /api/auth/me — 当前用户信息
|
||||
// GET /api/auth/me — 当前用户信息(V2:JWT 不含 role,从本地 DB 查询)
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { verifyJwt } from '@shared/lib/auth/jwt'
|
||||
import { verifyJwtV2, extractIss } from '@shared/lib/auth/jwt-v2'
|
||||
import { authConfig } from '@/lib/auth-config'
|
||||
import { dbQueryParams } from '@/lib/db'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const token = request.cookies.get('tlyq_session')?.value
|
||||
|
|
@ -9,16 +10,24 @@ export async function GET(request: NextRequest) {
|
|||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 })
|
||||
}
|
||||
|
||||
const payload = verifyJwt(token, authConfig.jwtSecret)
|
||||
// Layer 3 defense-in-depth:提取 iss → 自引用验签
|
||||
const iss = extractIss(token) || '*'
|
||||
const payload = verifyJwtV2(token, authConfig.jwtSecret, iss)
|
||||
if (!payload) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 })
|
||||
}
|
||||
|
||||
const username = payload.username as string
|
||||
|
||||
// 从本地 DB 查询角色(V2 JWT 不含 role,认证与鉴权分离)
|
||||
const rows = dbQueryParams<{ role: string; display_name: string }>(
|
||||
'SELECT role, display_name FROM users WHERE username = ? AND is_active = 1',
|
||||
[username]
|
||||
)
|
||||
const role = rows[0]?.role || 'viewer'
|
||||
const displayName = rows[0]?.display_name || (payload.displayName as string) || username
|
||||
|
||||
return NextResponse.json({
|
||||
user: {
|
||||
username: payload.username,
|
||||
display_name: payload.displayName || payload.username,
|
||||
role: payload.role || 'viewer',
|
||||
},
|
||||
user: { username, display_name: displayName, role },
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
// GET /api/health — monitor-ai 自身健康检查
|
||||
export async function GET() {
|
||||
return Response.json({ status: 'OK', timestamp: new Date().toISOString() })
|
||||
const d = new Date()
|
||||
const timestamp = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}T${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}+08:00`
|
||||
return Response.json({ status: 'OK', timestamp })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
// GET /api/internal/roles — 供 OA 查询 monitor 支持的角色列表
|
||||
import { NextResponse } from 'next/server'
|
||||
import { ROLE_DEFAULT_PERMISSIONS, PERMISSIONS } from '@/lib/permissions'
|
||||
|
||||
const INTERNAL_KEY = process.env.INTERNAL_API_KEY || 'oa-internal-key-tlyq-2026'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const key = request.headers.get('x-internal-key')
|
||||
if (key !== INTERNAL_KEY) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
|
||||
const roles = Object.keys(ROLE_DEFAULT_PERMISSIONS).map(name => ({
|
||||
name,
|
||||
permissions: ROLE_DEFAULT_PERMISSIONS[name].map(k => PERMISSIONS.find(p => p.key === k)?.name || k),
|
||||
}))
|
||||
|
||||
return NextResponse.json({ roles })
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// GET /api/internal/users — 供 OA 查询 monitor 用户列表
|
||||
// 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'
|
||||
const DB_PATH = process.env.DATABASE_PATH || '/app/data/monitor.db'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const key = request.headers.get('x-internal-key')
|
||||
if (key !== INTERNAL_KEY) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
|
||||
try {
|
||||
const out = execFileSync('sqlite3', [DB_PATH, 'SELECT username, display_name, role FROM users WHERE is_active = 1 ORDER BY username'], { timeout: 3000, encoding: 'utf8' }).trim()
|
||||
const users = out.split('\n').filter(Boolean).map(line => {
|
||||
const [username, display_name, role] = line.split('|')
|
||||
return { username, display_name: display_name || username, role }
|
||||
})
|
||||
return NextResponse.json({ users })
|
||||
} catch {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -5,13 +5,14 @@ import { authConfig } from '@/lib/auth-config'
|
|||
import { dbQuery } from '@/lib/db'
|
||||
import { HttpChecker, DockerChecker, HealthChecker } from '@shared/lib/alert/health-checker'
|
||||
import { hasPermission } from '@/lib/permissions'
|
||||
import { getRole } from '@/lib/get-role'
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
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(String(payload.role || 'viewer'), 'services:manage')) {
|
||||
if (!hasPermission(getRole(payload), 'services:manage')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ import { authConfig } from '@/lib/auth-config'
|
|||
import { dbQuery } from '@/lib/db'
|
||||
import { writeAuditLog } from '@/lib/audit'
|
||||
import { hasPermission } from '@/lib/permissions'
|
||||
import { getRole } from '@/lib/get-role'
|
||||
import { containerOp, type ContainerAction } from '@/lib/container-ops'
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
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 || !hasPermission(String(payload.role || 'viewer'), 'services:manage')) {
|
||||
if (!payload || !hasPermission(getRole(payload), 'services:manage')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ import { authConfig } from '@/lib/auth-config'
|
|||
import { dbQuery, dbExec, escapeSql } from '@/lib/db'
|
||||
import { writeAuditLog } from '@/lib/audit'
|
||||
import { hasPermission } from '@/lib/permissions'
|
||||
import { getRole } from '@/lib/get-role'
|
||||
|
||||
async function checkAdmin(request: NextRequest) {
|
||||
const token = request.cookies.get('tlyq_session')?.value
|
||||
if (!token) return null
|
||||
const payload = verifyJwt(token, authConfig.jwtSecret)
|
||||
if (!payload) return null
|
||||
if (!hasPermission(String(payload.role || 'viewer'), 'services:manage')) return null
|
||||
if (!hasPermission(getRole(payload), 'services:manage')) return null
|
||||
return payload
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,17 @@ import { authConfig } from '@/lib/auth-config'
|
|||
import { dbQuery, dbExec, escapeSql } from '@/lib/db'
|
||||
import { writeAuditLog } from '@/lib/audit'
|
||||
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), 'services:view')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
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 || '[]')) }))
|
||||
|
|
@ -19,7 +28,7 @@ export async function POST(request: NextRequest) {
|
|||
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(String(payload.role || 'viewer'), 'services:manage')) {
|
||||
if (!hasPermission(getRole(payload), 'services:manage')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,10 +1,23 @@
|
|||
// 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++ })
|
||||
return NextResponse.json({ services, counts, timestamp: new Date().toISOString() })
|
||||
const d = new Date()
|
||||
const timestamp = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}T${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}+08:00`
|
||||
return NextResponse.json({ services, counts, timestamp })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
// 服务端专用:从 JWT + DB 获取角色和权限(含 child_process 依赖,不可在客户端导入)
|
||||
import { verifyJwt } from '@shared/lib/auth/jwt'
|
||||
import { authConfig } from '@/lib/auth-config'
|
||||
import { dbQueryParams } from '@/lib/db'
|
||||
import { hasPermission } from '@/lib/permissions'
|
||||
|
||||
export function getRole(payload: Record<string, unknown> | null): string {
|
||||
if (!payload?.username) return 'viewer'
|
||||
const rows = dbQueryParams<{ role: string }>(
|
||||
'SELECT role FROM users WHERE username = ? AND is_active = 1',
|
||||
[payload.username as string]
|
||||
)
|
||||
return rows[0]?.role || 'viewer'
|
||||
}
|
||||
|
||||
export function checkPermission(request: { cookies: { get(name: string): { value: string } | undefined } }, permissionKey: string): boolean {
|
||||
const token = request.cookies.get('tlyq_session')?.value
|
||||
if (!token) return false
|
||||
const payload = verifyJwt(token, authConfig.jwtSecret)
|
||||
if (!payload) return false
|
||||
return hasPermission(getRole(payload), permissionKey)
|
||||
}
|
||||
|
|
@ -85,8 +85,8 @@ export class MonitorWorker {
|
|||
}
|
||||
|
||||
// 数据清理(每日,UTC+8 时区)
|
||||
const now = new Date()
|
||||
const today = new Date(now.getTime() + 8 * 3600000).toISOString().slice(0, 10)
|
||||
const d = new Date()
|
||||
const today = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
|
||||
if (today !== this.lastCleanupDate) {
|
||||
this.cleanupOldData()
|
||||
this.lastCleanupDate = today
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
// src/lib/permissions.ts — RBAC 权限定义 + 检查函数
|
||||
import { verifyJwt } from '@shared/lib/auth/jwt'
|
||||
import { authConfig } from '@/lib/auth-config'
|
||||
// src/lib/permissions.ts — RBAC 权限定义 + 客户端安全函数(不含 DB 依赖)
|
||||
export const PERMISSIONS = [
|
||||
{ key: 'dashboard:view', name: '查看仪表盘' },
|
||||
{ key: 'services:view', name: '查看服务列表' },
|
||||
|
|
@ -15,27 +13,17 @@ export const PERMISSIONS = [
|
|||
{ key: 'roles:manage', name: '管理角色权限' },
|
||||
] as const
|
||||
|
||||
// 角色默认权限映射
|
||||
export const ROLE_DEFAULT_PERMISSIONS: Record<string, string[]> = {
|
||||
admin: PERMISSIONS.map(p => p.key),
|
||||
editor: ['dashboard:view', 'services:view', 'alerts:view', 'status_history:view', 'status_history:stats'],
|
||||
viewer: ['dashboard:view', 'services:view', 'status_history:view'],
|
||||
}
|
||||
|
||||
// 检查角色是否拥有某项权限
|
||||
export function hasPermission(role: string, permissionKey: string): boolean {
|
||||
if (role === 'localadmin') return true
|
||||
if (role === 'admin') return true // admin 拥有全部权限
|
||||
if (role === 'admin') return true
|
||||
const perms = ROLE_DEFAULT_PERMISSIONS[role]
|
||||
return perms ? perms.includes(permissionKey) : false
|
||||
}
|
||||
|
||||
// API 服务端权限检查(从 cookie 中读取 role,验证 JWT 签名)
|
||||
export function checkPermission(request: { cookies: { get(name: string): { value: string } | undefined } }, permissionKey: string): boolean {
|
||||
const token = request.cookies.get('tlyq_session')?.value
|
||||
if (!token) return false
|
||||
const payload = verifyJwt(token, authConfig.jwtSecret)
|
||||
if (!payload) return false
|
||||
const role = (payload.role as string) || 'viewer'
|
||||
return hasPermission(role, permissionKey)
|
||||
}
|
||||
// checkPermission 已移至 @/lib/get-role.ts(服务端专用,API routes 请从 get-role 导入)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
// src/middleware.ts — 使用共享 middleware 工厂
|
||||
import { createMiddleware } from '@shared/lib/auth/middleware'
|
||||
// src/middleware.ts — V2 单 cookie 模型,Edge 验签 + iss 校验
|
||||
import { createMiddlewareV2 } from '@shared/lib/auth/middleware-v2'
|
||||
|
||||
export const middleware = createMiddleware({
|
||||
adminPaths: ['/admin', '/services', '/settings', '/alerts'],
|
||||
const jwtSecret = process.env.JWT_SECRET || 'oa-shared-jwt-secret-tlyq-2026'
|
||||
const cookieDomain = process.env.COOKIE_DOMAIN || '.tlyq.ai'
|
||||
|
||||
export const middleware = createMiddlewareV2({
|
||||
jwtSecret,
|
||||
cookieDomain,
|
||||
allowedIssuers: ['*'],
|
||||
enableApiKey: false,
|
||||
publicPaths: ['/login', '/api/auth', '/api/health', '/api/internal', '/_next', '/favicon.ico'],
|
||||
})
|
||||
|
||||
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] }
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue