mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 12:50:09 +00:00
af0eaceb98
Introduce a provider-based notification system and wire it through the API and admin UI. Added INotificationProvider + notification service implementation and providers (apprise, discord, ntfy, pushover), plus a GET /api/admin/notifications/providers endpoint to expose provider metadata. Refactored code to use provider type strings (removed enum coupling), updated masking/encryption calls, and simplified the test notification endpoint to accept backendId or type+config and call sendToBackend directly. UI: NotificationsTab now fetches provider metadata and renders provider cards and dynamic config forms (fields driven by provider metadata). Added config field rendering, improved backend cards, and edit/delete actions. APIs: New providers route, updated admin notification CRUD routes to validate provider types dynamically, updated test route schema. Added download-client categories POST API to fetch categories from clients and wired postImportCategory handling in download-client routes. Other notable changes: BookDate now fetches Claude models dynamically from Anthropic's Models API; added paginated model fetch helper. Added ALLOW_WEAK_PASSWORD flag exposure to auth providers and password change logic. Doc updates and various tests added/updated. File-organization doc clarifies EPERM fix using stream-based copy.
188 lines
5.1 KiB
TypeScript
188 lines
5.1 KiB
TypeScript
/**
|
|
* Component: User Password Change API
|
|
* Documentation: documentation/backend/services/auth.md
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
|
|
import { prisma } from '@/lib/db';
|
|
import bcrypt from 'bcrypt';
|
|
import { getEncryptionService } from '@/lib/services/encryption.service';
|
|
import { RMABLogger } from '@/lib/utils/logger';
|
|
|
|
const logger = RMABLogger.create('API.Auth.ChangePassword');
|
|
|
|
/**
|
|
* POST /api/auth/change-password
|
|
* Change password for any authenticated local user
|
|
*
|
|
* Security:
|
|
* - Only available to local users (authProvider='local')
|
|
* - Requires current password verification
|
|
* - New password must be at least 8 characters
|
|
* - New password must be different from current password
|
|
*/
|
|
export async function POST(request: NextRequest) {
|
|
return requireAuth(request, async (req: AuthenticatedRequest) => {
|
|
try {
|
|
const { currentPassword, newPassword, confirmPassword } = await request.json();
|
|
|
|
// Validate input
|
|
if (!currentPassword || !newPassword || !confirmPassword) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'All fields are required',
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Validate new password length
|
|
const allowWeakPassword = process.env.ALLOW_WEAK_PASSWORD === 'true';
|
|
if (!allowWeakPassword && newPassword.length < 8) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'New password must be at least 8 characters',
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Validate passwords match
|
|
if (newPassword !== confirmPassword) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'New passwords do not match',
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Validate new password is different from current
|
|
if (currentPassword === newPassword) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'New password must be different from current password',
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Get user from database
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: req.user!.id },
|
|
select: {
|
|
id: true,
|
|
authToken: true,
|
|
authProvider: true,
|
|
plexId: true,
|
|
plexUsername: true,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'User not found',
|
|
},
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
// Check if user is a local user (can change password)
|
|
if (user.authProvider !== 'local') {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'Password change is only available for local users. Your account is managed by an external provider.',
|
|
},
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
|
|
if (!user.authToken) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'Invalid account configuration',
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Decrypt the stored hash before comparing
|
|
const encryptionService = getEncryptionService();
|
|
let decryptedHash: string;
|
|
try {
|
|
decryptedHash = encryptionService.decrypt(user.authToken);
|
|
} catch (error) {
|
|
logger.error('Failed to decrypt password hash', {
|
|
userId: user.id,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'Failed to verify current password',
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
// Verify current password
|
|
const currentPasswordValid = await bcrypt.compare(currentPassword, decryptedHash);
|
|
|
|
if (!currentPasswordValid) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'Current password is incorrect',
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Hash new password
|
|
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
|
|
|
// Encrypt the new hash before storing
|
|
const encryptedHash = encryptionService.encrypt(hashedPassword);
|
|
|
|
// Update password in database
|
|
await prisma.user.update({
|
|
where: { id: user.id },
|
|
data: {
|
|
authToken: encryptedHash,
|
|
updatedAt: new Date(),
|
|
},
|
|
});
|
|
|
|
logger.info('Password changed successfully', {
|
|
userId: user.id,
|
|
username: user.plexUsername
|
|
});
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: 'Password changed successfully',
|
|
});
|
|
} catch (error) {
|
|
logger.error('Failed to change password', {
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to change password',
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
});
|
|
}
|