新登录验证机制
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import {
|
||||
getUserDevices,
|
||||
revokeAllRefreshTokens,
|
||||
revokeRefreshToken,
|
||||
} from '@/lib/refresh-token';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
// 获取所有设备
|
||||
export async function GET(request: NextRequest) {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const devices = await getUserDevices(authInfo.username);
|
||||
|
||||
// 标记当前设备
|
||||
const devicesWithCurrent = devices.map((device) => ({
|
||||
...device,
|
||||
isCurrent: device.tokenId === authInfo.tokenId,
|
||||
}));
|
||||
|
||||
return NextResponse.json({ devices: devicesWithCurrent });
|
||||
} catch (error) {
|
||||
console.error('Failed to get devices:', error);
|
||||
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 撤销指定设备
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { tokenId } = await request.json();
|
||||
|
||||
if (!tokenId) {
|
||||
return NextResponse.json({ error: 'Token ID required' }, { status: 400 });
|
||||
}
|
||||
|
||||
await revokeRefreshToken(authInfo.username, tokenId);
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to revoke device:', error);
|
||||
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 登出所有设备
|
||||
export async function POST(request: NextRequest) {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
await revokeAllRefreshTokens(authInfo.username);
|
||||
|
||||
const response = NextResponse.json({ ok: true });
|
||||
|
||||
// 清除当前设备的 Cookie
|
||||
response.cookies.set('auth', '', {
|
||||
path: '/',
|
||||
expires: new Date(0),
|
||||
sameSite: 'lax',
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Failed to revoke all devices:', error);
|
||||
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,12 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { db } from '@/lib/db';
|
||||
import {
|
||||
generateRefreshToken,
|
||||
generateTokenId,
|
||||
storeRefreshToken,
|
||||
TOKEN_CONFIG,
|
||||
} from '@/lib/refresh-token';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -30,18 +36,66 @@ async function generateSignature(
|
||||
.join('');
|
||||
}
|
||||
|
||||
// 获取设备信息
|
||||
function getDeviceInfo(userAgent: string): string {
|
||||
const ua = userAgent.toLowerCase();
|
||||
|
||||
if (ua.includes('mobile') || ua.includes('android') || ua.includes('iphone')) {
|
||||
if (ua.includes('android')) return 'Android Mobile';
|
||||
if (ua.includes('iphone')) return 'iPhone';
|
||||
return 'Mobile Device';
|
||||
}
|
||||
|
||||
if (ua.includes('tablet') || ua.includes('ipad')) {
|
||||
return 'Tablet';
|
||||
}
|
||||
|
||||
if (ua.includes('windows')) return 'Windows PC';
|
||||
if (ua.includes('mac')) return 'Mac';
|
||||
if (ua.includes('linux')) return 'Linux';
|
||||
|
||||
return 'Unknown Device';
|
||||
}
|
||||
|
||||
// 生成认证Cookie
|
||||
async function generateAuthCookie(
|
||||
username: string,
|
||||
role: 'owner' | 'admin' | 'user'
|
||||
role: 'owner' | 'admin' | 'user',
|
||||
deviceInfo: string
|
||||
): Promise<string> {
|
||||
const authData: any = { role };
|
||||
|
||||
if (username && process.env.PASSWORD) {
|
||||
authData.username = username;
|
||||
const signature = await generateSignature(username, process.env.PASSWORD);
|
||||
authData.signature = signature;
|
||||
authData.timestamp = Date.now();
|
||||
|
||||
// 生成签名(包含 username, role, timestamp)
|
||||
const dataToSign = JSON.stringify({
|
||||
username: authData.username,
|
||||
role: authData.role,
|
||||
timestamp: authData.timestamp
|
||||
});
|
||||
const signature = await generateSignature(dataToSign, process.env.PASSWORD);
|
||||
authData.signature = signature;
|
||||
|
||||
// 生成双 Token
|
||||
const tokenId = generateTokenId();
|
||||
const refreshToken = generateRefreshToken();
|
||||
const now = Date.now();
|
||||
const refreshExpires = now + TOKEN_CONFIG.REFRESH_TOKEN_AGE;
|
||||
|
||||
authData.tokenId = tokenId;
|
||||
authData.refreshToken = refreshToken;
|
||||
authData.refreshExpires = refreshExpires;
|
||||
|
||||
// 存储 Refresh Token
|
||||
await storeRefreshToken(username, tokenId, {
|
||||
token: refreshToken,
|
||||
deviceInfo,
|
||||
createdAt: now,
|
||||
expiresAt: refreshExpires,
|
||||
lastUsed: now,
|
||||
});
|
||||
}
|
||||
|
||||
return encodeURIComponent(JSON.stringify(authData));
|
||||
@@ -182,9 +236,10 @@ export async function GET(request: NextRequest) {
|
||||
if (username) {
|
||||
// 用户已存在,直接登录
|
||||
const response = NextResponse.redirect(new URL('/', origin));
|
||||
const cookieValue = await generateAuthCookie(username, userRole);
|
||||
const expires = new Date();
|
||||
expires.setDate(expires.getDate() + 7);
|
||||
const userAgent = request.headers.get('user-agent') || 'Unknown';
|
||||
const deviceInfo = getDeviceInfo(userAgent);
|
||||
const cookieValue = await generateAuthCookie(username, userRole, deviceInfo);
|
||||
const expires = new Date(Date.now() + TOKEN_CONFIG.REFRESH_TOKEN_AGE);
|
||||
|
||||
response.cookies.set('auth', cookieValue, {
|
||||
path: '/',
|
||||
|
||||
@@ -3,6 +3,12 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { db } from '@/lib/db';
|
||||
import {
|
||||
generateRefreshToken,
|
||||
generateTokenId,
|
||||
storeRefreshToken,
|
||||
TOKEN_CONFIG,
|
||||
} from '@/lib/refresh-token';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -30,18 +36,66 @@ async function generateSignature(
|
||||
.join('');
|
||||
}
|
||||
|
||||
// 获取设备信息
|
||||
function getDeviceInfo(userAgent: string): string {
|
||||
const ua = userAgent.toLowerCase();
|
||||
|
||||
if (ua.includes('mobile') || ua.includes('android') || ua.includes('iphone')) {
|
||||
if (ua.includes('android')) return 'Android Mobile';
|
||||
if (ua.includes('iphone')) return 'iPhone';
|
||||
return 'Mobile Device';
|
||||
}
|
||||
|
||||
if (ua.includes('tablet') || ua.includes('ipad')) {
|
||||
return 'Tablet';
|
||||
}
|
||||
|
||||
if (ua.includes('windows')) return 'Windows PC';
|
||||
if (ua.includes('mac')) return 'Mac';
|
||||
if (ua.includes('linux')) return 'Linux';
|
||||
|
||||
return 'Unknown Device';
|
||||
}
|
||||
|
||||
// 生成认证Cookie
|
||||
async function generateAuthCookie(
|
||||
username: string,
|
||||
role: 'owner' | 'admin' | 'user'
|
||||
role: 'owner' | 'admin' | 'user',
|
||||
deviceInfo: string
|
||||
): Promise<string> {
|
||||
const authData: any = { role };
|
||||
|
||||
if (username && process.env.PASSWORD) {
|
||||
authData.username = username;
|
||||
const signature = await generateSignature(username, process.env.PASSWORD);
|
||||
authData.signature = signature;
|
||||
authData.timestamp = Date.now();
|
||||
|
||||
// 生成签名(包含 username, role, timestamp)
|
||||
const dataToSign = JSON.stringify({
|
||||
username: authData.username,
|
||||
role: authData.role,
|
||||
timestamp: authData.timestamp
|
||||
});
|
||||
const signature = await generateSignature(dataToSign, process.env.PASSWORD);
|
||||
authData.signature = signature;
|
||||
|
||||
// 生成双 Token
|
||||
const tokenId = generateTokenId();
|
||||
const refreshToken = generateRefreshToken();
|
||||
const now = Date.now();
|
||||
const refreshExpires = now + TOKEN_CONFIG.REFRESH_TOKEN_AGE;
|
||||
|
||||
authData.tokenId = tokenId;
|
||||
authData.refreshToken = refreshToken;
|
||||
authData.refreshExpires = refreshExpires;
|
||||
|
||||
// 存储 Refresh Token
|
||||
await storeRefreshToken(username, tokenId, {
|
||||
token: refreshToken,
|
||||
deviceInfo,
|
||||
createdAt: now,
|
||||
expiresAt: refreshExpires,
|
||||
lastUsed: now,
|
||||
});
|
||||
}
|
||||
|
||||
return encodeURIComponent(JSON.stringify(authData));
|
||||
@@ -171,9 +225,10 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// 设置认证cookie
|
||||
const response = NextResponse.json({ ok: true, message: '注册成功' });
|
||||
const cookieValue = await generateAuthCookie(username, 'user');
|
||||
const expires = new Date();
|
||||
expires.setDate(expires.getDate() + 7);
|
||||
const userAgent = request.headers.get('user-agent') || 'Unknown';
|
||||
const deviceInfo = getDeviceInfo(userAgent);
|
||||
const cookieValue = await generateAuthCookie(username, 'user', deviceInfo);
|
||||
const expires = new Date(Date.now() + TOKEN_CONFIG.REFRESH_TOKEN_AGE);
|
||||
|
||||
response.cookies.set('auth', cookieValue, {
|
||||
path: '/',
|
||||
|
||||
Reference in New Issue
Block a user