feat(profile): /settings/profile page + change-password endpoint
The user-menu's Profile link previously 404'd, and CRM users had no way
to change their password from inside the app.
- /api/v1/me/password POST wraps better-auth changePassword, surfaces a
friendlier "Current password is incorrect" on the typical failure
mode, and writes an audit_log row with metadata.revokedOtherSessions.
- /{port}/settings/profile renders display name + email + change-password
card with current/new/confirm fields and a 'Sign out other devices'
toggle.
End-to-end verified: wrong current pw → 400 with mapped message;
correct → 200 + audit row; revert → 200.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
5
src/app/(dashboard)/[portSlug]/settings/profile/page.tsx
Normal file
5
src/app/(dashboard)/[portSlug]/settings/profile/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { UserProfile } from '@/components/settings/user-profile';
|
||||
|
||||
export default function ProfilePage() {
|
||||
return <UserProfile />;
|
||||
}
|
||||
54
src/app/api/v1/me/password/route.ts
Normal file
54
src/app/api/v1/me/password/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { auth } from '@/lib/auth';
|
||||
import { withAuth } from '@/lib/api/helpers';
|
||||
import { parseBody } from '@/lib/api/route-helpers';
|
||||
import { createAuditLog } from '@/lib/audit';
|
||||
import { errorResponse, ValidationError } from '@/lib/errors';
|
||||
|
||||
const bodySchema = z.object({
|
||||
currentPassword: z.string().min(1, 'Current password is required'),
|
||||
newPassword: z.string().min(9, 'Password must be at least 9 characters'),
|
||||
revokeOtherSessions: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const POST = withAuth(async (req, ctx) => {
|
||||
try {
|
||||
const body = await parseBody(req, bodySchema);
|
||||
const result = await auth.api.changePassword({
|
||||
body: {
|
||||
currentPassword: body.currentPassword,
|
||||
newPassword: body.newPassword,
|
||||
revokeOtherSessions: body.revokeOtherSessions,
|
||||
},
|
||||
headers: req.headers,
|
||||
});
|
||||
|
||||
void createAuditLog({
|
||||
portId: ctx.portId || null,
|
||||
userId: ctx.userId,
|
||||
action: 'password_change',
|
||||
entityType: 'user',
|
||||
entityId: ctx.userId,
|
||||
metadata: {
|
||||
revokedOtherSessions: !!body.revokeOtherSessions,
|
||||
},
|
||||
ipAddress: ctx.ipAddress,
|
||||
userAgent: ctx.userAgent,
|
||||
});
|
||||
|
||||
return NextResponse.json({ data: { ok: true, user: result.user } });
|
||||
} catch (err) {
|
||||
if (err && typeof err === 'object' && 'message' in err) {
|
||||
const msg = String((err as { message?: unknown }).message ?? '');
|
||||
if (
|
||||
msg.toLowerCase().includes('invalid password') ||
|
||||
msg.toLowerCase().includes('incorrect')
|
||||
) {
|
||||
return errorResponse(new ValidationError('Current password is incorrect'));
|
||||
}
|
||||
}
|
||||
return errorResponse(err);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user