@rovela-ai/sdk 0.19.4 → 0.19.5

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.
Files changed (202) hide show
  1. package/docs/changelog/README.md +53 -0
  2. package/docs/changelog/epoch-01-chrome-consent.md +63 -0
  3. package/docs/changelog/epoch-02-platform-commerce.md +108 -0
  4. package/docs/changelog/epoch-03-i18n-files.md +72 -0
  5. package/docs/changelog/epoch-04-emails.md +65 -0
  6. package/docs/changelog/epoch-05-admin-experience.md +98 -0
  7. package/docs/changelog/epoch-06-mobile-integrity.md +72 -0
  8. package/docs/design/admin-ledger-visual-universe.html +1961 -0
  9. package/docs/design/admin-sidebar-and-band.html +542 -0
  10. package/package.json +3 -1
  11. package/templates/store-template/.claude/skills/add-section/SKILL.md +438 -0
  12. package/templates/store-template/.claude/skills/build-feature/SKILL.md +201 -0
  13. package/templates/store-template/.claude/skills/customize-theme/SKILL.md +167 -0
  14. package/templates/store-template/.claude/skills/debug-errors/SKILL.md +68 -0
  15. package/templates/store-template/.claude/skills/frontend-design/SKILL.md +176 -0
  16. package/templates/store-template/.claude/skills/generate-images/SKILL.md +115 -0
  17. package/templates/store-template/.claude/skills/manage-products/SKILL.md +187 -0
  18. package/templates/store-template/.claude/skills/override-sdk-component/SKILL.md +154 -0
  19. package/templates/store-template/.env.example +50 -0
  20. package/templates/store-template/.mcp.json +8 -0
  21. package/templates/store-template/.rovela/blueprint.example.json +190 -0
  22. package/templates/store-template/CLAUDE.md +14 -0
  23. package/templates/store-template/app/LayoutContent.tsx +106 -0
  24. package/templates/store-template/app/[...path]/page.tsx +79 -0
  25. package/templates/store-template/app/account/addresses/page.tsx +45 -0
  26. package/templates/store-template/app/account/layout.tsx +17 -0
  27. package/templates/store-template/app/account/orders/[id]/invoice/page.tsx +277 -0
  28. package/templates/store-template/app/account/orders/[id]/page.tsx +725 -0
  29. package/templates/store-template/app/account/orders/page.tsx +153 -0
  30. package/templates/store-template/app/account/page.tsx +184 -0
  31. package/templates/store-template/app/account/profile/page.tsx +434 -0
  32. package/templates/store-template/app/admin/(dashboard)/account/page.tsx +16 -0
  33. package/templates/store-template/app/admin/(dashboard)/analytics/events/page.tsx +14 -0
  34. package/templates/store-template/app/admin/(dashboard)/analytics/layout.tsx +48 -0
  35. package/templates/store-template/app/admin/(dashboard)/analytics/page.tsx +15 -0
  36. package/templates/store-template/app/admin/(dashboard)/analytics/visitors/page.tsx +14 -0
  37. package/templates/store-template/app/admin/(dashboard)/categories/[id]/page.tsx +38 -0
  38. package/templates/store-template/app/admin/(dashboard)/categories/new/page.tsx +35 -0
  39. package/templates/store-template/app/admin/(dashboard)/categories/page.tsx +223 -0
  40. package/templates/store-template/app/admin/(dashboard)/customers/[id]/page.tsx +26 -0
  41. package/templates/store-template/app/admin/(dashboard)/customers/page.tsx +33 -0
  42. package/templates/store-template/app/admin/(dashboard)/discounts/page.tsx +16 -0
  43. package/templates/store-template/app/admin/(dashboard)/emails/page.tsx +16 -0
  44. package/templates/store-template/app/admin/(dashboard)/files/page.tsx +16 -0
  45. package/templates/store-template/app/admin/(dashboard)/layout.tsx +44 -0
  46. package/templates/store-template/app/admin/(dashboard)/loading.tsx +88 -0
  47. package/templates/store-template/app/admin/(dashboard)/locations/page.tsx +16 -0
  48. package/templates/store-template/app/admin/(dashboard)/orders/[id]/page.tsx +66 -0
  49. package/templates/store-template/app/admin/(dashboard)/orders/page.tsx +26 -0
  50. package/templates/store-template/app/admin/(dashboard)/page.tsx +146 -0
  51. package/templates/store-template/app/admin/(dashboard)/payments/page.tsx +15 -0
  52. package/templates/store-template/app/admin/(dashboard)/products/[id]/page.tsx +38 -0
  53. package/templates/store-template/app/admin/(dashboard)/products/new/page.tsx +35 -0
  54. package/templates/store-template/app/admin/(dashboard)/products/page.tsx +31 -0
  55. package/templates/store-template/app/admin/(dashboard)/settings/page.tsx +46 -0
  56. package/templates/store-template/app/admin/(dashboard)/shipping/page.tsx +15 -0
  57. package/templates/store-template/app/admin/(dashboard)/tax/page.tsx +15 -0
  58. package/templates/store-template/app/admin/(dashboard)/users/[id]/page.tsx +114 -0
  59. package/templates/store-template/app/admin/(dashboard)/users/page.tsx +16 -0
  60. package/templates/store-template/app/admin/accept-invite/page.tsx +36 -0
  61. package/templates/store-template/app/admin/forgot-password/page.tsx +13 -0
  62. package/templates/store-template/app/admin/layout.tsx +47 -0
  63. package/templates/store-template/app/admin/login/page.tsx +528 -0
  64. package/templates/store-template/app/admin/reset-password/page.tsx +36 -0
  65. package/templates/store-template/app/api/account/addresses/[id]/route.ts +10 -0
  66. package/templates/store-template/app/api/account/addresses/route.ts +10 -0
  67. package/templates/store-template/app/api/account/change-password/route.ts +107 -0
  68. package/templates/store-template/app/api/account/orders/[id]/refund/route.ts +11 -0
  69. package/templates/store-template/app/api/account/orders/[id]/return/route.ts +14 -0
  70. package/templates/store-template/app/api/account/orders/[id]/route.ts +42 -0
  71. package/templates/store-template/app/api/account/orders/route.ts +29 -0
  72. package/templates/store-template/app/api/account/profile/route.ts +165 -0
  73. package/templates/store-template/app/api/admin/accept-invite/route.ts +11 -0
  74. package/templates/store-template/app/api/admin/analytics/events/route.ts +9 -0
  75. package/templates/store-template/app/api/admin/analytics/route.ts +10 -0
  76. package/templates/store-template/app/api/admin/analytics/visitors/route.ts +9 -0
  77. package/templates/store-template/app/api/admin/branding/favicon/route.ts +9 -0
  78. package/templates/store-template/app/api/admin/branding/logo/route.ts +9 -0
  79. package/templates/store-template/app/api/admin/categories/[id]/route.ts +9 -0
  80. package/templates/store-template/app/api/admin/categories/route.ts +9 -0
  81. package/templates/store-template/app/api/admin/check/route.ts +10 -0
  82. package/templates/store-template/app/api/admin/customer-groups/[id]/route.ts +10 -0
  83. package/templates/store-template/app/api/admin/customer-groups/route.ts +10 -0
  84. package/templates/store-template/app/api/admin/customers/[id]/addresses/[addressId]/route.ts +15 -0
  85. package/templates/store-template/app/api/admin/customers/[id]/addresses/route.ts +15 -0
  86. package/templates/store-template/app/api/admin/customers/[id]/route.ts +11 -0
  87. package/templates/store-template/app/api/admin/customers/bulk/route.ts +8 -0
  88. package/templates/store-template/app/api/admin/customers/route.ts +9 -0
  89. package/templates/store-template/app/api/admin/discounts/[id]/route.ts +11 -0
  90. package/templates/store-template/app/api/admin/discounts/route.ts +10 -0
  91. package/templates/store-template/app/api/admin/emails/preview/route.ts +4 -0
  92. package/templates/store-template/app/api/admin/emails/route.ts +5 -0
  93. package/templates/store-template/app/api/admin/emails/test/route.ts +4 -0
  94. package/templates/store-template/app/api/admin/files/route.ts +8 -0
  95. package/templates/store-template/app/api/admin/forgot-password/route.ts +7 -0
  96. package/templates/store-template/app/api/admin/linked-customer/route.ts +11 -0
  97. package/templates/store-template/app/api/admin/locations/[id]/route.ts +10 -0
  98. package/templates/store-template/app/api/admin/locations/route.ts +10 -0
  99. package/templates/store-template/app/api/admin/me/password/route.ts +13 -0
  100. package/templates/store-template/app/api/admin/me/route.ts +11 -0
  101. package/templates/store-template/app/api/admin/orders/[id]/refund/route.ts +7 -0
  102. package/templates/store-template/app/api/admin/orders/[id]/return/approve/route.ts +7 -0
  103. package/templates/store-template/app/api/admin/orders/[id]/return/reject/route.ts +7 -0
  104. package/templates/store-template/app/api/admin/orders/[id]/route.ts +9 -0
  105. package/templates/store-template/app/api/admin/orders/[id]/ship-with-shippo/route.ts +6 -0
  106. package/templates/store-template/app/api/admin/orders/[id]/shippo-rates/route.ts +6 -0
  107. package/templates/store-template/app/api/admin/orders/bulk/route.ts +8 -0
  108. package/templates/store-template/app/api/admin/orders/route.ts +9 -0
  109. package/templates/store-template/app/api/admin/payment-methods/[id]/route.ts +10 -0
  110. package/templates/store-template/app/api/admin/payment-methods/route.ts +10 -0
  111. package/templates/store-template/app/api/admin/products/[id]/route.ts +9 -0
  112. package/templates/store-template/app/api/admin/products/bulk/route.ts +9 -0
  113. package/templates/store-template/app/api/admin/products/route.ts +9 -0
  114. package/templates/store-template/app/api/admin/products/stats/route.ts +10 -0
  115. package/templates/store-template/app/api/admin/reset-password/route.ts +10 -0
  116. package/templates/store-template/app/api/admin/settings/route.ts +11 -0
  117. package/templates/store-template/app/api/admin/setup/route.ts +10 -0
  118. package/templates/store-template/app/api/admin/setup-guide/manual-mark/route.ts +13 -0
  119. package/templates/store-template/app/api/admin/setup-guide/route.ts +10 -0
  120. package/templates/store-template/app/api/admin/shipping/carriers/[id]/route.ts +9 -0
  121. package/templates/store-template/app/api/admin/shipping/carriers/route.ts +9 -0
  122. package/templates/store-template/app/api/admin/shipping/rates/[id]/route.ts +9 -0
  123. package/templates/store-template/app/api/admin/shipping/rates/route.ts +9 -0
  124. package/templates/store-template/app/api/admin/shipping/shippo/api-key/route.ts +1 -0
  125. package/templates/store-template/app/api/admin/shipping/shippo/validate/route.ts +1 -0
  126. package/templates/store-template/app/api/admin/shipping/zones/[id]/route.ts +9 -0
  127. package/templates/store-template/app/api/admin/shipping/zones/route.ts +9 -0
  128. package/templates/store-template/app/api/admin/stats/route.ts +9 -0
  129. package/templates/store-template/app/api/admin/stripe-status/route.ts +1 -0
  130. package/templates/store-template/app/api/admin/tax-zones/[id]/route.ts +7 -0
  131. package/templates/store-template/app/api/admin/tax-zones/route.ts +7 -0
  132. package/templates/store-template/app/api/admin/users/[id]/route.ts +14 -0
  133. package/templates/store-template/app/api/admin/users/route.ts +12 -0
  134. package/templates/store-template/app/api/admin-auth/[...nextauth]/route.ts +17 -0
  135. package/templates/store-template/app/api/analytics/track/route.ts +12 -0
  136. package/templates/store-template/app/api/auth/[...nextauth]/route.ts +19 -0
  137. package/templates/store-template/app/api/auth/forgot-password/route.ts +9 -0
  138. package/templates/store-template/app/api/auth/register/route.ts +9 -0
  139. package/templates/store-template/app/api/auth/resend-verification/route.ts +9 -0
  140. package/templates/store-template/app/api/auth/reset-password/route.ts +10 -0
  141. package/templates/store-template/app/api/auth/verify-email/route.ts +10 -0
  142. package/templates/store-template/app/api/categories/route.ts +10 -0
  143. package/templates/store-template/app/api/checkout/countries/route.ts +7 -0
  144. package/templates/store-template/app/api/checkout/discount/route.ts +9 -0
  145. package/templates/store-template/app/api/checkout/manual/route.ts +10 -0
  146. package/templates/store-template/app/api/checkout/payment-methods/route.ts +8 -0
  147. package/templates/store-template/app/api/checkout/pickup-locations/route.ts +8 -0
  148. package/templates/store-template/app/api/checkout/route.ts +10 -0
  149. package/templates/store-template/app/api/checkout/shipping/route.ts +21 -0
  150. package/templates/store-template/app/api/checkout/status/route.ts +10 -0
  151. package/templates/store-template/app/api/contact/route.ts +164 -0
  152. package/templates/store-template/app/api/media/presign/route.ts +7 -0
  153. package/templates/store-template/app/api/media/route.ts +7 -0
  154. package/templates/store-template/app/api/products/[slug]/route.ts +10 -0
  155. package/templates/store-template/app/api/products/route.ts +10 -0
  156. package/templates/store-template/app/api/store/settings/route.ts +12 -0
  157. package/templates/store-template/app/api/webhooks/stripe/route.ts +63 -0
  158. package/templates/store-template/app/auth/forgot-password/page.tsx +38 -0
  159. package/templates/store-template/app/auth/layout.tsx +17 -0
  160. package/templates/store-template/app/auth/reset-password/page.tsx +81 -0
  161. package/templates/store-template/app/auth/signin/page.tsx +62 -0
  162. package/templates/store-template/app/auth/signup/page.tsx +38 -0
  163. package/templates/store-template/app/auth/verify-email/page.tsx +185 -0
  164. package/templates/store-template/app/cart/page.tsx +137 -0
  165. package/templates/store-template/app/checkout/page.tsx +88 -0
  166. package/templates/store-template/app/checkout/success/page.tsx +50 -0
  167. package/templates/store-template/app/contact/layout.tsx +24 -0
  168. package/templates/store-template/app/contact/page.tsx +321 -0
  169. package/templates/store-template/app/faq/page.tsx +153 -0
  170. package/templates/store-template/app/globals.css +99 -0
  171. package/templates/store-template/app/layout.tsx +269 -0
  172. package/templates/store-template/app/not-found.tsx +35 -0
  173. package/templates/store-template/app/page.tsx +23 -0
  174. package/templates/store-template/app/privacy/page.tsx +281 -0
  175. package/templates/store-template/app/products/[slug]/ProductDetailClient.tsx +179 -0
  176. package/templates/store-template/app/products/[slug]/page.tsx +149 -0
  177. package/templates/store-template/app/products/layout.tsx +26 -0
  178. package/templates/store-template/app/products/page.tsx +102 -0
  179. package/templates/store-template/app/robots.ts +25 -0
  180. package/templates/store-template/app/sitemap.ts +76 -0
  181. package/templates/store-template/app/terms/page.tsx +313 -0
  182. package/templates/store-template/components/PathSyncBridge.tsx +61 -0
  183. package/templates/store-template/components/Providers.tsx +37 -0
  184. package/templates/store-template/components/layout/Footer.tsx +150 -0
  185. package/templates/store-template/components/layout/Header.tsx +162 -0
  186. package/templates/store-template/components/sections/.gitkeep +7 -0
  187. package/templates/store-template/components/sections/CTA.tsx +51 -0
  188. package/templates/store-template/components/sections/Features.tsx +91 -0
  189. package/templates/store-template/components/sections/Hero.tsx +85 -0
  190. package/templates/store-template/eslint.config.mjs +18 -0
  191. package/templates/store-template/lib/store-config.ts +77 -0
  192. package/templates/store-template/lib/store-policy.ts +68 -0
  193. package/templates/store-template/lib/utils.ts +21 -0
  194. package/templates/store-template/next-env.d.ts +6 -0
  195. package/templates/store-template/next.config.js +105 -0
  196. package/templates/store-template/package.json +32 -0
  197. package/templates/store-template/postcss.config.js +6 -0
  198. package/templates/store-template/proxy.ts +150 -0
  199. package/templates/store-template/public/rovela-logo.png +0 -0
  200. package/templates/store-template/tailwind.config.ts +74 -0
  201. package/templates/store-template/tsconfig.json +41 -0
  202. package/templates/store-template/types/next-auth.d.ts +32 -0
