Fase 3 (isla): cambiar correo en React/TSX

change_email_view ya devolvía JSON en POST (sesión requerida). ChangeEmailForm.tsx
replica el form (contraseña actual con mostrar-ocultar, correo actual/nuevo/confirmar,
token) y hace fetch form-urlencoded + CSRF a change-email; mensaje HTML. Sustituye el
<form> + change_email_response.js por #change-email-app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 21:29:29 +00:00
parent d0febb4855
commit b1107c0a54
4 changed files with 122 additions and 31 deletions
+102
View File
@@ -0,0 +1,102 @@
import { useState } from 'react'
interface Props {
url: string
csrfToken: string
}
interface Field {
name: string
type: 'password' | 'email' | 'text'
placeholder: string
maxLength?: number
toggle?: boolean
}
const FIELDS: Field[] = [
{ name: 'cur-password', type: 'password', placeholder: 'Contraseña actual', maxLength: 16, toggle: true },
{ name: 'cur-email', type: 'email', placeholder: 'Correo actual' },
{ name: 'new-email', type: 'email', placeholder: 'Nuevo correo' },
{ name: 'conf-new-email', type: 'email', placeholder: 'Confirmar nuevo correo' },
{ name: 'security-token', type: 'text', placeholder: 'Token de seguridad', maxLength: 6 },
]
export function ChangeEmailForm({ url, csrfToken }: Props) {
const [values, setValues] = useState<Record<string, string>>({})
const [showPw, setShowPw] = useState(false)
const [busy, setBusy] = useState(false)
const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null)
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (busy) return
setBusy(true)
setMessage(null)
const body = new URLSearchParams()
for (const f of FIELDS) body.set(f.name, (values[f.name] || '').trim())
body.set('csrfmiddlewaretoken', csrfToken)
try {
const resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': csrfToken,
Accept: 'application/json',
},
credentials: 'same-origin',
body: body.toString(),
})
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
const data: { success?: boolean; message?: string } = await resp.json()
setMessage({ ok: !!data.success, html: data.message || '' })
} catch {
setMessage({ ok: false, html: '<span class="red-form-response">Error en el servidor. Inténtalo más tarde.</span>' })
} finally {
setBusy(false)
}
}
return (
<form id="nw-change-email-form" onSubmit={handleSubmit} acceptCharset="utf-8">
<table className="middle-center-table">
<tbody>
{FIELDS.map((f) => (
<tr key={f.name}>
<td>
<input
type={f.toggle ? (showPw ? 'text' : 'password') : f.type}
name={f.name}
id={f.name}
placeholder={f.placeholder}
maxLength={f.maxLength}
required
value={values[f.name] || ''}
onChange={(e) => setValues((v) => ({ ...v, [f.name]: e.target.value }))}
/>
{f.toggle && (
<span
className={`far toggle-password ${showPw ? 'fa-eye-slash' : 'fa-eye'}`}
onClick={() => setShowPw((s) => !s)}
/>
)}
</td>
</tr>
))}
<tr>
<td>
<button type="submit" className="change-email-button" disabled={busy}>
{busy ? 'Cambiando…' : 'Cambiar Correo'}
</button>
</td>
</tr>
</tbody>
</table>
<hr />
<div className="alert-message" id="change-email-response" style={{ display: message ? 'block' : 'none' }}>
{message && <span dangerouslySetInnerHTML={{ __html: message.html }} />}
</div>
</form>
)
}
+12
View File
@@ -0,0 +1,12 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { ChangeEmailForm } from '../components/ChangeEmailForm'
const el = document.getElementById('change-email-app')
if (el) {
createRoot(el).render(
<StrictMode>
<ChangeEmailForm url={el.dataset.url || ''} csrfToken={el.dataset.csrf || ''} />
</StrictMode>,
)
}
+1
View File
@@ -23,6 +23,7 @@ export default defineConfig({
my_account: resolve(__dirname, 'src/entries/my_account.tsx'),
change_password: resolve(__dirname, 'src/entries/change_password.tsx'),
security_token: resolve(__dirname, 'src/entries/security_token.tsx'),
change_email: resolve(__dirname, 'src/entries/change_email.tsx'),
},
},
},
+7 -31
View File
@@ -15,37 +15,13 @@
<br>
<br>
<form id="nw-change-email-form" method="POST" accept-charset="utf-8">
{% csrf_token %}
<table class="middle-center-table">
<script src="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-js-handlers/change_email_response.js"></script>
<tr>
<td><input type="password" maxlength="16" name="cur-password" id="cur-password" placeholder="Contraseña actual" required></td>
</tr>
<tr>
<td><input type="email" name="cur-email" id="cur-email" placeholder="Correo actual" required></td>
</tr>
<tr>
<td><input type="email" name="new-email" id="new-email" placeholder="Nuevo correo" required></td>
</tr>
<tr>
<td><input type="email" name="conf-new-email" id="conf-new-email" placeholder="Confirmar nuevo correo" required></td>
</tr>
<tr>
<td>
<input type="text" maxlength="6" name="security-token" id="security-token" placeholder="Token de seguridad" required>
<span class="far fa-eye toggle-token"></span>
</td>
</tr>
<tr>
<td>
<button type="submit" class="change-email-button">Cambiar Correo</button>
</td>
</tr>
</table>
<hr>
<div class="alert-message" id="change-email-response"></div>
</form>
{% load django_vite %}
<!-- Isla React (Vite): cambio de correo. POST a la propia vista (JSON) -> genera
hashes y envía confirmaciones a los correos antiguo y nuevo. -->
<div id="change-email-app"
data-csrf="{{ csrf_token }}"
data-url="{% url 'change-email' %}"></div>
{% vite_asset 'src/entries/change_email.tsx' %}