Help Center/ Relational Database Service_RDS for PostgreSQL/ Best Practices/ AI Application Development Platform/ Source Code Configuration Guide for RDS Supabase Email Authentication Demo
Updated on 2026-10-30 GMT+08:00
Copied.
Source Code Configuration Guide for RDS Supabase Email Authentication Demo
Scenarios
This section describes how to create a demo project and all source code files. Create a project and related directories on the local machine strictly according to the directory and file structure. Create the corresponding files in sequence and copy all source code.
Directory and File Structure
Figure 1 Directory and file structure of the demo project
The directory and file structure of the demo project is as follows:
supabase_auth_email // Project root directory
├── .env // Supabase environment variable configuration (URL/Anon Key)
├── .env.example // Example template of environment variables
├── index.html // Homepage entry interface
├── package.json // Build dependency configuration (React 18 + Vite 5 + Supabase)
├── vite.config.js // Vite build configuration
└── src // Core functional source code directory
├── App.jsx // Root component: dispatches pages based on login state, with Chinese-English switching support
├── main.jsx // React application entry mounting
├── index.css // Global styles
├── components // UI component directory
│ ├── AuthForm.jsx // Login/Registration form
│ ├── ChangeEmail.jsx // Email change component
│ ├── Dashboard.jsx // User panel after login
│ ├── Message.jsx // General message prompt
│ ├── ResetPassword.jsx // Password reset (request/recovery mode)
│ └── ResetPasswordInline.jsx // Inline password reset
├── context // React state management directory
│ ├── AuthContext.jsx // Authentication status (Supabase session)
│ └── LanguageContext.jsx // Language switching between Chinese and English
├── i18n // Internationalization text directory
│ ├── en.js // English text
│ └── zh.js // Chinese text
└── lib // Utility library directory
├── errors.js // Error code to user message mapping
└── supabase.js // Supabase client initialization File Content
.env
# Replace with the public access address of RDS Supabase.
VITE_SUPABASE_URL={supabase_url}
# Replace with the anon API key of RDS Supabase.
VITE_SUPABASE_ANON_KEY={supabase_api_key} .env.example
VITE_SUPABASE_URL=http://10.83.34.249:8443 VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzg1NzQ1MDc3LCJleHAiOjE5NDM0MjUwNzd9.YHK5wU2jJEviNPSCAxp6P658ZLXcUy3JudZOu21Gk0I
index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Supabase Email Authentication System</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html> package.json
{
"name": "supabase-email-auth-test",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@supabase/supabase-js": "2.98.1-canary.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"vite": "^5.0.0"
}
} vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
},
}) App.jsx
import { useState } from 'react'
import { AuthProvider, useAuth } from './context/AuthContext'
import { LanguageProvider, useLanguage } from './context/LanguageContext'
import AuthForm from './components/AuthForm'
import Dashboard from './components/Dashboard'
import ResetPassword from './components/ResetPassword'
function LanguageSwitch() {
const { language, toggleLanguage } = useLanguage()
return (
<button className="lang-switch" onClick={toggleLanguage}>
{language === 'zh' ? 'EN' : 'zh'}
</button>
)
}
function AppContent() {
const { user, loading, isPasswordRecovery } = useAuth()
const { t } = useLanguage()
const [showReset, setShowReset] = useState(false)
if (loading) {
return (
<div className="app-wrapper">
<LanguageSwitch />
<div className="loading-screen">
<div className="loading-spinner" />
<p>{t.common.loading}</p>
</div>
</div>
)
}
if (isPasswordRecovery) {
return (
<div className="app-wrapper">
<LanguageSwitch />
<ResetPassword type="recovery" />
</div>
)
}
if (user) {
return (
<div className="app-wrapper">
<LanguageSwitch />
<Dashboard />
</div>
)
}
if (showReset) {
return (
<div className="app-wrapper">
<LanguageSwitch />
<ResetPassword type="request" />
<button className="back-link" onClick={() => setShowReset(false)}>
{t.common.backToSignIn}
</button>
</div>
)
}
return (
<div className="app-wrapper">
<LanguageSwitch />
<AuthForm />
<button className="forgot-link" onClick={() => setShowReset(true)}>
{t.auth.forgotPassword}
</button>
</div>
)
}
export default function App() {
return (
<LanguageProvider>
<AuthProvider>
<AppContent />
</AuthProvider>
</LanguageProvider>
)
} index.css
:root {
--color-primary: #3ecf8e;
--color-primary-dark: #1a7f5a;
--color-bg: #f0fdf4;
--color-bg-gradient-start: #f0fdf4;
--color-bg-gradient-end: #dcfce7;
--color-card: #ffffff;
--color-text: #1f2937;
--color-text-secondary: #6b7280;
--color-border: #bbf7d0;
--color-error: #ef4444;
--color-error-bg: #fef2f2;
--color-success: #10b981;
--color-success-bg: #ecfdf5;
--color-info: #3b82f6;
--color-info-bg: #eff6ff;
--radius: 12px;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 4px 16px rgba(0, 0, 0, 0.04);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, sans-serif;
background: linear-gradient(135deg, var(--color-bg-gradient-start) 0%, var(--color-bg-gradient-end) 100%);
color: var(--color-text);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
.app-wrapper {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
position: relative;
}
.app-wrapper::before,
.app-wrapper::after {
content: '';
position: fixed;
border-radius: 50%;
filter: blur(80px);
opacity: 0.4;
pointer-events: none;
z-index: 0;
}
.app-wrapper::before {
width: 400px;
height: 400px;
background: var(--color-primary);
top: -100px;
right: -100px;
}
.app-wrapper::after {
width: 300px;
height: 300px;
background: var(--color-primary-dark);
bottom: -50px;
left: -50px;
opacity: 0.2;
}
.app-wrapper > * {
position: relative;
z-index: 1;
}
.loading-screen {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
color: var(--color-text-secondary);
}
.loading-spinner {
width: 36px;
height: 36px;
border: 3px solid var(--color-border);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.auth-card,
.reset-card,
.dashboard {
background: var(--color-card);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 2rem;
width: 100%;
max-width: 420px;
border: 1px solid var(--color-border);
}
.auth-header {
text-align: center;
margin-bottom: 1.5rem;
}
.auth-logo {
display: flex;
justify-content: center;
margin-bottom: 1rem;
}
.auth-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text);
margin-bottom: 0.25rem;
}
.auth-subtitle {
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.auth-tabs {
display: flex;
gap: 0;
margin-bottom: 1.25rem;
background: var(--color-bg);
border-radius: 8px;
padding: 4px;
}
.auth-tab {
flex: 1;
padding: 0.5rem 1rem;
border: none;
background: transparent;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s;
}
.auth-tab--active {
background: var(--color-card);
color: var(--color-text);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}
.auth-form,
.reset-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-field {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.form-label {
font-size: 0.8125rem;
font-weight: 500;
color: var(--color-text);
}
.form-input {
padding: 0.625rem 0.875rem;
border: 1px solid var(--color-border);
border-radius: 8px;
font-size: 0.875rem;
color: var(--color-text);
background: var(--color-card);
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(62, 207, 142, 0.15);
}
.form-input::placeholder {
color: #9ca3af;
}
.password-strength {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.25rem;
}
.strength-bar {
flex: 1;
height: 4px;
background: var(--color-border);
border-radius: 2px;
overflow: hidden;
}
.strength-fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s, background 0.3s;
}
.strength-weak { background: var(--color-error); }
.strength-medium { background: #f59e0b; }
.strength-strong { background: var(--color-success); }
.strength-label {
font-size: 0.75rem;
font-weight: 500;
min-width: 48px;
}
.strength-label-weak { color: var(--color-error); }
.strength-label-medium { color: #f59e0b; }
.strength-label-strong { color: var(--color-success); }
.btn-submit {
padding: 0.75rem 1rem;
background: var(--color-primary);
color: #fff;
border: none;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
margin-top: 0.5rem;
}
.btn-submit:hover {
background: var(--color-primary-dark);
}
.btn-submit:active {
transform: scale(0.98);
}
.btn-submit:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.alert {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
border-radius: 8px;
font-size: 0.8125rem;
margin-bottom: 1rem;
border: 1px solid transparent;
}
.alert-icon {
font-weight: 700;
flex-shrink: 0;
}
.alert-success {
background: var(--color-success-bg);
color: #065f46;
border-color: #a7f3d0;
}
.alert-error {
background: var(--color-error-bg);
color: #991b1b;
border-color: #fecaca;
}
.alert-info {
background: var(--color-info-bg);
color: #1e40af;
border-color: #bfdbfe;
}
.dashboard-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
font-weight: 700;
flex-shrink: 0;
}
.dashboard-greeting {
font-size: 1.25rem;
font-weight: 700;
}
.dashboard-email {
font-size: 0.8125rem;
color: var(--color-text-secondary);
}
.dashboard-details {
background: var(--color-bg);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.detail-row {
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.detail-label {
font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-secondary);
font-weight: 600;
}
.detail-value {
font-size: 0.8125rem;
color: var(--color-text);
word-break: break-all;
}
.detail-id {
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.75rem;
}
.btn-signout {
width: 100%;
padding: 0.75rem;
background: var(--color-card);
color: var(--color-error);
border: 1px solid var(--color-error);
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-signout:hover {
background: var(--color-error-bg);
}
.btn-signout--confirm {
background: var(--color-error);
color: #fff;
}
.btn-change-email {
width: 100%;
padding: 0.75rem;
background: var(--color-card);
color: var(--color-primary-dark);
border: 1px solid var(--color-primary);
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 0.75rem;
}
.btn-change-email:hover {
background: rgba(62, 207, 142, 0.08);
}
.btn-reset-password {
width: 100%;
padding: 0.75rem;
background: var(--color-card);
color: var(--color-info);
border: 1px solid var(--color-info);
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 0.75rem;
}
.btn-reset-password:hover {
background: var(--color-info-bg);
}
.btn-reauthenticate {
width: 100%;
padding: 0.75rem;
background: var(--color-card);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 0.75rem;
}
.btn-reauthenticate:hover {
background: var(--color-bg);
}
.btn-reauthenticate:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.reset-password-section {
background: var(--color-bg);
border-radius: 8px;
padding: 1.25rem;
margin-bottom: 0.75rem;
border: 1px solid var(--color-border);
}
.reset-password-title {
font-size: 1rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.reset-password-subtitle {
font-size: 0.8125rem;
color: var(--color-text-secondary);
margin-bottom: 1rem;
}
.reset-password-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.reset-password-actions {
display: flex;
gap: 0.75rem;
}
.reset-password-actions .btn-submit {
flex: 1;
margin-top: 0;
}
.change-email-section {
background: var(--color-bg);
border-radius: 8px;
padding: 1.25rem;
margin-bottom: 0.75rem;
border: 1px solid var(--color-border);
}
.change-email-title {
font-size: 1rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.change-email-subtitle {
font-size: 0.8125rem;
color: var(--color-text-secondary);
margin-bottom: 1rem;
}
.change-email-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.change-email-actions {
display: flex;
gap: 0.75rem;
}
.change-email-actions .btn-submit {
flex: 1;
margin-top: 0;
}
.btn-cancel {
padding: 0.75rem 1rem;
background: var(--color-card);
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-cancel:hover {
background: var(--color-bg);
color: var(--color-text);
}
.btn-cancel:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.dashboard-email--pending {
color: var(--color-info);
font-style: italic;
}
.reset-card {
text-align: center;
}
.reset-title {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.reset-subtitle {
font-size: 0.8125rem;
color: var(--color-text-secondary);
margin-bottom: 1.25rem;
}
.reset-form {
text-align: left;
}
.reset-success {
padding: 1rem 0;
}
.reset-success-icon {
margin-bottom: 1rem;
}
.reset-success .reset-subtitle {
margin-bottom: 0;
}
.forgot-link,
.back-link {
margin-top: 1rem;
background: none;
border: none;
color: var(--color-text-secondary);
font-size: 0.8125rem;
cursor: pointer;
padding: 0.5rem;
transition: color 0.2s;
}
.forgot-link:hover,
.back-link:hover {
color: var(--color-primary-dark);
}
.lang-switch {
position: fixed;
top: 1rem;
right: 1rem;
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid var(--color-border);
background: var(--color-card);
color: var(--color-text);
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--shadow);
}
.lang-switch:hover {
border-color: var(--color-primary);
color: var(--color-primary);
transform: scale(1.05);
}
.magic-link-success {
text-align: center;
padding: 1rem 0;
}
.magic-link-success-icon {
margin-bottom: 1rem;
}
.magic-link-success-title {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.magic-link-success-subtitle {
font-size: 0.8125rem;
color: var(--color-text-secondary);
margin-bottom: 0;
} main.jsx
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css'
createRoot(document.getElementById('root')).render(<App />) AuthForm.jsx
import { useState } from 'react'
import { useAuth } from '../context/AuthContext'
import { useLanguage } from '../context/LanguageContext'
import Message from './Message'
import { getErrorMessage } from '../lib/errors'
function getPasswordStrength(password) {
if (!password) return { level: 0, label: '' }
let score = 0
if (password.length >= 6) score++
if (password.length >= 10) score++
if (/[A-Z]/.test(password)) score++
if (/[0-9]/.test(password)) score++
if (/[^A-Za-z0-9]/.test(password)) score++
if (score <= 1) return { level: 1, label: 'weak' }
if (score <= 3) return { level: 2, label: 'medium' }
return { level: 3, label: 'strong' }
}
export default function AuthForm() {
const { signUp, signIn, sendMagicLink } = useAuth()
const { t, language } = useLanguage()
const [mode, setMode] = useState('signin')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState({ type: '', text: '' })
const [magicLinkSent, setMagicLinkSent] = useState(false)
const [sentEmail, setSentEmail] = useState('')
const strength = getPasswordStrength(password)
const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
setMessage({ type: '', text: '' })
if (mode === 'magiclink') {
const { error } = await sendMagicLink(email)
setLoading(false)
if (error) {
setMessage({ type: 'error', text: getErrorMessage(error, language) })
} else {
setSentEmail(email)
setMagicLinkSent(true)
setMessage({ type: 'success', text: t.message.magicLinkSent })
setEmail('')
}
return
}
const action = mode === 'signin' ? signIn : signUp
const { data, error } = await action(email, password)
setLoading(false)
if (error) {
setMessage({ type: 'error', text: getErrorMessage(error, language) })
return
}
if (mode === 'signup' && data.user && !data.session) {
setMessage({ type: 'success', text: t.message.signUpSuccess })
setEmail('')
setPassword('')
} else {
setMessage({ type: 'success', text: mode === 'signin' ? t.message.signInSuccess : t.message.signUpCreated })
}
}
const switchMode = (newMode) => {
setMode(newMode)
setMessage({ type: '', text: '' })
setMagicLinkSent(false)
setSentEmail('')
}
const showPasswordField = mode !== 'magiclink'
return (
<div className="auth-card">
<div className="auth-header">
<div className="auth-logo">
<svg width="40" height="40" viewBox="0 0 40 40" fill="none">
<rect width="40" height="40" rx="10" fill="url(#grad)" />
<path d="M12 20l6 6 10-12" stroke="#fff" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
<defs>
<linearGradient id="grad" x1="0" y1="0" x2="40" y2="40">
<stop stopColor="#3ecf8e" />
<stop offset="1" stopColor="#1a7f5a" />
</linearGradient>
</defs>
</svg>
</div>
<h1 className="auth-title">{t.auth.title}</h1>
<p className="auth-subtitle">{t.auth.subtitle}</p>
</div>
<div className="auth-tabs">
<button
className={`auth-tab ${mode === 'signin' ? 'auth-tab--active' : ''}`}
onClick={() => switchMode('signin')}
>
{t.auth.signIn}
</button>
<button
className={`auth-tab ${mode === 'signup' ? 'auth-tab--active' : ''}`}
onClick={() => switchMode('signup')}
>
{t.auth.signUp}
</button>
<button
className={`auth-tab ${mode === 'magiclink' ? 'auth-tab--active' : ''}`}
onClick={() => switchMode('magiclink')}
>
{t.auth.magicLink}
</button>
</div>
<Message type={message.type} text={message.text} />
{magicLinkSent ? (
<div className="magic-link-success">
<div className="magic-link-success-icon">
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
<circle cx="24" cy="24" r="24" fill="#3ecf8e" fillOpacity="0.15" />
<path d="M16 24l6 6 10-12" stroke="#3ecf8e" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<h2 className="magic-link-success-title">{t.reset.checkEmail}</h2>
<p className="magic-link-success-subtitle" dangerouslySetInnerHTML={{ __html: t.reset.magicLinkEmailSent.replace('{email}', sentEmail) }} />
</div>
) : (
<form className="auth-form" onSubmit={handleSubmit}>
<div className="form-field">
<label className="form-label" htmlFor="email">{t.auth.email}</label>
<input
id="email"
type="email"
className="form-input"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={t.auth.emailPlaceholder}
required
/>
</div>
{showPasswordField && (
<div className="form-field">
<label className="form-label" htmlFor="password">{t.auth.password}</label>
<input
id="password"
type="password"
className="form-input"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={t.auth.passwordPlaceholder}
minLength={6}
required
/>
{mode === 'signup' && password && (
<div className="password-strength">
<div className="strength-bar">
<div
className={`strength-fill strength-${strength.label}`}
style={{ width: `${strength.level * 33.33}%` }}
/>
</div>
<span className={`strength-label strength-label-${strength.label}`}>
{t.password[strength.label]}
</span>
</div>
)}
</div>
)}
<button className="btn-submit" type="submit" disabled={loading}>
{loading
? t.auth.processing
: mode === 'signin'
? t.auth.signIn
: mode === 'signup'
? t.auth.createAccount
: t.auth.magicLink + 'Sign in'}
</button>
</form>
)}
</div>
)
} ChangeEmail.jsx
import { useState } from 'react'
import { useAuth } from '../context/AuthContext'
import { useLanguage } from '../context/LanguageContext'
import Message from './Message'
import { getErrorMessage } from '../lib/errors'
function withTimeout(promise, ms) {
let timer
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error('timeout')), ms)
})
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer))
}
export default function ChangeEmail({ onCancel }) {
const { user, updateEmail } = useAuth()
const { t, language } = useLanguage()
const [newEmail, setNewEmail] = useState('')
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState({ type: '', text: '' })
const handleSubmit = async (e) => {
e.preventDefault()
setMessage({ type: '', text: '' })
if (newEmail.trim().toLowerCase() === user.email?.toLowerCase()) {
setMessage({ type: 'error', text: t.changeEmail.sameEmail })
return
}
setLoading(true)
let result
try {
result = await withTimeout(updateEmail(newEmail.trim()), 15000)
} catch {
result = { error: { message: 'timeout' } }
}
setLoading(false)
const { error } = result
if (error) {
setMessage({ type: 'error', text: getErrorMessage(error, language) })
} else {
setMessage({ type: 'success', text: t.changeEmail.success })
setNewEmail('')
}
}
return (
<div className="change-email-section">
<h2 className="change-email-title">{t.changeEmail.title}</h2>
<p className="change-email-subtitle">{t.changeEmail.subtitle}</p>
<Message type={message.type} text={message.text} />
<form className="change-email-form" onSubmit={handleSubmit}>
<div className="form-field">
<label className="form-label" htmlFor="new-email">{t.changeEmail.newEmail}</label>
<input
id="new-email"
type="email"
className="form-input"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
placeholder={t.changeEmail.newEmailPlaceholder}
required
/>
</div>
<div className="change-email-actions">
<button className="btn-submit" type="submit" disabled={loading}>
{loading ? t.changeEmail.sending : t.changeEmail.submit}
</button>
<button className="btn-cancel" type="button" onClick={onCancel} disabled={loading}>
{t.changeEmail.cancel}
</button>
</div>
</form>
</div>
)
} Dashboard.jsx
import { useState, useEffect } from 'react'
import { useAuth } from '../context/AuthContext'
import { useLanguage } from '../context/LanguageContext'
import Message from './Message'
import ChangeEmail from './ChangeEmail'
import ResetPasswordInline from './ResetPasswordInline'
import { getErrorMessage } from '../lib/errors'
export default function Dashboard() {
const { user, signOut, reauthenticate, reauthCompleted } = useAuth()
const { t, language } = useLanguage()
const [message, setMessage] = useState({ type: '', text: '' })
const [confirming, setConfirming] = useState(false)
const [reauthLoading, setReauthLoading] = useState(false)
const [showChangeEmail, setShowChangeEmail] = useState(false)
const [showResetPassword, setShowResetPassword] = useState(false)
// After the user returns to the page through the re-authentication email link, a message is displayed indicating that the re-authentication is successful.
useEffect(() => {
if (reauthCompleted) {
setMessage({ type: 'success', text: t.message.reauthSuccess })
}
}, [reauthCompleted, t])
const handleSignOut = async () => {
const { error } = await signOut()
if (error) {
setMessage({ type: 'error', text: getErrorMessage(error, language) })
}
}
const handleReauthenticate = async () => {
setMessage({ type: '', text: '' })
setReauthLoading(true)
const { error } = await reauthenticate()
setReauthLoading(false)
if (error) {
setMessage({ type: 'error', text: getErrorMessage(error, language) })
} else {
setMessage({ type: 'success', text: t.message.reauthSent })
}
}
const handleConfirmSignOut = () => {
if (confirming) {
handleSignOut()
} else {
setConfirming(true)
setTimeout(() => setConfirming(false), 3000)
}
}
const locale = language === 'zh' ? 'zh-CN' : 'en-US'
const createdAt = new Date(user.created_at)
const formattedDate = createdAt.toLocaleDateString(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
})
const formattedTime = createdAt.toLocaleTimeString(locale, {
hour: '2-digit',
minute: '2-digit',
})
return (
<div className="dashboard">
<div className="dashboard-header">
<div className="avatar">
{user.email?.charAt(0).toUpperCase()}
</div>
<div>
<h1 className="dashboard-greeting">{t.dashboard.welcome}</h1>
<p className="dashboard-email">{user.email}</p>
{user.new_email && (
<p className="dashboard-email dashboard-email--pending">
{t.changeEmail.title}: {user.new_email}
</p>
)}
</div>
</div>
<Message type={message.type} text={message.text} />
<div className="dashboard-details">
<div className="detail-row">
<span className="detail-label">{t.dashboard.userId}</span>
<span className="detail-value detail-id">{user.id}</span>
</div>
<div className="detail-row">
<span className="detail-label">{t.dashboard.registered}</span>
<span className="detail-value">{formattedDate} {formattedTime}</span>
</div>
<div className="detail-row">
<span className="detail-label">{t.dashboard.lastSignIn}</span>
<span className="detail-value">{new Date(user.last_sign_in_at).toLocaleString(locale)}</span>
</div>
</div>
{showChangeEmail ? (
<ChangeEmail onCancel={() => setShowChangeEmail(false)} />
) : showResetPassword ? (
<ResetPasswordInline onCancel={() => setShowResetPassword(false)} />
) : (
<>
<button
className="btn-change-email"
onClick={() => setShowChangeEmail(true)}
>
{t.dashboard.changeEmail}
</button>
<button
className="btn-reset-password"
onClick={() => setShowResetPassword(true)}
>
{t.dashboard.resetPassword}
</button>
<button
className="btn-reauthenticate"
onClick={handleReauthenticate}
disabled={reauthLoading}
>
{reauthLoading ? t.auth.processing : t.dashboard.reauthenticate}
</button>
</>
)}
<button
className={`btn-signout ${confirming ? 'btn-signout--confirm' : ''}`}
onClick={handleConfirmSignOut}
>
{confirming ? t.dashboard.confirmSignOut : t.dashboard.signOut}
</button>
</div>
)
} Message.jsx
export default function Message({ type, text }) {
if (!text) return null
const icons = {
success: '\u2713',
error: '\u2717',
info: '\u2139',
}
return (
<div className={`alert alert-${type}`}>
<span className="alert-icon">{icons[type] || ''}</span>
<span>{text}</span>
</div>
)
} ResetPassword.jsx
import { useState } from 'react'
import { useAuth } from '../context/AuthContext'
import { useLanguage } from '../context/LanguageContext'
import Message from './Message'
import { getErrorMessage } from '../lib/errors'
export default function ResetPassword({ type }) {
const { resetPassword, updatePassword } = useAuth()
const { t, language } = useLanguage()
const [email, setEmail] = useState('')
const [newPassword, setNewPassword] = useState('')
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState({ type: '', text: '' })
const [emailSent, setEmailSent] = useState(false)
const handleSendReset = async (e) => {
e.preventDefault()
setLoading(true)
setMessage({ type: '', text: '' })
const { error } = await resetPassword(email)
setLoading(false)
if (error) {
setMessage({ type: 'error', text: getErrorMessage(error, language) })
} else {
setEmailSent(true)
setMessage({ type: 'success', text: t.message.resetSent })
}
}
const handleUpdatePassword = async (e) => {
e.preventDefault()
setLoading(true)
setMessage({ type: '', text: '' })
const { error } = await updatePassword(newPassword)
setLoading(false)
if (error) {
setMessage({ type: 'error', text: getErrorMessage(error, language) })
} else {
setMessage({ type: 'success', text: t.message.passwordUpdated })
setNewPassword('')
}
}
if (type === 'recovery') {
return (
<div className="reset-card">
<h2 className="reset-title">{t.reset.newTitle}</h2>
<p className="reset-subtitle">{t.reset.newSubtitle}</p>
<Message type={message.type} text={message.text} />
<form className="reset-form" onSubmit={handleUpdatePassword}>
<div className="form-field">
<label className="form-label" htmlFor="new-password">{t.reset.newPassword}</label>
<input
id="new-password"
type="password"
className="form-input"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder={t.auth.passwordPlaceholder}
minLength={6}
required
/>
</div>
<button className="btn-submit" type="submit" disabled={loading}>
{loading ? t.reset.updating : t.reset.updatePassword}
</button>
</form>
</div>
)
}
if (emailSent) {
return (
<div className="reset-card">
<div className="reset-success">
<div className="reset-success-icon">
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
<circle cx="24" cy="24" r="24" fill="#3ecf8e" fillOpacity="0.15" />
<path d="M16 24l6 6 10-12" stroke="#3ecf8e" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<h2 className="reset-title">{t.reset.checkEmail}</h2>
<p className="reset-subtitle" dangerouslySetInnerHTML={{ __html: t.reset.emailSent.replace('{email}', email) }} />
</div>
</div>
)
}
return (
<div className="reset-card">
<h2 className="reset-title">{t.reset.title}</h2>
<p className="reset-subtitle">{t.reset.subtitle}</p>
<Message type={message.type} text={message.text} />
<form className="reset-form" onSubmit={handleSendReset}>
<div className="form-field">
<label className="form-label" htmlFor="reset-email">{t.auth.email}</label>
<input
id="reset-email"
type="email"
className="form-input"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={t.auth.emailPlaceholder}
required
/>
</div>
<button className="btn-submit" type="submit" disabled={loading}>
{loading ? t.reset.sending : t.reset.sendResetLink}
</button>
</form>
</div>
)
} ResetPasswordInline.jsx
import { useState } from 'react'
import { useAuth } from '../context/AuthContext'
import { useLanguage } from '../context/LanguageContext'
import Message from './Message'
import { getErrorMessage } from '../lib/errors'
export default function ResetPasswordInline({ onCancel }) {
const { updatePassword } = useAuth()
const { t, language } = useLanguage()
const [newPassword, setNewPassword] = useState('')
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState({ type: '', text: '' })
const handleSubmit = async (e) => {
e.preventDefault()
setMessage({ type: '', text: '' })
setLoading(true)
const { error } = await updatePassword(newPassword)
setLoading(false)
if (error) {
setMessage({ type: 'error', text: getErrorMessage(error, language) })
} else {
setMessage({ type: 'success', text: t.message.passwordUpdated })
setNewPassword('')
}
}
return (
<div className="reset-password-section">
<h2 className="reset-password-title">{t.resetPassword.title}</h2>
<p className="reset-password-subtitle">{t.resetPassword.subtitle}</p>
<Message type={message.type} text={message.text} />
<form className="reset-password-form" onSubmit={handleSubmit}>
<div className="form-field">
<label className="form-label" htmlFor="new-password">{t.resetPassword.newPassword}</label>
<input
id="new-password"
type="password"
className="form-input"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder={t.auth.passwordPlaceholder}
minLength={6}
required
/>
</div>
<div className="reset-password-actions">
<button className="btn-submit" type="submit" disabled={loading}>
{loading ? t.resetPassword.updating : t.resetPassword.updatePassword}
</button>
<button className="btn-cancel" type="button" onClick={onCancel} disabled={loading}>
{t.resetPassword.cancel}
</button>
</div>
</form>
</div>
)
} AuthContext.jsx
import { createContext, useContext, useState, useEffect } from 'react'
import { supabase } from '../lib/supabase'
const REAUTH_PENDING_KEY = 'supabase_reauth_pending'
const AuthContext = createContext(null)
export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [isPasswordRecovery, setIsPasswordRecovery] = useState(false)
const [reauthCompleted, setReauthCompleted] = useState(false)
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null)
setLoading(false)
})
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
if (event === 'PASSWORD_RECOVERY') {
setIsPasswordRecovery(true)
}
// When the user returns to the page through the re-authentication email link, the re-authentication is marked as completed.
if (event === 'SIGNED_IN' && localStorage.getItem(REAUTH_PENDING_KEY)) {
localStorage.removeItem(REAUTH_PENDING_KEY)
setReauthCompleted(true)
}
setUser(session?.user ?? null)
})
return () => subscription.unsubscribe()
}, [])
const signUp = async (email, password) => {
const { data, error } = await supabase.auth.signUp({ email, password })
return { data, error }
}
const signIn = async (email, password) => {
const { data, error } = await supabase.auth.signInWithPassword({ email, password })
return { data, error }
}
const signOut = async () => {
const { error } = await supabase.auth.signOut()
return { error }
}
const resetPassword = async (email) => {
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: window.location.origin,
})
return { error }
}
const updatePassword = async (newPassword) => {
const { error } = await supabase.auth.updateUser({ password: newPassword })
if (!error) {
setIsPasswordRecovery(false)
}
return { error }
}
const updateEmail = async (newEmail) => {
const { data, error } = await supabase.auth.updateUser({ email: newEmail })
return { data, error }
}
const sendMagicLink = async (email) => {
const { data, error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: window.location.origin,
},
})
return { data, error }
}
const reauthenticate = async () => {
const { data, error } = await supabase.auth.reauthenticate()
if (!error) {
localStorage.setItem(REAUTH_PENDING_KEY, '1')
}
return { data, error }
}
const value = {
user,
loading,
isPasswordRecovery,
reauthCompleted,
signUp,
signIn,
signOut,
resetPassword,
updatePassword,
updateEmail,
sendMagicLink,
reauthenticate,
}
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
} LanguageContext.jsx
import { createContext, useContext, useState } from 'react'
import zh from '../i18n/zh'
import en from '../i18n/en'
const translations = { zh, en }
const LanguageContext = createContext(null)
export function LanguageProvider({ children }) {
const [language, setLanguage] = useState('zh')
const t = translations[language]
const toggleLanguage = () => setLanguage(prev => prev === 'zh' ? 'en' : 'zh')
return (
<LanguageContext.Provider value={{ language, t, toggleLanguage }}>
{children}
</LanguageContext.Provider>
)
}
export function useLanguage() {
const context = useContext(LanguageContext)
if (!context) {
throw new Error('useLanguage must be used within LanguageProvider')
}
return context
} en.js
export default {
auth: {
title: 'Supabase Auth',
subtitle: 'Email authentication demo',
signIn: 'Sign In',
signUp: 'Sign Up',
magicLink: 'Magic Link',
email: 'Email',
password: 'Password',
emailPlaceholder: 'you@example.com',
passwordPlaceholder: 'Minimum 6 characters',
processing: 'Processing...',
createAccount: 'Create Account',
forgotPassword: 'Forgot password?',
},
password: {
weak: 'Weak',
medium: 'Medium',
strong: 'Strong',
},
reset: {
title: 'Reset Password',
subtitle: 'Enter your email to receive a reset link',
sendResetLink: 'Send Reset Link',
sending: 'Sending...',
newTitle: 'Set New Password',
newSubtitle: 'Enter your new password below',
newPassword: 'New Password',
updatePassword: 'Update Password',
updating: 'Updating...',
checkEmail: 'Check Your Email',
emailSent: 'We sent a password reset link to <strong>{email}</strong>',
magicLinkEmailSent: 'We sent a magic link to <strong>{email}</strong>',
},
dashboard: {
welcome: 'Welcome!',
userId: 'User ID',
registered: 'Registered',
lastSignIn: 'Last Sign In',
signOut: 'Sign Out',
confirmSignOut: 'Click again to confirm',
changeEmail: 'Change Email',
resetPassword: 'Reset Password',
reauthenticate: 'Re-authenticate',
},
changeEmail: {
title: 'Change Email',
subtitle: 'Enter a new email address and we will send a confirmation link',
newEmail: 'New Email',
newEmailPlaceholder: 'new@example.com',
submit: 'Send Confirmation',
sending: 'Sending...',
cancel: 'Cancel',
success: 'A confirmation email has been sent to your new address. Please check and click the link to complete the change.',
emailChanged: 'Email changed successfully!',
sameEmail: 'The new email is the same as the current one',
networkError: 'Request timed out or service unavailable. Please check your network and retry.',
},
resetPassword: {
title: 'Reset Password',
subtitle: 'Enter your new password below',
newPassword: 'New Password',
updatePassword: 'Update Password',
updating: 'Updating...',
cancel: 'Cancel',
},
message: {
signUpSuccess: 'Registration successful! Please check your email to confirm your account.',
signInSuccess: 'Welcome back!',
signUpCreated: 'Account created successfully!',
resetSent: 'Password reset email sent. Please check your inbox.',
passwordUpdated: 'Password updated successfully!',
magicLinkSent: 'A Magic Link has been sent to your email. Please click the link in the email to complete sign in.',
reauthSent: 'A re-authentication email has been sent. Please click the link in the email to complete re-authentication.',
reauthSuccess: 'Re-authentication successful!',
},
common: {
loading: 'Loading...',
backToSignIn: '← Back to Sign In',
},
} errors.js
const errorMessages = {
en: {
'Invalid login credentials': 'Invalid login credentials',
'Email not confirmed': 'Email not confirmed. Please verify your email first',
'User already registered': 'This email is already registered',
'Password should be at least 6 characters': 'Password must be at least 6 characters',
'Unable to validate email address: invalid format': 'Invalid email format',
'rate_limit_exceeded': 'Too many requests. Please try again later',
'timeout': 'Request timed out or service unavailable. Please check your network and retry',
'default': 'Operation failed. Please try again',
},
}
export function getErrorMessage(error, language = 'zh') {
if (!error) return ''
const message = error.message || 'default'
const translations = errorMessages[language] || errorMessages.zh
return translations[message] || translations.default
} supabase.js
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
if (!supabaseUrl || !supabaseAnonKey) {
console.error('Missing VITE_SUPABASE_URL or VITE_SUPABASE_ANON_KEY in .env')
}
export const supabase = createClient(supabaseUrl || '', supabaseAnonKey || '') Parent Topic: AI Application Development Platform
What is your overall rating for this page?
0
1
2
3
4
5
6
7
8
9
10
Very dissatisfiedVery satisfied
Thank you very much for your feedback. We will continue working to improve the documentation.See the reply and handling status in My Cloud VOC.
The system is busy. Please try again later.
For any further questions, feel free to contact us through the chatbot.
Chatbot