create-mantiq 0.7.0 → 0.7.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.
Files changed (220) hide show
  1. package/package.json +2 -1
  2. package/skeleton/.env.example +64 -0
  3. package/skeleton/README.md +46 -0
  4. package/skeleton/app/Console/Commands/.gitkeep +0 -0
  5. package/skeleton/app/Enums/UserStatus.ts +7 -0
  6. package/skeleton/app/Http/Controllers/HomeController.ts +78 -0
  7. package/skeleton/app/Http/Middleware/.gitkeep +0 -0
  8. package/skeleton/app/Models/User.ts +7 -0
  9. package/skeleton/app/Providers/AppServiceProvider.ts +25 -0
  10. package/skeleton/app/Providers/DatabaseServiceProvider.ts +17 -0
  11. package/skeleton/bootstrap/.gitkeep +0 -0
  12. package/skeleton/config/ai.ts +51 -0
  13. package/skeleton/config/app.ts +108 -0
  14. package/skeleton/config/auth.ts +51 -0
  15. package/skeleton/config/broadcasting.ts +93 -0
  16. package/skeleton/config/cache.ts +61 -0
  17. package/skeleton/config/cors.ts +77 -0
  18. package/skeleton/config/database.ts +120 -0
  19. package/skeleton/config/filesystem.ts +58 -0
  20. package/skeleton/config/hashing.ts +47 -0
  21. package/skeleton/config/heartbeat.ts +112 -0
  22. package/skeleton/config/logging.ts +58 -0
  23. package/skeleton/config/mail.ts +93 -0
  24. package/skeleton/config/notify.ts +141 -0
  25. package/skeleton/config/queue.ts +59 -0
  26. package/skeleton/config/search.ts +96 -0
  27. package/skeleton/config/services.ts +110 -0
  28. package/skeleton/config/session.ts +84 -0
  29. package/skeleton/config/vite.ts +33 -0
  30. package/skeleton/database/factories/.gitkeep +0 -0
  31. package/skeleton/database/migrations/001_create_users_table.ts +19 -0
  32. package/skeleton/database/migrations/002_create_personal_access_tokens_table.ts +22 -0
  33. package/skeleton/database/seeders/DatabaseSeeder.ts +7 -0
  34. package/skeleton/index.ts +20 -0
  35. package/skeleton/mantiq.ts +8 -0
  36. package/skeleton/package.json +34 -0
  37. package/skeleton/public/.gitkeep +0 -0
  38. package/skeleton/routes/api.ts +8 -0
  39. package/skeleton/routes/channels.ts +23 -0
  40. package/skeleton/routes/console.ts +24 -0
  41. package/skeleton/routes/web.ts +6 -0
  42. package/skeleton/storage/cache/.gitkeep +0 -0
  43. package/skeleton/storage/framework/.gitkeep +0 -0
  44. package/skeleton/tests/feature/api.test.ts +14 -0
  45. package/skeleton/tests/feature/home.test.ts +17 -0
  46. package/skeleton/tests/unit/example.test.ts +11 -0
  47. package/skeleton/tsconfig.json +27 -0
  48. package/src/index.ts +289 -25
  49. package/src/templates.ts +141 -945
  50. package/src/terminal.ts +64 -0
  51. package/stubs/api-only/routes/api.ts.stub +24 -0
  52. package/stubs/api-only/tests/feature/token-auth.test.ts.stub +69 -0
  53. package/stubs/auth/api/app/Http/Controllers/ApiAuthController.ts.stub +57 -0
  54. package/stubs/auth/api/routes/api.ts.stub +24 -0
  55. package/stubs/auth/api/tests/feature/token-auth.test.ts.stub +69 -0
  56. package/stubs/auth/shared/app/Http/Requests/LoginRequest.ts.stub +10 -0
  57. package/stubs/auth/shared/app/Http/Requests/RegisterRequest.ts.stub +11 -0
  58. package/stubs/auth/web/app/Http/Controllers/AuthController.ts.stub +43 -0
  59. package/stubs/auth/web/app/Http/Controllers/PageController.ts.stub +66 -0
  60. package/stubs/auth/web/routes/web.ts.stub +25 -0
  61. package/stubs/auth/web/svelte/src/App.svelte.stub +77 -0
  62. package/stubs/auth/web/svelte/src/pages.ts.stub +17 -0
  63. package/stubs/auth/web/tests/feature/auth.test.ts.stub +69 -0
  64. package/stubs/auth/web/vue/src/App.vue.stub +74 -0
  65. package/stubs/auth/web/vue/src/pages.ts.stub +17 -0
  66. package/stubs/manifest.json +630 -2
  67. package/stubs/noauth/app/Http/Controllers/PageController.ts.stub +41 -0
  68. package/stubs/noauth/app/Models/User.ts.stub +5 -0
  69. package/stubs/noauth/database/migrations/001_create_users_table.ts.stub +17 -0
  70. package/stubs/noauth/routes/api.ts.stub +16 -0
  71. package/stubs/noauth/routes/web.ts.stub +15 -0
  72. package/stubs/noauth/svelte/src/App.svelte.stub +68 -0
  73. package/stubs/noauth/svelte/src/pages.ts.stub +7 -0
  74. package/stubs/noauth/vue/src/App.vue.stub +62 -0
  75. package/stubs/noauth/vue/src/pages.ts.stub +7 -0
  76. package/stubs/react/src/App.tsx.stub +4 -2
  77. package/stubs/react/src/components/layout/search-dialog.tsx.stub +2 -2
  78. package/stubs/react/src/components/layout/sidebar-data.ts.stub +2 -2
  79. package/stubs/react/src/lib/api.ts.stub +30 -6
  80. package/stubs/react/src/pages/Login.tsx.stub +3 -3
  81. package/stubs/react/src/pages/users/dialogs.tsx.stub +7 -26
  82. package/stubs/react/vite.config.ts.stub +26 -3
  83. package/stubs/shared/app/Http/Controllers/ApiAuthController.ts.stub +57 -0
  84. package/stubs/shared/app/Http/Controllers/AuthController.ts.stub +14 -38
  85. package/stubs/shared/app/Http/Controllers/PageController.ts.stub +3 -3
  86. package/stubs/shared/app/Http/Controllers/UserController.ts.stub +61 -0
  87. package/stubs/shared/app/Http/Requests/LoginRequest.ts.stub +10 -0
  88. package/stubs/shared/app/Http/Requests/RegisterRequest.ts.stub +11 -0
  89. package/stubs/shared/app/Http/Requests/StoreUserRequest.ts.stub +11 -0
  90. package/stubs/shared/app/Http/Requests/UpdateUserRequest.ts.stub +11 -0
  91. package/stubs/shared/config/app.ts.stub +36 -0
  92. package/stubs/shared/config/vite.ts.stub +8 -0
  93. package/stubs/shared/database/factories/UserFactory.ts.stub +4 -6
  94. package/stubs/shared/routes/api.ts.stub +12 -102
  95. package/stubs/shared/routes/web.ts.stub +5 -3
  96. package/stubs/shared/tests/feature/auth.test.ts.stub +69 -0
  97. package/stubs/shared/tests/feature/users.test.ts.stub +90 -0
  98. package/stubs/svelte/src/App.svelte.stub +1 -1
  99. package/stubs/svelte/src/lib/api.ts.stub +30 -6
  100. package/stubs/svelte/src/main.ts.stub +3 -1
  101. package/stubs/svelte/src/pages/Login.svelte.stub +3 -3
  102. package/stubs/svelte/vite.config.ts.stub +20 -1
  103. package/stubs/tailwind-only/react/src/components/layout/app-sidebar.tsx.stub +68 -0
  104. package/stubs/tailwind-only/react/src/components/layout/authenticated-layout.tsx.stub +57 -0
  105. package/stubs/tailwind-only/react/src/components/layout/header.tsx.stub +52 -0
  106. package/stubs/tailwind-only/react/src/components/layout/main.tsx.stub +21 -0
  107. package/stubs/tailwind-only/react/src/components/layout/nav-group.tsx.stub +185 -0
  108. package/stubs/tailwind-only/react/src/components/layout/nav-user.tsx.stub +106 -0
  109. package/stubs/tailwind-only/react/src/components/layout/sidebar-data.ts.stub +58 -0
  110. package/stubs/tailwind-only/react/src/components/layout/theme-toggle.tsx.stub +36 -0
  111. package/stubs/tailwind-only/react/src/components/layout/top-nav.tsx.stub +72 -0
  112. package/stubs/tailwind-only/react/src/lib/utils.ts.stub +6 -0
  113. package/stubs/tailwind-only/react/src/pages/Dashboard.tsx.stub +205 -0
  114. package/stubs/tailwind-only/react/src/pages/Login.tsx.stub +122 -0
  115. package/stubs/tailwind-only/react/src/pages/Register.tsx.stub +137 -0
  116. package/stubs/tailwind-only/react/src/pages/Users.tsx.stub +426 -0
  117. package/stubs/tailwind-only/react/src/pages/account/layout.tsx.stub +64 -0
  118. package/stubs/tailwind-only/react/src/pages/account/preferences.tsx.stub +80 -0
  119. package/stubs/tailwind-only/react/src/pages/account/profile.tsx.stub +67 -0
  120. package/stubs/tailwind-only/react/src/pages/account/security.tsx.stub +91 -0
  121. package/stubs/tailwind-only/react/src/style.css.stub +14 -0
  122. package/stubs/tailwind-only/svelte/src/lib/components/layout/app-sidebar.svelte.stub +104 -0
  123. package/stubs/tailwind-only/svelte/src/lib/components/layout/authenticated-layout.svelte.stub +51 -0
  124. package/stubs/tailwind-only/svelte/src/lib/components/layout/header.svelte.stub +66 -0
  125. package/stubs/tailwind-only/svelte/src/lib/components/layout/main.svelte.stub +26 -0
  126. package/stubs/tailwind-only/svelte/src/lib/components/layout/nav-group.svelte.stub +131 -0
  127. package/stubs/tailwind-only/svelte/src/lib/components/layout/nav-user.svelte.stub +104 -0
  128. package/stubs/tailwind-only/svelte/src/lib/components/layout/sidebar-data.ts.stub +57 -0
  129. package/stubs/tailwind-only/svelte/src/lib/components/layout/theme-toggle.svelte.stub +28 -0
  130. package/stubs/tailwind-only/svelte/src/lib/components/layout/top-nav.svelte.stub +99 -0
  131. package/stubs/tailwind-only/svelte/src/lib/utils.ts.stub +6 -0
  132. package/stubs/tailwind-only/svelte/src/pages/Dashboard.svelte.stub +192 -0
  133. package/stubs/tailwind-only/svelte/src/pages/Login.svelte.stub +120 -0
  134. package/stubs/tailwind-only/svelte/src/pages/Register.svelte.stub +134 -0
  135. package/stubs/tailwind-only/svelte/src/pages/Users.svelte.stub +293 -0
  136. package/stubs/tailwind-only/svelte/src/pages/account/Layout.svelte.stub +61 -0
  137. package/stubs/tailwind-only/svelte/src/pages/account/Preferences.svelte.stub +81 -0
  138. package/stubs/tailwind-only/svelte/src/pages/account/Profile.svelte.stub +76 -0
  139. package/stubs/tailwind-only/svelte/src/pages/account/Security.svelte.stub +140 -0
  140. package/stubs/tailwind-only/svelte/src/style.css.stub +127 -0
  141. package/stubs/tailwind-only/vue/src/components/layout/AppSidebar.vue.stub +60 -0
  142. package/stubs/tailwind-only/vue/src/components/layout/AuthenticatedLayout.vue.stub +73 -0
  143. package/stubs/tailwind-only/vue/src/components/layout/Header.vue.stub +54 -0
  144. package/stubs/tailwind-only/vue/src/components/layout/Main.vue.stub +22 -0
  145. package/stubs/tailwind-only/vue/src/components/layout/NavGroup.vue.stub +107 -0
  146. package/stubs/tailwind-only/vue/src/components/layout/NavUser.vue.stub +104 -0
  147. package/stubs/tailwind-only/vue/src/components/layout/ThemeToggle.vue.stub +28 -0
  148. package/stubs/tailwind-only/vue/src/components/layout/TopNav.vue.stub +86 -0
  149. package/stubs/tailwind-only/vue/src/components/layout/sidebar-data.ts.stub +57 -0
  150. package/stubs/tailwind-only/vue/src/lib/utils.ts.stub +7 -0
  151. package/stubs/tailwind-only/vue/src/pages/Dashboard.vue.stub +195 -0
  152. package/stubs/tailwind-only/vue/src/pages/Login.vue.stub +121 -0
  153. package/stubs/tailwind-only/vue/src/pages/Register.vue.stub +137 -0
  154. package/stubs/tailwind-only/vue/src/pages/Users.vue.stub +401 -0
  155. package/stubs/tailwind-only/vue/src/pages/account/Layout.vue.stub +56 -0
  156. package/stubs/tailwind-only/vue/src/pages/account/Preferences.vue.stub +81 -0
  157. package/stubs/tailwind-only/vue/src/pages/account/Profile.vue.stub +69 -0
  158. package/stubs/tailwind-only/vue/src/pages/account/Security.vue.stub +85 -0
  159. package/stubs/tailwind-only/vue/src/style.css.stub +26 -0
  160. package/stubs/themes/corporate/react/src/components/layout/app-sidebar.tsx.stub +74 -0
  161. package/stubs/themes/corporate/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
  162. package/stubs/themes/corporate/react/src/pages/Dashboard.tsx.stub +310 -0
  163. package/stubs/themes/corporate/react/src/pages/Login.tsx.stub +122 -0
  164. package/stubs/themes/corporate/react/src/pages/Register.tsx.stub +144 -0
  165. package/stubs/themes/corporate/react/src/style.css.stub +135 -0
  166. package/stubs/themes/corporate/svelte/src/pages/Dashboard.svelte.stub +271 -0
  167. package/stubs/themes/corporate/svelte/src/pages/Login.svelte.stub +117 -0
  168. package/stubs/themes/corporate/svelte/src/pages/Register.svelte.stub +138 -0
  169. package/stubs/themes/corporate/svelte/src/style.css.stub +134 -0
  170. package/stubs/themes/corporate/vue/src/pages/Dashboard.vue.stub +325 -0
  171. package/stubs/themes/corporate/vue/src/pages/Login.vue.stub +118 -0
  172. package/stubs/themes/corporate/vue/src/pages/Register.vue.stub +139 -0
  173. package/stubs/themes/corporate/vue/src/style.css.stub +134 -0
  174. package/stubs/themes/minimal/react/src/components/layout/app-sidebar.tsx.stub +68 -0
  175. package/stubs/themes/minimal/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
  176. package/stubs/themes/minimal/react/src/pages/Dashboard.tsx.stub +166 -0
  177. package/stubs/themes/minimal/react/src/pages/Login.tsx.stub +95 -0
  178. package/stubs/themes/minimal/react/src/pages/Register.tsx.stub +73 -0
  179. package/stubs/themes/minimal/react/src/style.css.stub +142 -0
  180. package/stubs/themes/minimal/svelte/src/pages/Dashboard.svelte.stub +149 -0
  181. package/stubs/themes/minimal/svelte/src/pages/Login.svelte.stub +90 -0
  182. package/stubs/themes/minimal/svelte/src/pages/Register.svelte.stub +70 -0
  183. package/stubs/themes/minimal/svelte/src/style.css.stub +142 -0
  184. package/stubs/themes/minimal/vue/src/pages/Dashboard.vue.stub +163 -0
  185. package/stubs/themes/minimal/vue/src/pages/Login.vue.stub +91 -0
  186. package/stubs/themes/minimal/vue/src/pages/Register.vue.stub +73 -0
  187. package/stubs/themes/minimal/vue/src/style.css.stub +142 -0
  188. package/stubs/themes/starter/react/src/components/layout/app-sidebar.tsx.stub +74 -0
  189. package/stubs/themes/starter/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
  190. package/stubs/themes/starter/react/src/pages/Dashboard.tsx.stub +236 -0
  191. package/stubs/themes/starter/react/src/pages/Login.tsx.stub +131 -0
  192. package/stubs/themes/starter/react/src/pages/Register.tsx.stub +145 -0
  193. package/stubs/themes/starter/react/src/style.css.stub +141 -0
  194. package/stubs/themes/starter/svelte/src/pages/Dashboard.svelte.stub +212 -0
  195. package/stubs/themes/starter/svelte/src/pages/Login.svelte.stub +126 -0
  196. package/stubs/themes/starter/svelte/src/pages/Register.svelte.stub +139 -0
  197. package/stubs/themes/starter/svelte/src/style.css.stub +141 -0
  198. package/stubs/themes/starter/vue/src/pages/Dashboard.vue.stub +228 -0
  199. package/stubs/themes/starter/vue/src/pages/Login.vue.stub +127 -0
  200. package/stubs/themes/starter/vue/src/pages/Register.vue.stub +140 -0
  201. package/stubs/themes/starter/vue/src/style.css.stub +141 -0
  202. package/stubs/themes/workspace/react/src/components/layout/app-sidebar.tsx.stub +97 -0
  203. package/stubs/themes/workspace/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
  204. package/stubs/themes/workspace/react/src/pages/Dashboard.tsx.stub +304 -0
  205. package/stubs/themes/workspace/react/src/pages/Login.tsx.stub +131 -0
  206. package/stubs/themes/workspace/react/src/pages/Register.tsx.stub +82 -0
  207. package/stubs/themes/workspace/react/src/style.css.stub +138 -0
  208. package/stubs/themes/workspace/svelte/src/pages/Dashboard.svelte.stub +215 -0
  209. package/stubs/themes/workspace/svelte/src/pages/Login.svelte.stub +124 -0
  210. package/stubs/themes/workspace/svelte/src/pages/Register.svelte.stub +76 -0
  211. package/stubs/themes/workspace/svelte/src/style.css.stub +134 -0
  212. package/stubs/themes/workspace/vue/src/pages/Dashboard.vue.stub +220 -0
  213. package/stubs/themes/workspace/vue/src/pages/Login.vue.stub +128 -0
  214. package/stubs/themes/workspace/vue/src/pages/Register.vue.stub +80 -0
  215. package/stubs/themes/workspace/vue/src/style.css.stub +134 -0
  216. package/stubs/vue/src/App.vue.stub +2 -1
  217. package/stubs/vue/src/lib/api.ts.stub +30 -6
  218. package/stubs/vue/src/main.ts.stub +3 -1
  219. package/stubs/vue/src/pages/Login.vue.stub +3 -3
  220. package/stubs/vue/vite.config.ts.stub +20 -1
