|
1 | 1 | /** |
2 | | - * Login page component |
3 | | - * Uses template string approach for Alpine.js compatibility |
| 2 | + * Login page component - Modular version |
| 3 | + * Composes login components |
4 | 4 | * @param {{ totpEnabled: boolean }} props |
5 | 5 | * @returns {string} HTML string |
6 | 6 | */ |
| 7 | +import { Head } from './components/Head.js' |
| 8 | +import { LoginCard } from './components/LoginCard.js' |
| 9 | +import { loginAppScript } from './scripts/loginApp.js' |
| 10 | + |
7 | 11 | export const LoginPage = ({ totpEnabled = false }) => ` |
8 | 12 | <!DOCTYPE html> |
9 | 13 | <html lang="en"> |
10 | 14 | <head> |
11 | | - <meta charset="UTF-8"> |
12 | | - <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
13 | | - <title>Login - Backup Manager</title> |
14 | | - <script src="https://cdn.tailwindcss.com"></script> |
15 | | - <script> |
16 | | - tailwind.config = { |
17 | | - theme: { |
18 | | - extend: { |
19 | | - fontFamily: { |
20 | | - sans: ['Montserrat', 'sans-serif'], |
21 | | - }, |
22 | | - colors: { |
23 | | - gray: { |
24 | | - 50: '#F9FAFB', |
25 | | - 100: '#F3F4F6', |
26 | | - 200: '#E5E7EB', |
27 | | - 300: '#D1D5DB', |
28 | | - 400: '#9CA3AF', |
29 | | - 500: '#6B7280', |
30 | | - 600: '#4B5563', |
31 | | - 700: '#374151', |
32 | | - 800: '#1F2937', |
33 | | - 900: '#111827', |
34 | | - } |
35 | | - } |
36 | | - } |
37 | | - } |
38 | | - } |
39 | | - </script> |
40 | | - <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> |
41 | | - <script src="https://unpkg.com/lucide@latest"></script> |
42 | | - <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet"> |
43 | | - <style> |
44 | | - [x-cloak] { display: none !important; } |
45 | | - </style> |
| 15 | + ${Head({ title: 'Login - Backup Manager' })} |
46 | 16 | </head> |
47 | 17 | <body class="bg-gray-50 min-h-screen flex items-center justify-center p-6 antialiased"> |
48 | | - <div class="w-full max-w-md" x-data="loginApp()"> |
49 | | - <!-- Login Card --> |
50 | | - <div class="bg-white rounded-2xl border border-gray-100 shadow-[0_8px_30px_rgba(0,0,0,0.08)] overflow-hidden"> |
51 | | - <!-- Header --> |
52 | | - <div class="p-10 text-center border-b border-gray-100"> |
53 | | - <div class="w-16 h-16 bg-gray-900 rounded-full flex items-center justify-center mx-auto mb-4"> |
54 | | - <i data-lucide="shield-check" class="w-8 h-8 text-white"></i> |
55 | | - </div> |
56 | | - <h1 class="text-2xl font-bold text-gray-900 mb-2">Backup Manager</h1> |
57 | | - <p class="text-sm text-gray-500">Access Control Panel</p> |
58 | | - </div> |
59 | | -
|
60 | | - <!-- Form --> |
61 | | - <div class="p-10"> |
62 | | - <form @submit.prevent="login" class="space-y-6"> |
63 | | - <!-- Error Message --> |
64 | | - <div x-show="error" x-cloak class="bg-red-50 border border-red-200 rounded-lg p-4"> |
65 | | - <div class="flex items-center gap-3"> |
66 | | - <i data-lucide="alert-circle" class="w-5 h-5 text-red-600"></i> |
67 | | - <span class="text-sm text-red-800 font-medium" x-text="error"></span> |
68 | | - </div> |
69 | | - </div> |
70 | | -
|
71 | | - <!-- Username --> |
72 | | - <div> |
73 | | - <label class="block text-sm font-semibold text-gray-700 mb-2">Username</label> |
74 | | - <div class="relative"> |
75 | | - <div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none"> |
76 | | - <i data-lucide="user" class="w-5 h-5 text-gray-400"></i> |
77 | | - </div> |
78 | | - <input |
79 | | - type="text" |
80 | | - x-model="username" |
81 | | - required |
82 | | - class="w-full bg-gray-50 border border-gray-200 rounded-lg pl-12 pr-4 py-3 text-gray-900 focus:ring-2 focus:ring-gray-900 focus:border-transparent outline-none transition-all font-medium" |
83 | | - placeholder="Enter your username" |
84 | | - autofocus |
85 | | - > |
86 | | - </div> |
87 | | - </div> |
88 | | -
|
89 | | - <!-- Password --> |
90 | | - <div> |
91 | | - <label class="block text-sm font-semibold text-gray-700 mb-2">Password</label> |
92 | | - <div class="relative"> |
93 | | - <div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none"> |
94 | | - <i data-lucide="lock" class="w-5 h-5 text-gray-400"></i> |
95 | | - </div> |
96 | | - <input |
97 | | - type="password" |
98 | | - x-model="password" |
99 | | - required |
100 | | - class="w-full bg-gray-50 border border-gray-200 rounded-lg pl-12 pr-4 py-3 text-gray-900 focus:ring-2 focus:ring-gray-900 focus:border-transparent outline-none transition-all font-medium" |
101 | | - placeholder="Enter your password" |
102 | | - > |
103 | | - </div> |
104 | | - </div> |
105 | | -
|
106 | | - <!-- TOTP Code (only shown when TOTP is enabled) --> |
107 | | - <div x-show="totpEnabled" x-cloak> |
108 | | - <label class="block text-sm font-semibold text-gray-700 mb-2">Authenticator Code</label> |
109 | | - <div class="relative"> |
110 | | - <div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none"> |
111 | | - <i data-lucide="smartphone" class="w-5 h-5 text-gray-400"></i> |
112 | | - </div> |
113 | | - <input |
114 | | - type="text" |
115 | | - x-model="totpCode" |
116 | | - inputmode="numeric" |
117 | | - pattern="[0-9]*" |
118 | | - maxlength="6" |
119 | | - :required="totpEnabled" |
120 | | - class="w-full bg-gray-50 border border-gray-200 rounded-lg pl-12 pr-4 py-3 text-gray-900 focus:ring-2 focus:ring-gray-900 focus:border-transparent outline-none transition-all font-medium tracking-widest text-center text-lg" |
121 | | - placeholder="000000" |
122 | | - > |
123 | | - </div> |
124 | | - <p class="text-xs text-gray-500 mt-2 flex items-center gap-1"> |
125 | | - <i data-lucide="info" class="w-3 h-3"></i> |
126 | | - Enter the 6-digit code from your authenticator app |
127 | | - </p> |
128 | | - </div> |
129 | | -
|
130 | | - <!-- Submit Button --> |
131 | | - <button |
132 | | - type="submit" |
133 | | - :disabled="loading" |
134 | | - class="w-full bg-gray-900 hover:bg-gray-800 text-white font-bold py-3.5 px-6 rounded-xl transition-all shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none" |
135 | | - > |
136 | | - <span x-show="!loading" class="flex items-center gap-2"> |
137 | | - <span>Sign In</span> |
138 | | - <i data-lucide="arrow-right" class="w-5 h-5"></i> |
139 | | - </span> |
140 | | - <span x-show="loading" class="flex items-center gap-2"> |
141 | | - <i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i> |
142 | | - <span>Authenticating...</span> |
143 | | - </span> |
144 | | - </button> |
145 | | - </form> |
146 | | - </div> |
147 | | - </div> |
148 | | -
|
149 | | - <!-- Footer --> |
150 | | - <div class="text-center mt-6 text-sm text-gray-500"> |
151 | | - <i data-lucide="info" class="w-4 h-4 inline-block mr-1"></i> |
152 | | - Secure connection required for production use |
153 | | - </div> |
154 | | - </div> |
155 | | -
|
156 | | - <script> |
157 | | - document.addEventListener('alpine:init', () => { |
158 | | - Alpine.data('loginApp', () => ({ |
159 | | - username: '', |
160 | | - password: '', |
161 | | - totpCode: '', |
162 | | - totpEnabled: ${totpEnabled}, |
163 | | - loading: false, |
164 | | - error: '', |
165 | | -
|
166 | | - init() { |
167 | | - this.$nextTick(() => lucide.createIcons()); |
168 | | - }, |
169 | | -
|
170 | | - async login() { |
171 | | - this.loading = true; |
172 | | - this.error = ''; |
173 | | -
|
174 | | - try { |
175 | | - const payload = { |
176 | | - username: this.username, |
177 | | - password: this.password |
178 | | - }; |
179 | | - |
180 | | - if (this.totpEnabled && this.totpCode) { |
181 | | - payload.totpCode = this.totpCode; |
182 | | - } |
183 | | -
|
184 | | - const response = await fetch('/backup/auth/login', { |
185 | | - method: 'POST', |
186 | | - headers: { 'Content-Type': 'application/json' }, |
187 | | - body: JSON.stringify(payload) |
188 | | - }); |
189 | | -
|
190 | | - const data = await response.json(); |
| 18 | + ${LoginCard({ totpEnabled })} |
191 | 19 |
|
192 | | - if (response.ok && data.status === 'success') { |
193 | | - window.location.href = '/backup'; |
194 | | - } else { |
195 | | - this.error = data.message || 'Invalid credentials'; |
196 | | - this.$nextTick(() => lucide.createIcons()); |
197 | | - } |
198 | | - } catch (err) { |
199 | | - this.error = 'Connection failed. Please try again.'; |
200 | | - this.$nextTick(() => lucide.createIcons()); |
201 | | - } finally { |
202 | | - this.loading = false; |
203 | | - this.$nextTick(() => lucide.createIcons()); |
204 | | - } |
205 | | - } |
206 | | - })); |
207 | | - }); |
208 | | - </script> |
| 20 | + ${loginAppScript({ totpEnabled })} |
209 | 21 | </body> |
210 | 22 | </html> |
211 | 23 | ` |
0 commit comments