feat: implement email verification flow and enhance user experience
- Added EmailVerificationBanner component to notify users about unverified email status and provide a resend verification option. - Introduced VerifyPage for handling email verification via a token in the URL, including success and error handling. - Updated LoginPage and AccountSettingsPage to allow users to resend verification emails directly from these pages. - Enhanced API utility with new functions for verifying emails and resending verification requests. - Updated routing to include the new verification page and improved link structure for verification links.
This commit is contained in:
parent
c6569abe1a
commit
2646bc776a
|
|
@ -1,3 +1,8 @@
|
|||
# === Deploy (.env auf dem Host) ============================================
|
||||
# Kopiere diese Datei nach `.env` im SELBEN Verzeichnis wie `docker-compose.yml` (Pi: ~/docker/shinkan).
|
||||
# Docker Compose liest `.env` beim Start — dadurch wird z. B. SMTP_HOST=${SMTP_HOST} gefüllt.
|
||||
# Ist die Datei weg/leer oder steht SMTP_PASS nicht darin → im Container keine SMTP-Daten ([SMTP] nicht konfiguriert).
|
||||
|
||||
# Database
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import os
|
|||
import secrets
|
||||
import smtplib
|
||||
import ssl
|
||||
from urllib.parse import quote
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from email.mime.text import MIMEText
|
||||
|
|
@ -168,8 +169,8 @@ def _public_app_base() -> str:
|
|||
|
||||
|
||||
def verification_link(token: str) -> str:
|
||||
"""Öffentlicher Link zur Bestätigung (FastAPI unter /api/auth)."""
|
||||
return f"{_public_app_base()}/api/auth/verify/{token}"
|
||||
"""Link zur Web-App (`/verify?token=`); die SPA ruft wie bei Mitai die API auf."""
|
||||
return f"{_public_app_base()}/verify?token={quote(token, safe='')}"
|
||||
|
||||
|
||||
def registration_role(cur, email_lower: str) -> str:
|
||||
|
|
@ -303,8 +304,6 @@ Bitte bestätige deine E-Mail-Adresse, um die Registrierung abzuschließen:
|
|||
|
||||
Der Link ist 24 Stunden gültig.
|
||||
|
||||
Nach dem Aufruf des Links bist du bestätigt (Browser zeigt eine kurze JSON-Antwort – das ist in Ordnung).
|
||||
|
||||
Dein Shinkan Jinkendo Team
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { AuthProvider, useAuth } from './context/AuthContext'
|
|||
import DesktopSidebar from './components/DesktopSidebar'
|
||||
import { getMainNavItems } from './config/appNav'
|
||||
import LoginPage from './pages/LoginPage'
|
||||
import VerifyPage from './pages/VerifyPage'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
import AccountSettingsPage from './pages/AccountSettingsPage'
|
||||
import ExercisesListPage from './pages/ExercisesListPage'
|
||||
|
|
@ -133,6 +134,8 @@ function PublicRoute({ children }) {
|
|||
function AppRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/verify" element={<VerifyPage />} />
|
||||
|
||||
<Route
|
||||
path="/login"
|
||||
element={
|
||||
|
|
|
|||
76
frontend/src/components/EmailVerificationBanner.jsx
Normal file
76
frontend/src/components/EmailVerificationBanner.jsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { useState } from 'react'
|
||||
import api from '../utils/api'
|
||||
|
||||
/**
|
||||
* Hinweis + „Erneut senden“, wenn eingeloggt aber E-Mail noch nicht verifiziert (wie Mitai).
|
||||
*/
|
||||
export default function EmailVerificationBanner({ profile }) {
|
||||
const [resending, setResending] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
if (!profile?.email || profile.email_verified !== false) return null
|
||||
|
||||
const handleResend = async () => {
|
||||
if (!profile.email) return
|
||||
setResending(true)
|
||||
setError('')
|
||||
setSuccess(false)
|
||||
try {
|
||||
await api.resendVerification(profile.email)
|
||||
setSuccess(true)
|
||||
setTimeout(() => setSuccess(false), 6000)
|
||||
} catch (err) {
|
||||
setError(err.message || 'Versand fehlgeschlagen')
|
||||
setTimeout(() => setError(''), 7000)
|
||||
} finally {
|
||||
setResending(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="card"
|
||||
style={{
|
||||
marginBottom: '1.25rem',
|
||||
borderLeft: '4px solid #D97706',
|
||||
background: 'var(--surface2)',
|
||||
}}
|
||||
role="status"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '1.5rem', lineHeight: 1 }} aria-hidden>
|
||||
📧
|
||||
</span>
|
||||
<div style={{ flex: '1 1 200px' }}>
|
||||
<div style={{ fontWeight: 700, fontSize: '0.95rem', color: '#D97706', marginBottom: 4 }}>
|
||||
E-Mail noch nicht bestätigt
|
||||
</div>
|
||||
<p style={{ fontSize: '0.875rem', color: 'var(--text2)', lineHeight: 1.45, margin: 0 }}>
|
||||
Bitte prüfe dein Postfach und öffne den Bestätigungslink (auch Spam-Ordner).
|
||||
{success && (
|
||||
<span style={{ color: 'var(--accent-dark)', fontWeight: 600, marginLeft: 8 }}>Neue Mail wurde angefordert.</span>
|
||||
)}
|
||||
{error && <span style={{ color: 'var(--danger)', fontWeight: 600, marginLeft: 8 }}>{error}</span>}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={handleResend}
|
||||
disabled={resending || success}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{resending ? 'Sende…' : success ? '✓ Unterwegs' : 'Erneut senden'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { createContext, useContext, useState, useEffect } from 'react'
|
||||
import { createContext, useContext, useState, useEffect, useCallback } from 'react'
|
||||
import api from '../utils/api'
|
||||
|
||||
const AuthContext = createContext(null)
|
||||
|
|
@ -7,11 +7,7 @@ export function AuthProvider({ children }) {
|
|||
const [user, setUser] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth()
|
||||
}, [])
|
||||
|
||||
const checkAuth = async () => {
|
||||
const checkAuth = useCallback(async () => {
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (!token) {
|
||||
setLoading(false)
|
||||
|
|
@ -27,7 +23,11 @@ export function AuthProvider({ children }) {
|
|||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth()
|
||||
}, [checkAuth])
|
||||
|
||||
/** Fallback, falls ohne checkAuth gesetzt wird (Legacy / Token-Injektion) */
|
||||
const login = (payload) => {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ function AccountSettingsPage() {
|
|||
const [newPw1, setNewPw1] = useState('')
|
||||
const [newPw2, setNewPw2] = useState('')
|
||||
const [savingPw, setSavingPw] = useState(false)
|
||||
const [resendingVerify, setResendingVerify] = useState(false)
|
||||
|
||||
const [message, setMessage] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
|
@ -54,7 +55,20 @@ function AccountSettingsPage() {
|
|||
}
|
||||
}
|
||||
|
||||
const handleChangePassword = async (e) => {
|
||||
const handleResendVerification = async () => {
|
||||
const em = user?.email
|
||||
if (!em) return
|
||||
setResendingVerify(true)
|
||||
try {
|
||||
await api.resendVerification(em)
|
||||
showOk('Falls diese Adresse einen unbestätigten Account hat: E-Mail ist unterwegs — Postfach prüfen.')
|
||||
} catch (err) {
|
||||
showErr(err.message || 'Konnte keine E-Mail senden.')
|
||||
} finally {
|
||||
setResendingVerify(false)
|
||||
}
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
if (newPw1.length < 4) {
|
||||
showErr('Neues Passwort: mindestens 4 Zeichen.')
|
||||
|
|
@ -146,6 +160,18 @@ function AccountSettingsPage() {
|
|||
noch nicht bestätigt
|
||||
</span>
|
||||
)}
|
||||
{!verified && user?.email ? (
|
||||
<div style={{ marginTop: '0.75rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={resendingVerify}
|
||||
onClick={handleResendVerification}
|
||||
>
|
||||
{resendingVerify ? 'Sende…' : 'Bestätigung erneut senden'}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSaveName}>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import api from '../utils/api'
|
||||
import EmailVerificationBanner from '../components/EmailVerificationBanner'
|
||||
|
||||
function Dashboard() {
|
||||
const [version, setVersion] = useState(null)
|
||||
|
|
@ -43,6 +44,7 @@ function Dashboard() {
|
|||
<p style={{ color: 'var(--text2)', marginTop: '0.5rem' }}>
|
||||
Willkommen, {user?.name || user?.email}!
|
||||
</p>
|
||||
{profile && <EmailVerificationBanner profile={profile} />}
|
||||
{/* Welcome Card */}
|
||||
<div className="card" style={{ marginTop: '1.5rem', marginBottom: '1.5rem' }}>
|
||||
<h2>Willkommen bei Shinkan Jinkendo</h2>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ function LoginPage() {
|
|||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [success, setSuccess] = useState('')
|
||||
const [resending, setResending] = useState(false)
|
||||
|
||||
const navigate = useNavigate()
|
||||
const { checkAuth } = useAuth()
|
||||
|
|
@ -29,7 +30,7 @@ function LoginPage() {
|
|||
navigate('/')
|
||||
} else {
|
||||
await api.register(email, password, name)
|
||||
setSuccess('Registrierung erfolgreich! Bitte prüfe deine E-Mails.')
|
||||
setSuccess('Registrierung erfolgreich! Bitte prüfe deine E-Mails (auch Spam).')
|
||||
setMode('login')
|
||||
setPassword('')
|
||||
}
|
||||
|
|
@ -40,6 +41,25 @@ function LoginPage() {
|
|||
}
|
||||
}
|
||||
|
||||
const handleResendVerification = async () => {
|
||||
if (!email.trim()) {
|
||||
setError('Zuerst die E-Mail-Adresse eintragen.')
|
||||
return
|
||||
}
|
||||
setError('')
|
||||
setResending(true)
|
||||
try {
|
||||
await api.resendVerification(email.trim().toLowerCase())
|
||||
setSuccess(
|
||||
'Wenn diese Adresse für einen noch unbestätigten Account existiert, erhältst du gleich eine E-Mail.'
|
||||
)
|
||||
} catch (err) {
|
||||
setError(err.message || 'Versand fehlgeschlagen')
|
||||
} finally {
|
||||
setResending(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-container" style={{
|
||||
minHeight: '100vh',
|
||||
|
|
@ -59,15 +79,23 @@ function LoginPage() {
|
|||
|
||||
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1.5rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
className={mode === 'login' ? 'btn btn-primary' : 'btn btn-secondary'}
|
||||
onClick={() => setMode('login')}
|
||||
onClick={() => {
|
||||
setMode('login')
|
||||
setError('')
|
||||
}}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={mode === 'register' ? 'btn btn-primary' : 'btn btn-secondary'}
|
||||
onClick={() => setMode('register')}
|
||||
onClick={() => {
|
||||
setMode('register')
|
||||
setError('')
|
||||
}}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
Registrieren
|
||||
|
|
@ -147,6 +175,35 @@ function LoginPage() {
|
|||
</button>
|
||||
</form>
|
||||
|
||||
{mode === 'login' && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1.25rem',
|
||||
paddingTop: '1rem',
|
||||
borderTop: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-full"
|
||||
disabled={resending || !email.trim()}
|
||||
onClick={handleResendVerification}
|
||||
>
|
||||
{resending ? 'Sende…' : 'Bestätigungs-Link erneut senden'}
|
||||
</button>
|
||||
<p
|
||||
style={{
|
||||
marginTop: '0.45rem',
|
||||
fontSize: '0.76rem',
|
||||
color: 'var(--text3)',
|
||||
lineHeight: 1.4,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Nutzt die E-Mail-Adresse vom Formular — für noch nicht verifizierte Konten (Rate Limit 3 h).
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<p style={{ textAlign: 'center', color: 'var(--text3)', marginTop: '1.5rem', fontSize: '0.875rem' }}>
|
||||
v0.1.0 • Development
|
||||
</p>
|
||||
|
|
|
|||
182
frontend/src/pages/VerifyPage.jsx
Normal file
182
frontend/src/pages/VerifyPage.jsx
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import api from '../utils/api'
|
||||
|
||||
/**
|
||||
* E-Mail-Bestätigung (?token=) — gleiches Muster wie Mitai: SPA ruft /api/auth/verify/{token} auf.
|
||||
*/
|
||||
export default function VerifyPage() {
|
||||
const [searchParams] = useSearchParams()
|
||||
const token = searchParams.get('token')
|
||||
const navigate = useNavigate()
|
||||
const { checkAuth } = useAuth()
|
||||
|
||||
const [status, setStatus] = useState('loading')
|
||||
const [error, setError] = useState(null)
|
||||
const [email, setEmail] = useState('')
|
||||
const [resending, setResending] = useState(false)
|
||||
const [resendSuccess, setResendSuccess] = useState(false)
|
||||
const [hasStarted, setHasStarted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (hasStarted) return
|
||||
if (!token?.trim()) {
|
||||
setStatus('error')
|
||||
setError('Kein Verifikations-Token in der URL')
|
||||
return
|
||||
}
|
||||
setHasStarted(true)
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
const result = await api.verifyEmail(token.trim())
|
||||
if (result.token) {
|
||||
localStorage.setItem('authToken', result.token)
|
||||
await checkAuth()
|
||||
}
|
||||
setStatus('success')
|
||||
setTimeout(() => navigate('/', { replace: true }), 1500)
|
||||
} catch (err) {
|
||||
const msg = err.message || 'Verifizierung fehlgeschlagen'
|
||||
|
||||
if (msg.includes('bereits bestätigt') || msg.includes('bereits verwendet')) {
|
||||
setStatus('already_verified')
|
||||
setError(msg)
|
||||
setTimeout(() => navigate('/login', { replace: true }), 3500)
|
||||
} else if (msg.includes('abgelaufen')) {
|
||||
setStatus('expired')
|
||||
setError(msg)
|
||||
} else {
|
||||
setStatus('error')
|
||||
setError(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
run()
|
||||
}, [token, hasStarted, checkAuth, navigate])
|
||||
|
||||
const handleResend = async () => {
|
||||
if (!email.trim()) {
|
||||
setError('Bitte E-Mail-Adresse eingeben')
|
||||
return
|
||||
}
|
||||
setResending(true)
|
||||
setError(null)
|
||||
try {
|
||||
await api.resendVerification(email.trim().toLowerCase())
|
||||
setResendSuccess(true)
|
||||
} catch (err) {
|
||||
setError(err.message || 'Versand fehlgeschlagen')
|
||||
} finally {
|
||||
setResending(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div style={{ maxWidth: 420, margin: '0 auto', padding: '3rem 1rem', textAlign: 'center' }}>
|
||||
<div className="spinner" style={{ width: 48, height: 48, margin: '0 auto 1rem' }} />
|
||||
<h2 style={{ fontSize: '1.25rem' }}>E-Mail wird bestätigt…</h2>
|
||||
<p style={{ color: 'var(--text2)' }}>Einen Moment bitte.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'expired') {
|
||||
return (
|
||||
<div style={{ maxWidth: 420, margin: '0 auto', padding: '2rem 1rem' }}>
|
||||
<div className="card" style={{ marginBottom: '1rem' }}>
|
||||
<h2 style={{ fontSize: '1.15rem', marginBottom: '0.5rem' }}>Link abgelaufen</h2>
|
||||
<p style={{ color: 'var(--text2)', fontSize: '0.9rem', lineHeight: 1.5 }}>
|
||||
{error || 'Bitte fordere eine neue Bestätigungs-E-Mail an.'}
|
||||
</p>
|
||||
{resendSuccess ? (
|
||||
<p style={{ color: 'var(--accent)', fontWeight: 600, marginTop: '1rem' }}>E-Mail unterwegs — Postfach prüfen.</p>
|
||||
) : (
|
||||
<>
|
||||
<label className="form-label" style={{ marginTop: '1rem' }}>
|
||||
Registrierte E-Mail
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
className="form-input"
|
||||
placeholder="du@beispiel.de"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
/>
|
||||
{error && !resendSuccess && (
|
||||
<p style={{ color: 'var(--danger)', fontSize: '0.85rem', marginTop: '0.5rem' }}>{error}</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-full"
|
||||
style={{ marginTop: '0.85rem' }}
|
||||
disabled={resending || !email.trim()}
|
||||
onClick={handleResend}
|
||||
>
|
||||
{resending ? 'Sende…' : 'Erneut senden'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/login')}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: 'var(--text3)',
|
||||
textDecoration: 'underline',
|
||||
cursor: 'pointer',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
Zum Login
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'already_verified') {
|
||||
return (
|
||||
<div style={{ maxWidth: 420, margin: '0 auto', padding: '2rem 1rem', textAlign: 'center' }}>
|
||||
<div className="card">
|
||||
<h2 style={{ fontSize: '1.15rem' }}>Schon erledigt</h2>
|
||||
<p style={{ color: 'var(--text2)', fontSize: '0.92rem', marginTop: '0.5rem' }}>
|
||||
{error || 'E-Mail bereits bestätigt — du kannst dich anmelden.'}
|
||||
</p>
|
||||
<p style={{ fontSize: '0.82rem', color: 'var(--text3)', marginTop: '0.75rem' }}>Weiterleitung zum Login…</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return (
|
||||
<div style={{ maxWidth: 420, margin: '0 auto', padding: '2rem 1rem', textAlign: 'center' }}>
|
||||
<div className="card" style={{ borderColor: 'var(--danger)' }}>
|
||||
<h2 style={{ fontSize: '1.15rem', color: 'var(--danger)' }}>Konnte nicht bestätigen</h2>
|
||||
<p style={{ color: 'var(--text2)', fontSize: '0.92rem', marginTop: '0.5rem' }}>{error}</p>
|
||||
<button type="button" className="btn btn-primary btn-full" style={{ marginTop: '1rem' }} onClick={() => navigate('/login')}>
|
||||
Zum Login
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* success */
|
||||
return (
|
||||
<div style={{ maxWidth: 420, margin: '0 auto', padding: '2rem 1rem', textAlign: 'center' }}>
|
||||
<div className="card">
|
||||
<h2 style={{ fontSize: '1.15rem', color: 'var(--accent-dark)', marginBottom: '0.5rem' }}>
|
||||
Bestätigung erfolgreich
|
||||
</h2>
|
||||
<p style={{ color: 'var(--text2)', fontSize: '0.92rem' }}>Du wirst weitergeleitet…</p>
|
||||
<div className="spinner" style={{ width: 32, height: 32, margin: '1rem auto 0' }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -13,10 +13,16 @@ const API_URL = import.meta.env.VITE_API_URL || ''
|
|||
*/
|
||||
async function request(endpoint, options = {}) {
|
||||
const token = localStorage.getItem('authToken')
|
||||
const method = (options.method || 'GET').toUpperCase()
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
...options.headers,
|
||||
}
|
||||
// GET ohne Body: kein Content-Type: application/json (manche Proxies/Headers stören sich)
|
||||
if (method !== 'GET' && method !== 'HEAD') {
|
||||
if (!headers['Content-Type'] && !headers['content-type']) {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
if (token) {
|
||||
|
|
@ -107,6 +113,21 @@ export async function changePassword(newPassword) {
|
|||
})
|
||||
}
|
||||
|
||||
/** GET /api/auth/verify/{token} — keine Auth nötig; Token gehört zur URL des Bestätigungslinks */
|
||||
export async function verifyEmail(token) {
|
||||
const t = encodeURIComponent(token)
|
||||
return request(`/api/auth/verify/${t}`, {
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
export async function resendVerification(email) {
|
||||
return request('/api/auth/resend-verification', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email }),
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Clubs & Groups
|
||||
// ============================================================================
|
||||
|
|
@ -881,6 +902,8 @@ export const api = {
|
|||
getCurrentProfile,
|
||||
updateProfile,
|
||||
changePassword,
|
||||
verifyEmail,
|
||||
resendVerification,
|
||||
|
||||
// Clubs & Groups
|
||||
listClubs,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user