create-nala 3.1.1 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.agents/skills/nala-project/SKILL.md +24 -20
- package/template/AGENTS.md +114 -14
- package/template/CLAUDE.md +83 -13
- package/template/GEMINI.md +83 -13
- package/template/package.json +1 -1
- package/template/src/components/BrandIcon.vue +258 -0
- package/template/src/components.d.ts +1 -0
- package/template/src/layouts/AuthSplitLayout.vue +216 -0
- package/template/src/router/index.ts +100 -0
- package/template/src/style.css +0 -2
- package/template/src/views/auth/LockScreenView.vue +1 -1
- package/template/src/views/auth/MagicLinkView.vue +278 -0
- package/template/src/views/auth/SelectTenantView.vue +673 -0
- package/template/src/views/auth/SsoLoginView.vue +337 -0
- package/template/src/views/auth/SuspendedView.vue +341 -0
- package/template/src/views/auth/TwoFactorChallengeView.vue +307 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { useRouter } from 'vue-router'
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'
|
|
5
|
+
import { Button } from '@/components/ui/button'
|
|
6
|
+
import { Input } from '@/components/ui/input'
|
|
7
|
+
import { Label } from '@/components/ui/label'
|
|
8
|
+
import { Alert, AlertDescription } from '@/components/ui/alert'
|
|
9
|
+
import { toast } from '@/components/ui/sonner'
|
|
10
|
+
import {
|
|
11
|
+
Building2,
|
|
12
|
+
CheckCircle2,
|
|
13
|
+
ArrowRight,
|
|
14
|
+
ArrowLeft,
|
|
15
|
+
Loader2,
|
|
16
|
+
ShieldCheck,
|
|
17
|
+
AlertCircle,
|
|
18
|
+
Key,
|
|
19
|
+
Globe,
|
|
20
|
+
Lock,
|
|
21
|
+
Layers,
|
|
22
|
+
} from '@lucide/vue'
|
|
23
|
+
|
|
24
|
+
const router = useRouter()
|
|
25
|
+
|
|
26
|
+
// SSO Mode: 'email' (smart domain detection) | 'domain' (manual org slug / SAML entity ID)
|
|
27
|
+
const ssoMode = ref<'email' | 'domain'>('email')
|
|
28
|
+
const workEmail = ref('')
|
|
29
|
+
const orgSlug = ref('')
|
|
30
|
+
const isConnecting = ref(false)
|
|
31
|
+
const selectedProvider = ref<string | null>(null)
|
|
32
|
+
const isAuthenticated = ref(false)
|
|
33
|
+
const errorMessage = ref('')
|
|
34
|
+
|
|
35
|
+
// Domain detection dictionary for realistic enterprise demo
|
|
36
|
+
interface SsoProviderConfig {
|
|
37
|
+
name: string
|
|
38
|
+
provider: 'okta' | 'azure' | 'google' | 'saml'
|
|
39
|
+
logoBg: string
|
|
40
|
+
badgeText: string
|
|
41
|
+
authDomain: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const KNOWN_DOMAINS: Record<string, SsoProviderConfig> = {
|
|
45
|
+
'acme.com': { name: 'Acme Corp', provider: 'okta', logoBg: 'bg-blue-500/10 text-blue-600 dark:text-blue-400', badgeText: 'Okta Verified', authDomain: 'acme.okta.com' },
|
|
46
|
+
'microsoft.com': { name: 'Microsoft 365', provider: 'azure', logoBg: 'bg-sky-500/10 text-sky-600 dark:text-sky-400', badgeText: 'Microsoft Entra ID', authDomain: 'login.microsoftonline.com' },
|
|
47
|
+
'google.com': { name: 'Google Workspace', provider: 'google', logoBg: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400', badgeText: 'Google Workspace', authDomain: 'accounts.google.com' },
|
|
48
|
+
'nala.dev': { name: 'Nala Enterprise', provider: 'saml', logoBg: 'bg-purple-500/10 text-purple-600 dark:text-purple-400', badgeText: 'Custom SAML 2.0', authDomain: 'sso.nala.dev' },
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const detectedProvider = computed<SsoProviderConfig | null>(() => {
|
|
52
|
+
if (!workEmail.value.includes('@')) return null
|
|
53
|
+
const domain = workEmail.value.split('@')[1]?.toLowerCase().trim()
|
|
54
|
+
if (!domain) return null
|
|
55
|
+
return KNOWN_DOMAINS[domain] || {
|
|
56
|
+
name: domain.charAt(0).toUpperCase() + domain.slice(1),
|
|
57
|
+
provider: 'saml',
|
|
58
|
+
logoBg: 'bg-primary/10 text-primary',
|
|
59
|
+
badgeText: 'Enterprise SAML SSO',
|
|
60
|
+
authDomain: `sso.${domain}`,
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const isValidInput = computed(() => {
|
|
65
|
+
if (ssoMode.value === 'email') {
|
|
66
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(workEmail.value)
|
|
67
|
+
}
|
|
68
|
+
return orgSlug.value.trim().length >= 3
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const handleInitiateSso = (providerType?: string) => {
|
|
72
|
+
if (!isValidInput.value && !providerType) return
|
|
73
|
+
|
|
74
|
+
isConnecting.value = true
|
|
75
|
+
errorMessage.value = ''
|
|
76
|
+
selectedProvider.value = providerType || detectedProvider.value?.name || orgSlug.value
|
|
77
|
+
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
isConnecting.value = false
|
|
80
|
+
isAuthenticated.value = true
|
|
81
|
+
toast.success(`Redirected from ${selectedProvider.value} Identity Provider`)
|
|
82
|
+
}, 1200)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const handleQuickDomain = (domain: string) => {
|
|
86
|
+
workEmail.value = `alex@${domain}`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const handleProceed = () => {
|
|
90
|
+
router.push('/')
|
|
91
|
+
}
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<template>
|
|
95
|
+
<Card flush class="w-full max-w-md shadow-xl border-border/80 bg-card">
|
|
96
|
+
<!-- Success State -->
|
|
97
|
+
<template v-if="isAuthenticated">
|
|
98
|
+
<CardHeader class="pt-8 pb-6 px-6 sm:px-8 space-y-2 text-center">
|
|
99
|
+
<div class="mx-auto mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
|
|
100
|
+
<CheckCircle2 class="h-6 w-6" />
|
|
101
|
+
</div>
|
|
102
|
+
<CardTitle class="text-2xl font-bold tracking-tight">SSO Connected</CardTitle>
|
|
103
|
+
<CardDescription class="text-sm">
|
|
104
|
+
Successfully authenticated via
|
|
105
|
+
<span class="font-semibold text-foreground">{{ selectedProvider }}</span>.
|
|
106
|
+
Single Sign-On session established.
|
|
107
|
+
</CardDescription>
|
|
108
|
+
</CardHeader>
|
|
109
|
+
|
|
110
|
+
<CardContent class="px-6 sm:px-8 pb-8 space-y-4">
|
|
111
|
+
<div class="rounded-lg border border-border/70 bg-muted/20 p-4 text-xs space-y-1.5">
|
|
112
|
+
<div class="flex items-center justify-between text-muted-foreground">
|
|
113
|
+
<span>Identity Provider:</span>
|
|
114
|
+
<span class="font-medium text-foreground">{{ selectedProvider }}</span>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="flex items-center justify-between text-muted-foreground">
|
|
117
|
+
<span>Protocol:</span>
|
|
118
|
+
<span class="font-mono text-[11px] font-medium text-foreground">SAML 2.0 / OIDC</span>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="flex items-center justify-between text-muted-foreground">
|
|
121
|
+
<span>User Principal:</span>
|
|
122
|
+
<span class="font-mono text-[11px] font-medium text-foreground">{{ workEmail || `${orgSlug}@enterprise` }}</span>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<Button class="w-full font-medium cursor-pointer" @click="handleProceed">
|
|
127
|
+
<span class="flex items-center justify-center gap-2">
|
|
128
|
+
Continue to Dashboard
|
|
129
|
+
<ArrowRight class="h-4 w-4" />
|
|
130
|
+
</span>
|
|
131
|
+
</Button>
|
|
132
|
+
</CardContent>
|
|
133
|
+
</template>
|
|
134
|
+
|
|
135
|
+
<!-- Main SSO Form -->
|
|
136
|
+
<template v-else>
|
|
137
|
+
<CardHeader class="pt-8 pb-6 px-6 sm:px-8 space-y-1.5 text-center">
|
|
138
|
+
<div class="mx-auto mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10 text-primary">
|
|
139
|
+
<Building2 class="h-6 w-6" />
|
|
140
|
+
</div>
|
|
141
|
+
<CardTitle class="text-2xl font-bold tracking-tight">Enterprise Single Sign-On</CardTitle>
|
|
142
|
+
<CardDescription class="text-sm">
|
|
143
|
+
Sign in seamlessly using your corporate enterprise credentials (Okta, Azure AD, SAML, or Google Workspace).
|
|
144
|
+
</CardDescription>
|
|
145
|
+
</CardHeader>
|
|
146
|
+
|
|
147
|
+
<CardContent class="px-6 sm:px-8 pb-6 space-y-5">
|
|
148
|
+
<!-- Error Alert Banner -->
|
|
149
|
+
<Alert v-if="errorMessage" variant="destructive" class="py-2.5 px-3.5">
|
|
150
|
+
<AlertCircle class="h-4 w-4" />
|
|
151
|
+
<AlertDescription class="text-xs font-medium ml-2">
|
|
152
|
+
{{ errorMessage }}
|
|
153
|
+
</AlertDescription>
|
|
154
|
+
</Alert>
|
|
155
|
+
|
|
156
|
+
<!-- Mode Toggle (Email vs Org Slug) -->
|
|
157
|
+
<div class="flex rounded-lg bg-muted p-1 text-xs">
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
class="flex-1 py-1.5 font-medium rounded-md transition-all cursor-pointer text-center"
|
|
161
|
+
:class="ssoMode === 'email' ? 'bg-background text-foreground shadow-2xs font-semibold' : 'text-muted-foreground hover:text-foreground'"
|
|
162
|
+
@click="ssoMode = 'email'"
|
|
163
|
+
>
|
|
164
|
+
Work Email
|
|
165
|
+
</button>
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
class="flex-1 py-1.5 font-medium rounded-md transition-all cursor-pointer text-center"
|
|
169
|
+
:class="ssoMode === 'domain' ? 'bg-background text-foreground shadow-2xs font-semibold' : 'text-muted-foreground hover:text-foreground'"
|
|
170
|
+
@click="ssoMode = 'domain'"
|
|
171
|
+
>
|
|
172
|
+
Organization Slug
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<!-- Mode 1: Work Email with Auto Detection -->
|
|
177
|
+
<div v-if="ssoMode === 'email'" class="space-y-3">
|
|
178
|
+
<div class="space-y-1.5">
|
|
179
|
+
<Label for="work-email" class="text-xs font-medium">Work Email Address</Label>
|
|
180
|
+
<Input
|
|
181
|
+
id="work-email"
|
|
182
|
+
v-model="workEmail"
|
|
183
|
+
type="email"
|
|
184
|
+
placeholder="name@company.com"
|
|
185
|
+
class="h-10 text-sm"
|
|
186
|
+
:disabled="isConnecting"
|
|
187
|
+
@keyup.enter="() => isValidInput && handleInitiateSso()"
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<!-- Detected IdP Badge Strip -->
|
|
192
|
+
<div
|
|
193
|
+
v-if="detectedProvider"
|
|
194
|
+
class="rounded-lg border border-border/80 bg-muted/30 p-3 text-xs flex items-center justify-between animate-in fade-in slide-in-from-top-1 duration-200"
|
|
195
|
+
>
|
|
196
|
+
<div class="flex items-center gap-2">
|
|
197
|
+
<span :class="['px-2 py-0.5 rounded text-[10px] font-semibold tracking-wide uppercase', detectedProvider.logoBg]">
|
|
198
|
+
{{ detectedProvider.provider }}
|
|
199
|
+
</span>
|
|
200
|
+
<div>
|
|
201
|
+
<div class="font-medium text-foreground">{{ detectedProvider.name }}</div>
|
|
202
|
+
<div class="text-[11px] text-muted-foreground font-mono">{{ detectedProvider.authDomain }}</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
<span class="text-xs text-primary font-medium flex items-center gap-1">
|
|
206
|
+
<ShieldCheck class="h-3.5 w-3.5" />
|
|
207
|
+
Detected
|
|
208
|
+
</span>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<!-- Quick Test Domain Chips -->
|
|
212
|
+
<div class="space-y-1.5 pt-1">
|
|
213
|
+
<span class="text-[11px] text-muted-foreground">Try demo corporate domains:</span>
|
|
214
|
+
<div class="flex flex-wrap gap-1.5">
|
|
215
|
+
<button
|
|
216
|
+
v-for="domain in Object.keys(KNOWN_DOMAINS)"
|
|
217
|
+
:key="domain"
|
|
218
|
+
type="button"
|
|
219
|
+
class="text-[11px] px-2 py-0.5 rounded-md border border-border/70 hover:border-primary/50 hover:bg-muted/60 transition-colors text-muted-foreground hover:text-foreground cursor-pointer"
|
|
220
|
+
@click="handleQuickDomain(domain)"
|
|
221
|
+
>
|
|
222
|
+
@{{ domain }}
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<!-- Mode 2: Org Slug / SAML Entity ID -->
|
|
229
|
+
<div v-else class="space-y-3">
|
|
230
|
+
<div class="space-y-1.5">
|
|
231
|
+
<Label for="org-slug" class="text-xs font-medium">Organization Workspace ID</Label>
|
|
232
|
+
<div class="relative flex items-center">
|
|
233
|
+
<span class="absolute left-3 text-xs text-muted-foreground font-mono select-none">sso.nala.dev/</span>
|
|
234
|
+
<Input
|
|
235
|
+
id="org-slug"
|
|
236
|
+
v-model="orgSlug"
|
|
237
|
+
placeholder="acme-corp"
|
|
238
|
+
class="pl-27 font-mono text-sm h-10"
|
|
239
|
+
:disabled="isConnecting"
|
|
240
|
+
@keyup.enter="() => isValidInput && handleInitiateSso()"
|
|
241
|
+
/>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
<p class="text-[11px] text-muted-foreground">
|
|
245
|
+
Contact your IT Administrator if you do not know your organization slug.
|
|
246
|
+
</p>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<!-- Action Button -->
|
|
250
|
+
<Button
|
|
251
|
+
class="w-full font-medium h-10 cursor-pointer"
|
|
252
|
+
:disabled="!isValidInput || isConnecting"
|
|
253
|
+
@click="() => handleInitiateSso()"
|
|
254
|
+
>
|
|
255
|
+
<Loader2 v-if="isConnecting" class="h-4 w-4 animate-spin mr-2" />
|
|
256
|
+
<span v-else class="flex items-center justify-center gap-1.5">
|
|
257
|
+
Continue with Single Sign-On
|
|
258
|
+
<ArrowRight class="h-3.5 w-3.5" />
|
|
259
|
+
</span>
|
|
260
|
+
</Button>
|
|
261
|
+
|
|
262
|
+
<!-- Divider with Direct IdP shortcuts -->
|
|
263
|
+
<div class="relative py-1">
|
|
264
|
+
<div class="absolute inset-0 flex items-center">
|
|
265
|
+
<span class="w-full border-t border-border" />
|
|
266
|
+
</div>
|
|
267
|
+
<div class="relative flex justify-center text-xs uppercase">
|
|
268
|
+
<span class="bg-card px-2 text-[10px] tracking-wider text-muted-foreground">
|
|
269
|
+
Or connect directly with
|
|
270
|
+
</span>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<!-- Direct Identity Providers Grid -->
|
|
275
|
+
<div class="grid grid-cols-2 gap-2">
|
|
276
|
+
<Button
|
|
277
|
+
type="button"
|
|
278
|
+
variant="outline"
|
|
279
|
+
size="sm"
|
|
280
|
+
class="h-9 justify-start text-xs font-medium cursor-pointer"
|
|
281
|
+
:disabled="isConnecting"
|
|
282
|
+
@click="() => handleInitiateSso('Okta SSO')"
|
|
283
|
+
>
|
|
284
|
+
<Key class="h-3.5 w-3.5 text-blue-500 mr-2 shrink-0" />
|
|
285
|
+
<span class="truncate">Okta Verify</span>
|
|
286
|
+
</Button>
|
|
287
|
+
|
|
288
|
+
<Button
|
|
289
|
+
type="button"
|
|
290
|
+
variant="outline"
|
|
291
|
+
size="sm"
|
|
292
|
+
class="h-9 justify-start text-xs font-medium cursor-pointer"
|
|
293
|
+
:disabled="isConnecting"
|
|
294
|
+
@click="() => handleInitiateSso('Microsoft Entra ID')"
|
|
295
|
+
>
|
|
296
|
+
<Lock class="h-3.5 w-3.5 text-sky-500 mr-2 shrink-0" />
|
|
297
|
+
<span class="truncate">Microsoft Entra</span>
|
|
298
|
+
</Button>
|
|
299
|
+
|
|
300
|
+
<Button
|
|
301
|
+
type="button"
|
|
302
|
+
variant="outline"
|
|
303
|
+
size="sm"
|
|
304
|
+
class="h-9 justify-start text-xs font-medium cursor-pointer"
|
|
305
|
+
:disabled="isConnecting"
|
|
306
|
+
@click="() => handleInitiateSso('Google Workspace')"
|
|
307
|
+
>
|
|
308
|
+
<Globe class="h-3.5 w-3.5 text-emerald-500 mr-2 shrink-0" />
|
|
309
|
+
<span class="truncate">Google Workspace</span>
|
|
310
|
+
</Button>
|
|
311
|
+
|
|
312
|
+
<Button
|
|
313
|
+
type="button"
|
|
314
|
+
variant="outline"
|
|
315
|
+
size="sm"
|
|
316
|
+
class="h-9 justify-start text-xs font-medium cursor-pointer"
|
|
317
|
+
:disabled="isConnecting"
|
|
318
|
+
@click="() => handleInitiateSso('SAML 2.0 Provider')"
|
|
319
|
+
>
|
|
320
|
+
<Layers class="h-3.5 w-3.5 text-purple-500 mr-2 shrink-0" />
|
|
321
|
+
<span class="truncate">Custom SAML 2.0</span>
|
|
322
|
+
</Button>
|
|
323
|
+
</div>
|
|
324
|
+
</CardContent>
|
|
325
|
+
|
|
326
|
+
<CardFooter class="flex justify-center border-t border-border/60 py-4 bg-muted/10">
|
|
327
|
+
<router-link
|
|
328
|
+
to="/auth/login"
|
|
329
|
+
class="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
330
|
+
>
|
|
331
|
+
<ArrowLeft class="h-3.5 w-3.5" />
|
|
332
|
+
Return to standard email & password
|
|
333
|
+
</router-link>
|
|
334
|
+
</CardFooter>
|
|
335
|
+
</template>
|
|
336
|
+
</Card>
|
|
337
|
+
</template>
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { useRouter } from 'vue-router'
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'
|
|
5
|
+
import { Button } from '@/components/ui/button'
|
|
6
|
+
import { Alert, AlertDescription } from '@/components/ui/alert'
|
|
7
|
+
import { toast } from '@/components/ui/sonner'
|
|
8
|
+
import {
|
|
9
|
+
Ban,
|
|
10
|
+
Clock,
|
|
11
|
+
Download,
|
|
12
|
+
FileText,
|
|
13
|
+
HeartHandshake,
|
|
14
|
+
HelpCircle,
|
|
15
|
+
Lock,
|
|
16
|
+
LogIn,
|
|
17
|
+
Mail,
|
|
18
|
+
RefreshCw,
|
|
19
|
+
UserX,
|
|
20
|
+
} from '@lucide/vue'
|
|
21
|
+
|
|
22
|
+
const router = useRouter()
|
|
23
|
+
|
|
24
|
+
// --- Account Status Types ---
|
|
25
|
+
type AccountStatus = 'suspended' | 'frozen' | 'pending_verification' | 'deactivated'
|
|
26
|
+
|
|
27
|
+
interface StatusConfig {
|
|
28
|
+
icon: unknown
|
|
29
|
+
badge: string
|
|
30
|
+
badgeClass: string
|
|
31
|
+
title: string
|
|
32
|
+
subtitle: string
|
|
33
|
+
color: string
|
|
34
|
+
borderColor: string
|
|
35
|
+
bgGradient: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Mock Simulation State ---
|
|
39
|
+
// In a real app this would come from the auth store / API response
|
|
40
|
+
const activeStatus = ref<AccountStatus>('suspended')
|
|
41
|
+
|
|
42
|
+
const statusConfigs: Record<AccountStatus, StatusConfig> = {
|
|
43
|
+
suspended: {
|
|
44
|
+
icon: Ban,
|
|
45
|
+
badge: 'Account Suspended',
|
|
46
|
+
badgeClass: 'bg-red-500/15 text-red-600 dark:text-red-400 border-red-500/30',
|
|
47
|
+
title: 'Your account has been suspended.',
|
|
48
|
+
subtitle: 'Access to your workspace and all associated services has been temporarily revoked due to a policy violation or outstanding payment.',
|
|
49
|
+
color: 'text-red-500',
|
|
50
|
+
borderColor: 'border-red-500/30',
|
|
51
|
+
bgGradient: 'from-red-500/10 via-red-500/5 to-transparent',
|
|
52
|
+
},
|
|
53
|
+
frozen: {
|
|
54
|
+
icon: Lock,
|
|
55
|
+
badge: 'Account Frozen',
|
|
56
|
+
badgeClass: 'bg-blue-500/15 text-blue-600 dark:text-blue-400 border-blue-500/30',
|
|
57
|
+
title: 'Your account is temporarily frozen.',
|
|
58
|
+
subtitle: 'Your account has been placed on hold by your organization administrator. No changes can be made until the hold is lifted.',
|
|
59
|
+
color: 'text-blue-500',
|
|
60
|
+
borderColor: 'border-blue-500/30',
|
|
61
|
+
bgGradient: 'from-blue-500/10 via-blue-500/5 to-transparent',
|
|
62
|
+
},
|
|
63
|
+
pending_verification: {
|
|
64
|
+
icon: Clock,
|
|
65
|
+
badge: 'Verification Pending',
|
|
66
|
+
badgeClass: 'bg-amber-500/15 text-amber-600 dark:text-amber-400 border-amber-500/30',
|
|
67
|
+
title: 'Your account is awaiting verification.',
|
|
68
|
+
subtitle: 'Your identity or billing information is under review. Access will be restored once verification is complete — usually within 24 business hours.',
|
|
69
|
+
color: 'text-amber-500',
|
|
70
|
+
borderColor: 'border-amber-500/30',
|
|
71
|
+
bgGradient: 'from-amber-500/10 via-amber-500/5 to-transparent',
|
|
72
|
+
},
|
|
73
|
+
deactivated: {
|
|
74
|
+
icon: UserX,
|
|
75
|
+
badge: 'Account Deactivated',
|
|
76
|
+
badgeClass: 'bg-slate-500/15 text-slate-600 dark:text-slate-400 border-slate-500/30',
|
|
77
|
+
title: 'Your account has been permanently deactivated.',
|
|
78
|
+
subtitle: 'This account has been closed and is no longer accessible. If this was an error, you can submit a reactivation appeal below.',
|
|
79
|
+
color: 'text-slate-500',
|
|
80
|
+
borderColor: 'border-slate-500/30',
|
|
81
|
+
bgGradient: 'from-slate-500/10 via-slate-500/5 to-transparent',
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const currentStatus = computed<StatusConfig>(() => statusConfigs[activeStatus.value])
|
|
86
|
+
|
|
87
|
+
// --- Ticket Info (from API in real app) ---
|
|
88
|
+
const ticketId = ref('TKT-20260905-89421')
|
|
89
|
+
const suspensionDate = ref('September 4, 2026')
|
|
90
|
+
const reviewEstimate = ref('2–5 business days')
|
|
91
|
+
|
|
92
|
+
// --- Appeal Form ---
|
|
93
|
+
const appealMessage = ref('')
|
|
94
|
+
const isSubmittingAppeal = ref(false)
|
|
95
|
+
const appealSubmitted = ref(false)
|
|
96
|
+
|
|
97
|
+
const handleAppeal = () => {
|
|
98
|
+
if (!appealMessage.value.trim()) {
|
|
99
|
+
toast.error('Please describe your situation before submitting an appeal.')
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
isSubmittingAppeal.value = true
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
isSubmittingAppeal.value = false
|
|
105
|
+
appealSubmitted.value = true
|
|
106
|
+
toast.success('Appeal submitted successfully!', {
|
|
107
|
+
description: `Your case has been assigned ticket ID ${ticketId.value}. Our team will respond within ${reviewEstimate.value}.`,
|
|
108
|
+
})
|
|
109
|
+
}, 1200)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- Data Export ---
|
|
113
|
+
const isExporting = ref(false)
|
|
114
|
+
|
|
115
|
+
const handleDataExport = () => {
|
|
116
|
+
isExporting.value = true
|
|
117
|
+
toast.info('Generating your data archive...', {
|
|
118
|
+
description: 'A download link will be emailed to your registered address within 24 hours.',
|
|
119
|
+
})
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
isExporting.value = false
|
|
122
|
+
toast.success('Data export requested!', {
|
|
123
|
+
description: 'Check your inbox for a secure download link.',
|
|
124
|
+
})
|
|
125
|
+
}, 1500)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- Contact Support ---
|
|
129
|
+
const handleContactSupport = () => {
|
|
130
|
+
toast.info('Opening support portal...', {
|
|
131
|
+
description: 'Reference your Ticket ID when contacting support: ' + ticketId.value,
|
|
132
|
+
})
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
window.open('mailto:support@nala.app?subject=Account+Issue+' + ticketId.value, '_blank')
|
|
135
|
+
}, 500)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- Status Demo Switcher (showcase only) ---
|
|
139
|
+
const statusOptions: { value: AccountStatus; label: string }[] = [
|
|
140
|
+
{ value: 'suspended', label: 'Suspended' },
|
|
141
|
+
{ value: 'frozen', label: 'Frozen' },
|
|
142
|
+
{ value: 'pending_verification', label: 'Pending Verification' },
|
|
143
|
+
{ value: 'deactivated', label: 'Deactivated' },
|
|
144
|
+
]
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<template>
|
|
148
|
+
<div class="w-full space-y-5">
|
|
149
|
+
<!-- Demo State Switcher (visible in showcase) -->
|
|
150
|
+
<div class="flex items-center gap-1.5 flex-wrap p-2.5 rounded-xl bg-muted/40 border border-border/60">
|
|
151
|
+
<span class="text-[11px] font-medium text-muted-foreground mr-1 shrink-0">Preview status:</span>
|
|
152
|
+
<button
|
|
153
|
+
v-for="opt in statusOptions"
|
|
154
|
+
:key="opt.value"
|
|
155
|
+
type="button"
|
|
156
|
+
:class="[
|
|
157
|
+
'px-2.5 py-1 text-[11px] font-medium rounded-md transition-all cursor-pointer',
|
|
158
|
+
activeStatus === opt.value ? 'bg-background text-foreground shadow-2xs' : 'text-muted-foreground hover:text-foreground'
|
|
159
|
+
]"
|
|
160
|
+
@click="activeStatus = opt.value; appealSubmitted = false"
|
|
161
|
+
>
|
|
162
|
+
{{ opt.label }}
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<!-- Status Indicator Header -->
|
|
167
|
+
<Card class="border-border/80 shadow-md overflow-hidden">
|
|
168
|
+
<!-- Ambient Gradient Accent Band -->
|
|
169
|
+
<div :class="['h-0.5 w-full bg-linear-to-r', currentStatus.bgGradient]" />
|
|
170
|
+
|
|
171
|
+
<CardHeader class="pb-4">
|
|
172
|
+
<!-- Icon + Status Badge Row -->
|
|
173
|
+
<div class="flex flex-col sm:flex-row sm:items-start gap-4">
|
|
174
|
+
<!-- Status Icon Block -->
|
|
175
|
+
<div
|
|
176
|
+
:class="[
|
|
177
|
+
'h-12 w-12 rounded-xl flex items-center justify-center shrink-0 border',
|
|
178
|
+
currentStatus.borderColor, 'bg-card'
|
|
179
|
+
]"
|
|
180
|
+
>
|
|
181
|
+
<component :is="currentStatus.icon" :class="['h-6 w-6', currentStatus.color]" />
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div class="space-y-1.5 flex-1">
|
|
185
|
+
<!-- Status Badge -->
|
|
186
|
+
<span
|
|
187
|
+
:class="[
|
|
188
|
+
'inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full border text-[11px] font-semibold',
|
|
189
|
+
currentStatus.badgeClass
|
|
190
|
+
]"
|
|
191
|
+
>
|
|
192
|
+
<span class="h-1.5 w-1.5 rounded-full bg-current animate-pulse" />
|
|
193
|
+
{{ currentStatus.badge }}
|
|
194
|
+
</span>
|
|
195
|
+
|
|
196
|
+
<CardTitle class="text-lg font-bold text-foreground leading-snug">
|
|
197
|
+
{{ currentStatus.title }}
|
|
198
|
+
</CardTitle>
|
|
199
|
+
<CardDescription class="text-xs text-muted-foreground leading-relaxed">
|
|
200
|
+
{{ currentStatus.subtitle }}
|
|
201
|
+
</CardDescription>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</CardHeader>
|
|
205
|
+
|
|
206
|
+
<CardContent class="space-y-4 pt-0 pb-6">
|
|
207
|
+
<!-- Ticket & Metadata Strip -->
|
|
208
|
+
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
|
|
209
|
+
<div class="flex flex-col gap-1 p-3 rounded-lg bg-muted/40 border border-border/60">
|
|
210
|
+
<div class="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">Support Ticket</div>
|
|
211
|
+
<div class="text-xs font-mono font-semibold text-foreground tracking-wider">{{ ticketId }}</div>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="flex flex-col gap-1 p-3 rounded-lg bg-muted/40 border border-border/60">
|
|
214
|
+
<div class="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">Action Date</div>
|
|
215
|
+
<div class="text-xs font-semibold text-foreground">{{ suspensionDate }}</div>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="flex flex-col gap-1 p-3 rounded-lg bg-muted/40 border border-border/60">
|
|
218
|
+
<div class="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">Review Estimate</div>
|
|
219
|
+
<div class="text-xs font-semibold text-foreground">{{ reviewEstimate }}</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<!-- Appeal Already Submitted State -->
|
|
224
|
+
<Alert v-if="appealSubmitted" class="border-emerald-500/30 bg-emerald-500/10">
|
|
225
|
+
<AlertDescription class="text-xs text-emerald-700 dark:text-emerald-400 flex items-center gap-2">
|
|
226
|
+
<HeartHandshake class="h-4 w-4 shrink-0" />
|
|
227
|
+
Your appeal has been submitted. Our compliance team will review your case and respond via email. Refer to ticket <strong class="font-mono">{{ ticketId }}</strong>.
|
|
228
|
+
</AlertDescription>
|
|
229
|
+
</Alert>
|
|
230
|
+
|
|
231
|
+
<!-- Appeal Form -->
|
|
232
|
+
<div v-else class="space-y-3 p-4 rounded-xl border border-border/70 bg-card">
|
|
233
|
+
<div class="flex items-center gap-2">
|
|
234
|
+
<HeartHandshake class="h-4 w-4 text-primary shrink-0" />
|
|
235
|
+
<h3 class="text-xs font-semibold text-foreground">Submit an Appeal</h3>
|
|
236
|
+
</div>
|
|
237
|
+
<p class="text-[11px] text-muted-foreground">
|
|
238
|
+
Believe this action was made in error? Provide context below and our team will review your case promptly.
|
|
239
|
+
</p>
|
|
240
|
+
|
|
241
|
+
<div class="space-y-1.5">
|
|
242
|
+
<Label for="appeal-message" class="text-xs font-medium">Describe Your Situation</Label>
|
|
243
|
+
<textarea
|
|
244
|
+
id="appeal-message"
|
|
245
|
+
v-model="appealMessage"
|
|
246
|
+
rows="3"
|
|
247
|
+
placeholder="e.g. My account was suspended after an automated billing flag, but the payment was processed successfully. Please find the receipt attached..."
|
|
248
|
+
class="w-full rounded-md border border-input bg-background px-3 py-2 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary resize-none"
|
|
249
|
+
/>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<Button
|
|
253
|
+
class="w-full gap-2 text-xs cursor-pointer"
|
|
254
|
+
size="sm"
|
|
255
|
+
:disabled="isSubmittingAppeal || !appealMessage.trim()"
|
|
256
|
+
@click="handleAppeal"
|
|
257
|
+
>
|
|
258
|
+
<RefreshCw v-if="isSubmittingAppeal" class="h-3.5 w-3.5 animate-spin" />
|
|
259
|
+
<HeartHandshake v-else class="h-3.5 w-3.5" />
|
|
260
|
+
{{ isSubmittingAppeal ? 'Submitting Appeal...' : 'Submit Appeal' }}
|
|
261
|
+
</Button>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<!-- Secondary Actions Row -->
|
|
265
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
|
|
266
|
+
<!-- Request Data Export -->
|
|
267
|
+
<div class="flex flex-col gap-2.5 p-3.5 rounded-xl border border-border/70 bg-card/60">
|
|
268
|
+
<div class="flex items-center gap-2">
|
|
269
|
+
<Download class="h-4 w-4 text-muted-foreground shrink-0" />
|
|
270
|
+
<span class="text-xs font-semibold text-foreground">Export Your Data</span>
|
|
271
|
+
</div>
|
|
272
|
+
<p class="text-[11px] text-muted-foreground leading-relaxed">
|
|
273
|
+
You have the right to request a copy of all data associated with your account before permanent deletion.
|
|
274
|
+
</p>
|
|
275
|
+
<Button
|
|
276
|
+
variant="outline"
|
|
277
|
+
size="sm"
|
|
278
|
+
class="text-xs cursor-pointer gap-1.5 mt-auto"
|
|
279
|
+
:disabled="isExporting"
|
|
280
|
+
@click="handleDataExport"
|
|
281
|
+
>
|
|
282
|
+
<RefreshCw v-if="isExporting" class="h-3 w-3 animate-spin" />
|
|
283
|
+
<Download v-else class="h-3 w-3" />
|
|
284
|
+
{{ isExporting ? 'Requesting...' : 'Request Data Archive' }}
|
|
285
|
+
</Button>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<!-- Contact Support -->
|
|
289
|
+
<div class="flex flex-col gap-2.5 p-3.5 rounded-xl border border-border/70 bg-card/60">
|
|
290
|
+
<div class="flex items-center gap-2">
|
|
291
|
+
<Mail class="h-4 w-4 text-muted-foreground shrink-0" />
|
|
292
|
+
<span class="text-xs font-semibold text-foreground">Contact Support</span>
|
|
293
|
+
</div>
|
|
294
|
+
<p class="text-[11px] text-muted-foreground leading-relaxed">
|
|
295
|
+
For urgent matters or questions about this action, reach our compliance and trust & safety team directly.
|
|
296
|
+
</p>
|
|
297
|
+
<Button
|
|
298
|
+
variant="outline"
|
|
299
|
+
size="sm"
|
|
300
|
+
class="text-xs cursor-pointer gap-1.5 mt-auto"
|
|
301
|
+
@click="handleContactSupport"
|
|
302
|
+
>
|
|
303
|
+
<Mail class="h-3 w-3" />
|
|
304
|
+
Email Support Team
|
|
305
|
+
</Button>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<!-- Policy Notice -->
|
|
310
|
+
<Alert class="border-border/60 bg-muted/30">
|
|
311
|
+
<AlertDescription class="text-[11px] text-muted-foreground flex items-start gap-2">
|
|
312
|
+
<FileText class="h-3.5 w-3.5 shrink-0 mt-0.5 text-muted-foreground" />
|
|
313
|
+
<span>
|
|
314
|
+
Account actions are governed by our
|
|
315
|
+
<a href="#" class="text-primary hover:underline">Terms of Service</a> and
|
|
316
|
+
<a href="#" class="text-primary hover:underline">Acceptable Use Policy</a>.
|
|
317
|
+
Data is retained for 30 days after deactivation in accordance with our
|
|
318
|
+
<a href="#" class="text-primary hover:underline">Data Retention Policy</a>.
|
|
319
|
+
</span>
|
|
320
|
+
</AlertDescription>
|
|
321
|
+
</Alert>
|
|
322
|
+
</CardContent>
|
|
323
|
+
|
|
324
|
+
<CardFooter class="py-4 flex flex-col sm:flex-row items-center justify-between gap-3 border-t border-border/40">
|
|
325
|
+
<button
|
|
326
|
+
type="button"
|
|
327
|
+
class="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
|
328
|
+
@click="router.push('/auth/login')"
|
|
329
|
+
>
|
|
330
|
+
<LogIn class="h-3.5 w-3.5" />
|
|
331
|
+
Sign in with a different account
|
|
332
|
+
</button>
|
|
333
|
+
<div class="flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
334
|
+
<HelpCircle class="h-3.5 w-3.5" />
|
|
335
|
+
Need help?
|
|
336
|
+
<a href="#" class="text-primary hover:underline">Visit Help Center</a>
|
|
337
|
+
</div>
|
|
338
|
+
</CardFooter>
|
|
339
|
+
</Card>
|
|
340
|
+
</div>
|
|
341
|
+
</template>
|