@questpie/admin 0.0.1 → 1.0.0

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 (250) hide show
  1. package/README.md +439 -424
  2. package/dist/auth-layout-M8K8_q5R.mjs +181 -0
  3. package/dist/auth-layout-M8K8_q5R.mjs.map +1 -0
  4. package/dist/bulk-upload-dialog-h7zXD78Y.mjs +274 -0
  5. package/dist/bulk-upload-dialog-h7zXD78Y.mjs.map +1 -0
  6. package/dist/{components/ui/card.mjs → card-BKHjBQfw.mjs} +8 -8
  7. package/dist/card-BKHjBQfw.mjs.map +1 -0
  8. package/dist/client/styles/index.css +434 -0
  9. package/dist/client-BCGpkAz6.mjs +22635 -0
  10. package/dist/client-BCGpkAz6.mjs.map +1 -0
  11. package/dist/client-CcWZbkBP.d.mts +13585 -0
  12. package/dist/client-CcWZbkBP.d.mts.map +1 -0
  13. package/dist/client.d.mts +3 -0
  14. package/dist/client.mjs +14 -0
  15. package/dist/content-locales-provider-BXvuIgfg.mjs +1650 -0
  16. package/dist/content-locales-provider-BXvuIgfg.mjs.map +1 -0
  17. package/dist/dashboard-page-B4PGEdc2.mjs +2500 -0
  18. package/dist/dashboard-page-B4PGEdc2.mjs.map +1 -0
  19. package/dist/dashboard-page-CVlyR40m.mjs +6 -0
  20. package/dist/dropzone-Do3awXKd.mjs +634 -0
  21. package/dist/dropzone-Do3awXKd.mjs.map +1 -0
  22. package/dist/{views/auth/forgot-password-form.mjs → forgot-password-page-Bcp-An4Y.mjs} +87 -14
  23. package/dist/forgot-password-page-Bcp-An4Y.mjs.map +1 -0
  24. package/dist/forgot-password-page-CIILVhfo.mjs +7 -0
  25. package/dist/index-B9Xwk4hi.d.mts +2753 -0
  26. package/dist/index-B9Xwk4hi.d.mts.map +1 -0
  27. package/dist/index.d.mts +3 -0
  28. package/dist/index.mjs +14 -0
  29. package/dist/login-page-8K7fo0qK.mjs +7 -0
  30. package/dist/login-page-CP4gA-dl.mjs +298 -0
  31. package/dist/login-page-CP4gA-dl.mjs.map +1 -0
  32. package/dist/preview-utils-BKQ9-TMa.mjs +65 -0
  33. package/dist/preview-utils-BKQ9-TMa.mjs.map +1 -0
  34. package/dist/{views/auth/reset-password-form.mjs → reset-password-page-BqfDmLxA.mjs} +111 -14
  35. package/dist/reset-password-page-BqfDmLxA.mjs.map +1 -0
  36. package/dist/reset-password-page-DLATv0xQ.mjs +7 -0
  37. package/dist/runtime-6VZM878K.mjs +69 -0
  38. package/dist/runtime-6VZM878K.mjs.map +1 -0
  39. package/dist/saved-views.types-BMsz5mCy.d.mts +42 -0
  40. package/dist/saved-views.types-BMsz5mCy.d.mts.map +1 -0
  41. package/dist/server.d.mts +250 -0
  42. package/dist/server.d.mts.map +1 -0
  43. package/dist/server.mjs +832 -0
  44. package/dist/server.mjs.map +1 -0
  45. package/dist/setup-page-CMZ5P_OE.mjs +6 -0
  46. package/dist/setup-page-YAP_fzqh.mjs +264 -0
  47. package/dist/setup-page-YAP_fzqh.mjs.map +1 -0
  48. package/dist/shared.d.mts +57 -0
  49. package/dist/shared.d.mts.map +1 -0
  50. package/dist/shared.mjs +3 -0
  51. package/dist/{hooks/use-auth.mjs → use-auth-BoLmWtmU.mjs} +42 -30
  52. package/dist/use-auth-BoLmWtmU.mjs.map +1 -0
  53. package/package.json +48 -197
  54. package/.turbo/turbo-build.log +0 -108
  55. package/CHANGELOG.md +0 -10
  56. package/STATUS.md +0 -917
  57. package/VALIDATION.md +0 -602
  58. package/components.json +0 -24
  59. package/dist/__tests__/setup.mjs +0 -38
  60. package/dist/__tests__/test-utils.mjs +0 -45
  61. package/dist/__tests__/vitest.d.mjs +0 -3
  62. package/dist/components/admin-app.mjs +0 -69
  63. package/dist/components/fields/array-field.mjs +0 -190
  64. package/dist/components/fields/checkbox-field.mjs +0 -34
  65. package/dist/components/fields/custom-field.mjs +0 -32
  66. package/dist/components/fields/date-field.mjs +0 -41
  67. package/dist/components/fields/datetime-field.mjs +0 -42
  68. package/dist/components/fields/email-field.mjs +0 -37
  69. package/dist/components/fields/embedded-collection.mjs +0 -253
  70. package/dist/components/fields/field-types.mjs +0 -1
  71. package/dist/components/fields/field-utils.mjs +0 -10
  72. package/dist/components/fields/field-wrapper.mjs +0 -34
  73. package/dist/components/fields/index.mjs +0 -23
  74. package/dist/components/fields/json-field.mjs +0 -243
  75. package/dist/components/fields/locale-badge.mjs +0 -16
  76. package/dist/components/fields/number-field.mjs +0 -39
  77. package/dist/components/fields/password-field.mjs +0 -37
  78. package/dist/components/fields/relation-field.mjs +0 -104
  79. package/dist/components/fields/relation-picker.mjs +0 -229
  80. package/dist/components/fields/relation-select.mjs +0 -188
  81. package/dist/components/fields/rich-text-editor/index.mjs +0 -897
  82. package/dist/components/fields/select-field.mjs +0 -41
  83. package/dist/components/fields/switch-field.mjs +0 -34
  84. package/dist/components/fields/text-field.mjs +0 -38
  85. package/dist/components/fields/textarea-field.mjs +0 -38
  86. package/dist/components/index.mjs +0 -59
  87. package/dist/components/primitives/checkbox-input.mjs +0 -127
  88. package/dist/components/primitives/date-input.mjs +0 -303
  89. package/dist/components/primitives/index.mjs +0 -12
  90. package/dist/components/primitives/number-input.mjs +0 -104
  91. package/dist/components/primitives/select-input.mjs +0 -177
  92. package/dist/components/primitives/tag-input.mjs +0 -135
  93. package/dist/components/primitives/text-input.mjs +0 -39
  94. package/dist/components/primitives/textarea-input.mjs +0 -37
  95. package/dist/components/primitives/toggle-input.mjs +0 -31
  96. package/dist/components/primitives/types.mjs +0 -12
  97. package/dist/components/ui/accordion.mjs +0 -55
  98. package/dist/components/ui/avatar.mjs +0 -54
  99. package/dist/components/ui/badge.mjs +0 -34
  100. package/dist/components/ui/button.mjs +0 -48
  101. package/dist/components/ui/checkbox.mjs +0 -21
  102. package/dist/components/ui/combobox.mjs +0 -163
  103. package/dist/components/ui/dialog.mjs +0 -95
  104. package/dist/components/ui/dropdown-menu.mjs +0 -138
  105. package/dist/components/ui/field.mjs +0 -113
  106. package/dist/components/ui/input-group.mjs +0 -82
  107. package/dist/components/ui/input.mjs +0 -17
  108. package/dist/components/ui/label.mjs +0 -15
  109. package/dist/components/ui/popover.mjs +0 -56
  110. package/dist/components/ui/scroll-area.mjs +0 -38
  111. package/dist/components/ui/select.mjs +0 -100
  112. package/dist/components/ui/separator.mjs +0 -16
  113. package/dist/components/ui/sheet.mjs +0 -90
  114. package/dist/components/ui/sidebar.mjs +0 -387
  115. package/dist/components/ui/skeleton.mjs +0 -14
  116. package/dist/components/ui/spinner.mjs +0 -16
  117. package/dist/components/ui/switch.mjs +0 -22
  118. package/dist/components/ui/table.mjs +0 -68
  119. package/dist/components/ui/tabs.mjs +0 -48
  120. package/dist/components/ui/textarea.mjs +0 -15
  121. package/dist/components/ui/tooltip.mjs +0 -44
  122. package/dist/config/component-registry.mjs +0 -38
  123. package/dist/config/index.mjs +0 -129
  124. package/dist/hooks/admin-provider.mjs +0 -70
  125. package/dist/hooks/index.mjs +0 -7
  126. package/dist/hooks/store.mjs +0 -178
  127. package/dist/hooks/use-collection-db.mjs +0 -146
  128. package/dist/hooks/use-collection.mjs +0 -112
  129. package/dist/hooks/use-global.mjs +0 -46
  130. package/dist/hooks/use-mobile.mjs +0 -20
  131. package/dist/lib/utils.mjs +0 -10
  132. package/dist/styles/index.css +0 -336
  133. package/dist/styles/index.mjs +0 -1
  134. package/dist/utils/index.mjs +0 -9
  135. package/dist/views/auth/auth-layout.mjs +0 -52
  136. package/dist/views/auth/index.mjs +0 -6
  137. package/dist/views/auth/login-form.mjs +0 -156
  138. package/dist/views/collection/auto-form-fields.mjs +0 -525
  139. package/dist/views/collection/collection-form.mjs +0 -91
  140. package/dist/views/collection/collection-list.mjs +0 -76
  141. package/dist/views/collection/form-field.mjs +0 -42
  142. package/dist/views/collection/index.mjs +0 -6
  143. package/dist/views/common/index.mjs +0 -4
  144. package/dist/views/common/locale-switcher.mjs +0 -39
  145. package/dist/views/common/version-history.mjs +0 -272
  146. package/dist/views/index.mjs +0 -9
  147. package/dist/views/layout/admin-layout.mjs +0 -40
  148. package/dist/views/layout/admin-router.mjs +0 -95
  149. package/dist/views/layout/admin-sidebar.mjs +0 -63
  150. package/dist/views/layout/index.mjs +0 -5
  151. package/src/__tests__/setup.ts +0 -44
  152. package/src/__tests__/test-utils.tsx +0 -49
  153. package/src/__tests__/vitest.d.ts +0 -9
  154. package/src/components/admin-app.tsx +0 -221
  155. package/src/components/fields/array-field.tsx +0 -237
  156. package/src/components/fields/checkbox-field.tsx +0 -47
  157. package/src/components/fields/custom-field.tsx +0 -50
  158. package/src/components/fields/date-field.tsx +0 -65
  159. package/src/components/fields/datetime-field.tsx +0 -67
  160. package/src/components/fields/email-field.tsx +0 -51
  161. package/src/components/fields/embedded-collection.tsx +0 -315
  162. package/src/components/fields/field-types.ts +0 -162
  163. package/src/components/fields/field-utils.ts +0 -6
  164. package/src/components/fields/field-wrapper.tsx +0 -52
  165. package/src/components/fields/index.ts +0 -66
  166. package/src/components/fields/json-field.tsx +0 -440
  167. package/src/components/fields/locale-badge.tsx +0 -15
  168. package/src/components/fields/number-field.tsx +0 -57
  169. package/src/components/fields/password-field.tsx +0 -51
  170. package/src/components/fields/relation-field.tsx +0 -243
  171. package/src/components/fields/relation-picker.tsx +0 -402
  172. package/src/components/fields/relation-select.tsx +0 -327
  173. package/src/components/fields/rich-text-editor/index.tsx +0 -1337
  174. package/src/components/fields/select-field.tsx +0 -61
  175. package/src/components/fields/switch-field.tsx +0 -47
  176. package/src/components/fields/text-field.tsx +0 -55
  177. package/src/components/fields/textarea-field.tsx +0 -55
  178. package/src/components/index.ts +0 -40
  179. package/src/components/primitives/checkbox-input.tsx +0 -193
  180. package/src/components/primitives/date-input.tsx +0 -401
  181. package/src/components/primitives/index.ts +0 -24
  182. package/src/components/primitives/number-input.tsx +0 -132
  183. package/src/components/primitives/select-input.tsx +0 -296
  184. package/src/components/primitives/tag-input.tsx +0 -200
  185. package/src/components/primitives/text-input.tsx +0 -49
  186. package/src/components/primitives/textarea-input.tsx +0 -46
  187. package/src/components/primitives/toggle-input.tsx +0 -36
  188. package/src/components/primitives/types.ts +0 -235
  189. package/src/components/ui/accordion.tsx +0 -72
  190. package/src/components/ui/avatar.tsx +0 -106
  191. package/src/components/ui/badge.tsx +0 -48
  192. package/src/components/ui/button.tsx +0 -53
  193. package/src/components/ui/card.tsx +0 -94
  194. package/src/components/ui/checkbox.tsx +0 -27
  195. package/src/components/ui/combobox.tsx +0 -290
  196. package/src/components/ui/dialog.tsx +0 -151
  197. package/src/components/ui/dropdown-menu.tsx +0 -254
  198. package/src/components/ui/field.tsx +0 -227
  199. package/src/components/ui/input-group.tsx +0 -149
  200. package/src/components/ui/input.tsx +0 -20
  201. package/src/components/ui/label.tsx +0 -18
  202. package/src/components/ui/popover.tsx +0 -88
  203. package/src/components/ui/scroll-area.tsx +0 -53
  204. package/src/components/ui/select.tsx +0 -192
  205. package/src/components/ui/separator.tsx +0 -23
  206. package/src/components/ui/sheet.tsx +0 -127
  207. package/src/components/ui/sidebar.tsx +0 -723
  208. package/src/components/ui/skeleton.tsx +0 -13
  209. package/src/components/ui/spinner.tsx +0 -10
  210. package/src/components/ui/switch.tsx +0 -32
  211. package/src/components/ui/table.tsx +0 -99
  212. package/src/components/ui/tabs.tsx +0 -82
  213. package/src/components/ui/textarea.tsx +0 -18
  214. package/src/components/ui/tooltip.tsx +0 -70
  215. package/src/config/component-registry.ts +0 -190
  216. package/src/config/index.ts +0 -1099
  217. package/src/hooks/README.md +0 -269
  218. package/src/hooks/admin-provider.tsx +0 -110
  219. package/src/hooks/index.ts +0 -41
  220. package/src/hooks/store.ts +0 -248
  221. package/src/hooks/use-auth.ts +0 -168
  222. package/src/hooks/use-collection-db.ts +0 -209
  223. package/src/hooks/use-collection.ts +0 -156
  224. package/src/hooks/use-global.ts +0 -69
  225. package/src/hooks/use-mobile.ts +0 -21
  226. package/src/lib/utils.ts +0 -6
  227. package/src/styles/index.css +0 -340
  228. package/src/utils/index.ts +0 -6
  229. package/src/views/auth/auth-layout.tsx +0 -77
  230. package/src/views/auth/forgot-password-form.tsx +0 -192
  231. package/src/views/auth/index.ts +0 -21
  232. package/src/views/auth/login-form.tsx +0 -229
  233. package/src/views/auth/reset-password-form.tsx +0 -232
  234. package/src/views/collection/auto-form-fields.tsx +0 -982
  235. package/src/views/collection/collection-form.tsx +0 -186
  236. package/src/views/collection/collection-list.tsx +0 -223
  237. package/src/views/collection/form-field.tsx +0 -52
  238. package/src/views/collection/index.ts +0 -15
  239. package/src/views/common/index.ts +0 -8
  240. package/src/views/common/locale-switcher.tsx +0 -45
  241. package/src/views/common/version-history.tsx +0 -406
  242. package/src/views/index.ts +0 -25
  243. package/src/views/layout/admin-layout.tsx +0 -117
  244. package/src/views/layout/admin-router.tsx +0 -206
  245. package/src/views/layout/admin-sidebar.tsx +0 -185
  246. package/src/views/layout/index.ts +0 -12
  247. package/tsconfig.json +0 -13
  248. package/tsconfig.tsbuildinfo +0 -1
  249. package/tsdown.config.ts +0 -13
  250. package/vitest.config.ts +0 -29
