LoginPage.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import { useState } from 'react';
  2. import { useNavigate } from 'react-router-dom';
  3. import { useMutation, useQuery } from '@tanstack/react-query';
  4. import { useTranslation } from 'react-i18next';
  5. import { useAuth } from '../contexts/AuthContext';
  6. import { useToast } from '../contexts/ToastContext';
  7. import { useTheme } from '../contexts/ThemeContext';
  8. import { X, Mail } from 'lucide-react';
  9. import { api } from '../api/client';
  10. import { Card, CardHeader, CardContent } from '../components/Card';
  11. import { Button } from '../components/Button';
  12. export function LoginPage() {
  13. const navigate = useNavigate();
  14. const { t } = useTranslation();
  15. const { login } = useAuth();
  16. const { showToast } = useToast();
  17. const { mode } = useTheme();
  18. const [username, setUsername] = useState('');
  19. const [password, setPassword] = useState('');
  20. const [showForgotPassword, setShowForgotPassword] = useState(false);
  21. const [forgotEmail, setForgotEmail] = useState('');
  22. // Check if advanced auth is enabled
  23. const { data: advancedAuthStatus } = useQuery({
  24. queryKey: ['advancedAuthStatus'],
  25. queryFn: () => api.getAdvancedAuthStatus(),
  26. });
  27. const loginMutation = useMutation({
  28. mutationFn: () => login(username, password),
  29. onSuccess: () => {
  30. showToast(t('login.loginSuccess'));
  31. navigate('/');
  32. },
  33. onError: (error: Error) => {
  34. showToast(error.message || t('login.loginFailed'), 'error');
  35. },
  36. });
  37. const forgotPasswordMutation = useMutation({
  38. mutationFn: (email: string) => api.forgotPassword({ email }),
  39. onSuccess: (data) => {
  40. showToast(data.message, 'success');
  41. setShowForgotPassword(false);
  42. setForgotEmail('');
  43. },
  44. onError: (error: Error) => {
  45. showToast(error.message, 'error');
  46. },
  47. });
  48. const handleSubmit = (e: React.FormEvent) => {
  49. e.preventDefault();
  50. if (!username || !password) {
  51. showToast(t('login.enterCredentials'), 'error');
  52. return;
  53. }
  54. loginMutation.mutate();
  55. };
  56. const handleForgotPassword = (e: React.FormEvent) => {
  57. e.preventDefault();
  58. if (!forgotEmail) {
  59. showToast('Please enter your email address', 'error');
  60. return;
  61. }
  62. forgotPasswordMutation.mutate(forgotEmail);
  63. };
  64. return (
  65. <div className="min-h-screen flex items-center justify-center bg-bambu-dark p-4">
  66. <div className="max-w-md w-full space-y-8 p-8 bg-gradient-to-br from-bambu-card to-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary shadow-lg">
  67. <div className="text-center">
  68. <div className="flex items-center justify-center mb-6">
  69. <img
  70. src={mode === 'dark' ? '/img/bambuddy_logo_dark_transparent.png' : '/img/bambuddy_logo_light.png'}
  71. alt="Bambuddy"
  72. className="h-16"
  73. />
  74. </div>
  75. <h2 className="text-3xl font-bold text-white">
  76. {t('login.title')}
  77. </h2>
  78. <p className="mt-2 text-sm text-bambu-gray">
  79. {t('login.subtitle')}
  80. </p>
  81. </div>
  82. <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
  83. <div className="space-y-4">
  84. <div>
  85. <label htmlFor="username" className="block text-sm font-medium text-white mb-2">
  86. {advancedAuthStatus?.advanced_auth_enabled
  87. ? t('login.usernameOrEmail') || 'Username or Email'
  88. : t('login.username')}
  89. </label>
  90. <input
  91. id="username"
  92. type="text"
  93. required
  94. value={username}
  95. onChange={(e) => setUsername(e.target.value)}
  96. className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
  97. placeholder={advancedAuthStatus?.advanced_auth_enabled
  98. ? t('login.usernameOrEmailPlaceholder') || 'Enter your username or email'
  99. : t('login.usernamePlaceholder')}
  100. autoComplete="username"
  101. />
  102. </div>
  103. <div>
  104. <label htmlFor="password" className="block text-sm font-medium text-white mb-2">
  105. {t('login.password')}
  106. </label>
  107. <input
  108. id="password"
  109. type="password"
  110. required
  111. value={password}
  112. onChange={(e) => setPassword(e.target.value)}
  113. className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
  114. placeholder={t('login.passwordPlaceholder')}
  115. autoComplete="current-password"
  116. />
  117. </div>
  118. </div>
  119. <div>
  120. <button
  121. type="submit"
  122. disabled={loginMutation.isPending}
  123. className="w-full flex justify-center py-3 px-4 bg-bambu-green hover:bg-bambu-green-light text-white font-medium rounded-lg shadow-lg shadow-bambu-green/20 hover:shadow-bambu-green/30 focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:ring-offset-2 focus:ring-offset-bambu-dark-secondary transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-bambu-green"
  124. >
  125. {loginMutation.isPending ? t('login.signingIn') : t('login.signIn')}
  126. </button>
  127. </div>
  128. {advancedAuthStatus?.advanced_auth_enabled && (
  129. <div className="text-center">
  130. <button
  131. type="button"
  132. onClick={() => setShowForgotPassword(true)}
  133. className="text-sm text-bambu-gray hover:text-bambu-green transition-colors"
  134. >
  135. {t('login.forgotPassword')}
  136. </button>
  137. </div>
  138. )}
  139. </form>
  140. </div>
  141. {/* Forgot Password Modal */}
  142. {showForgotPassword && (
  143. <div
  144. className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
  145. onClick={() => setShowForgotPassword(false)}
  146. >
  147. <Card
  148. className="w-full max-w-md"
  149. onClick={(e: React.MouseEvent) => e.stopPropagation()}
  150. >
  151. <CardHeader>
  152. <div className="flex items-center justify-between">
  153. <div className="flex items-center gap-2">
  154. <Mail className="w-5 h-5 text-bambu-green" />
  155. <h2 className="text-lg font-semibold text-white">{t('login.forgotPasswordTitle')}</h2>
  156. </div>
  157. <Button
  158. variant="ghost"
  159. size="sm"
  160. onClick={() => {
  161. setShowForgotPassword(false);
  162. setForgotEmail('');
  163. }}
  164. >
  165. <X className="w-5 h-5" />
  166. </Button>
  167. </div>
  168. </CardHeader>
  169. <CardContent>
  170. {advancedAuthStatus?.advanced_auth_enabled ? (
  171. <form onSubmit={handleForgotPassword} className="space-y-4">
  172. <p className="text-bambu-gray text-sm">
  173. {t('login.forgotPasswordEmailMessage') || 'Enter your email address and we\'ll send you a new password.'}
  174. </p>
  175. <div>
  176. <label htmlFor="forgot-email" className="block text-sm font-medium text-white mb-2">
  177. {t('login.emailAddress') || 'Email Address'}
  178. </label>
  179. <input
  180. id="forgot-email"
  181. type="email"
  182. required
  183. value={forgotEmail}
  184. onChange={(e) => setForgotEmail(e.target.value)}
  185. className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
  186. placeholder={t('login.emailPlaceholder') || 'your.email@example.com'}
  187. />
  188. </div>
  189. <div className="flex gap-2">
  190. <Button
  191. type="button"
  192. variant="secondary"
  193. className="flex-1"
  194. onClick={() => {
  195. setShowForgotPassword(false);
  196. setForgotEmail('');
  197. }}
  198. >
  199. {t('login.cancel') || 'Cancel'}
  200. </Button>
  201. <Button
  202. type="submit"
  203. className="flex-1"
  204. disabled={forgotPasswordMutation.isPending}
  205. >
  206. {forgotPasswordMutation.isPending
  207. ? (t('login.sending') || 'Sending...')
  208. : (t('login.sendResetEmail') || 'Send Reset Email')}
  209. </Button>
  210. </div>
  211. </form>
  212. ) : (
  213. <div className="space-y-4">
  214. <p className="text-bambu-gray">
  215. {t('login.forgotPasswordMessage')}
  216. </p>
  217. <div className="bg-bambu-dark rounded-lg p-4 space-y-2">
  218. <p className="text-sm text-white font-medium">{t('login.howToReset')}</p>
  219. <ol className="text-sm text-bambu-gray space-y-1 list-decimal list-inside">
  220. <li>{t('login.resetStep1')}</li>
  221. <li>{t('login.resetStep2')}</li>
  222. <li>{t('login.resetStep3')}</li>
  223. <li>{t('login.resetStep4')}</li>
  224. </ol>
  225. </div>
  226. <Button
  227. variant="secondary"
  228. className="w-full"
  229. onClick={() => setShowForgotPassword(false)}
  230. >
  231. {t('login.gotIt')}
  232. </Button>
  233. </div>
  234. )}
  235. </CardContent>
  236. </Card>
  237. </div>
  238. )}
  239. </div>
  240. );
  241. }