diff --git a/frontend/src/components/ChangePasswordForm.tsx b/frontend/src/components/ChangePasswordForm.tsx new file mode 100644 index 0000000..b453674 --- /dev/null +++ b/frontend/src/components/ChangePasswordForm.tsx @@ -0,0 +1,108 @@ +import { useState } from 'react' + +interface Props { + url: string + csrfToken: string + loginUrl: string +} + +interface Field { + key: 'cur' | 'new' | 'conf' | 'token' + name: string + placeholder: string + maxLength: number +} + +const FIELDS: Field[] = [ + { key: 'cur', name: 'cur-password', placeholder: 'Contraseña actual', maxLength: 16 }, + { key: 'new', name: 'new-password', placeholder: 'Contraseña nueva', maxLength: 16 }, + { key: 'conf', name: 'conf-new-password', placeholder: 'Confirmar contraseña nueva', maxLength: 16 }, + { key: 'token', name: 'security-token', placeholder: 'Token de seguridad', maxLength: 6 }, +] + +export function ChangePasswordForm({ url, csrfToken, loginUrl }: Props) { + const [values, setValues] = useState>({}) + const [shown, setShown] = useState>({}) + const [busy, setBusy] = useState(false) + const [done, setDone] = useState(false) + const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + if (busy || done) return + setBusy(true) + setMessage(null) + + const body = new URLSearchParams() + for (const f of FIELDS) body.set(f.name, (values[f.key] || '').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; redirect?: boolean } = await resp.json() + setMessage({ ok: !!data.success, html: data.message || '' }) + if (data.success && data.redirect) { + setDone(true) + setTimeout(() => { + window.location.href = loginUrl + }, 3000) + } else { + setBusy(false) + } + } catch { + setMessage({ ok: false, html: 'Error en el servidor. Inténtalo más tarde.' }) + setBusy(false) + } + } + + return ( + <> +
+ + + {FIELDS.map((f) => ( + + + + ))} + + + + +
+ setValues((v) => ({ ...v, [f.key]: e.target.value }))} + /> + setShown((s) => ({ ...s, [f.key]: !s[f.key] }))} + /> +
+ +
+
+
+
+ {message && } +
+ + ) +} diff --git a/frontend/src/components/SecurityTokenForm.tsx b/frontend/src/components/SecurityTokenForm.tsx new file mode 100644 index 0000000..1deed41 --- /dev/null +++ b/frontend/src/components/SecurityTokenForm.tsx @@ -0,0 +1,62 @@ +import { useState } from 'react' + +interface Props { + url: string + csrfToken: string + initialDate: string +} + +export function SecurityTokenForm({ url, csrfToken, initialDate }: Props) { + const [date, setDate] = useState(initialDate) + 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() + 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; token_date?: string } = await resp.json() + setMessage({ ok: !!data.success, html: data.message || '' }) + if (data.success && data.token_date) setDate(data.token_date) + } catch { + setMessage({ ok: false, html: 'Error en el servidor. Inténtalo más tarde.' }) + } finally { + setBusy(false) + } + } + + return ( + <> +

Fecha de solicitud de Token de seguridad:

+

{date}

+
+
+ +
+
+
+
+ {message && } +
+ + ) +} diff --git a/frontend/src/entries/change_password.tsx b/frontend/src/entries/change_password.tsx new file mode 100644 index 0000000..b6c5a67 --- /dev/null +++ b/frontend/src/entries/change_password.tsx @@ -0,0 +1,16 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { ChangePasswordForm } from '../components/ChangePasswordForm' + +const el = document.getElementById('change-password-app') +if (el) { + createRoot(el).render( + + + , + ) +} diff --git a/frontend/src/entries/security_token.tsx b/frontend/src/entries/security_token.tsx new file mode 100644 index 0000000..09f56a6 --- /dev/null +++ b/frontend/src/entries/security_token.tsx @@ -0,0 +1,16 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { SecurityTokenForm } from '../components/SecurityTokenForm' + +const el = document.getElementById('security-token-app') +if (el) { + createRoot(el).render( + + + , + ) +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 2ac2272..b96948d 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -21,6 +21,8 @@ export default defineConfig({ recover: resolve(__dirname, 'src/entries/recover.tsx'), reset_password: resolve(__dirname, 'src/entries/reset_password.tsx'), 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'), }, }, }, diff --git a/home/templates/partials/change_password.html b/home/templates/partials/change_password.html index 04c81bb..a6fe3c8 100644 --- a/home/templates/partials/change_password.html +++ b/home/templates/partials/change_password.html @@ -13,34 +13,14 @@


-
- - - - - - - - - - - - - - - - - -
-
-
-
-
-
-
-
+ {% load django_vite %} + +
+ {% vite_asset 'src/entries/change_password.tsx' %}
diff --git a/home/templates/partials/security_token.html b/home/templates/partials/security_token.html index e062bbd..a154a2e 100644 --- a/home/templates/partials/security_token.html +++ b/home/templates/partials/security_token.html @@ -3,7 +3,6 @@

Token de seguridad

-

@@ -24,16 +23,14 @@


-

Fecha de solicitud de Token de seguridad:

-

{{ token_date }}

-
-
- {% csrf_token %} - -
-
-
-
+ {% load django_vite %} + +
+ {% vite_asset 'src/entries/security_token.tsx' %}

He olvidado el Token de seguridad