docs: update README files for clarity on deployment steps and features

This commit is contained in:
shuaiplus
2026-03-01 19:31:03 +08:00
committed by Shuai
parent f5a2523f91
commit 26447cd9b4
13 changed files with 164 additions and 100 deletions
+22 -4
View File
@@ -1,6 +1,7 @@
import { Env, User, ProfileResponse, DEFAULT_DEV_SECRET } from '../types';
import { StorageService } from '../services/storage';
import { AuthService } from '../services/auth';
import { RateLimitService, getClientIdentifier } from '../services/ratelimit';
import { jsonResponse, errorResponse } from '../utils/response';
import { generateUUID } from '../utils/uuid';
import { LIMITS } from '../config/limits';
@@ -449,6 +450,7 @@ export async function handleGetTotpRecoveryCode(request: Request, env: Env, user
export async function handleRecoverTwoFactor(request: Request, env: Env): Promise<Response> {
const storage = new StorageService(env.DB);
const auth = new AuthService(env);
const rateLimit = new RateLimitService(env.DB);
let body: Record<string, string | undefined>;
try {
@@ -466,20 +468,35 @@ export async function handleRecoverTwoFactor(request: Request, env: Env): Promis
const email = String(body.email || body.username || '').trim().toLowerCase();
const masterPasswordHash = String(body.masterPasswordHash || body.password || '').trim();
const recoveryCode = normalizeRecoveryCodeInput(String(body.recoveryCode || body.twoFactorToken || body.recovery_code || ''));
const recoverLimitKey = `${getClientIdentifier(request)}:recover-2fa:${email || 'unknown'}`;
const recoverAttemptCheck = await rateLimit.checkLoginAttempt(recoverLimitKey);
if (!recoverAttemptCheck.allowed) {
return errorResponse(
`Too many failed recovery attempts. Try again in ${Math.ceil((recoverAttemptCheck.retryAfterSeconds || 60) / 60)} minutes.`,
429
);
}
if (!email || !masterPasswordHash || !recoveryCode) {
return errorResponse('Email, masterPasswordHash and recoveryCode are required', 400);
}
const user = await storage.getUser(email);
if (!user) return errorResponse('Invalid credentials', 400);
if (user.status !== 'active') return errorResponse('Account is disabled', 403);
if (!user || user.status !== 'active') {
await rateLimit.recordFailedLogin(recoverLimitKey);
return errorResponse('Invalid credentials or recovery code', 400);
}
const validPassword = await auth.verifyPassword(masterPasswordHash, user.masterPasswordHash);
if (!validPassword) return errorResponse('Invalid credentials', 400);
if (!validPassword) {
await rateLimit.recordFailedLogin(recoverLimitKey);
return errorResponse('Invalid credentials or recovery code', 400);
}
if (!recoveryCodeEquals(recoveryCode, user.totpRecoveryCode)) {
return errorResponse('Recovery code is incorrect. Try again.', 400);
await rateLimit.recordFailedLogin(recoverLimitKey);
return errorResponse('Invalid credentials or recovery code', 400);
}
user.totpSecret = null;
@@ -488,6 +505,7 @@ export async function handleRecoverTwoFactor(request: Request, env: Env): Promis
user.updatedAt = new Date().toISOString();
await storage.saveUser(user);
await storage.deleteRefreshTokensByUserId(user.id);
await rateLimit.clearLoginAttempts(recoverLimitKey);
return jsonResponse({
success: true,
+11 -1
View File
@@ -105,6 +105,7 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
}
const grantType = body.grant_type;
const clientIdentifier = getClientIdentifier(request);
if (grantType === 'password') {
// Login with password
@@ -113,7 +114,7 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
const twoFactorToken = body.twoFactorToken;
const twoFactorProvider = body.twoFactorProvider;
const twoFactorRemember = body.twoFactorRemember;
const loginIdentifier = getClientIdentifier(request);
const loginIdentifier = clientIdentifier;
const deviceInfo = readAuthRequestDeviceInfo(body, request);
if (!email || !passwordHash) {
@@ -266,6 +267,15 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
return jsonResponse(response);
} else if (grantType === 'send_access') {
const sendAccessLimit = await rateLimit.consumePublicSendAccessBudget(`${clientIdentifier}:public-send-oauth`);
if (!sendAccessLimit.allowed) {
return identityErrorResponse(
`Too many public Send requests. Try again in ${sendAccessLimit.retryAfterSeconds} seconds.`,
'TooManyRequests',
429
);
}
const sendId = String(body.send_id || body.sendId || '').trim();
if (!sendId) {
return jsonResponse(
+18 -2
View File
@@ -322,6 +322,14 @@ function hasEmailAuth(send: Send): boolean {
return send.authType === SendAuthType.Email;
}
function getSafeJwtSecret(env: Env): { ok: true; secret: string } | { ok: false; response: Response } {
const secret = (env.JWT_SECRET || '').trim();
if (!secret || secret.length < LIMITS.auth.jwtSecretMinLength || secret === DEFAULT_DEV_SECRET) {
return { ok: false, response: errorResponse('Server configuration error', 500) };
}
return { ok: true, secret };
}
function extractBearerToken(request: Request): string | null {
const authHeader = request.headers.get('Authorization');
if (!authHeader) return null;
@@ -1078,12 +1086,15 @@ export async function handleAccessSendFile(
// POST /api/sends/access (v2 bearer)
export async function handleAccessSendV2(request: Request, env: Env): Promise<Response> {
const jwt = getSafeJwtSecret(env);
if (!jwt.ok) return jwt.response;
const token = extractBearerToken(request);
if (!token) {
return errorResponse('Unauthorized', 401);
}
const claims = await verifySendAccessToken(token, env.JWT_SECRET);
const claims = await verifySendAccessToken(token, jwt.secret);
if (!claims) {
return errorResponse('Unauthorized', 401);
}
@@ -1196,6 +1207,11 @@ export async function issueSendAccessToken(
passwordHashB64?: string | null,
password?: string | null
): Promise<{ token: string } | { error: Response }> {
const jwt = getSafeJwtSecret(env);
if (!jwt.ok) {
return { error: jwt.response };
}
const storage = new StorageService(env.DB);
const send = await resolveSendFromIdOrAccessId(storage, sendIdOrAccessId);
@@ -1260,7 +1276,7 @@ export async function issueSendAccessToken(
}
}
const token = await createSendAccessToken(send.id, env.JWT_SECRET);
const token = await createSendAccessToken(send.id, jwt.secret);
return { token };
}