File size: 3,232 Bytes
c7d34c1
 
 
 
 
 
 
3e8ea5d
c7d34c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e8ea5d
c7d34c1
 
 
 
32151b6
c7d34c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import { appLogger, readAppLogEntries, subscribeAppLogs, type AppLogEntry } from '@/lib/app-logger';
import { verifyPasswordHash } from '@/lib/server-runtime';
import { NextRequest, NextResponse } from 'next/server';

export const dynamic = 'force-dynamic';

function isAuthorized(request: NextRequest): boolean {
    const appPassword = process.env.APP_PASSWORD?.trim();
    if (!appPassword) return false;

    const authorization = request.headers.get('authorization') || '';
    const [scheme, passwordHash] = authorization.split(/\s+/, 2);
    if (scheme.toLowerCase() !== 'bearer') return false;
    return typeof passwordHash === 'string' && verifyPasswordHash(passwordHash, appPassword);
}

function encodeSseEvent(encoder: TextEncoder, entry: AppLogEntry): Uint8Array {
    return encoder.encode(`data: ${JSON.stringify(entry)}\n\n`);
}

function encodeSseComment(encoder: TextEncoder, value: string): Uint8Array {
    return encoder.encode(`: ${value}\n\n`);
}

export async function GET(request: NextRequest) {
    if (!process.env.APP_PASSWORD?.trim()) {
        return NextResponse.json({ error: '日志查看需要先配置 APP_PASSWORD。' }, { status: 403 });
    }

    if (!isAuthorized(request)) {
        return NextResponse.json({ error: '未授权:缺少或无效的访问码哈希。' }, { status: 401 });
    }

    appLogger.info('日志查看器已连接。');

    const encoder = new TextEncoder();
    let unsubscribe = () => {};
    const readableStream = new ReadableStream({
        start(controller) {
            controller.enqueue(encodeSseComment(encoder, 'connected'));
            const sendEntry = (entry: AppLogEntry) => {
                try {
                    controller.enqueue(encodeSseEvent(encoder, entry));
                } catch (error) {
                    appLogger.warn('日志查看器发送实时日志失败。', {
                        error: error instanceof Error ? error.message : String(error)
                    });
                    unsubscribe();
                }
            };

            readAppLogEntries().forEach((entry) => {
                try {
                    controller.enqueue(encodeSseEvent(encoder, entry));
                } catch (error) {
                    appLogger.warn('日志查看器发送历史日志失败。', {
                        error: error instanceof Error ? error.message : String(error)
                    });
                    unsubscribe();
                }
            });
            unsubscribe = subscribeAppLogs(sendEntry);

            request.signal.addEventListener(
                'abort',
                () => {
                    unsubscribe();
                    try {
                        controller.close();
                    } catch {
                        // 连接可能已由客户端关闭。
                    }
                },
                { once: true }
            );
        },
        cancel() {
            unsubscribe();
        }
    });

    return new Response(readableStream, {
        headers: {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache, no-transform',
            Connection: 'keep-alive'
        }
    });
}