feat:token持久化

This commit is contained in:
2025-11-13 09:33:37 +08:00
parent 42c80b09e3
commit fd442c2f8b
2 changed files with 65 additions and 17 deletions

View File

@@ -0,0 +1,22 @@
import mongoose, { type InferSchemaType } from 'mongoose';
const refreshTokenSchema = new mongoose.Schema(
{
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', index: true, required: true },
tokenHash: { type: String, unique: true, index: true, required: true },
familyId: { type: String, index: true },
createdAt: { type: Date, default: () => new Date(), required: true },
expiresAt: { type: Date, required: true },
revoked: { type: Boolean, default: false },
replacedBy: { type: String },
ip: { type: String },
userAgent: { type: String }
},
{ timestamps: false }
);
export type RefreshTokenDocument = InferSchemaType<typeof refreshTokenSchema>;
export const RefreshTokenModel =
mongoose.models.RefreshToken ?? mongoose.model('RefreshToken', refreshTokenSchema);

View File

@@ -1,12 +1,15 @@
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import mongoose from 'mongoose';
import crypto from 'node:crypto';
import { env } from '../../config/env.js';
import { UserModel, type UserDocument } from '../../models/user.model.js';
import { RefreshTokenModel, type RefreshTokenDocument } from '../../models/refresh-token.model.js';
import { logger } from '../../config/logger.js';
const ACCESS_TOKEN_TTL = '15m';
const REFRESH_TOKEN_TTL = '7d';
// Increase token validity as requested
const ACCESS_TOKEN_TTL = '60m';
const REFRESH_TOKEN_TTL = '30d';
interface AuthUser {
id: string;
@@ -25,8 +28,6 @@ interface AuthTokens {
refreshExpiresIn: number;
}
const refreshTokenStore = new Map<string, { userId: string; expiresAt: Date }>();
type UserDocumentWithId = UserDocument & { _id: mongoose.Types.ObjectId };
const toAuthUser = (doc: UserDocumentWithId): AuthUser => ({
@@ -58,7 +59,9 @@ async function getUserById(id: string): Promise<AuthUser | null> {
return doc ? toAuthUser(doc) : null;
}
const signTokens = (user: AuthUser): AuthTokens => {
const hashToken = (token: string) => crypto.createHash('sha256').update(token).digest('hex');
const signTokens = async (user: AuthUser): Promise<AuthTokens> => {
const nowSeconds = Math.floor(Date.now() / 1000);
const accessToken = jwt.sign(
{
@@ -79,13 +82,23 @@ const signTokens = (user: AuthUser): AuthTokens => {
{ expiresIn: REFRESH_TOKEN_TTL }
);
const refreshExpiresIn = jwt.decode(refreshToken, { json: true })?.exp ?? nowSeconds + 7 * 24 * 60 * 60;
refreshTokenStore.set(refreshToken, { userId: user.id, expiresAt: new Date(refreshExpiresIn * 1000) });
const refreshExpiresIn = jwt.decode(refreshToken, { json: true })?.exp ?? nowSeconds + 30 * 24 * 60 * 60;
// persist refresh token hash in DB
try {
await RefreshTokenModel.create({
userId: new mongoose.Types.ObjectId(user.id),
tokenHash: hashToken(refreshToken),
familyId: user.id,
expiresAt: new Date(refreshExpiresIn * 1000)
});
} catch (e) {
logger.warn({ e }, 'Failed to persist refresh token');
}
return {
accessToken,
refreshToken,
expiresIn: nowSeconds + 15 * 60,
expiresIn: nowSeconds + 60 * 60,
refreshExpiresIn
};
};
@@ -115,7 +128,7 @@ export async function registerUser(input: {
}
const user = toAuthUser(createdDoc);
const tokens = signTokens(user);
const tokens = await signTokens(user);
return { user: profileFromUser(user), tokens };
}
@@ -130,7 +143,7 @@ export async function authenticateUser(input: { email: string; password: string
throw Object.assign(new Error('Invalid credentials'), { statusCode: 401 });
}
const tokens = signTokens(user);
const tokens = await signTokens(user);
return { user: profileFromUser(user), tokens };
}
@@ -138,19 +151,28 @@ export async function refreshSession(refreshToken: string) {
const refreshTokenSecret = env.JWT_REFRESH_SECRET ?? env.JWT_SECRET;
try {
const payload = jwt.verify(refreshToken, refreshTokenSecret) as { sub: string };
const stored = refreshTokenStore.get(refreshToken);
if (!stored || stored.userId !== payload.sub) {
const tokenHash = hashToken(refreshToken);
const record = await RefreshTokenModel.findOne({ tokenHash, userId: payload.sub, revoked: { $ne: true } })
.lean<RefreshTokenDocument & { _id: mongoose.Types.ObjectId }>()
.exec();
if (!record) {
throw new Error('Refresh token revoked');
}
if (record.expiresAt.getTime() <= Date.now()) {
throw new Error('Refresh token expired');
}
const user = await getUserById(payload.sub);
if (!user) {
throw new Error('User not found');
}
refreshTokenStore.delete(refreshToken);
const tokens = signTokens(user);
// rotate: revoke old and issue new
await RefreshTokenModel.updateOne({ tokenHash }, { $set: { revoked: true } }).exec();
const tokens = await signTokens(user);
await RefreshTokenModel.updateOne(
{ tokenHash },
{ $set: { replacedBy: hashToken(tokens.refreshToken) } }
).exec();
return { user: profileFromUser(user), tokens };
} catch (error) {
logger.warn({ error }, 'Failed to refresh session');
@@ -159,7 +181,11 @@ export async function refreshSession(refreshToken: string) {
}
export async function revokeRefreshToken(refreshToken: string) {
refreshTokenStore.delete(refreshToken);
try {
await RefreshTokenModel.updateOne({ tokenHash: hashToken(refreshToken) }, { $set: { revoked: true } }).exec();
} catch (e) {
logger.warn({ e }, 'Failed to revoke refresh token');
}
}
export async function requestPasswordReset(email: string) {