SetupPage.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import { useState } from 'react';
  2. import { useNavigate } from 'react-router-dom';
  3. import { useMutation } from '@tanstack/react-query';
  4. import { useTranslation } from 'react-i18next';
  5. import { api } from '../api/client';
  6. import { useToast } from '../contexts/ToastContext';
  7. import { useTheme } from '../contexts/ThemeContext';
  8. import { useAuth } from '../contexts/AuthContext';
  9. import { Info } from 'lucide-react';
  10. export function SetupPage() {
  11. const navigate = useNavigate();
  12. const { t } = useTranslation();
  13. const { showToast } = useToast();
  14. const { mode } = useTheme();
  15. const { refreshAuth } = useAuth();
  16. const [authEnabled, setAuthEnabled] = useState(false);
  17. const [adminUsername, setAdminUsername] = useState('');
  18. const [adminPassword, setAdminPassword] = useState('');
  19. const [confirmPassword, setConfirmPassword] = useState('');
  20. const setupMutation = useMutation({
  21. mutationFn: () =>
  22. api.setupAuth({
  23. auth_enabled: authEnabled,
  24. admin_username: authEnabled ? adminUsername : undefined,
  25. admin_password: authEnabled ? adminPassword : undefined,
  26. }),
  27. onSuccess: async (data) => {
  28. // Refresh auth status after setup
  29. await refreshAuth();
  30. if (data.auth_enabled) {
  31. if (data.admin_created) {
  32. showToast(t('setup.toast.authEnabledAdminCreated'));
  33. navigate('/login');
  34. } else {
  35. showToast(t('setup.toast.authEnabledExistingAdmins'));
  36. navigate('/login');
  37. }
  38. } else {
  39. showToast(t('setup.toast.setupCompleted'));
  40. navigate('/');
  41. }
  42. },
  43. onError: (error: Error) => {
  44. showToast(error.message, 'error');
  45. },
  46. });
  47. const handleSubmit = (e: React.FormEvent) => {
  48. e.preventDefault();
  49. if (authEnabled) {
  50. // Only validate if credentials are provided
  51. // If no credentials provided, backend will use existing admin users if they exist
  52. if (adminUsername || adminPassword) {
  53. if (!adminUsername || !adminPassword) {
  54. showToast(t('setup.toast.enterBothCredentials'), 'error');
  55. return;
  56. }
  57. if (adminPassword !== confirmPassword) {
  58. showToast(t('setup.toast.passwordsDoNotMatch'), 'error');
  59. return;
  60. }
  61. if (adminPassword.length < 6) {
  62. showToast(t('setup.toast.passwordTooShort'), 'error');
  63. return;
  64. }
  65. }
  66. }
  67. setupMutation.mutate();
  68. };
  69. return (
  70. <div className="min-h-screen flex items-center justify-center bg-bambu-dark p-4">
  71. <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">
  72. <div className="text-center">
  73. <div className="flex items-center justify-center mb-6">
  74. <img
  75. src={mode === 'dark' ? '/img/bambuddy_logo_dark_transparent.png' : '/img/bambuddy_logo_light.png'}
  76. alt="Bambuddy"
  77. className="h-16"
  78. />
  79. </div>
  80. <h2 className="text-3xl font-bold text-white">
  81. {t('setup.title')}
  82. </h2>
  83. <p className="mt-2 text-sm text-bambu-gray">
  84. {t('setup.subtitle')}
  85. </p>
  86. </div>
  87. <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
  88. <div className="space-y-4">
  89. <div className="flex items-center p-4 bg-bambu-dark-secondary/50 rounded-lg border border-bambu-dark-tertiary">
  90. <input
  91. id="auth-enabled"
  92. type="checkbox"
  93. checked={authEnabled}
  94. onChange={(e) => setAuthEnabled(e.target.checked)}
  95. className="h-4 w-4 text-bambu-green focus:ring-bambu-green border-bambu-dark-tertiary rounded bg-bambu-dark-secondary"
  96. />
  97. <label htmlFor="auth-enabled" className="ml-3 block text-sm font-medium text-white">
  98. {t('setup.enableAuth')}
  99. </label>
  100. </div>
  101. {authEnabled && (
  102. <div className="space-y-4 mt-4">
  103. <div className="p-3 bg-bambu-dark-secondary/50 border border-bambu-dark-tertiary rounded-lg">
  104. <div className="flex items-start gap-2">
  105. <Info className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
  106. <div className="text-sm text-bambu-gray">
  107. <p className="text-white font-medium mb-1">{t('setup.adminAccount')}</p>
  108. <p>
  109. {t('setup.adminAccountDesc')}
  110. </p>
  111. </div>
  112. </div>
  113. </div>
  114. <div>
  115. <label htmlFor="admin-username" className="block text-sm font-medium text-white mb-2">
  116. {t('setup.adminUsername')} <span className="text-bambu-gray text-xs">{t('setup.optionalIfAdminExists')}</span>
  117. </label>
  118. <input
  119. id="admin-username"
  120. type="text"
  121. value={adminUsername}
  122. onChange={(e) => setAdminUsername(e.target.value)}
  123. 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"
  124. placeholder={t('setup.adminUsernamePlaceholder')}
  125. autoComplete="username"
  126. />
  127. </div>
  128. <div>
  129. <label htmlFor="admin-password" className="block text-sm font-medium text-white mb-2">
  130. {t('setup.adminPassword')} <span className="text-bambu-gray text-xs">{t('setup.optionalIfAdminExists')}</span>
  131. </label>
  132. <input
  133. id="admin-password"
  134. type="password"
  135. value={adminPassword}
  136. onChange={(e) => setAdminPassword(e.target.value)}
  137. 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"
  138. placeholder={t('setup.adminPasswordPlaceholder')}
  139. minLength={6}
  140. autoComplete="new-password"
  141. />
  142. </div>
  143. {adminPassword && (
  144. <div>
  145. <label htmlFor="confirm-password" className="block text-sm font-medium text-white mb-2">
  146. {t('setup.confirmPassword')}
  147. </label>
  148. <input
  149. id="confirm-password"
  150. type="password"
  151. value={confirmPassword}
  152. onChange={(e) => setConfirmPassword(e.target.value)}
  153. 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"
  154. placeholder={t('setup.confirmPasswordPlaceholder')}
  155. minLength={6}
  156. autoComplete="new-password"
  157. />
  158. </div>
  159. )}
  160. </div>
  161. )}
  162. </div>
  163. <div>
  164. <button
  165. type="submit"
  166. disabled={setupMutation.isPending}
  167. 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"
  168. >
  169. {setupMutation.isPending ? t('setup.settingUp') : t('setup.completeSetup')}
  170. </button>
  171. </div>
  172. </form>
  173. </div>
  174. </div>
  175. );
  176. }