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,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,11 @@
1
+ import { FormRequest } from '@mantiq/validation'
2
+
3
+ export class StoreUserRequest 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,11 @@
1
+ import { FormRequest } from '@mantiq/validation'
2
+
3
+ export class UpdateUserRequest extends FormRequest {
4
+ override rules() {
5
+ return {
6
+ name: 'string|max:255',
7
+ email: 'email|max:255',
8
+ password: 'string|min:6',
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,36 @@
1
+ import { env } from '@mantiq/core'
2
+
3
+ export default {
4
+
5
+ /*
6
+ |--------------------------------------------------------------------------
7
+ | Application Name
8
+ |--------------------------------------------------------------------------
9
+ */
10
+ name: env('APP_NAME', 'MantiqJS'),
11
+ env: env('APP_ENV', 'production'),
12
+ debug: env('APP_DEBUG', false),
13
+ key: env('APP_KEY', ''),
14
+ url: env('APP_URL', 'http://localhost:3000'),
15
+ port: Number(env('APP_PORT', '3000')),
16
+ basePath: import.meta.dir + '/..',
17
+
18
+ /*
19
+ |--------------------------------------------------------------------------
20
+ | Middleware Groups
21
+ |--------------------------------------------------------------------------
22
+ |
23
+ | Middleware groups are applied automatically based on the route file:
24
+ | routes/web.ts → 'web' group (stateful: sessions, CSRF, cookies)
25
+ | routes/api.ts → 'api' group (stateful: sessions, cookies — for SPA)
26
+ |
27
+ | The 'api' group includes sessions so the SPA can use cookie-based auth
28
+ | for API calls. CSRF is not included — the SPA sends the X-XSRF-TOKEN
29
+ | header instead (set by the web group on page load).
30
+ |
31
+ */
32
+ middlewareGroups: {
33
+ web: ['cors', 'encrypt.cookies', 'session', 'csrf'],
34
+ api: ['cors', 'encrypt.cookies', 'session', 'throttle'],
35
+ },
36
+ }
@@ -0,0 +1,8 @@
1
+ export default {
2
+ reactRefresh: {{REACT_REFRESH}},
3
+
4
+ ssr: {
5
+ entry: '{{SSR_ENTRY}}',
6
+ bundle: 'bootstrap/ssr/ssr.js',
7
+ },
8
+ }
@@ -1,16 +1,14 @@
1
1
  import { Factory, Faker } from '@mantiq/database'
2
- import { HashManager } from '@mantiq/core'
3
2
  import { User } from '../../app/Models/User.ts'
4
3
 
5
4
  export class UserFactory extends Factory<User> {
6
5
  protected model = User
7
6
 
8
- override async definition(faker: Faker): Promise<Record<string, any>> {
9
- const hasher = new HashManager({ bcrypt: { rounds: 10 } })
7
+ override definition(index: number, fake: Faker): Record<string, any> {
10
8
  return {
11
- name: faker.name(),
12
- email: faker.email(),
13
- password: await hasher.make('password'),
9
+ name: fake.name(),
10
+ email: fake.email(),
11
+ password: 'password',
14
12
  }
15
13
  }
16
14
  }
@@ -1,106 +1,16 @@
1
1
  import type { Router } from '@mantiq/core'
2
- import { MantiqResponse, HashManager } from '@mantiq/core'
3
- import { User } from '../app/Models/User.ts'
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'
4
6
 
5
7
  export default function (router: Router) {
6
- router.get('/api/ping', () => {
7
- return MantiqResponse.json({ status: 'ok', timestamp: new Date().toISOString() })
8
- })
9
-
10
- // Users CRUD — server-side search + pagination
11
- router.get('/api/users', async (request: any) => {
12
- const search = request.query('search') ?? ''
13
- const page = Math.max(1, Number(request.query('page') ?? 1))
14
- const perPage = Math.min(100, Math.max(1, Number(request.query('per_page') ?? 10)))
15
- const sortBy = request.query('sort') ?? 'created_at'
16
- const sortDir = request.query('dir') === 'asc' ? 'asc' : 'desc'
17
-
18
- let query = User.query()
19
-
20
- if (search) {
21
- query = query.where('name', 'LIKE', `%${search}%`)
22
- .orWhere('email', 'LIKE', `%${search}%`) as any
23
- }
24
-
25
- const total = await (User.query() as any).count() as number
26
- const filteredQuery = search
27
- ? User.query().where('name', 'LIKE', `%${search}%`).orWhere('email', 'LIKE', `%${search}%`) as any
28
- : User.query()
29
-
30
- const filteredTotal = await filteredQuery.count() as number
31
-
32
- const users = search
33
- ? await User.query()
34
- .where('name', 'LIKE', `%${search}%`)
35
- .orWhere('email', 'LIKE', `%${search}%`)
36
- .orderBy(sortBy, sortDir)
37
- .limit(perPage)
38
- .offset((page - 1) * perPage)
39
- .get() as any[]
40
- : await User.query()
41
- .orderBy(sortBy, sortDir)
42
- .limit(perPage)
43
- .offset((page - 1) * perPage)
44
- .get() as any[]
45
-
46
- return MantiqResponse.json({
47
- data: users.map((u: any) => u.toObject()),
48
- meta: {
49
- total,
50
- filtered_total: filteredTotal,
51
- page,
52
- per_page: perPage,
53
- last_page: Math.ceil(filteredTotal / perPage),
54
- },
55
- })
56
- }).middleware('auth')
57
-
58
- router.post('/api/users', async (request: any) => {
59
- const body = await request.input()
60
- if (!body?.name || !body?.email || !body?.password) {
61
- return MantiqResponse.json({ error: 'Name, email and password are required.' }, 422)
62
- }
63
-
64
- // Check duplicate
65
- const existing = await User.where('email', body.email).first()
66
- if (existing) {
67
- return MantiqResponse.json({ error: 'Email already exists.' }, 422)
68
- }
69
-
70
- const hasher = new HashManager({ bcrypt: { rounds: 10 } })
71
- const user = await User.create({
72
- name: body.name,
73
- email: body.email,
74
- password: await hasher.make(body.password),
75
- })
76
-
77
- return MantiqResponse.json({ data: user.toObject() }, 201)
78
- }).middleware('auth')
79
-
80
- router.put('/api/users/:id', async (request: any) => {
81
- const id = request.param('id')
82
- const body = await request.input()
83
- const user = await User.find(Number(id))
84
- if (!user) return MantiqResponse.json({ error: 'User not found.' }, 404)
85
-
86
- if (body.name) user.setAttribute('name', body.name)
87
- if (body.email) user.setAttribute('email', body.email)
88
-
89
- if (body.password) {
90
- const hasher = new HashManager({ bcrypt: { rounds: 10 } })
91
- user.setAttribute('password', await hasher.make(body.password))
92
- }
93
-
94
- await user.save()
95
- return MantiqResponse.json({ data: user.toObject() })
96
- }).middleware('auth')
97
-
98
- router.delete('/api/users/:id', async (request: any) => {
99
- const id = request.param('id')
100
- const user = await User.find(Number(id))
101
- if (!user) return MantiqResponse.json({ error: 'User not found.' }, 404)
102
-
103
- await user.delete()
104
- return MantiqResponse.json({ success: true })
105
- }).middleware('auth')
8
+ // Public
9
+ router.get('/ping', () => json({ status: 'ok', timestamp: new Date().toISOString() }))
10
+
11
+ // Protected — FormRequest auto-validates before controller runs
12
+ router.get('/users', [UserController, 'index']).middleware('auth')
13
+ router.post('/users', [UserController, 'store', StoreUserRequest]).middleware('auth')
14
+ router.put('/users/:id', [UserController, 'update', UpdateUserRequest]).middleware('auth')
15
+ router.delete('/users/:id', [UserController, 'destroy']).middleware('auth')
106
16
  }
@@ -2,6 +2,8 @@ import type { Router } from '@mantiq/core'
2
2
  import { HomeController } from '../app/Http/Controllers/HomeController.ts'
3
3
  import { PageController } from '../app/Http/Controllers/PageController.ts'
4
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'
5
7
 
6
8
  export default function (router: Router) {
7
9
  router.get('/', [HomeController, 'index'])
@@ -16,8 +18,8 @@ export default function (router: Router) {
16
18
  router.get('/account/security', [PageController, 'security']).middleware('auth')
17
19
  router.get('/account/preferences', [PageController, 'preferences']).middleware('auth')
18
20
 
19
- // Auth actions
20
- router.post('/login', [AuthController, 'login'])
21
- router.post('/register', [AuthController, 'register'])
21
+ // Auth actions — FormRequest auto-validates before controller runs
22
+ router.post('/login', [AuthController, 'login', LoginRequest])
23
+ router.post('/register', [AuthController, 'register', RegisterRequest])
22
24
  router.post('/logout', [AuthController, 'logout']).middleware('auth')
23
25
  }
@@ -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
+ })
@@ -0,0 +1,90 @@
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 admin = {
9
+ name: 'Admin',
10
+ email: 'admin@example.com',
11
+ password: 'password123',
12
+ }
13
+
14
+ /** Register and login before CRUD tests. */
15
+ async function login() {
16
+ await t.client.initSession()
17
+ await t.client.post('/register', admin)
18
+ }
19
+
20
+ describe('Users CRUD', () => {
21
+ test('can list users', async () => {
22
+ await login()
23
+ const res = await t.client.get('/api/users')
24
+ res.assertOk()
25
+ await res.assertJsonHasKey('data', 'meta')
26
+ await res.assertJsonPath('meta.page', 1)
27
+ })
28
+
29
+ test('can create a user', async () => {
30
+ await login()
31
+ const res = await t.client.post('/api/users', {
32
+ name: 'New User',
33
+ email: 'new@example.com',
34
+ password: 'secret123',
35
+ })
36
+ res.assertCreated()
37
+ await res.assertJsonPath('data.name', 'New User')
38
+ await res.assertJsonPath('data.email', 'new@example.com')
39
+ await t.assertDatabaseHas('users', { email: 'new@example.com' })
40
+ })
41
+
42
+ test('cannot create user with missing fields', async () => {
43
+ await login()
44
+ const res = await t.client.post('/api/users', { name: 'No Email' })
45
+ res.assertUnprocessable()
46
+ })
47
+
48
+ test('can update a user', async () => {
49
+ await login()
50
+ const createRes = await t.client.post('/api/users', {
51
+ name: 'Update Me',
52
+ email: 'update@example.com',
53
+ password: 'secret123',
54
+ })
55
+ const userId = (await createRes.json()).data.id
56
+
57
+ const res = await t.client.put(`/api/users/${userId}`, { name: 'Updated' })
58
+ res.assertOk()
59
+ await res.assertJsonPath('data.name', 'Updated')
60
+ })
61
+
62
+ test('can delete a user', async () => {
63
+ await login()
64
+ const createRes = await t.client.post('/api/users', {
65
+ name: 'Delete Me',
66
+ email: 'delete@example.com',
67
+ password: 'secret123',
68
+ })
69
+ const userId = (await createRes.json()).data.id
70
+
71
+ const res = await t.client.delete(`/api/users/${userId}`)
72
+ res.assertOk()
73
+ await t.assertDatabaseMissing('users', { email: 'delete@example.com' })
74
+ })
75
+
76
+ test('can search users', async () => {
77
+ await login()
78
+ const res = await t.client.get('/api/users?search=Admin')
79
+ res.assertOk()
80
+ const data = await res.json()
81
+ expect(data.data.length).toBeGreaterThan(0)
82
+ })
83
+
84
+ test('can paginate users', async () => {
85
+ await login()
86
+ const res = await t.client.get('/api/users?page=1&per_page=1')
87
+ res.assertOk()
88
+ await res.assertJsonPath('meta.per_page', 1)
89
+ })
90
+ })
@@ -9,7 +9,7 @@
9
9
  initialData?: Record<string, any>
10
10
  } = $props()
11
11
 
12
- const windowData = typeof window !== 'undefined' ? (window as any).__MANTIQ_DATA__ : {}
12
+ const windowData = typeof window !== 'undefined' ? (window as Record<string, any>).__MANTIQ_DATA__ ?? {} : {}
13
13
  const bootstrapData = (() => initialData ?? windowData)()
14
14
 
15
15
  let currentPage = $state<string>(bootstrapData._page ?? 'Login')
@@ -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
  }
@@ -1,10 +1,12 @@
1
+ declare global { interface Window { __MANTIQ_DATA__?: Record<string, any> } }
2
+
1
3
  import './style.css'
2
4
  import { mount } from 'svelte'
3
5
  import App from './App.svelte'
4
6
  import { pages } from './pages.ts'
5
7
 
6
8
  const target = document.getElementById('app')!
7
- const data = (window as any).__MANTIQ_DATA__ ?? {}
9
+ const data = window.__MANTIQ_DATA__ ?? {}
8
10
 
9
11
  // Clear SSR content and mount fresh — avoids hydration mismatches
10
12
  // with shadcn-svelte sidebar components
@@ -13,8 +13,8 @@
13
13
  [key: string]: any
14
14
  } = $props()
