# RDS Supabase邮箱认证演示源码配置指导
#### 操作场景
本章节用于指导如何完整的创建演示Demo项目，并创建好所有的源码文件。请严格按照目录及文件结构，在本地创建项目工程及相关目录；并依次新建对应的文件，拷贝所有源码内容。
#### 目录及文件结构
图1Demo项目目录及文件结构概览   
![](https://support.huaweicloud.com/bestpractice-rds-pg/zh-cn_image_0000002801316877.png)
Demo项目目录及文件结构层次关系如下：
```
supabase_auth_email  //项目根目录
├── .env                    //Supabase环境变量配置(URL/Anon Key)
├── .env.example            //环境变量示例模板
├── index.html              //首页入口界面
├── package.json            //打包依赖配置(React18 + Vite5 + Supabase)
├── vite.config.js          //Vite构建配置
└── src                     //核心功能源码目录
    ├── App.jsx             //根组件：按登录态分发页面,含中英切换
    ├── main.jsx            //React应用入口挂载
    ├── index.css           //全局样式
    ├── components          //UI组件目录
    │   ├── AuthForm.jsx        //登录/注册表单
    │   ├── ChangeEmail.jsx     //修改邮箱组件
    │   ├── Dashboard.jsx       //登录后用户面板
    │   ├── Message.jsx         //通用消息提示
    │   ├── ResetPassword.jsx   //重置密码(请求/恢复模式)
    │   └── ResetPasswordInline.jsx //内联重置密码
    ├── context             //React状态管理目录
    │   ├── AuthContext.jsx     //认证状态(Supabase会话)
    │   └── LanguageContext.jsx //中英语言切换
    ├── i18n                //国际化文案目录
    │   ├── en.js               //英文文案
    │   └── zh.js               //中文文案
    └── lib                 //工具库目录
        ├── errors.js           //错误码→用户提示映射
        └── supabase.js         //Supabase客户端初始化
```
#### 文件内容
#### .env
```
# 替换为RDS Supabase的公网访问地址
VITE_SUPABASE_URL={supabase_url}
# 替换为RDS Supabase的Anon API Key
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 邮箱认证系统</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' : '中'}
    </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" />
            <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 + ' 登录'}
          </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)
  // 通过重新认证邮件链接返回页面后，提示重新认证成功
  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)
      }
      // 通过重新认证邮件链接回到页面时，标记重新认证已完成
      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',
  },
}
```
#### zh.js
```
export default {
  auth: {
    title: 'Supabase 认证',
    subtitle: '邮箱认证系统',
    signIn: '登录',
    signUp: '注册',
    magicLink: 'Magic Link',
    email: '邮箱',
    password: '密码',
    emailPlaceholder: 'you@example.com',
    passwordPlaceholder: '至少6位字符',
    processing: '处理中...',
    createAccount: '创建账户',
    forgotPassword: '忘记密码？',
  },
  password: {
    weak: '弱',
    medium: '中',
    strong: '强',
  },
  reset: {
    title: '重置密码',
    subtitle: '输入您的邮箱以接收重置链接',
    sendResetLink: '发送重置链接',
    sending: '发送中...',
    newTitle: '设置新密码',
    newSubtitle: '在下方输入您的新密码',
    newPassword: '新密码',
    updatePassword: '更新密码',
    updating: '更新中...',
    checkEmail: '检查您的邮箱',
    emailSent: '我们已向 <strong>{email}</strong> 发送了密码重置链接',
    magicLinkEmailSent: '我们已向 <strong>{email}</strong> 发送了魔法链接',
  },
  dashboard: {
    welcome: '欢迎回来！',
    userId: '用户 ID',
    registered: '注册时间',
    lastSignIn: '最后登录',
    signOut: '退出登录',
    confirmSignOut: '再次点击确认',
    changeEmail: '更换邮箱',
    resetPassword: '重置密码',
    reauthenticate: '重新认证',
  },
  changeEmail: {
    title: '更换邮箱',
    subtitle: '输入新的邮箱地址，我们将发送确认邮件',
    newEmail: '新邮箱',
    newEmailPlaceholder: 'new@example.com',
    submit: '发送确认邮件',
    sending: '发送中...',
    cancel: '取消',
    success: '确认邮件已发送至新邮箱，请查收并点击确认链接以完成更换。',
    emailChanged: '邮箱更换成功！',
    sameEmail: '新邮箱与当前邮箱相同',
    networkError: '请求超时或服务不可用，请检查网络后重试。',
  },
  resetPassword: {
    title: '重置密码',
    subtitle: '在下方输入您的新密码',
    newPassword: '新密码',
    updatePassword: '更新密码',
    updating: '更新中...',
    cancel: '取消',
  },
  message: {
    signUpSuccess: '注册成功！请检查您的邮箱以确认账户。',
    signInSuccess: '欢迎回来！',
    signUpCreated: '账户创建成功！',
    resetSent: '密码重置邮件已发送，请检查您的邮箱。',
    passwordUpdated: '密码更新成功！',
    magicLinkSent: 'Magic Link 已发送至您的邮箱，请点击邮件中的链接完成登录。',
    reauthSent: '重新认证邮件已发送，请点击邮件中的链接完成认证。',
    reauthSuccess: '重新认证成功！',
  },
  common: {
    loading: '加载中...',
    backToSignIn: '← 返回登录',
  },
}
```
#### errors.js
```
const errorMessages = {
  zh: {
    'Invalid login credentials': '登录凭证无效，请检查邮箱和密码',
    'Email not confirmed': '邮箱未确认，请先确认您的邮箱',
    'User already registered': '该邮箱已注册',
    'Password should be at least 6 characters': '密码至少6位字符',
    'Unable to validate email address: invalid format': '邮箱格式无效',
    'rate_limit_exceeded': '请求过于频繁，请稍后重试',
    'timeout': '请求超时或服务不可用，请检查网络后重试',
    'default': '操作失败，请重试',
  },
  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 || '')
```