@@ -0,0 +1,832 @@
1
+ import { r as getPreviewSecret } from "./preview-utils-BKQ9-TMa.mjs";
2
+ import { z } from "zod";
3
+ import { fn, q, starterModule } from "questpie";
4
+ import { boolean, jsonb, uniqueIndex, varchar } from "drizzle-orm/pg-core";
5
+ import { createHmac, timingSafeEqual } from "node:crypto";
6
+ import { eq, sql } from "drizzle-orm";
7
+
8
+ //#region src/server/auth-helpers.ts
9
+ /**
10
+ * Check if user is authenticated with required role on the server.
11
+ * Returns a redirect Response if not authenticated, null if authenticated.
12
+ *
13
+ * Use this in server loaders/middleware to protect routes.
14
+ *
15
+ * @example TanStack Router
16
+ * ```ts
17
+ * export const Route = createFileRoute("/admin")({
18
+ * beforeLoad: async ({ context }) => {
19
+ * const redirect = await requireAdminAuth({
20
+ * request: context.request,
21
+ * cms,
22
+ * loginPath: "/admin/login",
23
+ * });
24
+ * if (redirect) throw redirect;
25
+ * },
26
+ * });
27
+ * ```
28
+ *
29
+ * @example Next.js Middleware
30
+ * ```ts
31
+ * export async function middleware(request: NextRequest) {
32
+ * const redirect = await requireAdminAuth({
33
+ * request,
34
+ * cms,
35
+ * loginPath: "/admin/login",
36
+ * });
37
+ * if (redirect) return redirect;
38
+ * return NextResponse.next();
39
+ * }
40
+ * ```
41
+ */
42
+ async function requireAdminAuth({ request, cms, loginPath = "/admin/login", requiredRole = "admin", redirectParam = "redirect" }) {
43
+ if (!cms.auth) {
44
+ console.warn("requireAdminAuth: Auth not configured on CMS instance");
45
+ return null;
46
+ }
47
+ try {
48
+ const session = await cms.auth.api.getSession({ headers: request.headers });
49
+ if (!session || !session.user) {
50
+ const currentUrl = new URL(request.url);
51
+ const redirectUrl = new URL(loginPath, currentUrl.origin);
52
+ redirectUrl.searchParams.set(redirectParam, currentUrl.pathname);
53
+ return Response.redirect(redirectUrl.toString(), 302);
54
+ }
55
+ if (session.user.role !== requiredRole) {
56
+ const currentUrl = new URL(request.url);
57
+ const redirectUrl = new URL(loginPath, currentUrl.origin);
58
+ redirectUrl.searchParams.set(redirectParam, currentUrl.pathname);
59
+ return Response.redirect(redirectUrl.toString(), 302);
60
+ }
61
+ return null;
62
+ } catch (error) {
63
+ console.error("requireAdminAuth: Error checking session", error);
64
+ const currentUrl = new URL(request.url);
65
+ const redirectUrl = new URL(loginPath, currentUrl.origin);
66
+ return Response.redirect(redirectUrl.toString(), 302);
67
+ }
68
+ }
69
+ /**
70
+ * Get the current admin session on the server.
71
+ * Returns null if not authenticated.
72
+ *
73
+ * Use this when you need access to the session data in server code.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * const session = await getAdminSession({ request, cms });
78
+ * if (!session) {
79
+ * return redirect("/admin/login");
80
+ * }
81
+ * console.log("User:", session.user.name);
82
+ * ```
83
+ */
84
+ async function getAdminSession({ request, cms }) {
85
+ if (!cms.auth) return null;
86
+ try {
87
+ const session = await cms.auth.api.getSession({ headers: request.headers });
88
+ if (!session || !session.user) return null;
89
+ return session;
90
+ } catch (error) {
91
+ console.error("getAdminSession: Error getting session", error);
92
+ return null;
93
+ }
94
+ }
95
+ /**
96
+ * Check if the current user has admin role on the server.
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * const isAdmin = await isAdminUser({ request, cms });
101
+ * if (!isAdmin) {
102
+ * return json({ error: "Unauthorized" }, { status: 403 });
103
+ * }
104
+ * ```
105
+ */
106
+ async function isAdminUser({ request, cms, requiredRole = "admin" }) {
107
+ return ((await getAdminSession({
108
+ request,
109
+ cms
110
+ }))?.user)?.role === requiredRole;
111
+ }
112
+
113
+ //#endregion
114
+ //#region src/server/adapters/tanstack.ts
115
+ /**
116
+ * Create a TanStack Router beforeLoad guard for admin authentication.
117
+ *
118
+ * Returns a function that can be used as beforeLoad in route definitions.
119
+ * Throws a redirect Response when authentication fails.
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * import { createFileRoute } from "@tanstack/react-router";
124
+ * import { createTanStackAuthGuard } from "@questpie/admin/server/adapters/tanstack";
125
+ * import { cms } from "~/questpie/server/cms";
126
+ *
127
+ * export const Route = createFileRoute("/admin")({
128
+ * beforeLoad: createTanStackAuthGuard({
129
+ * cms,
130
+ * loginPath: "/admin/login",
131
+ * requiredRole: "admin",
132
+ * }),
133
+ * component: AdminLayout,
134
+ * });
135
+ * ```
136
+ *
137
+ * @example With custom context
138
+ * ```ts
139
+ * export const Route = createFileRoute("/admin")({
140
+ * beforeLoad: async (ctx) => {
141
+ * // Run auth guard
142
+ * await createTanStackAuthGuard({ cms })(ctx);
143
+ *
144
+ * // Add additional context
145
+ * return { user: ctx.context.user };
146
+ * },
147
+ * });
148
+ * ```
149
+ */
150
+ function createTanStackAuthGuard({ cms, loginPath = "/admin/login", requiredRole = "admin", redirectParam = "redirect" }) {
151
+ return async function beforeLoad({ context }) {
152
+ const request = context.request;
153
+ if (!request) {
154
+ console.warn("createTanStackAuthGuard: No request in context. Make sure you're using TanStack Start with SSR enabled.");
155
+ return;
156
+ }
157
+ const redirect = await requireAdminAuth({
158
+ request,
159
+ cms,
160
+ loginPath,
161
+ requiredRole,
162
+ redirectParam
163
+ });
164
+ if (redirect) throw redirect;
165
+ };
166
+ }
167
+ /**
168
+ * Create a TanStack Router loader that injects the admin session into context.
169
+ *
170
+ * Use this when you need access to the session in your components.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * import { createTanStackSessionLoader } from "@questpie/admin/server/adapters/tanstack";
175
+ *
176
+ * export const Route = createFileRoute("/admin")({
177
+ * loader: createTanStackSessionLoader({ cms }),
178
+ * component: AdminLayout,
179
+ * });
180
+ *
181
+ * function AdminLayout() {
182
+ * const { session } = Route.useLoaderData();
183
+ * return <div>Hello {session?.user?.name}</div>;
184
+ * }
185
+ * ```
186
+ */
187
+ function createTanStackSessionLoader({ cms }) {
188
+ return async function loader({ context }) {
189
+ const request = context.request;
190
+ if (!request || !cms.auth) return { session: null };
191
+ try {
192
+ return { session: await cms.auth.api.getSession({ headers: request.headers }) ?? null };
193
+ } catch {
194
+ return { session: null };
195
+ }
196
+ };
197
+ }
198
+
199
+ //#endregion
200
+ //#region src/server/adapters/nextjs.ts
201
+ /**
202
+ * Check if a path matches any of the given patterns
203
+ */
204
+ function pathMatches(pathname, patterns) {
205
+ return patterns.some((pattern) => pathname === pattern || pathname.startsWith(`${pattern}/`) || pathname.startsWith(pattern));
206
+ }
207
+ /**
208
+ * Create a Next.js middleware for admin authentication.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * // middleware.ts
213
+ * import { createNextAuthMiddleware } from "@questpie/admin/server/adapters/nextjs";
214
+ * import { cms } from "./questpie/server/cms";
215
+ *
216
+ * export default createNextAuthMiddleware({
217
+ * cms,
218
+ * loginPath: "/admin/login",
219
+ * });
220
+ *
221
+ * export const config = {
222
+ * matcher: ["/admin/:path*"],
223
+ * };
224
+ * ```
225
+ */
226
+ function createNextAuthMiddleware({ cms, loginPath = "/admin/login", requiredRole = "admin", protectedPaths = ["/admin"], publicPaths = [
227
+ "/admin/login",
228
+ "/admin/forgot-password",
229
+ "/admin/reset-password",
230
+ "/admin/accept-invite"
231
+ ], redirectParam = "redirect" }) {
232
+ return async function middleware(request) {
233
+ const pathname = new URL(request.url).pathname;
234
+ const isProtected = pathMatches(pathname, protectedPaths);
235
+ const isPublic = pathMatches(pathname, publicPaths);
236
+ if (!isProtected || isPublic) return new Response(null, {
237
+ status: 200,
238
+ headers: { "x-middleware-next": "1" }
239
+ });
240
+ const redirect = await requireAdminAuth({
241
+ request,
242
+ cms,
243
+ loginPath,
244
+ requiredRole,
245
+ redirectParam
246
+ });
247
+ if (redirect) return redirect;
248
+ return new Response(null, {
249
+ status: 200,
250
+ headers: { "x-middleware-next": "1" }
251
+ });
252
+ };
253
+ }
254
+ /**
255
+ * Get the admin session in a Next.js server component or API route.
256
+ *
257
+ * @example Server Component
258
+ * ```tsx
259
+ * // app/admin/layout.tsx
260
+ * import { getNextAdminSession } from "@questpie/admin/server/adapters/nextjs";
261
+ * import { headers } from "next/headers";
262
+ * import { cms } from "~/questpie/server/cms";
263
+ *
264
+ * export default async function AdminLayout({ children }) {
265
+ * const headersList = headers();
266
+ * const session = await getNextAdminSession({
267
+ * headers: headersList,
268
+ * cms,
269
+ * });
270
+ *
271
+ * if (!session) {
272
+ * redirect("/admin/login");
273
+ * }
274
+ *
275
+ * return <div>{children}</div>;
276
+ * }
277
+ * ```
278
+ *
279
+ * @example API Route
280
+ * ```ts
281
+ * // app/api/admin/users/route.ts
282
+ * import { getNextAdminSession } from "@questpie/admin/server/adapters/nextjs";
283
+ * import { cms } from "~/questpie/server/cms";
284
+ *
285
+ * export async function GET(request: Request) {
286
+ * const session = await getNextAdminSession({
287
+ * headers: request.headers,
288
+ * cms,
289
+ * });
290
+ *
291
+ * if (!session || session.user.role !== "admin") {
292
+ * return Response.json({ error: "Unauthorized" }, { status: 401 });
293
+ * }
294
+ *
295
+ * // ... handle request
296
+ * }
297
+ * ```
298
+ */
299
+ async function getNextAdminSession({ headers, cms }) {
300
+ return getAdminSession({
301
+ request: new Request("http://localhost", { headers }),
302
+ cms
303
+ });
304
+ }
305
+
306
+ //#endregion
307
+ //#region src/server/modules/admin-preferences/collections/admin-preferences.collection.ts
308
+ /**
309
+ * Admin Preferences Collection
310
+ *
311
+ * Stores user-specific preferences for admin UI state.
312
+ * This includes view configurations (columns, filters, sort)
313
+ * that persist across devices and sessions.
314
+ *
315
+ * Key format: "viewState:{collectionName}" for view state preferences
316
+ *
317
+ * @example
318
+ * ```ts
319
+ * import { adminModule } from "@questpie/admin/server";
320
+ *
321
+ * const cms = q({ name: "my-app" })
322
+ * .use(adminModule)
323
+ * .collections({ ... })
324
+ * .build({ ... });
325
+ *
326
+ * // Access preferences
327
+ * const prefs = await cms.api.collections.admin_preferences.findOne({
328
+ * where: { userId: currentUser.id, key: "viewState:posts" }
329
+ * });
330
+ * ```
331
+ */
332
+ const adminPreferencesCollection = q.collection("admin_preferences").fields({
333
+ userId: varchar("user_id", { length: 255 }).notNull(),
334
+ key: varchar("key", { length: 255 }).notNull(),
335
+ value: jsonb("value").notNull()
336
+ }).options({ timestamps: true }).indexes(({ table }) => [uniqueIndex("admin_preferences_user_key_idx").on(table.userId, table.key)]);
337
+
338
+ //#endregion
339
+ //#region src/server/modules/admin-preferences/collections/saved-views.collection.ts
340
+ /**
341
+ * Admin Saved Views Collection
342
+ *
343
+ * Stores user-specific view configurations for collection lists.
344
+ * Each view can contain:
345
+ * - Filter rules (field/operator/value combinations)
346
+ * - Sort configuration (field/direction)
347
+ * - Visible columns selection
348
+ *
349
+ * @example
350
+ * ```ts
351
+ * import { adminModule } from "@questpie/admin/server";
352
+ *
353
+ * const cms = q({ name: "my-app" })
354
+ * .use(starterModule)
355
+ * .use(adminModule)
356
+ * .collections({ ... })
357
+ * .build({ ... });
358
+ *
359
+ * // Access saved views
360
+ * const views = await cms.api.collections.adminSavedViews.find({
361
+ * where: { collectionName: "posts", userId: currentUser.id }
362
+ * });
363
+ * ```
364
+ */
365
+ const savedViewsCollection = q.collection("admin_saved_views").fields({
366
+ userId: varchar("user_id", { length: 255 }).notNull(),
367
+ collectionName: varchar("collection_name", { length: 255 }).notNull(),
368
+ name: varchar("name", { length: 255 }).notNull(),
369
+ configuration: jsonb("configuration").notNull().$type(),
370
+ isDefault: boolean("is_default").default(false).notNull()
371
+ }).options({ timestamps: true });
372
+
373
+ //#endregion
374
+ //#region src/server/modules/admin/functions/locales.ts
375
+ /**
376
+ * Content Locales Functions
377
+ *
378
+ * Functions for retrieving available content locales from the CMS configuration.
379
+ * Used by the admin panel to populate locale switchers and validate locale selection.
380
+ */
381
+ /**
382
+ * Helper to get typed CMS app from handler context.
383
+ */
384
+ function getApp$1(ctx) {
385
+ return ctx.app;
386
+ }
387
+ const getContentLocalesSchema = z.object({}).optional();
388
+ const getContentLocalesOutputSchema = z.object({
389
+ locales: z.array(z.object({
390
+ code: z.string(),
391
+ label: z.string().optional(),
392
+ fallback: z.boolean().optional(),
393
+ flagCountryCode: z.string().optional()
394
+ })),
395
+ defaultLocale: z.string(),
396
+ fallbacks: z.record(z.string(), z.string()).optional()
397
+ });
398
+ /**
399
+ * Get available content locales from CMS configuration.
400
+ *
401
+ * Returns the list of available locales for content localization,
402
+ * the default locale, and any fallback mappings.
403
+ *
404
+ * @example
405
+ * ```ts
406
+ * const result = await client.rpc.getContentLocales({});
407
+ * // {
408
+ * // locales: [
409
+ * // { code: "en", label: "English", fallback: true },
410
+ * // { code: "sk", label: "Slovenčina" },
411
+ * // ],
412
+ * // defaultLocale: "en",
413
+ * // fallbacks: { "en-GB": "en" },
414
+ * // }
415
+ * ```
416
+ */
417
+ const getContentLocales = fn({
418
+ type: "query",
419
+ schema: getContentLocalesSchema,
420
+ outputSchema: getContentLocalesOutputSchema,
421
+ handler: async (ctx) => {
422
+ const localeConfig = getApp$1(ctx).config.locale;
423
+ if (!localeConfig) return {
424
+ locales: [{
425
+ code: "en",
426
+ label: "English",
427
+ fallback: true
428
+ }],
429
+ defaultLocale: "en"
430
+ };
431
+ return {
432
+ locales: (typeof localeConfig.locales === "function" ? await localeConfig.locales() : localeConfig.locales).map((l) => ({
433
+ code: l.code,
434
+ label: l.label,
435
+ fallback: l.fallback,
436
+ flagCountryCode: l.flagCountryCode
437
+ })),
438
+ defaultLocale: localeConfig.defaultLocale,
439
+ fallbacks: localeConfig.fallbacks
440
+ };
441
+ }
442
+ });
443
+ /**
444
+ * Bundle of locale-related functions.
445
+ */
446
+ const localeFunctions = { getContentLocales };
447
+
448
+ //#endregion
449
+ //#region src/server/modules/admin/functions/preview.ts
450
+ /**
451
+ * Preview Functions - Server-side
452
+ *
453
+ * RPC functions for draft mode preview.
454
+ * Handles token minting for secure, shareable preview links.
455
+ *
456
+ * Browser-safe utilities (isDraftMode, createDraftModeCookie, etc.) are in @questpie/admin/shared
457
+ */
458
+ function base64UrlEncode(input) {
459
+ return (Buffer.isBuffer(input) ? input : Buffer.from(input)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
460
+ }
461
+ function base64UrlDecode(input) {
462
+ let base64 = input.replace(/-/g, "+").replace(/_/g, "/");
463
+ const padding = base64.length % 4;
464
+ if (padding) base64 += "=".repeat(4 - padding);
465
+ return Buffer.from(base64, "base64").toString("utf8");
466
+ }
467
+ const mintPreviewTokenSchema = z.object({
468
+ path: z.string().min(1, "Path is required"),
469
+ ttlMs: z.number().positive().optional()
470
+ });
471
+ const mintPreviewTokenOutputSchema = z.object({
472
+ token: z.string(),
473
+ expiresAt: z.number()
474
+ });
475
+ const verifyPreviewTokenSchema = z.object({ token: z.string() });
476
+ const verifyPreviewTokenOutputSchema = z.object({
477
+ valid: z.boolean(),
478
+ path: z.string().optional(),
479
+ error: z.string().optional()
480
+ });
481
+ const DEFAULT_TTL_MS = 3600 * 1e3;
482
+ /**
483
+ * Create preview-related RPC functions.
484
+ *
485
+ * @param secret - Secret key for signing tokens
486
+ * @returns Object with preview functions
487
+ */
488
+ function createPreviewFunctions(secret) {
489
+ const signPayload = (payload) => {
490
+ return base64UrlEncode(createHmac("sha256", secret).update(payload).digest());
491
+ };
492
+ return {
493
+ mintPreviewToken: fn({
494
+ type: "mutation",
495
+ schema: mintPreviewTokenSchema,
496
+ outputSchema: mintPreviewTokenOutputSchema,
497
+ handler: async ({ input, session }) => {
498
+ if (!session) throw new Error("Unauthorized: Admin session required");
499
+ const { path, ttlMs = DEFAULT_TTL_MS } = input;
500
+ const expiresAt = Date.now() + ttlMs;
501
+ const payload = {
502
+ path,
503
+ exp: expiresAt
504
+ };
505
+ const encodedPayload = base64UrlEncode(JSON.stringify(payload));
506
+ return {
507
+ token: `${encodedPayload}.${signPayload(encodedPayload)}`,
508
+ expiresAt
509
+ };
510
+ }
511
+ }),
512
+ verifyPreviewToken: fn({
513
+ type: "query",
514
+ schema: verifyPreviewTokenSchema,
515
+ outputSchema: verifyPreviewTokenOutputSchema,
516
+ handler: async ({ input }) => {
517
+ const { token } = input;
518
+ const [encodedPayload, signature] = token.split(".");
519
+ if (!encodedPayload || !signature) return {
520
+ valid: false,
521
+ error: "Invalid token format"
522
+ };
523
+ const expectedSignature = signPayload(encodedPayload);
524
+ const signatureBuffer = Uint8Array.from(Buffer.from(signature));
525
+ const expectedBuffer = Uint8Array.from(Buffer.from(expectedSignature));
526
+ if (signatureBuffer.length !== expectedBuffer.length) return {
527
+ valid: false,
528
+ error: "Invalid signature"
529
+ };
530
+ if (!timingSafeEqual(signatureBuffer, expectedBuffer)) return {
531
+ valid: false,
532
+ error: "Invalid signature"
533
+ };
534
+ try {
535
+ const payload = JSON.parse(base64UrlDecode(encodedPayload));
536
+ if (!payload?.exp || typeof payload.exp !== "number") return {
537
+ valid: false,
538
+ error: "Invalid payload"
539
+ };
540
+ if (payload.exp < Date.now()) return {
541
+ valid: false,
542
+ error: "Token expired"
543
+ };
544
+ if (!payload.path || typeof payload.path !== "string") return {
545
+ valid: false,
546
+ error: "Invalid path"
547
+ };
548
+ return {
549
+ valid: true,
550
+ path: payload.path
551
+ };
552
+ } catch {
553
+ return {
554
+ valid: false,
555
+ error: "Invalid payload"
556
+ };
557
+ }
558
+ }
559
+ })
560
+ };
561
+ }
562
+ /**
563
+ * Verify a preview token without RPC.
564
+ * Used directly in route handlers where RPC is not available.
565
+ *
566
+ * @param token - The preview token to verify
567
+ * @param secret - The secret used to sign the token
568
+ * @returns The payload if valid, null otherwise
569
+ */
570
+ function verifyPreviewTokenDirect(token, secret) {
571
+ const [encodedPayload, signature] = token.split(".");
572
+ if (!encodedPayload || !signature) return null;
573
+ const expectedSignature = base64UrlEncode(createHmac("sha256", secret).update(encodedPayload).digest());
574
+ const signatureBuffer = Uint8Array.from(Buffer.from(signature));
575
+ const expectedBuffer = Uint8Array.from(Buffer.from(expectedSignature));
576
+ if (signatureBuffer.length !== expectedBuffer.length) return null;
577
+ if (!timingSafeEqual(signatureBuffer, expectedBuffer)) return null;
578
+ try {
579
+ const payload = JSON.parse(base64UrlDecode(encodedPayload));
580
+ if (!payload?.exp || typeof payload.exp !== "number") return null;
581
+ if (payload.exp < Date.now()) return null;
582
+ if (!payload.path || typeof payload.path !== "string") return null;
583
+ return payload;
584
+ } catch {
585
+ return null;
586
+ }
587
+ }
588
+ /**
589
+ * Create a preview token verifier with bound secret.
590
+ * Use this in route handlers to avoid passing secret repeatedly.
591
+ *
592
+ * @param secret - The secret used to sign tokens (optional, uses env if not provided)
593
+ * @returns A verify function that only needs the token
594
+ *
595
+ * @example
596
+ * ```ts
597
+ * // Create once at module level
598
+ * const verifyPreviewToken = createPreviewTokenVerifier();
599
+ *
600
+ * // Use in route handler
601
+ * const payload = verifyPreviewToken(token);
602
+ * if (!payload) {
603
+ * return new Response("Invalid token", { status: 401 });
604
+ * }
605
+ * ```
606
+ */
607
+ function createPreviewTokenVerifier(secret) {
608
+ const resolvedSecret = secret ?? getPreviewSecret();
609
+ return (token) => {
610
+ return verifyPreviewTokenDirect(token, resolvedSecret);
611
+ };
612
+ }
613
+ /**
614
+ * Default preview functions bundle with env-based secret.
615
+ * Used by adminModule to register preview RPC functions.
616
+ */
617
+ const previewFunctions = createPreviewFunctions(getPreviewSecret());
618
+
619
+ //#endregion
620
+ //#region src/server/modules/admin/functions/setup.ts
621
+ /**
622
+ * Setup Functions
623
+ *
624
+ * Built-in functions for bootstrapping the first admin user.
625
+ * Solves the chicken-and-egg problem where invitation-based systems
626
+ * need an existing admin to create the first invitation.
627
+ */
628
+ /**
629
+ * Helper to get typed CMS app from handler context.
630
+ * Used internally for better IDE support without affecting the public API.
631
+ */
632
+ function getApp(ctx) {
633
+ return ctx.app;
634
+ }
635
+ const isSetupRequiredSchema = z.object({});
636
+ const isSetupRequiredOutputSchema = z.object({ required: z.boolean() });
637
+ const createFirstAdminSchema = z.object({
638
+ email: z.string().email("Invalid email address"),
639
+ password: z.string().min(8, "Password must be at least 8 characters"),
640
+ name: z.string().min(2, "Name must be at least 2 characters")
641
+ });
642
+ const createFirstAdminOutputSchema = z.object({
643
+ success: z.boolean(),
644
+ user: z.object({
645
+ id: z.string(),
646
+ email: z.string(),
647
+ name: z.string()
648
+ }).optional(),
649
+ error: z.string().optional()
650
+ });
651
+ /**
652
+ * Check if setup is required (no users exist in the system).
653
+ *
654
+ * @example
655
+ * ```ts
656
+ * const result = await client.rpc.isSetupRequired({});
657
+ * if (result.required) {
658
+ * // Redirect to setup page
659
+ * }
660
+ * ```
661
+ */
662
+ const isSetupRequired = fn({
663
+ type: "query",
664
+ schema: isSetupRequiredSchema,
665
+ outputSchema: isSetupRequiredOutputSchema,
666
+ handler: async (ctx) => {
667
+ const app = getApp(ctx);
668
+ const userCollection = app.getCollectionConfig("user");
669
+ return { required: (await app.db.select({ count: sql`count(*)::int` }).from(userCollection.table))[0].count === 0 };
670
+ }
671
+ });
672
+ /**
673
+ * Create the first admin user in the system.
674
+ * This function only works when no users exist (setup mode).
675
+ *
676
+ * Security: Once any user exists, this function will refuse to create more users.
677
+ * This prevents unauthorized admin creation after initial setup.
678
+ *
679
+ * @example
680
+ * ```ts
681
+ * const result = await client.rpc.createFirstAdmin({
682
+ * email: "admin@example.com",
683
+ * password: "securepassword123",
684
+ * name: "Admin User",
685
+ * });
686
+ *
687
+ * if (result.success) {
688
+ * // Redirect to login page
689
+ * } else {
690
+ * console.error(result.error);
691
+ * }
692
+ * ```
693
+ */
694
+ const createFirstAdmin = fn({
695
+ type: "mutation",
696
+ schema: createFirstAdminSchema,
697
+ outputSchema: createFirstAdminOutputSchema,
698
+ handler: async (ctx) => {
699
+ const app = getApp(ctx);
700
+ const input = ctx.input;
701
+ const userCollection = app.getCollectionConfig("user");
702
+ if ((await app.db.select({ count: sql`count(*)::int` }).from(userCollection.table))[0].count > 0) return {
703
+ success: false,
704
+ error: "Setup already completed - users exist in the system"
705
+ };
706
+ try {
707
+ const signUpResult = await app.auth.api.signUpEmail({ body: {
708
+ email: input.email,
709
+ password: input.password,
710
+ name: input.name
711
+ } });
712
+ if (!signUpResult.user) return {
713
+ success: false,
714
+ error: "Failed to create user account"
715
+ };
716
+ await app.db.update(userCollection.table).set({
717
+ role: "admin",
718
+ emailVerified: true
719
+ }).where(eq(userCollection.table.id, signUpResult.user.id));
720
+ return {
721
+ success: true,
722
+ user: {
723
+ id: signUpResult.user.id,
724
+ email: signUpResult.user.email,
725
+ name: signUpResult.user.name
726
+ }
727
+ };
728
+ } catch (error) {
729
+ return {
730
+ success: false,
731
+ error: error instanceof Error ? error.message : "An unexpected error occurred"
732
+ };
733
+ }
734
+ }
735
+ });
736
+ /**
737
+ * Bundle of setup-related functions.
738
+ */
739
+ const setupFunctions = {
740
+ isSetupRequired,
741
+ createFirstAdmin
742
+ };
743
+
744
+ //#endregion
745
+ //#region src/server/modules/admin/index.ts
746
+ /**
747
+ * Admin Module
748
+ *
749
+ * Complete backend module for running the QuestPie admin panel.
750
+ * This is the main entry point for setting up the admin backend.
751
+ *
752
+ * Includes:
753
+ * - Auth collections (users, sessions, accounts, verifications, apikeys)
754
+ * - Assets collection with file upload support
755
+ * - Admin saved views collection (named view configurations)
756
+ * - Admin preferences collection (user-specific view state)
757
+ * - Setup functions for bootstrapping first admin
758
+ * - Core auth options (Better Auth configuration)
759
+ *
760
+ * @example
761
+ * ```ts
762
+ * import { q } from "questpie";
763
+ * import { adminModule } from "@questpie/admin/server";
764
+ *
765
+ * const cms = q({ name: "my-app" })
766
+ * .use(adminModule)
767
+ * .collections({
768
+ * posts: postsCollection,
769
+ * })
770
+ * .build({
771
+ * db: { url: process.env.DATABASE_URL },
772
+ * storage: { driver: s3Driver(...) },
773
+ * });
774
+ * ```
775
+ */
776
+ /**
777
+ * Admin Module - the complete backend for QuestPie admin panel.
778
+ *
779
+ * This module provides everything needed to run the admin panel:
780
+ * - User authentication (Better Auth) - from starterModule
781
+ * - File uploads (assets) - from starterModule
782
+ * - Saved views for collection filters (named configurations)
783
+ * - User preferences (view state synced across devices)
784
+ * - Setup flow for first admin creation
785
+ *
786
+ * @example
787
+ * ```ts
788
+ * import { q } from "questpie";
789
+ * import { adminModule } from "@questpie/admin/server";
790
+ *
791
+ * const cms = q({ name: "my-app" })
792
+ * .use(adminModule)
793
+ * .collections({
794
+ * posts: postsCollection,
795
+ * })
796
+ * .build({
797
+ * db: { url: process.env.DATABASE_URL },
798
+ * });
799
+ * ```
800
+ *
801
+ * @example
802
+ * ```ts
803
+ * // Extend assets collection with custom fields
804
+ * import { q, collection, varchar } from "questpie";
805
+ * import { adminModule } from "@questpie/admin/server";
806
+ *
807
+ * const cms = q({ name: "my-app" })
808
+ * .use(adminModule)
809
+ * .collections({
810
+ * // Override assets with additional fields
811
+ * assets: adminModule.state.collections.assets.merge(
812
+ * collection("assets").fields({
813
+ * folder: varchar("folder", { length: 255 }),
814
+ * tags: varchar("tags", { length: 1000 }),
815
+ * })
816
+ * ),
817
+ * })
818
+ * .build({ ... });
819
+ * ```
820
+ */
821
+ const adminModule = q({ name: "questpie-admin" }).use(starterModule).collections({
822
+ admin_saved_views: savedViewsCollection,
823
+ admin_preferences: adminPreferencesCollection
824
+ }).functions({
825
+ ...setupFunctions,
826
+ ...localeFunctions,
827
+ ...previewFunctions
828
+ });
829
+
830
+ //#endregion
831
+ export { adminModule, createFirstAdmin, createNextAuthMiddleware, createPreviewFunctions, createPreviewTokenVerifier, createTanStackAuthGuard, createTanStackSessionLoader, getAdminSession, getNextAdminSession, isAdminUser, isSetupRequired, requireAdminAuth, savedViewsCollection, setupFunctions, verifyPreviewTokenDirect };
832
+ //# sourceMappingURL=server.mjs.map