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
package/src/terminal.ts CHANGED
@@ -33,6 +33,8 @@ export class Terminal {
33
33
  write(` ${EMERALD}●${R} ${BOLD}mantiq${R} ${GRAY}│${R} ${DIM}The Bun framework for artisans${R}\n`)
34
34
  write(`\n`)
35
35
  write(` ${GRAY}───────────────────────────────────────────────${R}\n`)
36
+ const version = require('../package.json').version
37
+ write(` ${DIM}v${version}${R}\n`)
36
38
  write('\n\n')
37
39
  }
38
40
 
@@ -94,6 +96,68 @@ export class Terminal {
94
96
  })
95
97
  }
96
98
 
99
+ /** Checkbox-style multi-select prompt */
100
+ async multiSelect(label: string, options: SelectOption[]): Promise<string[]> {
101
+ let cursor = 0
102
+ const checked = new Set<number>()
103
+
104
+ const render = () => {
105
+ let out = ` ${EMERALD}◆${R} ${BOLD}${label}${R}\n`
106
+ for (let i = 0; i < options.length; i++) {
107
+ const opt = options[i]!
108
+ const active = i === cursor
109
+ const isChecked = checked.has(i)
110
+ const box = isChecked ? `${EMERALD}\u2611${R}` : `${GRAY}\u2610${R}`
111
+ const text = active ? `${WHITE}${BOLD}${opt.label}${R}` : `${GRAY}${opt.label}${R}`
112
+ const hint = opt.hint ? ` ${DIM}${opt.hint}${R}` : ''
113
+ out += ` ${box} ${text}${hint}\n`
114
+ }
115
+ out += `\n`
116
+ return out
117
+ }
118
+
119
+ const lines = options.length + 2
120
+ write(render())
121
+
122
+ if (process.stdin.isTTY) process.stdin.setRawMode(true)
123
+ write(HIDE_CURSOR)
124
+
125
+ return new Promise<string[]>((resolve) => {
126
+ const onData = (buf: Buffer) => {
127
+ const key = buf.toString()
128
+ if (key === '\x1b[A' || key === 'k') {
129
+ cursor = (cursor - 1 + options.length) % options.length
130
+ } else if (key === '\x1b[B' || key === 'j') {
131
+ cursor = (cursor + 1) % options.length
132
+ } else if (key === ' ') {
133
+ if (checked.has(cursor)) checked.delete(cursor)
134
+ else checked.add(cursor)
135
+ } else if (key === '\r' || key === '\n') {
136
+ process.stdin.removeListener('data', onData)
137
+ if (process.stdin.isTTY) process.stdin.setRawMode(false)
138
+ write(SHOW_CURSOR)
139
+ write(UP(lines) + CLEAR_LINE)
140
+ for (let i = 0; i < lines; i++) write(`${CLEAR_LINE}\n`)
141
+ write(UP(lines))
142
+ const selected = [...checked].sort().map(i => options[i]!.label)
143
+ const summary = selected.length > 0 ? selected.join(', ') : 'None'
144
+ write(` ${EMERALD}\u25C7${R} ${label} ${EMERALD}${summary}${R}\n\n`)
145
+ resolve([...checked].sort().map(i => options[i]!.value))
146
+ return
147
+ } else if (key === '\x03') {
148
+ process.stdin.removeListener('data', onData)
149
+ if (process.stdin.isTTY) process.stdin.setRawMode(false)
150
+ write(SHOW_CURSOR + '\n')
151
+ process.exit(0)
152
+ }
153
+ write(UP(lines))
154
+ write(render())
155
+ }
156
+ process.stdin.on('data', onData)
157
+ process.stdin.resume()
158
+ })
159
+ }
160
+
97
161
  /** Yes/No confirm prompt */
