用户变动撤销所有刷新token,立即失效accesstoken
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { invalidateDeviceAccessToken } from '@/lib/access-token-invalidation';
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { db, getStorage } from '@/lib/db';
|
||||
@@ -156,6 +157,7 @@ export async function DELETE(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
invalidateDeviceAccessToken(targetUsername, tokenId);
|
||||
await revokeRefreshToken(targetUsername, tokenId);
|
||||
const storage = getStorage();
|
||||
await storage.deletePushSubscriptionsByTokenId?.(targetUsername, tokenId);
|
||||
|
||||
@@ -2,14 +2,29 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { invalidateUserAccessTokens } from '@/lib/access-token-invalidation';
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { db } from '@/lib/db';
|
||||
import { db, getStorage } from '@/lib/db';
|
||||
import { sanitizeFeaturePermissions } from '@/lib/feature-permissions';
|
||||
import { revokeAllRefreshTokens } from '@/lib/refresh-token';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
// 支持的操作类型
|
||||
|
||||
async function terminateUserSessions(username: string, reason: string): Promise<void> {
|
||||
try {
|
||||
invalidateUserAccessTokens(username);
|
||||
await revokeAllRefreshTokens(username);
|
||||
const storage = getStorage();
|
||||
await storage.deleteAllPushSubscriptions?.(username);
|
||||
console.log(`Terminated all sessions for ${username}: ${reason}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to terminate sessions for ${username}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
const ACTIONS = [
|
||||
'add',
|
||||
'ban',
|
||||
@@ -194,6 +209,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// 只更新V2存储
|
||||
await db.updateUserInfoV2(targetUsername!, { banned: true });
|
||||
await terminateUserSessions(targetUsername!, 'user banned');
|
||||
break;
|
||||
}
|
||||
case 'unban': {
|
||||
@@ -262,6 +278,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// 只更新V2存储
|
||||
await db.updateUserInfoV2(targetUsername!, { role: 'user' });
|
||||
await terminateUserSessions(targetUsername!, 'admin role revoked');
|
||||
break;
|
||||
}
|
||||
case 'changePassword': {
|
||||
@@ -296,6 +313,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// 使用新版本修改密码(SHA256加密)
|
||||
await db.changePasswordV2(targetUsername!, targetPassword);
|
||||
await terminateUserSessions(targetUsername!, 'password changed by admin');
|
||||
break;
|
||||
}
|
||||
case 'deleteUser': {
|
||||
@@ -321,7 +339,8 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// 只删除V2存储中的用户
|
||||
// 先终止目标用户所有会话,再删除V2存储中的用户
|
||||
await terminateUserSessions(targetUsername!, 'user deleted');
|
||||
await db.deleteUserV2(targetUsername!);
|
||||
|
||||
break;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { invalidateDeviceAccessToken, invalidateUserAccessTokens } from '@/lib/access-token-invalidation';
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getStorage } from '@/lib/db';
|
||||
import {
|
||||
@@ -51,6 +52,7 @@ export async function DELETE(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Token ID required' }, { status: 400 });
|
||||
}
|
||||
|
||||
invalidateDeviceAccessToken(authInfo.username, tokenId);
|
||||
await revokeRefreshToken(authInfo.username, tokenId);
|
||||
const storage = getStorage();
|
||||
await storage.deletePushSubscriptionsByTokenId?.(authInfo.username, tokenId);
|
||||
@@ -71,6 +73,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
invalidateUserAccessTokens(authInfo.username);
|
||||
await revokeAllRefreshTokens(authInfo.username);
|
||||
const storage = getStorage();
|
||||
await storage.deleteAllPushSubscriptions?.(authInfo.username);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie, parseAuthInfo } from '@/lib/auth';
|
||||
import { db } from '@/lib/db';
|
||||
import { refreshAccessToken } from '@/lib/middleware-auth';
|
||||
import { TOKEN_CONFIG } from '@/lib/refresh-token';
|
||||
|
||||
@@ -73,6 +74,26 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
if (authInfo.username === process.env.USERNAME) {
|
||||
if (authInfo.role !== 'owner') {
|
||||
return NextResponse.json({ error: 'Role changed' }, { status: 401 });
|
||||
}
|
||||
} else {
|
||||
const userInfo = await db.getUserInfoV2(authInfo.username);
|
||||
|
||||
if (!userInfo) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (userInfo.banned) {
|
||||
return NextResponse.json({ error: 'User banned' }, { status: 403 });
|
||||
}
|
||||
|
||||
if (userInfo.role !== authInfo.role) {
|
||||
return NextResponse.json({ error: 'Role changed' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
// 只检查 Refresh Token 是否过期
|
||||
if (now >= authInfo.refreshExpires) {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { invalidateDeviceAccessToken } from '@/lib/access-token-invalidation';
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getStorage } from '@/lib/db';
|
||||
import { db } from '@/lib/db';
|
||||
@@ -59,6 +60,7 @@ export async function POST(request: NextRequest) {
|
||||
// 撤销所有非当前设备的 token
|
||||
for (const device of devices) {
|
||||
if (device.tokenId !== currentTokenId) {
|
||||
invalidateDeviceAccessToken(username, device.tokenId);
|
||||
await revokeRefreshToken(username, device.tokenId);
|
||||
await storage.deletePushSubscriptionsByTokenId?.(username, device.tokenId);
|
||||
console.log(`Revoked token ${device.tokenId} for ${username} after password change`);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { invalidateDeviceAccessToken } from '@/lib/access-token-invalidation';
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getStorage } from '@/lib/db';
|
||||
import { revokeRefreshToken } from '@/lib/refresh-token';
|
||||
@@ -12,6 +13,7 @@ export async function POST(request: NextRequest) {
|
||||
// 撤销当前设备的 Refresh Token
|
||||
if (authInfo && authInfo.username && authInfo.tokenId) {
|
||||
try {
|
||||
invalidateDeviceAccessToken(authInfo.username, authInfo.tokenId);
|
||||
await revokeRefreshToken(authInfo.username, authInfo.tokenId);
|
||||
const storage = getStorage();
|
||||
await storage.deletePushSubscriptionsByTokenId?.(authInfo.username, authInfo.tokenId);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { AuthInfo } from './auth';
|
||||
|
||||
const userInvalidBefore = new Map<string, number>();
|
||||
const deviceInvalidBefore = new Map<string, number>();
|
||||
|
||||
function makeDeviceKey(username: string, tokenId: string): string {
|
||||
return `${username}:${tokenId}`;
|
||||
}
|
||||
|
||||
export function invalidateUserAccessTokens(username: string, invalidatedAt = Date.now()): void {
|
||||
const current = userInvalidBefore.get(username) || 0;
|
||||
if (invalidatedAt > current) {
|
||||
userInvalidBefore.set(username, invalidatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
export function invalidateDeviceAccessToken(
|
||||
username: string,
|
||||
tokenId: string,
|
||||
invalidatedAt = Date.now()
|
||||
): void {
|
||||
const key = makeDeviceKey(username, tokenId);
|
||||
const current = deviceInvalidBefore.get(key) || 0;
|
||||
if (invalidatedAt > current) {
|
||||
deviceInvalidBefore.set(key, invalidatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
export function isAccessTokenInvalidated(authInfo: AuthInfo | null): boolean {
|
||||
if (!authInfo?.username || !authInfo.timestamp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const userInvalidatedAt = userInvalidBefore.get(authInfo.username);
|
||||
if (userInvalidatedAt && authInfo.timestamp <= userInvalidatedAt) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (authInfo.tokenId) {
|
||||
const deviceInvalidatedAt = deviceInvalidBefore.get(
|
||||
makeDeviceKey(authInfo.username, authInfo.tokenId)
|
||||
);
|
||||
if (deviceInvalidatedAt && authInfo.timestamp <= deviceInvalidatedAt) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getAccessTokenInvalidationState() {
|
||||
return {
|
||||
userInvalidBefore,
|
||||
deviceInvalidBefore,
|
||||
};
|
||||
}
|
||||
+5
-2
@@ -1,5 +1,7 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
import { isAccessTokenInvalidated } from './access-token-invalidation';
|
||||
|
||||
export type AuthInfo = {
|
||||
password?: string;
|
||||
username?: string;
|
||||
@@ -65,7 +67,7 @@ export function getAuthInfoFromCookie(request: NextRequest): AuthInfo | null {
|
||||
const headerValue = getAuthTokenFromHeader(authHeader);
|
||||
const headerAuthInfo = parseAuthInfo(headerValue);
|
||||
if (headerAuthInfo) {
|
||||
return headerAuthInfo;
|
||||
return isAccessTokenInvalidated(headerAuthInfo) ? null : headerAuthInfo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +77,8 @@ export function getAuthInfoFromCookie(request: NextRequest): AuthInfo | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parseAuthInfo(authCookie.value);
|
||||
const authInfo = parseAuthInfo(authCookie.value);
|
||||
return isAccessTokenInvalidated(authInfo) ? null : authInfo;
|
||||
}
|
||||
|
||||
// 从cookie获取认证信息 (客户端使用)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { isAccessTokenInvalidated } from '@/lib/access-token-invalidation';
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { TOKEN_CONFIG } from '@/lib/refresh-token';
|
||||
import { isTVModeEnabled } from '@/lib/tv-mode';
|
||||
@@ -89,6 +90,11 @@ export async function middleware(request: NextRequest) {
|
||||
return handleAuthFailure(request, pathname);
|
||||
}
|
||||
|
||||
if (isAccessTokenInvalidated(authInfo)) {
|
||||
console.log(`Access token invalidated for ${authInfo.username}`);
|
||||
return handleAuthFailure(request, pathname);
|
||||
}
|
||||
|
||||
// 签名验证通过
|
||||
// 注意:Token 续期由前端负责,Middleware 不再自动刷新
|
||||
return NextResponse.next();
|
||||
|
||||
Reference in New Issue
Block a user