mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
Add Audible region config and user password change modal
Implements configurable Audible region selection in setup and admin settings, affecting all Audible API calls and triggering data refresh on change. Adds a user-facing 'Change Password' modal in the header for local users, moving password change from admin-only to all local users via a new /api/auth/change-password endpoint. Updates documentation, API routes, and context to support these features, and removes the old admin-only password change flow.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Component: Audible Settings API
|
||||
* Documentation: documentation/integrations/audible.md
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { getConfigService } from '@/lib/services/config.service';
|
||||
import { getAudibleService } from '@/lib/integrations/audible.service';
|
||||
import { getJobQueueService } from '@/lib/services/job-queue.service';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
const logger = RMABLogger.create('API.Admin.Settings.Audible');
|
||||
|
||||
const VALID_REGIONS = ['us', 'ca', 'uk', 'au', 'in'];
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
return requireAuth(request, async (req: AuthenticatedRequest) => {
|
||||
return requireAdmin(req, async () => {
|
||||
try {
|
||||
const { region } = await request.json();
|
||||
|
||||
// Validate region
|
||||
if (!region || !VALID_REGIONS.includes(region)) {
|
||||
logger.warn('Invalid region provided', { region });
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Invalid Audible region. Must be one of: us, ca, uk, au, in' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Save region to configuration
|
||||
const configService = getConfigService();
|
||||
await configService.setMany([
|
||||
{
|
||||
key: 'audible.region',
|
||||
value: region,
|
||||
category: 'system',
|
||||
description: 'Audible region for metadata and search',
|
||||
},
|
||||
]);
|
||||
|
||||
// Clear config cache to ensure new region is loaded immediately
|
||||
configService.clearCache('audible.region');
|
||||
|
||||
// Force AudibleService to re-initialize with new region
|
||||
const audibleService = getAudibleService();
|
||||
audibleService.forceReinitialize();
|
||||
|
||||
logger.info('Audible region updated, triggering data refresh', { region });
|
||||
|
||||
// Trigger audible_refresh job to fetch data for new region
|
||||
try {
|
||||
const jobQueueService = getJobQueueService();
|
||||
await jobQueueService.addAudibleRefreshJob();
|
||||
logger.info('Audible refresh job triggered');
|
||||
} catch (jobError) {
|
||||
logger.warn('Failed to trigger audible refresh job', {
|
||||
error: jobError instanceof Error ? jobError.message : String(jobError),
|
||||
});
|
||||
// Don't fail the request if job trigger fails
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Audible region set to ${region.toUpperCase()}. Data refresh job triggered.`,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to update Audible region', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to update Audible region settings' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/**
|
||||
* Component: Local Admin Password Change API
|
||||
* Documentation: documentation/backend/services/auth.md
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, requireLocalAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { prisma } from '@/lib/db';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
const logger = RMABLogger.create('API.Admin.Settings.ChangePassword');
|
||||
|
||||
/**
|
||||
* POST /api/admin/settings/change-password
|
||||
* Change password for local admin user
|
||||
*
|
||||
* Security:
|
||||
* - Only available to local admin (isSetupAdmin=true AND plexId starts with '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) => {
|
||||
return requireLocalAdmin(req, async (authenticatedReq: 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
|
||||
if (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: authenticatedReq.user!.id },
|
||||
select: {
|
||||
id: true,
|
||||
authToken: true,
|
||||
plexId: true,
|
||||
isSetupAdmin: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || !user.authToken) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'User not found or invalid account type',
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
const currentPasswordValid = await bcrypt.compare(currentPassword, user.authToken);
|
||||
|
||||
if (!currentPasswordValid) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Current password is incorrect',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
|
||||
// Update password in database
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
authToken: hashedPassword,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`Local admin password changed successfully`, { userId: user.id });
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -36,6 +36,7 @@ export async function GET(request: NextRequest) {
|
||||
const settings = {
|
||||
backendMode: configMap.get('system.backend_mode') || 'plex',
|
||||
hasLocalUsers,
|
||||
audibleRegion: configMap.get('audible.region') || 'us',
|
||||
plex: {
|
||||
url: configMap.get('plex_url') || '',
|
||||
token: maskValue('token', configMap.get('plex_token')),
|
||||
|
||||
Reference in New Issue
Block a user