vue · 6373 bytes Raw Blame History
1 <template>
2 <div class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
3 <div class="max-w-md w-full space-y-8">
4 <!-- Header -->
5 <div class="text-center">
6 <h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-2">
7 ZephyrFS
8 </h1>
9 <h2 class="text-xl text-gray-600 dark:text-gray-400 mb-8">
10 Sign in to your account
11 </h2>
12 </div>
13
14 <!-- Login form -->
15 <form @submit.prevent="handleLogin" class="space-y-6">
16 <div class="space-y-4">
17 <!-- Username field -->
18 <div>
19 <label for="username" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
20 Username
21 </label>
22 <input
23 id="username"
24 v-model="form.username"
25 type="text"
26 required
27 class="input"
28 :class="{ 'border-red-500': errors.username }"
29 placeholder="Enter your username"
30 autocomplete="username"
31 />
32 <p v-if="errors.username" class="mt-1 text-sm text-red-600 dark:text-red-400">
33 {{ errors.username }}
34 </p>
35 </div>
36
37 <!-- Password field -->
38 <div>
39 <label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
40 Password
41 </label>
42 <div class="relative">
43 <input
44 id="password"
45 v-model="form.password"
46 :type="showPassword ? 'text' : 'password'"
47 required
48 class="input pr-10"
49 :class="{ 'border-red-500': errors.password }"
50 placeholder="Enter your password"
51 autocomplete="current-password"
52 />
53 <button
54 type="button"
55 @click="showPassword = !showPassword"
56 class="absolute inset-y-0 right-0 pr-3 flex items-center"
57 >
58 <EyeIcon v-if="!showPassword" class="w-4 h-4 text-gray-400" />
59 <EyeSlashIcon v-else class="w-4 h-4 text-gray-400" />
60 </button>
61 </div>
62 <p v-if="errors.password" class="mt-1 text-sm text-red-600 dark:text-red-400">
63 {{ errors.password }}
64 </p>
65 </div>
66 </div>
67
68 <!-- Remember me -->
69 <div class="flex items-center justify-between">
70 <label class="flex items-center">
71 <input
72 v-model="form.rememberMe"
73 type="checkbox"
74 class="mr-2 rounded border-gray-300 focus:ring-primary-500"
75 />
76 <span class="text-sm text-gray-600 dark:text-gray-400">
77 Remember me
78 </span>
79 </label>
80
81 <button
82 type="button"
83 class="text-sm text-primary-600 hover:text-primary-700 dark:text-primary-400"
84 >
85 Forgot password?
86 </button>
87 </div>
88
89 <!-- Error message -->
90 <div v-if="authStore.error" class="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md">
91 <div class="flex items-center">
92 <ExclamationTriangleIcon class="w-5 h-5 text-red-500 mr-2" />
93 <p class="text-sm text-red-600 dark:text-red-400">
94 {{ authStore.error }}
95 </p>
96 </div>
97 </div>
98
99 <!-- Submit button -->
100 <button
101 type="submit"
102 :disabled="authStore.loading || !isFormValid"
103 class="w-full btn btn-primary"
104 :class="{ 'opacity-50 cursor-not-allowed': authStore.loading || !isFormValid }"
105 >
106 <div v-if="authStore.loading" class="spinner w-4 h-4 mr-2"></div>
107 {{ authStore.loading ? 'Signing in...' : 'Sign in' }}
108 </button>
109 </form>
110
111 <!-- Demo credentials -->
112 <div class="mt-8 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md">
113 <h3 class="text-sm font-medium text-blue-700 dark:text-blue-300 mb-2">
114 Demo Credentials
115 </h3>
116 <div class="space-y-1 text-sm text-blue-600 dark:text-blue-400">
117 <p><strong>Admin:</strong> username: admin, password: admin</p>
118 <p><strong>Demo:</strong> username: demo, password: demo</p>
119 </div>
120 </div>
121
122 <!-- Footer -->
123 <div class="text-center text-sm text-gray-500 dark:text-gray-400">
124 <p>
125 Secure, decentralized file storage with zero-knowledge encryption
126 </p>
127 </div>
128 </div>
129 </div>
130 </template>
131
132 <script setup lang="ts">
133 import { ref, computed, onMounted } from 'vue'
134 import { useRouter, useRoute } from 'vue-router'
135 import { useAuthStore } from '@/stores/auth'
136 import { EyeIcon, EyeSlashIcon, ExclamationTriangleIcon } from '@heroicons/vue/24/outline'
137
138 // Store and router
139 const authStore = useAuthStore()
140 const router = useRouter()
141 const route = useRoute()
142
143 // Form state
144 const form = ref({
145 username: '',
146 password: '',
147 rememberMe: false,
148 })
149
150 const showPassword = ref(false)
151 const errors = ref<Record<string, string>>({})
152
153 // Computed
154 const isFormValid = computed(() => {
155 return form.value.username.trim().length > 0 && form.value.password.length > 0
156 })
157
158 // Methods
159 async function handleLogin() {
160 if (!isFormValid.value) return
161
162 // Clear previous errors
163 errors.value = {}
164 authStore.clearError()
165
166 // Validate form
167 if (form.value.username.trim().length < 2) {
168 errors.value.username = 'Username must be at least 2 characters'
169 return
170 }
171
172 if (form.value.password.length < 3) {
173 errors.value.password = 'Password must be at least 3 characters'
174 return
175 }
176
177 try {
178 await authStore.login({
179 username: form.value.username.trim(),
180 password: form.value.password,
181 })
182
183 // Redirect to intended page or dashboard
184 const redirectTo = (route.query.redirect as string) || '/'
185 router.push(redirectTo)
186 } catch (error) {
187 // Error is handled by the store
188 console.error('Login failed:', error)
189 }
190 }
191
192 // Auto-fill demo credentials for development
193 onMounted(() => {
194 if (import.meta.env.DEV) {
195 form.value.username = 'admin'
196 form.value.password = 'admin'
197 }
198 })
199 </script>