15
15
 
16
- let email = $state('admin@example.com')
17
- let password = $state('password')
16
+ let email = $state('')
17
+ let password = $state('')
18
18
  let error = $state('')
19
19
  let loading = $state(false)
20
20
 
@@ -82,7 +82,7 @@
82
82
  type="email"
83
83
  bind:value={email}
84
84
  required
85
- placeholder="admin@example.com"
85
+ placeholder="you@example.com"
86
86
  autocomplete="email"
87
87
  />
88
88
  </div>
@@ -2,9 +2,28 @@ import { defineConfig } from 'vite'
2
2
  import { svelte } from '@sveltejs/vite-plugin-svelte'
3
3
  import tailwindcss from '@tailwindcss/vite'
4
4
  import path from 'path'
5
+ import { writeFileSync, unlinkSync } from 'node:fs'
5
6
 
6
7
  export default defineConfig({
7
- plugins: [svelte(), tailwindcss()],
8
+ plugins: [
9
+ svelte(),
10
+ tailwindcss(),
11
+ {
12
+ name: 'mantiq-hot',
13
+ configureServer(server) {
14
+ const hotPath = path.resolve(__dirname, 'public/hot')
15
+ server.httpServer?.once('listening', () => {
16
+ const addr = server.httpServer!.address()
17
+ const url = typeof addr === 'string' ? addr : `http://localhost:${addr?.port}`
18
+ writeFileSync(hotPath, url)
19
+ })
20
+ const cleanup = () => { try { unlinkSync(hotPath) } catch {} }
21
+ process.on('exit', cleanup)
22
+ process.on('SIGINT', () => { cleanup(); process.exit() })
23
+ process.on('SIGTERM', () => { cleanup(); process.exit() })
24
+ },
25
+ },
26
+ ],
8
27
  publicDir: false,
9
28
  resolve: {
10
29
  alias: {
@@ -0,0 +1,68 @@
1
+ import { cn } from '@/lib/utils'
2
+ import { NavGroup } from './nav-group'
3
+ import { NavUser, type NavUserProps } from './nav-user'
4
+ import { sidebarData } from './sidebar-data'
5
+
6
+ export interface AppSidebarProps {
7
+ user: NavUserProps['user']
8
+ appName: string
9
+ activePath: string
10
+ navigate: (href: string) => void
11
+ onLogout: () => void
12
+ collapsed?: boolean
13
+ }
14
+
15
+ export function AppSidebar({
16
+ user,
17
+ appName,
18
+ activePath,
19
+ navigate,
20
+ onLogout,
21
+ collapsed,
22
+ }: AppSidebarProps) {
23
+ return (
24
+ <aside
25
+ className={cn(
26
+ 'flex h-screen flex-col border-r border-gray-200 bg-gray-50 transition-[width] duration-200 dark:border-gray-800 dark:bg-gray-900',
27
+ collapsed ? 'w-16' : 'w-64',
28
+ )}
29
+ >
30
+ {/* Header */}
31
+ <div className="flex h-14 items-center gap-2 border-b border-gray-200 px-4 dark:border-gray-800">
32
+ <button
33
+ type="button"
34
+ onClick={() => navigate('/dashboard')}
35
+ className="flex items-center gap-2"
36
+ >
37
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-emerald-600 text-xs font-bold text-white">
38
+ M
39
+ </div>
40
+ {!collapsed && (
41
+ <div className="grid text-left text-sm leading-tight">
42
+ <span className="truncate font-semibold text-gray-900 dark:text-gray-50">{appName}</span>
43
+ <span className="truncate text-xs text-gray-500 dark:text-gray-400">Admin Panel</span>
44
+ </div>
45
+ )}
46
+ </button>
47
+ </div>
48
+
49
+ {/* Navigation */}
50
+ <nav className="flex-1 overflow-y-auto py-2">
51
+ {sidebarData.map((group) => (
52
+ <NavGroup
53
+ key={group.title}
54
+ group={group}
55
+ activePath={activePath}
56
+ navigate={navigate}
57
+ collapsed={collapsed}
58
+ />
59
+ ))}
60
+ </nav>
61
+
62
+ {/* User footer */}
63
+ <div className="border-t border-gray-200 dark:border-gray-800">
64
+ <NavUser user={user} navigate={navigate} onLogout={onLogout} collapsed={collapsed} />
65
+ </div>
66
+ </aside>
67
+ )
68
+ }
@@ -0,0 +1,57 @@
1
+ import { useState, useCallback, createContext, useContext } from 'react'
2
+ import { post } from '@/lib/api'
3
+ import { AppSidebar } from './app-sidebar'
4
+
5
+ const SidebarContext = createContext<{ toggle: () => void }>({ toggle: () => {} })
6
+
7
+ export function useSidebarToggle() {
8
+ return useContext(SidebarContext)
9
+ }
10
+
11
+ interface AuthenticatedLayoutProps {
12
+ children: React.ReactNode
13
+ currentUser?: { name: string; email: string; role?: string } | null
14
+ appName?: string
15
+ navigate: (href: string) => void
16
+ activePath: string
17
+ }
18
+
19
+ export function AuthenticatedLayout({
20
+ children,
21
+ currentUser,
22
+ appName = 'Mantiq',
23
+ navigate,
24
+ activePath,
25
+ }: AuthenticatedLayoutProps) {
26
+ const [collapsed, setCollapsed] = useState(false)
27
+
28
+ const handleLogout = useCallback(async () => {
29
+ await post('/logout', {})
30
+ navigate('/login')
31
+ }, [navigate])
32
+
33
+ const user = currentUser ?? { name: 'User', email: 'user@example.com' }
34
+
35
+ return (
36
+ <SidebarContext.Provider value={{ toggle: () => setCollapsed((c) => !c) }}>
37
+ <div className="flex min-h-screen">
38
+ {/* Sidebar — hidden on mobile, visible on md+ */}
39
+ <div className="hidden md:block">
40
+ <AppSidebar
41
+ user={user}
42
+ appName={appName}
43
+ activePath={activePath}
44
+ navigate={navigate}
45
+ onLogout={handleLogout}
46
+ collapsed={collapsed}
47
+ />
48
+ </div>
49
+
50
+ {/* Main content */}
51
+ <div className="flex flex-1 flex-col">
52
+ {children}
53
+ </div>
54
+ </div>
55
+ </SidebarContext.Provider>
56
+ )
57
+ }