@@ -0,0 +1,528 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Admin Login/Setup Page
5
+ *
6
+ * This page handles both:
7
+ * - First-time admin setup (when no admins exist)
8
+ * - Admin login (when at least one admin exists)
9
+ *
10
+ * This page is OUTSIDE the (dashboard) route group, so it's not protected
11
+ * by AdminGuard and won't cause redirect loops.
12
+ *
13
+ * Chrome comes from the SDK's AdminAuthShell (favicon-first brand tile +
14
+ * store name + card + theme CSS) — the same front door as every other
15
+ * admin auth page. The forms themselves stay INLINED here (not the SDK's
16
+ * AdminLoginForm) so the postMessage auto-login handshake with the Rovela
17
+ * platform doesn't depend on an SDK import during initial load.
18
+ */
19
+
20
+ import { useState, useEffect } from 'react'
21
+ import { useRouter } from 'next/navigation'
22
+ import { Eye, EyeOff } from 'lucide-react'
23
+ import { useAdminSession, useAdminAuth, AdminAuthShell } from '@rovela-ai/sdk/admin'
24
+ // Import Rovela admin theme for visual consistency
25
+ import '@rovela-ai/sdk/admin/styles/admin-theme.css'
26
+
27
+ /**
28
+ * Admin check response type
29
+ */
30
+ interface AdminCheckResponse {
31
+ hasAdmin: boolean
32
+ ownerEmail: string
33
+ }
34
+
35
+ /**
36
+ * Setup Form Component (inline to avoid SDK import during initial load)
37
+ *
38
+ * Renders CARD CONTENT only — the page wraps it in AdminAuthShell.
39
+ */
40
+ function SetupForm({ ownerEmail }: { ownerEmail: string }) {
41
+ const [email, setEmail] = useState(ownerEmail)
42
+ const [name, setName] = useState('')
43
+ const [password, setPassword] = useState('')
44
+ const [confirmPassword, setConfirmPassword] = useState('')
45
+ const [showPassword, setShowPassword] = useState(false)
46
+ const [showConfirm, setShowConfirm] = useState(false)
47
+ const [error, setError] = useState<string | null>(null)
48
+ const [isSubmitting, setIsSubmitting] = useState(false)
49
+
50
+ const handleSubmit = async (e: React.FormEvent) => {
51
+ e.preventDefault()
52
+ setError(null)
53
+
54
+ // Validate passwords match
55
+ if (password !== confirmPassword) {
56
+ setError('Passwords do not match')
57
+ return
58
+ }
59
+
60
+ // Validate password length
61
+ if (password.length < 8) {
62
+ setError('Password must be at least 8 characters')
63
+ return
64
+ }
65
+
66
+ // Validate name
67
+ if (name.trim().length < 2) {
68
+ setError('Name must be at least 2 characters')
69
+ return
70
+ }
71
+
72
+ setIsSubmitting(true)
73
+
74
+ try {
75
+ const response = await fetch('/api/admin/setup', {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify({
79
+ email: email.trim(),
80
+ password,
81
+ name: name.trim(),
82
+ }),
83
+ })
84
+
85
+ const data = await response.json()
86
+
87
+ if (!response.ok) {
88
+ setError(data.error || 'Setup failed')
89
+ return
90
+ }
91
+
92
+ // Success - reload to show login form
93
+ window.location.reload()
94
+ } catch {
95
+ setError('An unexpected error occurred')
96
+ } finally {
97
+ setIsSubmitting(false)
98
+ }
99
+ }
100
+
101
+ return (
102
+ <>
103
+ <h1 className="admin-auth-title">Create your admin account</h1>
104
+ <p className="admin-auth-sub">
105
+ Set a password to start managing your store.
106
+ </p>
107
+
108
+ <form onSubmit={handleSubmit} className="space-y-4">
109
+ {error && (
110
+ <div className="admin-alert admin-alert-error">
111
+ <div className="admin-alert-description">{error}</div>
112
+ </div>
113
+ )}
114
+
115
+ <div>
116
+ <label htmlFor="setup-email" className="admin-label admin-label-required">
117
+ Email
118
+ </label>
119
+ <input
120
+ id="setup-email"
121
+ type="email"
122
+ value={email}
123
+ onChange={(e) => setEmail(e.target.value)}
124
+ required
125
+ disabled={isSubmitting}
126
+ className="admin-input w-full"
127
+ placeholder="you@example.com"
128
+ />
129
+ {ownerEmail && email === ownerEmail && (
130
+ <p className="mt-1 text-xs admin-text-muted">
131
+ Pre-filled with your store owner email
132
+ </p>
133
+ )}
134
+ </div>
135
+
136
+ <div>
137
+ <label htmlFor="setup-name" className="admin-label admin-label-required">
138
+ Name
139
+ </label>
140
+ <input
141
+ id="setup-name"
142
+ type="text"
143
+ value={name}
144
+ onChange={(e) => setName(e.target.value)}
145
+ required
146
+ disabled={isSubmitting}
147
+ className="admin-input w-full"
148
+ placeholder="Your name"
149
+ />
150
+ </div>
151
+
152
+ <div>
153
+ <label htmlFor="setup-password" className="admin-label admin-label-required">
154
+ Password
155
+ </label>
156
+ <div className="admin-auth-pw">
157
+ <input
158
+ id="setup-password"
159
+ type={showPassword ? 'text' : 'password'}
160
+ value={password}
161
+ onChange={(e) => setPassword(e.target.value)}
162
+ required
163
+ disabled={isSubmitting}
164
+ className="admin-input w-full"
165
+ placeholder="At least 8 characters"
166
+ />
167
+ <button
168
+ type="button"
169
+ className="admin-auth-eye"
170
+ onClick={() => setShowPassword((v) => !v)}
171
+ aria-label={showPassword ? 'Hide password' : 'Show password'}
172
+ tabIndex={-1}
173
+ >
174
+ {showPassword ? <EyeOff aria-hidden="true" /> : <Eye aria-hidden="true" />}
175
+ </button>
176
+ </div>
177
+ </div>
178
+
179
+ <div>
180
+ <label htmlFor="setup-confirm" className="admin-label admin-label-required">
181
+ Confirm password
182
+ </label>
183
+ <div className="admin-auth-pw">
184
+ <input
185
+ id="setup-confirm"
186
+ type={showConfirm ? 'text' : 'password'}
187
+ value={confirmPassword}
188
+ onChange={(e) => setConfirmPassword(e.target.value)}
189
+ required
190
+ disabled={isSubmitting}
191
+ className="admin-input w-full"
192
+ placeholder="Re-enter your password"
193
+ />
194
+ <button
195
+ type="button"
196
+ className="admin-auth-eye"
197
+ onClick={() => setShowConfirm((v) => !v)}
198
+ aria-label={showConfirm ? 'Hide password' : 'Show password'}
199
+ tabIndex={-1}
200
+ >
201
+ {showConfirm ? <EyeOff aria-hidden="true" /> : <Eye aria-hidden="true" />}
202
+ </button>
203
+ </div>
204
+ </div>
205
+
206
+ <button
207
+ type="submit"
208
+ disabled={isSubmitting}
209
+ className="admin-btn admin-btn-primary w-full"
210
+ >
211
+ {isSubmitting ? (
212
+ <span className="flex items-center justify-center gap-2">
213
+ <span className="admin-spinner admin-spinner-sm" />
214
+ Creating your account…
215
+ </span>
216
+ ) : (
217
+ 'Create admin account'
218
+ )}
219
+ </button>
220
+ </form>
221
+ </>
222
+ )
223
+ }
224
+
225
+ /**
226
+ * Login Form Component
227
+ *
228
+ * Renders CARD CONTENT only — the page wraps it in AdminAuthShell.
229
+ *
230
+ * Uses useSession().update() to refresh session state after sign-in.
231
+ * This is critical for iframe contexts where navigation happens before
232
+ * the session state updates, causing redirect loops.
233
+ *
234
+ * Also supports postMessage auto-login from Rovela platform for seamless
235
+ * admin access during store generation.
236
+ */
237
+ function LoginForm() {
238
+ const router = useRouter()
239
+ // Admin auth lives at `/api/admin-auth/*` (separate cookie from customer
240
+ // auth at `/api/auth/*`). `useAdminAuth().signIn` POSTs credentials to
241
+ // that endpoint; `useAdminSession().update` re-fetches the admin session
242
+ // after sign-in so the page sees the new state immediately.
243
+ const { signIn: adminSignIn } = useAdminAuth()
244
+ const { update } = useAdminSession()
245
+ const [email, setEmail] = useState('')
246
+ const [password, setPassword] = useState('')
247
+ const [showPassword, setShowPassword] = useState(false)
248
+ const [error, setError] = useState<string | null>(null)
249
+ const [isLoading, setIsLoading] = useState(false)
250
+ const [autoLoginAttempted, setAutoLoginAttempted] = useState(false)
251
+
252
+ // PostMessage listener for auto-login from Rovela platform
253
+ // Flow: Store requests credentials → Platform responds → Store auto-logs in
254
+ useEffect(() => {
255
+ let credentialsUsed = false // One-time use flag
256
+
257
+ // Allowed origins whitelist for security
258
+ // rovela.ai = platform (sends credentials)
259
+ // rovela.app = deployed stores (this page runs here)
260
+ const ALLOWED_ORIGINS = [
261
+ 'http://localhost:3000',
262
+ 'http://localhost:3001',
263
+ 'https://rovela.ai',
264
+ 'https://www.rovela.ai',
265
+ ]
266
+
267
+ const isAllowedOrigin = (origin: string): boolean => {
268
+ // Exact match against whitelist
269
+ if (ALLOWED_ORIGINS.includes(origin)) return true
270
+ // Pattern match for Rovela platform subdomains
271
+ if (origin.endsWith('.rovela.ai')) return true
272
+ // Allow localhost with any port during development
273
+ if (origin.startsWith('http://localhost:')) return true
274
+ return false
275
+ }
276
+
277
+ const handleMessage = async (event: MessageEvent) => {
278
+ // Only accept messages with our expected type
279
+ if (event.data?.type !== 'ROVELA_ADMIN_CREDENTIALS') return
280
+
281
+ // Security: Validate origin
282
+ if (!isAllowedOrigin(event.origin)) {
283
+ console.warn('[AdminLogin] Rejected credentials from untrusted origin:', event.origin)
284
+ return
285
+ }
286
+
287
+ // Prevent duplicate logins
288
+ if (credentialsUsed) return
289
+ credentialsUsed = true
290
+
291
+ const { email: credEmail, password: credPassword } = event.data
292
+ if (!credEmail || !credPassword) return
293
+
294
+ console.log('[AdminLogin] Received credentials via postMessage, auto-logging in...')
295
+ setIsLoading(true)
296
+ setError(null)
297
+
298
+ try {
299
+ const result = await adminSignIn({
300
+ email: credEmail,
301
+ password: credPassword,
302
+ })
303
+
304
+ if (!result.success) {
305
+ console.error('[AdminLogin] Auto-login failed:', result.error)
306
+ setError('Auto-login failed. Please sign in manually.')
307
+ // Pre-fill email for convenience
308
+ setEmail(credEmail)
309
+ } else {
310
+ console.log('[AdminLogin] Auto-login successful, redirecting...')
311
+ // adminSignIn already called update() internally, but call again
312
+ // to be absolutely sure the session cache is fresh before navigating.
313
+ await update()
314
+ router.push('/admin')
315
+ }
316
+ } catch (err) {
317
+ console.error('[AdminLogin] Auto-login error:', err)
318
+ setError('Auto-login failed. Please sign in manually.')
319
+ setEmail(credEmail)
320
+ } finally {
321
+ setIsLoading(false)
322
+ }
323
+ }
324
+
325
+ window.addEventListener('message', handleMessage)
326
+
327
+ // Request credentials from parent (Rovela platform)
328
+ // Only request once per mount
329
+ if (!autoLoginAttempted && window.parent !== window) {
330
+ setAutoLoginAttempted(true)
331
+ window.parent.postMessage({ type: 'ROVELA_REQUEST_CREDENTIALS' }, '*')
332
+ console.log('[AdminLogin] Requested credentials from parent')
333
+ }
334
+
335
+ return () => {
336
+ window.removeEventListener('message', handleMessage)
337
+ }
338
+ }, [router, update, adminSignIn, autoLoginAttempted])
339
+
340
+ const handleSubmit = async (e: React.FormEvent) => {
341
+ e.preventDefault()
342
+ setError(null)
343
+ setIsLoading(true)
344
+
345
+ try {
346
+ const result = await adminSignIn({ email, password })
347
+
348
+ if (!result.success) {
349
+ setError(result.error || 'Invalid email or password')
350
+ } else {
351
+ // adminSignIn already called update(); call again before navigation
352
+ // so AdminGuard sees the fresh session state on the next render.
353
+ await update()
354
+ router.push('/admin')
355
+ }
356
+ } catch {
357
+ setError('An error occurred. Please try again.')
358
+ } finally {
359
+ setIsLoading(false)
360
+ }
361
+ }
362
+
363
+ return (
364
+ <>
365
+ <h1 className="admin-auth-title">Sign in</h1>
366
+ <p className="admin-auth-sub">Manage your store from the admin dashboard.</p>
367
+
368
+ <form onSubmit={handleSubmit} className="space-y-4">
369
+ {error && (
370
+ <div className="admin-alert admin-alert-error">
371
+ <div className="admin-alert-description">{error}</div>
372
+ </div>
373
+ )}
374
+
375
+ <div>
376
+ <label htmlFor="login-email" className="admin-label admin-label-required">
377
+ Email
378
+ </label>
379
+ <input
380
+ id="login-email"
381
+ type="email"
382
+ value={email}
383
+ onChange={(e) => setEmail(e.target.value)}
384
+ required
385
+ disabled={isLoading}
386
+ className="admin-input w-full"
387
+ placeholder="you@example.com"
388
+ />
389
+ </div>
390
+
391
+ <div>
392
+ <div className="admin-auth-lblrow">
393
+ <label htmlFor="login-password" className="admin-label admin-label-required">
394
+ Password
395
+ </label>
396
+ <a href="/admin/forgot-password" className="admin-auth-link">
397
+ Forgot password?
398
+ </a>
399
+ </div>
400
+ <div className="admin-auth-pw">
401
+ <input
402
+ id="login-password"
403
+ type={showPassword ? 'text' : 'password'}
404
+ value={password}
405
+ onChange={(e) => setPassword(e.target.value)}
406
+ required
407
+ disabled={isLoading}
408
+ className="admin-input w-full"
409
+ placeholder="Your password"
410
+ />
411
+ <button
412
+ type="button"
413
+ className="admin-auth-eye"
414
+ onClick={() => setShowPassword((v) => !v)}
415
+ aria-label={showPassword ? 'Hide password' : 'Show password'}
416
+ tabIndex={-1}
417
+ >
418
+ {showPassword ? <EyeOff aria-hidden="true" /> : <Eye aria-hidden="true" />}
419
+ </button>
420
+ </div>
421
+ </div>
422
+
423
+ <button
424
+ type="submit"
425
+ disabled={isLoading}
426
+ className="admin-btn admin-btn-primary w-full"
427
+ >
428
+ {isLoading ? (
429
+ <span className="flex items-center justify-center gap-2">
430
+ <span className="admin-spinner admin-spinner-sm" />
431
+ Signing in…
432
+ </span>
433
+ ) : (
434
+ 'Sign in'
435
+ )}
436
+ </button>
437
+ </form>
438
+ </>
439
+ )
440
+ }
441
+
442
+ export default function AdminLoginPage() {
443
+ const router = useRouter()
444
+ // Admin-specific session — reads `/api/admin-auth/session`, not the
445
+ // customer endpoint. An active customer session won't redirect us away
446
+ // from the admin login screen (which is the correct behavior — the
447
+ // admin might be signed in as a customer simultaneously).
448
+ const { data: adminSession, status } = useAdminSession()
449
+
450
+ // State for admin check
451
+ const [checkState, setCheckState] = useState<{
452
+ loading: boolean
453
+ hasAdmin: boolean | null
454
+ ownerEmail: string
455
+ }>({
456
+ loading: true,
457
+ hasAdmin: null,
458
+ ownerEmail: '',
459
+ })
460
+
461
+ // Check if any admin exists
462
+ useEffect(() => {
463
+ async function checkAdmin() {
464
+ try {
465
+ const response = await fetch('/api/admin/check')
466
+ const data: AdminCheckResponse = await response.json()
467
+ setCheckState({
468
+ loading: false,
469
+ hasAdmin: data.hasAdmin,
470
+ ownerEmail: data.ownerEmail,
471
+ })
472
+ } catch (error) {
473
+ console.error('[AdminLoginPage] Failed to check admin:', error)
474
+ // On error, assume no admin exists to allow setup
475
+ setCheckState({
476
+ loading: false,
477
+ hasAdmin: false,
478
+ ownerEmail: '',
479
+ })
480
+ }
481
+ }
482
+
483
+ checkAdmin()
484
+ }, [])
485
+
486
+ // Redirect to admin dashboard if already authenticated AS ADMIN.
487
+ // `useAdminSession` only returns data when the admin cookie is active,
488
+ // so any truthy session here is definitionally an admin session.
489
+ useEffect(() => {
490
+ if (status === 'authenticated' && adminSession?.user) {
491
+ router.replace('/admin')
492
+ }
493
+ }, [status, adminSession, router])
494
+
495
+ // Show loading while checking session or admin status. The shell resolves
496
+ // store identity itself (skeletons while loading — never a wrong value).
497
+ if (status === 'loading' || checkState.loading) {
498
+ return (
499
+ <AdminAuthShell>
500
+ <div className="flex items-center justify-center py-8">
501
+ <span className="admin-spinner admin-spinner-lg" />
502
+ </div>
503
+ </AdminAuthShell>
504
+ )
505
+ }
506
+
507
+ // Don't render form if already authenticated AS ADMIN (will redirect).
508
+ if (status === 'authenticated' && adminSession?.user) {
509
+ return (
510
+ <AdminAuthShell>
511
+ <div className="flex flex-col items-center justify-center gap-3 py-8">
512
+ <span className="admin-spinner admin-spinner-lg" />
513
+ <p className="text-sm admin-text-muted">Taking you to your dashboard…</p>
514
+ </div>
515
+ </AdminAuthShell>
516
+ )
517
+ }
518
+
519
+ return (
520
+ <AdminAuthShell>
521
+ {checkState.hasAdmin ? (
522
+ <LoginForm />
523
+ ) : (
524
+ <SetupForm ownerEmail={checkState.ownerEmail} />
525
+ )}
526
+ </AdminAuthShell>
527
+ )
528
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Admin Reset Password Page
3
+ *
4
+ * Public page (outside the dashboard route group). The SDK's
5
+ * AdminResetPasswordForm renders its own AdminAuthShell (brand tile +
6
+ * store name + card + theme CSS), so this page is a bare mount.
7
+ *
8
+ * The form reads the `?token=X` query param via `useSearchParams`, so
9
+ * Next.js App Router requires a <Suspense> boundary here. The fallback
10
+ * reuses the shell so the suspense moment and the form's own validating
11
+ * state look identical.
12
+ */
13
+
14
+ import { Suspense } from 'react'
15
+ import {
16
+ AdminAuthShell,
17
+ AdminResetPasswordForm,
18
+ } from '@rovela-ai/sdk/admin/components'
19
+
20
+ function LoadingFallback() {
21
+ return (
22
+ <AdminAuthShell>
23
+ <div className="flex items-center justify-center py-8">
24
+ <span className="admin-spinner admin-spinner-lg" />
25
+ </div>
26
+ </AdminAuthShell>
27
+ )
28
+ }
29
+
30
+ export default function AdminResetPasswordPage() {
31
+ return (
32
+ <Suspense fallback={<LoadingFallback />}>
33
+ <AdminResetPasswordForm />
34
+ </Suspense>
35
+ )
36
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Customer Address Book API — update / set-default / archive one address.
3
+ *
4
+ * PATCH /api/account/addresses/[id] → update fields and/or set default
5
+ * DELETE /api/account/addresses/[id] → soft-delete (archived for compliance)
6
+ *
7
+ * Ownership is enforced inside the SDK handlers (scoped to the signed-in customer).
8
+ */
9
+
10
+ export { PATCH, DELETE } from '@rovela-ai/sdk/auth/api/address'
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Customer Address Book API — list + create.
3
+ *
4
+ * GET /api/account/addresses → list the signed-in customer's saved addresses
5
+ * POST /api/account/addresses → create a new address
6
+ *
7
+ * Both are `requireCustomer()`-gated inside the SDK handlers.
8
+ */
9
+
10
+ export { GET, POST } from '@rovela-ai/sdk/auth/api/addresses'
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Change Password API Route
3
+ *
4
+ * POST /api/account/change-password - Change customer password
5
+ *
6
+ * Requires current password verification before allowing password change.
7
+ * Protected by `requireCustomer()` — admins get a 403.
8
+ */
9
+
10
+ import {
11
+ requireCustomer,
12
+ findCustomerById,
13
+ updateCustomerPassword,
14
+ verifyPassword,
15
+ validatePassword,
16
+ MIN_PASSWORD_LENGTH,
17
+ } from '@rovela-ai/sdk/auth/server'
18
+
19
+ /**
20
+ * POST /api/account/change-password
21
+ *
22
+ * Changes the authenticated customer's password.
23
+ * Requires current password for verification.
24
+ *
25
+ * Body: { currentPassword: string, newPassword: string }
26
+ */
27
+ export async function POST(request: Request) {
28
+ const guard = await requireCustomer()
29
+ if (!guard.ok) return guard.response
30
+
31
+ try {
32
+ const body = await request.json()
33
+ const { currentPassword, newPassword } = body as {
34
+ currentPassword?: string
35
+ newPassword?: string
36
+ }
37
+
38
+ // Validate required fields
39
+ if (!currentPassword || typeof currentPassword !== 'string') {
40
+ return Response.json(
41
+ { error: 'Current password is required' },
42
+ { status: 400 }
43
+ )
44
+ }
45
+
46
+ if (!newPassword || typeof newPassword !== 'string') {
47
+ return Response.json(
48
+ { error: 'New password is required' },
49
+ { status: 400 }
50
+ )
51
+ }
52
+
53
+ // Validate new password strength
54
+ const passwordValidation = validatePassword(newPassword)
55
+ if (!passwordValidation.valid) {
56
+ return Response.json(
57
+ { error: passwordValidation.error || `Password must be at least ${MIN_PASSWORD_LENGTH} characters` },
58
+ { status: 400 }
59
+ )
60
+ }
61
+
62
+ // Check that new password is different from current
63
+ if (currentPassword === newPassword) {
64
+ return Response.json(
65
+ { error: 'New password must be different from current password' },
66
+ { status: 400 }
67
+ )
68
+ }
69
+
70
+ // Fetch customer to get current password hash
71
+ const customer = await findCustomerById(guard.customer.id)
72
+
73
+ if (!customer) {
74
+ return Response.json(
75
+ { error: 'Customer not found' },
76
+ { status: 404 }
77
+ )
78
+ }
79
+
80
+ // Verify current password
81
+ const isCurrentPasswordValid = await verifyPassword(
82
+ currentPassword,
83
+ customer.passwordHash
84
+ )
85
+
86
+ if (!isCurrentPasswordValid) {
87
+ return Response.json(
88
+ { error: 'Current password is incorrect' },
89
+ { status: 400 }
90
+ )
91
+ }
92
+
93
+ // Update password
94
+ await updateCustomerPassword(guard.customer.id, newPassword)
95
+
96
+ return Response.json({
97
+ success: true,
98
+ message: 'Password changed successfully',
99
+ })
100
+ } catch (error) {
101
+ console.error('[Change Password API] Failed to change password:', error)
102
+ return Response.json(
103
+ { error: 'Failed to change password' },
104
+ { status: 500 }
105
+ )
106
+ }
107
+ }