增加浏览器离线通知功能

This commit is contained in:
mtvpls
2026-06-02 20:50:33 +08:00
parent 16e48bef8b
commit d600317ade
20 changed files with 1739 additions and 91 deletions
+5
View File
@@ -3,6 +3,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getStorage } from '@/lib/db';
import {
getUserDevices,
revokeAllRefreshTokens,
@@ -51,6 +52,8 @@ export async function DELETE(request: NextRequest) {
}
await revokeRefreshToken(authInfo.username, tokenId);
const storage = getStorage();
await storage.deletePushSubscriptionsByTokenId?.(authInfo.username, tokenId);
return NextResponse.json({ ok: true });
} catch (error) {
@@ -69,6 +72,8 @@ export async function POST(request: NextRequest) {
try {
await revokeAllRefreshTokens(authInfo.username);
const storage = getStorage();
await storage.deleteAllPushSubscriptions?.(authInfo.username);
const response = NextResponse.json({ ok: true });
+3
View File
@@ -3,6 +3,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getStorage } from '@/lib/db';
import { db } from '@/lib/db';
import { getUserDevices, revokeRefreshToken } from '@/lib/refresh-token';
@@ -53,11 +54,13 @@ export async function POST(request: NextRequest) {
try {
const currentTokenId = authInfo.tokenId;
const devices = await getUserDevices(username);
const storage = getStorage();
// 撤销所有非当前设备的 token
for (const device of devices) {
if (device.tokenId !== currentTokenId) {
await revokeRefreshToken(username, device.tokenId);
await storage.deletePushSubscriptionsByTokenId?.(username, device.tokenId);
console.log(`Revoked token ${device.tokenId} for ${username} after password change`);
}
}
+3
View File
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getStorage } from '@/lib/db';
import { revokeRefreshToken } from '@/lib/refresh-token';
export const runtime = 'nodejs';
@@ -12,6 +13,8 @@ export async function POST(request: NextRequest) {
if (authInfo && authInfo.username && authInfo.tokenId) {
try {
await revokeRefreshToken(authInfo.username, authInfo.tokenId);
const storage = getStorage();
await storage.deletePushSubscriptionsByTokenId?.(authInfo.username, authInfo.tokenId);
} catch (error) {
console.error('Failed to revoke refresh token:', error);
}
+111
View File
@@ -0,0 +1,111 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getStorage } from '@/lib/db';
import {
createPushSubscriptionRecord,
getVapidPublicKey,
isWebPushConfigured,
} from '@/lib/web-push';
export const runtime = 'nodejs';
function extractSubscriptionKeys(subscription: any) {
const p256dh = subscription?.keys?.p256dh || subscription?.toJSON?.()?.keys?.p256dh;
const auth = subscription?.keys?.auth || subscription?.toJSON?.()?.keys?.auth;
return { p256dh, auth };
}
export async function GET(request: NextRequest) {
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo?.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const storage = getStorage();
const [configured, publicKey, subscriptions] = await Promise.all([
isWebPushConfigured(storage),
getVapidPublicKey(storage),
storage.getEnabledPushSubscriptions
? storage.getEnabledPushSubscriptions(authInfo.username)
: Promise.resolve([]),
]);
const currentDeviceSubscriptions = authInfo.tokenId
? subscriptions.filter((item) => item.tokenId === authInfo.tokenId)
: [];
return NextResponse.json({
configured,
publicKey,
pushNotifications: currentDeviceSubscriptions.length > 0,
hasDeviceToken: Boolean(authInfo.tokenId),
subscriptionCount: subscriptions.length,
currentDeviceSubscriptionCount: currentDeviceSubscriptions.length,
});
}
export async function POST(request: NextRequest) {
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo?.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { subscription, enabled } = body;
const storage = getStorage();
if (enabled === false) {
if (authInfo.tokenId) {
await storage.deletePushSubscriptionsByTokenId?.(authInfo.username, authInfo.tokenId);
}
return NextResponse.json({ ok: true });
}
if (subscription) {
const { p256dh, auth } = extractSubscriptionKeys(subscription);
if (!subscription.endpoint || !p256dh || !auth) {
return NextResponse.json({ error: 'Invalid push subscription' }, { status: 400 });
}
if (!authInfo.tokenId) {
return NextResponse.json(
{ error: '当前登录模式不支持设备级浏览器通知订阅' },
{ status: 400 }
);
}
await storage.upsertPushSubscription?.(
authInfo.username,
createPushSubscriptionRecord({
username: authInfo.username,
tokenId: authInfo.tokenId,
endpoint: subscription.endpoint,
p256dh,
auth,
userAgent: request.headers.get('user-agent'),
})
);
}
return NextResponse.json({ ok: true });
}
export async function DELETE(request: NextRequest) {
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo?.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json().catch(() => ({}));
const storage = getStorage();
if (body?.endpoint) {
await storage.deletePushSubscriptionByEndpoint?.(authInfo.username, body.endpoint);
} else if (authInfo.tokenId) {
await storage.deletePushSubscriptionsByTokenId?.(authInfo.username, authInfo.tokenId);
}
return NextResponse.json({ ok: true });
}
+7 -5
View File
@@ -6,7 +6,7 @@ import { getStorage } from '@/lib/db';
export const runtime = 'nodejs';
/**
* GET - 获取用户邮箱设置
* GET - 获取用户通知设置
*/
export async function GET(request: NextRequest) {
const authInfo = getAuthInfoFromCookie(request);
@@ -31,7 +31,7 @@ export async function GET(request: NextRequest) {
emailNotifications,
});
} catch (error) {
console.error('获取用户邮箱设置失败:', error);
console.error('获取用户通知设置失败:', error);
return NextResponse.json(
{ error: (error as Error).message },
{ status: 500 }
@@ -40,7 +40,7 @@ export async function GET(request: NextRequest) {
}
/**
* POST - 保存用户邮箱设置
* POST - 保存用户通知设置
*/
export async function POST(request: NextRequest) {
const authInfo = getAuthInfoFromCookie(request);
@@ -76,12 +76,14 @@ export async function POST(request: NextRequest) {
}
}
return NextResponse.json({
success: true,
message: '邮箱设置保存成功',
message: '通知设置保存成功',
});
} catch (error) {
console.error('保存用户邮箱设置失败:', error);
console.error('保存用户通知设置失败:', error);
return NextResponse.json(
{ error: (error as Error).message },
{ status: 500 }