@@ -0,0 +1,17 @@
1
+ import { Migration } from '@mantiq/database'
2
+ import type { SchemaBuilder } from '@mantiq/database'
3
+
4
+ export default class CreateUsersTable extends Migration {
5
+ override async up(schema: SchemaBuilder) {
6
+ await schema.create('users', (t) => {
7
+ t.id()
8
+ t.string('name', 100)
9
+ t.string('email', 150).unique()
10
+ t.timestamps()
11
+ })
12
+ }
13
+
14
+ override async down(schema: SchemaBuilder) {
15
+ await schema.dropIfExists('users')
16
+ }
17
+ }
@@ -0,0 +1,16 @@
1
+ import type { Router } from '@mantiq/core'
2
+ import { json } from '@mantiq/core'
3
+ import { UserController } from '../app/Http/Controllers/UserController.ts'
4
+ import { StoreUserRequest } from '../app/Http/Requests/StoreUserRequest.ts'
5
+ import { UpdateUserRequest } from '../app/Http/Requests/UpdateUserRequest.ts'
6
+
7
+ export default function (router: Router) {
8
+ // Public
9
+ router.get('/ping', () => json({ status: 'ok', timestamp: new Date().toISOString() }))
10
+
11
+ // Users CRUD — no auth middleware
12
+ router.get('/users', [UserController, 'index'])
13
+ router.post('/users', [UserController, 'store', StoreUserRequest])
14
+ router.put('/users/:id', [UserController, 'update', UpdateUserRequest])
15
+ router.delete('/users/:id', [UserController, 'destroy'])
16
+ }
@@ -0,0 +1,15 @@
1
+ import type { Router } from '@mantiq/core'
2
+ import { HomeController } from '../app/Http/Controllers/HomeController.ts'
3
+ import { PageController } from '../app/Http/Controllers/PageController.ts'
4
+
5
+ export default function (router: Router) {
6
+ router.get('/', [HomeController, 'index'])
7
+
8
+ router.get('/dashboard', [PageController, 'dashboard'])
9
+ router.get('/users', [PageController, 'users'])
10
+
11
+ // Account settings
12
+ router.get('/account/profile', [PageController, 'profile'])
13
+ router.get('/account/security', [PageController, 'security'])
14
+ router.get('/account/preferences', [PageController, 'preferences'])
15
+ }
@@ -0,0 +1,68 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy, setContext } from 'svelte'
3
+
4
+ let {
5
+ pages = {},
6
+ initialData = {},
7
+ }: {
8
+ pages?: Record<string, any>
9
+ initialData?: Record<string, any>
10
+ } = $props()
11
+
12
+ const windowData = typeof window !== 'undefined' ? (window as Record<string, any>).__MANTIQ_DATA__ ?? {} : {}
13
+ const bootstrapData = (() => initialData ?? windowData)()
14
+
15
+ let currentPage = $state<string>(bootstrapData._page ?? 'Dashboard')
16
+ let pageData = $state<Record<string, any>>(bootstrapData)
17
+
18
+ const PageComponent = $derived(pages[currentPage] ?? null)
19
+
20
+ // Initialize theme immediately to prevent flash
21
+ if (typeof window !== 'undefined') {
22
+ const theme = localStorage.getItem('theme') ||
23
+ (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
24
+ document.documentElement.classList.toggle('dark', theme === 'dark')
25
+ }
26
+
27
+ async function navigate(href: string) {
28
+ const res = await fetch(href, {
29
+ headers: { 'X-Mantiq': 'true', Accept: 'application/json' },
30
+ })
31
+
32
+ const newData = await res.json()
33
+ currentPage = newData._page
34
+ pageData = newData
35
+ history.pushState(null, '', newData._url)
36
+ }
37
+
38
+ setContext('navigate', navigate)
39
+
40
+ const spaRoutes = ['/dashboard', '/users']
41
+
42
+ function handleClick(e: MouseEvent) {
43
+ const anchor = (e.target as HTMLElement).closest('a')
44
+ const href = anchor?.getAttribute('href')
45
+ if (!href?.startsWith('/') || anchor?.target || e.ctrlKey || e.metaKey) return
46
+ if (!spaRoutes.some(r => href === r || href.startsWith(r + '?'))) return
47
+ e.preventDefault()
48
+ navigate(href)
49
+ }
50
+
51
+ function handlePop() { navigate(location.pathname) }
52
+
53
+ onMount(() => {
54
+ document.addEventListener('click', handleClick)
55
+ window.addEventListener('popstate', handlePop)
56
+ })
57
+
58
+ onDestroy(() => {
59
+ if (typeof window !== 'undefined') {
60
+ document.removeEventListener('click', handleClick)
61
+ window.removeEventListener('popstate', handlePop)
62
+ }
63
+ })
64
+ </script>
65
+
66
+ {#if PageComponent}
67
+ <PageComponent {...pageData} {navigate} />
68
+ {/if}
@@ -0,0 +1,7 @@
1
+ import Dashboard from './pages/Dashboard.svelte'
2
+ import Users from './pages/Users.svelte'
3
+
4
+ export const pages: Record<string, any> = {
5
+ Dashboard,
6
+ Users,
7
+ }
@@ -0,0 +1,62 @@
1
+ <script setup lang="ts">
2
+ import { ref, shallowRef, onMounted, onUnmounted, provide } from 'vue'
3
+
4
+ const props = defineProps<{
5
+ pages: Record<string, any>
6
+ initialData?: Record<string, any>
7
+ }>()
8
+
9
+ declare global { interface Window { __MANTIQ_DATA__?: Record<string, any> } }
10
+ const windowData = typeof window !== 'undefined' ? window.__MANTIQ_DATA__ ?? {} : {}
11
+ const initial = props.initialData ?? windowData
12
+
13
+ const currentPage = ref<string>(initial._page ?? 'Dashboard')
14
+ const pageData = ref<Record<string, any>>(initial)
15
+ const PageComponent = shallowRef(props.pages[currentPage.value] ?? null)
16
+
17
+ async function navigate(href: string) {
18
+ const res = await fetch(href, {
19
+ headers: { 'X-Mantiq': 'true', Accept: 'application/json' },
20
+ })
21
+
22
+ const newData = await res.json()
23
+ currentPage.value = newData._page
24
+ pageData.value = newData
25
+ PageComponent.value = props.pages[newData._page] ?? null
26
+ history.pushState(null, '', newData._url)
27
+ }
28
+
29
+ provide('navigate', navigate)
30
+
31
+ const spaRoutes = ['/dashboard', '/users']
32
+
33
+ function handleClick(e: MouseEvent) {
34
+ const anchor = (e.target as HTMLElement).closest('a')
35
+ const href = anchor?.getAttribute('href')
36
+ if (!href?.startsWith('/') || anchor?.target || e.ctrlKey || e.metaKey) return
37
+ if (!spaRoutes.some(r => href === r || href.startsWith(r + '?'))) return
38
+ e.preventDefault()
39
+ navigate(href)
40
+ }
41
+
42
+ function handlePop() { navigate(location.pathname + location.search) }
43
+
44
+ onMounted(() => {
45
+ document.addEventListener('click', handleClick)
46
+ window.addEventListener('popstate', handlePop)
47
+
48
+ // Initialize theme: localStorage > system preference > dark default
49
+ const theme = localStorage.getItem('theme') ||
50
+ (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
51
+ document.documentElement.classList.toggle('dark', theme === 'dark')
52
+ })
53
+
54
+ onUnmounted(() => {
55
+ document.removeEventListener('click', handleClick)
56
+ window.removeEventListener('popstate', handlePop)
57
+ })
58
+ </script>
59
+
60
+ <template>
61
+ <component :is="PageComponent" v-bind="pageData" :navigate="navigate" v-if="PageComponent" />
62
+ </template>
@@ -0,0 +1,7 @@
1
+ import Dashboard from './pages/Dashboard.vue'
2
+ import Users from './pages/Users.vue'
3
+
4
+ export const pages: Record<string, any> = {
5
+ Dashboard,
6
+ Users,
7
+ }
@@ -1,5 +1,7 @@
1
1
  import { useState, useCallback, useEffect } from 'react'
2
2
 
3
+ declare global { interface Window { __MANTIQ_DATA__?: Record<string, any> } }
4
+
3
5
  interface MantiqAppProps {
4
6
  pages: Record<string, React.ComponentType<any>>
5
7
  initialData?: Record<string, any>
@@ -15,8 +17,8 @@ function initTheme() {
15
17
  initTheme()
16
18
 
17
19
  export function MantiqApp({ pages, initialData }: MantiqAppProps) {
18
- const windowData = typeof window !== 'undefined' ? (window as any).__MANTIQ_DATA__ : {}
19
- const initial = initialData ?? windowData
20
+ const windowData = typeof window !== 'undefined' ? window.__MANTIQ_DATA__ : {}
21
+ const initial = initialData ?? windowData ?? {}
20
22
  const [page, setPage] = useState<string>(initial._page ?? 'Login')
21
23
  const [data, setData] = useState<Record<string, any>>(initial)
22
24
 
@@ -7,7 +7,7 @@ import {
7
7
  Lock,
8
8
  Palette,
9
9
  BookOpen,
10
- Github,
10
+ ExternalLink,
11
11
  FileText,
12
12
  ArrowRight,
13
13
  } from 'lucide-react'
@@ -30,7 +30,7 @@ const pages = [
30
30
  { title: 'Security', url: '/account/security', icon: Lock, group: 'Settings' },
31
31
  { title: 'Preferences', url: '/account/preferences', icon: Palette, group: 'Settings' },
32
32
  { title: 'Documentation', url: 'https://github.com/mantiqjs/mantiq#readme', icon: BookOpen, group: 'Links', external: true },
33
- { title: 'GitHub', url: 'https://github.com/mantiqjs/mantiq', icon: Github, group: 'Links', external: true },
33
+ { title: 'GitHub', url: 'https://github.com/mantiqjs/mantiq', icon: ExternalLink, group: 'Links', external: true },
34
34
  ]
35
35
 
36
36
  export function SearchDialog({ open, onOpenChange, navigate }: SearchDialogProps) {
@@ -6,7 +6,7 @@ import {
6
6
  Lock,
7
7
  Palette,
8
8
  BookOpen,
9
- Github,
9
+ ExternalLink,
10
10
  type LucideIcon,
11
11
  } from 'lucide-react'
12
12
 
@@ -36,7 +36,7 @@ export const sidebarData: NavGroup[] = [
36
36
  title: 'Documentation',
37
37
  items: [
38
38
  { title: 'Docs', url: 'https://github.com/mantiqjs/mantiq#readme', icon: BookOpen, external: true },
39
- { title: 'GitHub', url: 'https://github.com/mantiqjs/mantiq', icon: Github, external: true },
39
+ { title: 'GitHub', url: 'https://github.com/mantiqjs/mantiq', icon: ExternalLink, external: true },
40
40
  ],
41
41
  },
42
42
  {
@@ -1,17 +1,41 @@
1
- export async function api<T = any>(url: string, opts: RequestInit = {}): Promise<{ ok: boolean; status: number; data: T }> {
2
- const res = await fetch(url, { ...opts, headers: { Accept: 'application/json', ...opts.headers } })
1
+ function getCookie(name: string): string | null {
2
+ const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))
3
+ return match ? decodeURIComponent(match[1]!) : null
4
+ }
5
+
6
+ type ApiResult<T> = { ok: true; status: number; data: T } | { ok: false; status: number; data: any }
7
+
8
+ export async function api<T = any>(url: string, opts: RequestInit = {}): Promise<ApiResult<T>> {
9
+ const headers: Record<string, string> = { Accept: 'application/json', ...(opts.headers as Record<string, string>) }
10
+
11
+ // Attach XSRF token for CSRF protection on mutating requests
12
+ if (opts.method && !['GET', 'HEAD', 'OPTIONS'].includes(opts.method)) {
13
+ const xsrf = getCookie('XSRF-TOKEN')
14
+ if (xsrf) headers['X-XSRF-TOKEN'] = xsrf
15
+ }
16
+
17
+ const res = await fetch(url, { ...opts, credentials: 'same-origin', headers })
3
18
 
4
19
  // Session expired — redirect to login
5
20
  if (res.status === 401 || res.status === 419) {
6
21
  window.location.href = '/login'
7
- return { ok: false, status: res.status, data: null as any }
22
+ return { ok: false, status: res.status, data: null }
8
23
  }
9
24
 
10
25
  const ct = res.headers.get('content-type') ?? ''
11
26
  const data = ct.includes('json') ? await res.json() : null
12
- return { ok: res.ok, status: res.status, data }
27
+ if (!res.ok) return { ok: false, status: res.status, data }
28
+ return { ok: true, status: res.status, data }
29
+ }
30
+
31
+ export function post<T = any>(url: string, body: object) {
32
+ return api<T>(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
33
+ }
34
+
35
+ export function put<T = any>(url: string, body: object) {
36
+ return api<T>(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
13
37
  }
14
38
 
15
- export function post(url: string, body: object) {
16
- return api(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
39
+ export function del<T = any>(url: string) {
40
+ return api<T>(url, { method: 'DELETE' })
17
41
  }
@@ -11,8 +11,8 @@ interface LoginProps {
11
11
  }
12
12
 
13
13
  export default function Login({ appName = 'Mantiq', navigate }: LoginProps) {
14
- const [email, setEmail] = useState('admin@example.com')
15
- const [password, setPassword] = useState('password')
14
+ const [email, setEmail] = useState('')
15
+ const [password, setPassword] = useState('')
16
16
  const [error, setError] = useState('')
17
17
  const [loading, setLoading] = useState(false)
18
18
 
@@ -81,7 +81,7 @@ export default function Login({ appName = 'Mantiq', navigate }: LoginProps) {
81
81
  value={email}
82
82
  onChange={(e) => setEmail(e.target.value)}
83
83
  required
84
- placeholder="admin@example.com"
84
+ placeholder="you@example.com"
85
85
  autoComplete="email"
86
86
  />
87
87
  </div>
@@ -1,4 +1,5 @@
1
1
  import { useState, useEffect } from 'react'
2
+ import { post, put, del } from '@/lib/api'
2
3
  import {
3
4
  Dialog,
4
5
  DialogContent,
@@ -50,15 +51,8 @@ export function AddUserDialog({
50
51
  setSubmitting(true)
51
52
  setError('')
52
53
  try {
53
- const res = await fetch('/api/users', {
54
- method: 'POST',
55
- headers: { 'Content-Type': 'application/json' },
56
- body: JSON.stringify(form),
57
- })
58
- if (!res.ok) {
59
- const data = await res.json().catch(() => null)
60
- throw new Error(data?.message ?? `Request failed (${res.status})`)
61
- }
54
+ const { ok, data } = await post('/api/users', form)
55
+ if (!ok) throw new Error(data?.error ?? 'Request failed')
62
56
  reset()
63
57
  onOpenChange(false)
64
58
  onSuccess()
@@ -165,15 +159,8 @@ export function EditUserDialog({
165
159
  setSubmitting(true)
166
160
  setError('')
167
161
  try {
168
- const res = await fetch(`/api/users/${user.id}`, {
169
- method: 'PUT',
170
- headers: { 'Content-Type': 'application/json' },
171
- body: JSON.stringify(form),
172
- })
173
- if (!res.ok) {
174
- const data = await res.json().catch(() => null)
175
- throw new Error(data?.message ?? `Request failed (${res.status})`)
176
- }
162
+ const { ok, data } = await put(`/api/users/${user.id}`, form)
163
+ if (!ok) throw new Error(data?.error ?? 'Request failed')
177
164
  onOpenChange(false)
178
165
  onSuccess()
179
166
  } catch (err: any) {
@@ -252,14 +239,8 @@ export function DeleteUserDialog({
252
239
  setSubmitting(true)
253
240
  setError('')
254
241
  try {
255
- const res = await fetch(`/api/users/${user.id}`, {
256
- method: 'DELETE',
257
- headers: { 'Content-Type': 'application/json' },
258
- })
259
- if (!res.ok) {
260
- const data = await res.json().catch(() => null)
261
- throw new Error(data?.message ?? `Request failed (${res.status})`)
262
- }
242
+ const { ok, data } = await del(`/api/users/${user.id}`)
243
+ if (!ok) throw new Error(data?.error ?? 'Request failed')
263
244
  onOpenChange(false)
264
245
  onSuccess()
265
246
  } catch (err: any) {
@@ -1,10 +1,33 @@
1
1
  import { defineConfig } from 'vite'
2
2
  import react from '@vitejs/plugin-react'
3
3
  import tailwindcss from '@tailwindcss/vite'
4
+ import { mantiq } from '@mantiq/vite'
4
5
  import path from 'path'
6
+ import { writeFileSync, unlinkSync } from 'node:fs'
5
7
 
6
- export default defineConfig({
7
- plugins: [react(), tailwindcss()],
8
+ export default defineConfig(async () => ({
9
+ plugins: [
10
+ react(),
11
+ tailwindcss(),
12
+ // Auto-discovers @mantiq/* plugins (Studio admin panel, etc.)
13
+ ...await mantiq(),
14
+ // Write public/hot so the backend knows the dev server is running
15
+ {
16
+ name: 'mantiq-hot',
17
+ configureServer(server) {
18
+ const hotPath = path.resolve(__dirname, 'public/hot')
19
+ server.httpServer?.once('listening', () => {
20
+ const addr = server.httpServer!.address()
21
+ const url = typeof addr === 'string' ? addr : `http://localhost:${addr?.port}`
22
+ writeFileSync(hotPath, url)
23
+ })
24
+ const cleanup = () => { try { unlinkSync(hotPath) } catch {} }
25
+ process.on('exit', cleanup)
26
+ process.on('SIGINT', () => { cleanup(); process.exit() })
27
+ process.on('SIGTERM', () => { cleanup(); process.exit() })
28
+ },
29
+ },
30
+ ],
8
31
  publicDir: false,
9
32
  resolve: {
10
33
  alias: {
@@ -19,4 +42,4 @@ export default defineConfig({
19
42
  input: ['src/main.tsx', 'src/style.css'],
20
43
  },
21
44
  },
22
- })
45
+ }))
@@ -0,0 +1,57 @@
1
+ import type { MantiqRequest } from '@mantiq/core'
2
+ import { json, hash, hashCheck, abort } from '@mantiq/core'
3
+ import { auth } from '@mantiq/auth'
4
+ import { User } from '../../Models/User.ts'
5
+
6
+ /**
7
+ * Sanctum-style token authentication for API-only apps.
8
+ * Issues bearer tokens instead of session cookies.
9
+ */
10
+ export class ApiAuthController {
11
+ async register(request: MantiqRequest, data: Record<string, any>): Promise<Response> {
12
+ if (await User.where('email', data.email).first()) abort(422, 'A user with this email already exists.')
13
+
14
+ const user = await User.create({
15
+ name: data.name,
16
+ email: data.email,
17
+ password: await hash(data.password),
18
+ })
19
+
20
+ const { plainTextToken } = await user.createToken(data.device_name ?? 'api')
21
+
22
+ return json({ message: 'Registered.', user: user.toObject(), token: plainTextToken }, 201)
23
+ }
24
+
25
+ async login(request: MantiqRequest, data: Record<string, any>): Promise<Response> {
26
+ const user = await User.where('email', data.email).first()
27
+ if (!user || !(await hashCheck(data.password, user.getAuthPassword()))) {
28
+ abort(401, 'Invalid credentials.')
29
+ }
30
+
31
+ const { plainTextToken } = await user!.createToken(data.device_name ?? 'api')
32
+
33
+ return json({ message: 'Logged in.', user: user!.toObject(), token: plainTextToken })
34
+ }
35
+
36
+ async logout(request: MantiqRequest): Promise<Response> {
37
+ const manager = auth()
38
+ manager.setRequest(request)
39
+ const user = await manager.guard('api').user()
40
+
41
+ if (user) {
42
+ const token = user.currentAccessToken?.()
43
+ if (token) await token.delete()
44
+ }
45
+
46
+ return json({ message: 'Token revoked.' })
47
+ }
48
+
49
+ async user(request: MantiqRequest): Promise<Response> {
50
+ const manager = auth()
51
+ manager.setRequest(request)
52
+ const user = await manager.guard('api').user()
53
+ if (!user) abort(401, 'Unauthenticated.')
54
+
55
+ return json({ user: user!.toObject() })
56
+ }
57
+ }
@@ -1,67 +1,43 @@
1
1
  import type { MantiqRequest } from '@mantiq/core'
2
- import { MantiqResponse, HashManager } from '@mantiq/core'
2
+ import { json, hash, abort } from '@mantiq/core'
3
3
  import { auth } from '@mantiq/auth'
4
4
  import { User } from '../../Models/User.ts'
5
5
 
6
6
  export class AuthController {
7
- async register(request: MantiqRequest): Promise<Response> {
8
- const body = await request.input() as { name?: string; email?: string; password?: string }
9
-
10
- if (!body.name || !body.email || !body.password) {
11
- return MantiqResponse.json({ error: 'Name, email and password are required.' }, 422)
12
- }
13
- if (body.password.length < 6) {
14
- return MantiqResponse.json({ error: 'Password must be at least 6 characters.' }, 422)
15
- }
16
-
17
- const existing = await User.where('email', body.email).first()
18
- if (existing) {
19
- return MantiqResponse.json({ error: 'A user with this email already exists.' }, 422)
20
- }
21
-
22
- const hasher = new HashManager({ bcrypt: { rounds: 10 } })
23
- const hashed = await hasher.make(body.password)
7
+ async register(request: MantiqRequest, data: Record<string, any>): Promise<Response> {
8
+ if (await User.where('email', data.email).first()) abort(422, 'A user with this email already exists.')
24
9
 
25
10
  const user = await User.create({
26
- name: body.name,
27
- email: body.email,
28
- password: hashed,
11
+ name: data.name,
12
+ email: data.email,
13
+ password: await hash(data.password),
29
14
  })
30
15
 
31
16
  const manager = auth()
32
17
  manager.setRequest(request)
33
- await manager.login(user as any)
18
+ await manager.login(user)
34
19
 
35
- return MantiqResponse.json({ message: 'Registered.', user: user.toObject() }, 201)
20
+ return json({ message: 'Registered.', user: user.toObject() }, 201)
36
21
  }
37
22
 
38
- async login(request: MantiqRequest): Promise<Response> {
39
- const body = await request.input() as { email?: string; password?: string; remember?: boolean }
40
-
41
- if (!body.email || !body.password) {
42
- return MantiqResponse.json({ error: 'Email and password are required.' }, 422)
43
- }
44
-
23
+ async login(request: MantiqRequest, data: Record<string, any>): Promise<Response> {
45
24
  const manager = auth()
46
25
  manager.setRequest(request)
47
26
 
48
27
  const success = await manager.attempt(
49
- { email: body.email, password: body.password },
50
- body.remember ?? false,
28
+ { email: data.email, password: data.password },
29
+ data.remember ?? false,
51
30
  )
52
-
53
- if (!success) {
54
- return MantiqResponse.json({ error: 'Invalid credentials.' }, 401)
55
- }
31
+ if (!success) abort(401, 'Invalid credentials.')
56
32
 
57
33
  const user = await manager.user()
58
- return MantiqResponse.json({ message: 'Logged in.', user })
34
+ return json({ message: 'Logged in.', user: user?.toObject() })
59
35
  }
60
36
 
61
37
  async logout(request: MantiqRequest): Promise<Response> {
62
38
  const manager = auth()
63
39
  manager.setRequest(request)
64
40
  await manager.logout()
65
- return MantiqResponse.json({ message: 'Logged out.' })
41
+ return json({ message: 'Logged out.' })
66
42
  }
67
43
  }
@@ -10,9 +10,9 @@ async function getUser(request: MantiqRequest) {
10
10
  const user = await manager.user()
11
11
  if (!user) return null
12
12
  return {
13
- id: (user as any).getAttribute?.('id') ?? user.getAuthIdentifier(),
14
- name: (user as any).getAttribute?.('name') ?? '',
15
- email: (user as any).getAttribute?.('email') ?? '',
13
+ id: user.getAttribute('id') ?? user.getAuthIdentifier(),
14
+ name: user.getAttribute('name') ?? '',
15
+ email: user.getAttribute('email') ?? '',
16
16
  }
17
17
  }
18
18
 
@@ -0,0 +1,61 @@
1
+ import type { MantiqRequest } from '@mantiq/core'
2
+ import { json, abort, hash } from '@mantiq/core'
3
+ import { User } from '../../Models/User.ts'
4
+
5
+ export class UserController {
6
+ async index(request: MantiqRequest): Promise<Response> {
7
+ const search = request.query('search') ?? ''
8
+ const page = Math.max(1, Number(request.query('page') ?? 1))
9
+ const perPage = Math.min(100, Math.max(1, Number(request.query('per_page') ?? 10)))
10
+ const sortBy = request.query('sort') ?? 'created_at'
11
+ const sortDir = request.query('dir') === 'asc' ? 'asc' : 'desc'
12
+
13
+ const baseQuery = () => search
14
+ ? User.where('name', 'LIKE', `%${search}%`).orWhere('email', 'LIKE', `%${search}%`)
15
+ : User.query()
16
+
17
+ const total = await User.count()
18
+ const filteredTotal = await baseQuery().count()
19
+ const users = await baseQuery()
20
+ .orderBy(sortBy, sortDir)
21
+ .limit(perPage)
22
+ .offset((page - 1) * perPage)
23
+ .get()
24
+
25
+ return json({
26
+ data: users.map(u => u.toObject()),
27
+ meta: { total, filtered_total: filteredTotal, page, per_page: perPage, last_page: Math.ceil(filteredTotal / perPage) },
28
+ })
29
+ }
30
+
31
+ async store(request: MantiqRequest, data: Record<string, any>): Promise<Response> {
32
+ if (await User.where('email', data.email).first()) abort(422, 'Email already exists.')
33
+
34
+ const user = await User.create({
35
+ name: data.name,
36
+ email: data.email,
37
+ password: await hash(data.password),
38
+ })
39
+
40
+ return json({ data: user.toObject() }, 201)
41
+ }
42
+
43
+ async update(request: MantiqRequest, data: Record<string, any>): Promise<Response> {
44
+ const user = await User.find(Number(request.param('id')))
45
+ if (!user) abort(404, 'User not found.')
46
+ if (data.name) user.setAttribute('name', data.name)
47
+ if (data.email) user.setAttribute('email', data.email)
48
+ if (data.password) user.setAttribute('password', await hash(data.password))
49
+
50
+ await user.save()
51
+ return json({ data: user.toObject() })
52
+ }
53
+
54
+ async destroy(request: MantiqRequest): Promise<Response> {
55
+ const user = await User.find(Number(request.param('id')))
56
+ if (!user) abort(404, 'User not found.')
57
+
58
+ await user.delete()
59
+ return json({ success: true })
60
+ }
61
+ }
@@ -0,0 +1,10 @@
1
+ import { FormRequest } from '@mantiq/validation'
2
+
3
+ export class LoginRequest extends FormRequest {
4
+ override rules() {
5
+ return {
6
+ email: 'required|email',
7
+ password: 'required|string',
8
+ }
9
+ }
10
+ }