98
162
  async confirm(label: string, defaultVal = false): Promise<boolean> {
99
163
  const options: SelectOption[] = [
@@ -0,0 +1,24 @@
1
+ import type { Router } from '@mantiq/core'
2
+ import { json } from '@mantiq/core'
3
+ import { ApiAuthController } from '../app/Http/Controllers/ApiAuthController.ts'
4
+ import { UserController } from '../app/Http/Controllers/UserController.ts'
5
+ import { RegisterRequest } from '../app/Http/Requests/RegisterRequest.ts'
6
+ import { LoginRequest } from '../app/Http/Requests/LoginRequest.ts'
7
+ import { StoreUserRequest } from '../app/Http/Requests/StoreUserRequest.ts'
8
+ import { UpdateUserRequest } from '../app/Http/Requests/UpdateUserRequest.ts'
9
+
10
+ export default function (router: Router) {
11
+ router.get('/ping', () => json({ status: 'ok', timestamp: new Date().toISOString() }))
12
+
13
+ // Token auth — FormRequest auto-validates, controller receives validated data
14
+ router.post('/register', [ApiAuthController, 'register', RegisterRequest])
15
+ router.post('/login', [ApiAuthController, 'login', LoginRequest])
16
+ router.post('/logout', [ApiAuthController, 'logout']).middleware('auth:api')
17
+ router.get('/user', [ApiAuthController, 'user']).middleware('auth:api')
18
+
19
+ // Users CRUD (protected)
20
+ router.get('/users', [UserController, 'index']).middleware('auth:api')
21
+ router.post('/users', [UserController, 'store', StoreUserRequest]).middleware('auth:api')
22
+ router.put('/users/:id', [UserController, 'update', UpdateUserRequest]).middleware('auth:api')
23
+ router.delete('/users/:id', [UserController, 'destroy']).middleware('auth:api')
24
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { TestCase } from '@mantiq/testing'
3
+
4
+ const t = new TestCase()
5
+ t.refreshDatabase = true
6
+ t.setup()
7
+
8
+ const user = {
9
+ name: 'API User',
10
+ email: 'api@example.com',
11
+ password: 'password123',
12
+ }
13
+
14
+ describe('Token Authentication', () => {
15
+ test('can register and receive a token', async () => {
16
+ const res = await t.client.post('/api/register', user)
17
+ res.assertCreated()
18
+ await res.assertJsonHasKey('token')
19
+ const data = await res.json()
20
+ expect(data.token).toContain('|')
21
+ })
22
+
23
+ test('can login and receive a token', async () => {
24
+ await t.client.post('/api/register', user)
25
+ const res = await t.client.post('/api/login', {
26
+ email: user.email,
27
+ password: user.password,
28
+ })
29
+ res.assertOk()
30
+ await res.assertJsonHasKey('token')
31
+ })
32
+
33
+ test('cannot login with wrong credentials', async () => {
34
+ await t.client.post('/api/register', user)
35
+ const res = await t.client.post('/api/login', {
36
+ email: user.email,
37
+ password: 'wrong',
38
+ })
39
+ res.assertUnauthorized()
40
+ })
41
+
42
+ test('can access protected route with bearer token', async () => {
43
+ const regRes = await t.client.post('/api/register', user)
44
+ const { token } = await regRes.json()
45
+
46
+ t.client.withToken(token)
47
+ const res = await t.client.get('/api/user')
48
+ res.assertOk()
49
+ await res.assertJsonPath('user.email', user.email)
50
+ })
51
+
52
+ test('cannot access protected route without token', async () => {
53
+ const res = await t.client.get('/api/user')
54
+ res.assertUnauthorized()
55
+ })
56
+
57
+ test('can logout (revoke token)', async () => {
58
+ const regRes = await t.client.post('/api/register', user)
59
+ const { token } = await regRes.json()
60
+
61
+ t.client.withToken(token)
62
+ const logoutRes = await t.client.post('/api/logout')
63
+ logoutRes.assertOk()
64
+
65
+ // Token should be revoked
66
+ const userRes = await t.client.get('/api/user')
67
+ userRes.assertUnauthorized()
68
+ })
69
+ })
@@ -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
+ }
@@ -0,0 +1,24 @@
1
+ import type { Router } from '@mantiq/core'
2
+ import { json } from '@mantiq/core'
3
+ import { ApiAuthController } from '../app/Http/Controllers/ApiAuthController.ts'
4
+ import { UserController } from '../app/Http/Controllers/UserController.ts'
5
+ import { RegisterRequest } from '../app/Http/Requests/RegisterRequest.ts'
6
+ import { LoginRequest } from '../app/Http/Requests/LoginRequest.ts'
7
+ import { StoreUserRequest } from '../app/Http/Requests/StoreUserRequest.ts'
8
+ import { UpdateUserRequest } from '../app/Http/Requests/UpdateUserRequest.ts'
9
+
10
+ export default function (router: Router) {
11
+ router.get('/ping', () => json({ status: 'ok', timestamp: new Date().toISOString() }))
12
+
13
+ // Token auth — FormRequest auto-validates, controller receives validated data
14
+ router.post('/register', [ApiAuthController, 'register', RegisterRequest])
15
+ router.post('/login', [ApiAuthController, 'login', LoginRequest])
16
+ router.post('/logout', [ApiAuthController, 'logout']).middleware('auth:api')
17
+ router.get('/user', [ApiAuthController, 'user']).middleware('auth:api')
18
+
19
+ // Users CRUD (protected)
20
+ router.get('/users', [UserController, 'index']).middleware('auth:api')
21
+ router.post('/users', [UserController, 'store', StoreUserRequest]).middleware('auth:api')
22
+ router.put('/users/:id', [UserController, 'update', UpdateUserRequest]).middleware('auth:api')
23
+ router.delete('/users/:id', [UserController, 'destroy']).middleware('auth:api')
24
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { TestCase } from '@mantiq/testing'
3
+
4
+ const t = new TestCase()
5
+ t.refreshDatabase = true
6
+ t.setup()
7
+
8
+ const user = {
9
+ name: 'API User',
10
+ email: 'api@example.com',
11
+ password: 'password123',
12
+ }
13
+
14
+ describe('Token Authentication', () => {
15
+ test('can register and receive a token', async () => {
16
+ const res = await t.client.post('/api/register', user)
17
+ res.assertCreated()
18
+ await res.assertJsonHasKey('token')
19
+ const data = await res.json()
20
+ expect(data.token).toContain('|')
21
+ })
22
+
23
+ test('can login and receive a token', async () => {
24
+ await t.client.post('/api/register', user)
25
+ const res = await t.client.post('/api/login', {
26
+ email: user.email,
27
+ password: user.password,
28
+ })
29
+ res.assertOk()
30
+ await res.assertJsonHasKey('token')
31
+ })
32
+
33
+ test('cannot login with wrong credentials', async () => {
34
+ await t.client.post('/api/register', user)
35
+ const res = await t.client.post('/api/login', {
36
+ email: user.email,
37
+ password: 'wrong',
38
+ })
39
+ res.assertUnauthorized()
40
+ })
41
+
42
+ test('can access protected route with bearer token', async () => {
43
+ const regRes = await t.client.post('/api/register', user)
44
+ const { token } = await regRes.json()
45
+
46
+ t.client.withToken(token)
47
+ const res = await t.client.get('/api/user')
48
+ res.assertOk()
49
+ await res.assertJsonPath('user.email', user.email)
50
+ })
51
+
52
+ test('cannot access protected route without token', async () => {
53
+ const res = await t.client.get('/api/user')
54
+ res.assertUnauthorized()
55
+ })
56
+
57
+ test('can logout (revoke token)', async () => {
58
+ const regRes = await t.client.post('/api/register', user)
59
+ const { token } = await regRes.json()
60
+
61
+ t.client.withToken(token)
62
+ const logoutRes = await t.client.post('/api/logout')
63
+ logoutRes.assertOk()
64
+
65
+ // Token should be revoked
66
+ const userRes = await t.client.get('/api/user')
67
+ userRes.assertUnauthorized()
68
+ })
69
+ })
@@ -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
+ }
@@ -0,0 +1,11 @@
1
+ import { FormRequest } from '@mantiq/validation'
2
+
3
+ export class RegisterRequest extends FormRequest {
4
+ override rules() {
5
+ return {
6
+ name: 'required|string|max:255',
7
+ email: 'required|email|max:255',
8
+ password: 'required|string|min:6',
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,43 @@
1
+ import type { MantiqRequest } from '@mantiq/core'
2
+ import { json, hash, abort } from '@mantiq/core'
3
+ import { auth } from '@mantiq/auth'
4
+ import { User } from '../../Models/User.ts'
5
+
6
+ export class AuthController {
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.')
9
+
10
+ const user = await User.create({
11
+ name: data.name,
12
+ email: data.email,
13
+ password: await hash(data.password),
14
+ })
15
+
16
+ const manager = auth()
17
+ manager.setRequest(request)
18
+ await manager.login(user)
19
+
20
+ return json({ message: 'Registered.', user: user.toObject() }, 201)
21
+ }
22
+
23
+ async login(request: MantiqRequest, data: Record<string, any>): Promise<Response> {
24
+ const manager = auth()
25
+ manager.setRequest(request)
26
+
27
+ const success = await manager.attempt(
28
+ { email: data.email, password: data.password },
29
+ data.remember ?? false,
30
+ )
31
+ if (!success) abort(401, 'Invalid credentials.')
32
+
33
+ const user = await manager.user()
34
+ return json({ message: 'Logged in.', user: user?.toObject() })
35
+ }
36
+
37
+ async logout(request: MantiqRequest): Promise<Response> {
38
+ const manager = auth()
39
+ manager.setRequest(request)
40
+ await manager.logout()
41
+ return json({ message: 'Logged out.' })
42
+ }
43
+ }
@@ -0,0 +1,66 @@
1
+ import type { MantiqRequest } from '@mantiq/core'
2
+ import { config } from '@mantiq/core'
3
+ import { vite } from '@mantiq/vite'
4
+ import { auth } from '@mantiq/auth'
5
+ import { User } from '../../Models/User.ts'
6
+
7
+ async function getUser(request: MantiqRequest) {
8
+ const manager = auth()
9
+ manager.setRequest(request)
10
+ const user = await manager.user()
11
+ if (!user) return null
12
+ return {
13
+ id: user.getAttribute('id') ?? user.getAuthIdentifier(),
14
+ name: user.getAttribute('name') ?? '',
15
+ email: user.getAttribute('email') ?? '',
16
+ }
17
+ }
18
+
19
+ function render(request: MantiqRequest, page: string, title: string, data: Record<string, any> = {}) {
20
+ return vite().render(request, {
21
+ page,
22
+ entry: ['src/style.css', '{{MAIN_ENTRY}}'],
23
+ title: config('app.name') + ' — ' + title,
24
+ data: { appName: config('app.name'), ...data },
25
+ })
26
+ }
27
+
28
+ export class PageController {
29
+ async dashboard(request: MantiqRequest): Promise<Response> {
30
+ const currentUser = await getUser(request)
31
+ const users = await User.all()
32
+ return render(request, 'Dashboard', 'Dashboard', {
33
+ currentUser,
34
+ users: users.map((u: any) => u.toObject()),
35
+ })
36
+ }
37
+
38
+ async login(request: MantiqRequest): Promise<Response> {
39
+ return render(request, 'Login', 'Sign In')
40
+ }
41
+
42
+ async register(request: MantiqRequest): Promise<Response> {
43
+ return render(request, 'Register', 'Register')
44
+ }
45
+
46
+ async users(request: MantiqRequest): Promise<Response> {
47
+ const currentUser = await getUser(request)
48
+ const users = await User.all()
49
+ return render(request, 'Users', 'Users', {
50
+ currentUser,
51
+ users: users.map((u: any) => u.toObject()),
52
+ })
53
+ }
54
+
55
+ async profile(request: MantiqRequest): Promise<Response> {
56
+ return render(request, 'Profile', 'Profile', { currentUser: await getUser(request) })
57
+ }
58
+
59
+ async security(request: MantiqRequest): Promise<Response> {
60
+ return render(request, 'Security', 'Security', { currentUser: await getUser(request) })
61
+ }
62
+
63
+ async preferences(request: MantiqRequest): Promise<Response> {
64
+ return render(request, 'Preferences', 'Preferences', { currentUser: await getUser(request) })
65
+ }
66
+ }
@@ -0,0 +1,25 @@
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
+ import { AuthController } from '../app/Http/Controllers/AuthController.ts'
5
+ import { RegisterRequest } from '../app/Http/Requests/RegisterRequest.ts'
6
+ import { LoginRequest } from '../app/Http/Requests/LoginRequest.ts'
7
+
8
+ export default function (router: Router) {
9
+ router.get('/', [HomeController, 'index'])
10
+
11
+ router.get('/dashboard', [PageController, 'dashboard']).middleware('auth')
12
+ router.get('/login', [PageController, 'login']).middleware('guest')
13
+ router.get('/register', [PageController, 'register']).middleware('guest')
14
+ router.get('/users', [PageController, 'users']).middleware('auth')
15
+
16
+ // Account settings
17
+ router.get('/account/profile', [PageController, 'profile']).middleware('auth')
18
+ router.get('/account/security', [PageController, 'security']).middleware('auth')
19
+ router.get('/account/preferences', [PageController, 'preferences']).middleware('auth')
20
+
21
+ // Auth actions — FormRequest auto-validates before controller runs
22
+ router.post('/login', [AuthController, 'login', LoginRequest])
23
+ router.post('/register', [AuthController, 'register', RegisterRequest])
24
+ router.post('/logout', [AuthController, 'logout']).middleware('auth')
25
+ }
@@ -0,0 +1,77 @@
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 ?? 'Login')
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
+ // Handle 401/419 — session expired
33
+ if (res.status === 401 || res.status === 419) {
34
+ window.location.href = '/login'
35
+ return
36
+ }
37
+
38
+ const newData = await res.json()
39
+ currentPage = newData._page
40
+ pageData = newData
41
+ history.pushState(null, '', newData._url)
42
+ }
43
+
44
+ setContext('navigate', navigate)
45
+
46
+ const spaRoutes = [
47
+ '/login', '/register', '/dashboard', '/users',
48
+ '/account/profile', '/account/security', '/account/preferences',
49
+ ]
50
+
51
+ function handleClick(e: MouseEvent) {
52
+ const anchor = (e.target as HTMLElement).closest('a')
53
+ const href = anchor?.getAttribute('href')
54
+ if (!href?.startsWith('/') || anchor?.target || e.ctrlKey || e.metaKey) return
55
+ if (!spaRoutes.some(r => href === r || href.startsWith(r + '?'))) return
56
+ e.preventDefault()
57
+ navigate(href)
58
+ }
59
+
60
+ function handlePop() { navigate(location.pathname) }
61
+
62
+ onMount(() => {
63
+ document.addEventListener('click', handleClick)
64
+ window.addEventListener('popstate', handlePop)
65
+ })
66
+
67
+ onDestroy(() => {
68
+ if (typeof window !== 'undefined') {
69
+ document.removeEventListener('click', handleClick)
70
+ window.removeEventListener('popstate', handlePop)
71
+ }
72
+ })
73
+ </script>
74
+
75
+ {#if PageComponent}
76
+ <PageComponent {...pageData} {navigate} />
77
+ {/if}
@@ -0,0 +1,17 @@
1
+ import Login from './pages/Login.svelte'
2
+ import Register from './pages/Register.svelte'
3
+ import Dashboard from './pages/Dashboard.svelte'
4
+ import Users from './pages/Users.svelte'
5
+ import Profile from './pages/account/profile.svelte'
6
+ import Security from './pages/account/security.svelte'
7
+ import Preferences from './pages/account/preferences.svelte'
8
+
9
+ export const pages: Record<string, any> = {
10
+ Login,
11
+ Register,
12
+ Dashboard,
13
+ Users,
14
+ Profile,
15
+ Security,
16
+ Preferences,
17
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, test, beforeAll } from 'bun:test'
2
+ import { TestCase } from '@mantiq/testing'
3
+
4
+ const t = new TestCase()
5
+ t.refreshDatabase = true
6
+ t.setup()
7
+
8
+ const user = {
9
+ name: 'Test User',
10
+ email: 'test@example.com',
11
+ password: 'password123',
12
+ }
13
+
14
+ describe('Authentication', () => {
15
+ test('can register a new user', async () => {
16
+ await t.client.initSession()
17
+ const res = await t.client.post('/register', user)
18
+ res.assertCreated()
19
+ await res.assertJson({ message: 'Registered.' })
20
+ await res.assertJsonMissingKey('password')
21
+ await t.assertDatabaseHas('users', { email: user.email })
22
+ })
23
+
24
+ test('cannot register with duplicate email', async () => {
25
+ await t.client.initSession()
26
+ await t.client.post('/register', user)
27
+ const res = await t.client.post('/register', user)
28
+ res.assertUnprocessable()
29
+ })
30
+
31
+ test('can login with valid credentials', async () => {
32
+ await t.client.initSession()
33
+ await t.client.post('/register', user)
34
+ t.client.flushCookies()
35
+
36
+ await t.client.initSession()
37
+ const res = await t.client.post('/login', {
38
+ email: user.email,
39
+ password: user.password,
40
+ })
41
+ res.assertOk()
42
+ await res.assertJson({ message: 'Logged in.' })
43
+ })
44
+
45
+ test('cannot login with wrong password', async () => {
46
+ await t.client.initSession()
47
+ await t.client.post('/register', user)
48
+ t.client.flushCookies()
49
+
50
+ await t.client.initSession()
51
+ const res = await t.client.post('/login', {
52
+ email: user.email,
53
+ password: 'wrong',
54
+ })
55
+ res.assertUnauthorized()
56
+ })
57
+
58
+ test('can logout', async () => {
59
+ await t.client.initSession()
60
+ await t.client.post('/register', user)
61
+ const logoutRes = await t.client.post('/logout')
62
+ logoutRes.assertOk()
63
+ })
64
+
65
+ test('protected routes require authentication', async () => {
66
+ const res = await t.client.get('/api/users')
67
+ res.assertUnauthorized()
68
+ })
69
+ })