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.
- package/package.json +2 -1
- package/skeleton/.env.example +64 -0
- package/skeleton/README.md +46 -0
- package/skeleton/app/Console/Commands/.gitkeep +0 -0
- package/skeleton/app/Enums/UserStatus.ts +7 -0
- package/skeleton/app/Http/Controllers/HomeController.ts +78 -0
- package/skeleton/app/Http/Middleware/.gitkeep +0 -0
- package/skeleton/app/Models/User.ts +7 -0
- package/skeleton/app/Providers/AppServiceProvider.ts +25 -0
- package/skeleton/app/Providers/DatabaseServiceProvider.ts +17 -0
- package/skeleton/bootstrap/.gitkeep +0 -0
- package/skeleton/config/ai.ts +51 -0
- package/skeleton/config/app.ts +108 -0
- package/skeleton/config/auth.ts +51 -0
- package/skeleton/config/broadcasting.ts +93 -0
- package/skeleton/config/cache.ts +61 -0
- package/skeleton/config/cors.ts +77 -0
- package/skeleton/config/database.ts +120 -0
- package/skeleton/config/filesystem.ts +58 -0
- package/skeleton/config/hashing.ts +47 -0
- package/skeleton/config/heartbeat.ts +112 -0
- package/skeleton/config/logging.ts +58 -0
- package/skeleton/config/mail.ts +93 -0
- package/skeleton/config/notify.ts +141 -0
- package/skeleton/config/queue.ts +59 -0
- package/skeleton/config/search.ts +96 -0
- package/skeleton/config/services.ts +110 -0
- package/skeleton/config/session.ts +84 -0
- package/skeleton/config/vite.ts +33 -0
- package/skeleton/database/factories/.gitkeep +0 -0
- package/skeleton/database/migrations/001_create_users_table.ts +19 -0
- package/skeleton/database/migrations/002_create_personal_access_tokens_table.ts +22 -0
- package/skeleton/database/seeders/DatabaseSeeder.ts +7 -0
- package/skeleton/index.ts +20 -0
- package/skeleton/mantiq.ts +8 -0
- package/skeleton/package.json +34 -0
- package/skeleton/public/.gitkeep +0 -0
- package/skeleton/routes/api.ts +8 -0
- package/skeleton/routes/channels.ts +23 -0
- package/skeleton/routes/console.ts +24 -0
- package/skeleton/routes/web.ts +6 -0
- package/skeleton/storage/cache/.gitkeep +0 -0
- package/skeleton/storage/framework/.gitkeep +0 -0
- package/skeleton/tests/feature/api.test.ts +14 -0
- package/skeleton/tests/feature/home.test.ts +17 -0
- package/skeleton/tests/unit/example.test.ts +11 -0
- package/skeleton/tsconfig.json +27 -0
- package/src/index.ts +289 -25
- package/src/templates.ts +141 -945
- package/src/terminal.ts +64 -0
- package/stubs/api-only/routes/api.ts.stub +24 -0
- package/stubs/api-only/tests/feature/token-auth.test.ts.stub +69 -0
- package/stubs/auth/api/app/Http/Controllers/ApiAuthController.ts.stub +57 -0
- package/stubs/auth/api/routes/api.ts.stub +24 -0
- package/stubs/auth/api/tests/feature/token-auth.test.ts.stub +69 -0
- package/stubs/auth/shared/app/Http/Requests/LoginRequest.ts.stub +10 -0
- package/stubs/auth/shared/app/Http/Requests/RegisterRequest.ts.stub +11 -0
- package/stubs/auth/web/app/Http/Controllers/AuthController.ts.stub +43 -0
- package/stubs/auth/web/app/Http/Controllers/PageController.ts.stub +66 -0
- package/stubs/auth/web/routes/web.ts.stub +25 -0
- package/stubs/auth/web/svelte/src/App.svelte.stub +77 -0
- package/stubs/auth/web/svelte/src/pages.ts.stub +17 -0
- package/stubs/auth/web/tests/feature/auth.test.ts.stub +69 -0
- package/stubs/auth/web/vue/src/App.vue.stub +74 -0
- package/stubs/auth/web/vue/src/pages.ts.stub +17 -0
- package/stubs/manifest.json +630 -2
- package/stubs/noauth/app/Http/Controllers/PageController.ts.stub +41 -0
- package/stubs/noauth/app/Models/User.ts.stub +5 -0
- package/stubs/noauth/database/migrations/001_create_users_table.ts.stub +17 -0
- package/stubs/noauth/routes/api.ts.stub +16 -0
- package/stubs/noauth/routes/web.ts.stub +15 -0
- package/stubs/noauth/svelte/src/App.svelte.stub +68 -0
- package/stubs/noauth/svelte/src/pages.ts.stub +7 -0
- package/stubs/noauth/vue/src/App.vue.stub +62 -0
- package/stubs/noauth/vue/src/pages.ts.stub +7 -0
- package/stubs/react/src/App.tsx.stub +4 -2
- package/stubs/react/src/components/layout/search-dialog.tsx.stub +2 -2
- package/stubs/react/src/components/layout/sidebar-data.ts.stub +2 -2
- package/stubs/react/src/lib/api.ts.stub +30 -6
- package/stubs/react/src/pages/Login.tsx.stub +3 -3
- package/stubs/react/src/pages/users/dialogs.tsx.stub +7 -26
- package/stubs/react/vite.config.ts.stub +26 -3
- package/stubs/shared/app/Http/Controllers/ApiAuthController.ts.stub +57 -0
- package/stubs/shared/app/Http/Controllers/AuthController.ts.stub +14 -38
- package/stubs/shared/app/Http/Controllers/PageController.ts.stub +3 -3
- package/stubs/shared/app/Http/Controllers/UserController.ts.stub +61 -0
- package/stubs/shared/app/Http/Requests/LoginRequest.ts.stub +10 -0
- package/stubs/shared/app/Http/Requests/RegisterRequest.ts.stub +11 -0
- package/stubs/shared/app/Http/Requests/StoreUserRequest.ts.stub +11 -0
- package/stubs/shared/app/Http/Requests/UpdateUserRequest.ts.stub +11 -0
- package/stubs/shared/config/app.ts.stub +36 -0
- package/stubs/shared/config/vite.ts.stub +8 -0
- package/stubs/shared/database/factories/UserFactory.ts.stub +4 -6
- package/stubs/shared/routes/api.ts.stub +12 -102
- package/stubs/shared/routes/web.ts.stub +5 -3
- package/stubs/shared/tests/feature/auth.test.ts.stub +69 -0
- package/stubs/shared/tests/feature/users.test.ts.stub +90 -0
- package/stubs/svelte/src/App.svelte.stub +1 -1
- package/stubs/svelte/src/lib/api.ts.stub +30 -6
- package/stubs/svelte/src/main.ts.stub +3 -1
- package/stubs/svelte/src/pages/Login.svelte.stub +3 -3
- package/stubs/svelte/vite.config.ts.stub +20 -1
- package/stubs/tailwind-only/react/src/components/layout/app-sidebar.tsx.stub +68 -0
- package/stubs/tailwind-only/react/src/components/layout/authenticated-layout.tsx.stub +57 -0
- package/stubs/tailwind-only/react/src/components/layout/header.tsx.stub +52 -0
- package/stubs/tailwind-only/react/src/components/layout/main.tsx.stub +21 -0
- package/stubs/tailwind-only/react/src/components/layout/nav-group.tsx.stub +185 -0
- package/stubs/tailwind-only/react/src/components/layout/nav-user.tsx.stub +106 -0
- package/stubs/tailwind-only/react/src/components/layout/sidebar-data.ts.stub +58 -0
- package/stubs/tailwind-only/react/src/components/layout/theme-toggle.tsx.stub +36 -0
- package/stubs/tailwind-only/react/src/components/layout/top-nav.tsx.stub +72 -0
- package/stubs/tailwind-only/react/src/lib/utils.ts.stub +6 -0
- package/stubs/tailwind-only/react/src/pages/Dashboard.tsx.stub +205 -0
- package/stubs/tailwind-only/react/src/pages/Login.tsx.stub +122 -0
- package/stubs/tailwind-only/react/src/pages/Register.tsx.stub +137 -0
- package/stubs/tailwind-only/react/src/pages/Users.tsx.stub +426 -0
- package/stubs/tailwind-only/react/src/pages/account/layout.tsx.stub +64 -0
- package/stubs/tailwind-only/react/src/pages/account/preferences.tsx.stub +80 -0
- package/stubs/tailwind-only/react/src/pages/account/profile.tsx.stub +67 -0
- package/stubs/tailwind-only/react/src/pages/account/security.tsx.stub +91 -0
- package/stubs/tailwind-only/react/src/style.css.stub +14 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/app-sidebar.svelte.stub +104 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/authenticated-layout.svelte.stub +51 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/header.svelte.stub +66 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/main.svelte.stub +26 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/nav-group.svelte.stub +131 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/nav-user.svelte.stub +104 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/sidebar-data.ts.stub +57 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/theme-toggle.svelte.stub +28 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/top-nav.svelte.stub +99 -0
- package/stubs/tailwind-only/svelte/src/lib/utils.ts.stub +6 -0
- package/stubs/tailwind-only/svelte/src/pages/Dashboard.svelte.stub +192 -0
- package/stubs/tailwind-only/svelte/src/pages/Login.svelte.stub +120 -0
- package/stubs/tailwind-only/svelte/src/pages/Register.svelte.stub +134 -0
- package/stubs/tailwind-only/svelte/src/pages/Users.svelte.stub +293 -0
- package/stubs/tailwind-only/svelte/src/pages/account/Layout.svelte.stub +61 -0
- package/stubs/tailwind-only/svelte/src/pages/account/Preferences.svelte.stub +81 -0
- package/stubs/tailwind-only/svelte/src/pages/account/Profile.svelte.stub +76 -0
- package/stubs/tailwind-only/svelte/src/pages/account/Security.svelte.stub +140 -0
- package/stubs/tailwind-only/svelte/src/style.css.stub +127 -0
- package/stubs/tailwind-only/vue/src/components/layout/AppSidebar.vue.stub +60 -0
- package/stubs/tailwind-only/vue/src/components/layout/AuthenticatedLayout.vue.stub +73 -0
- package/stubs/tailwind-only/vue/src/components/layout/Header.vue.stub +54 -0
- package/stubs/tailwind-only/vue/src/components/layout/Main.vue.stub +22 -0
- package/stubs/tailwind-only/vue/src/components/layout/NavGroup.vue.stub +107 -0
- package/stubs/tailwind-only/vue/src/components/layout/NavUser.vue.stub +104 -0
- package/stubs/tailwind-only/vue/src/components/layout/ThemeToggle.vue.stub +28 -0
- package/stubs/tailwind-only/vue/src/components/layout/TopNav.vue.stub +86 -0
- package/stubs/tailwind-only/vue/src/components/layout/sidebar-data.ts.stub +57 -0
- package/stubs/tailwind-only/vue/src/lib/utils.ts.stub +7 -0
- package/stubs/tailwind-only/vue/src/pages/Dashboard.vue.stub +195 -0
- package/stubs/tailwind-only/vue/src/pages/Login.vue.stub +121 -0
- package/stubs/tailwind-only/vue/src/pages/Register.vue.stub +137 -0
- package/stubs/tailwind-only/vue/src/pages/Users.vue.stub +401 -0
- package/stubs/tailwind-only/vue/src/pages/account/Layout.vue.stub +56 -0
- package/stubs/tailwind-only/vue/src/pages/account/Preferences.vue.stub +81 -0
- package/stubs/tailwind-only/vue/src/pages/account/Profile.vue.stub +69 -0
- package/stubs/tailwind-only/vue/src/pages/account/Security.vue.stub +85 -0
- package/stubs/tailwind-only/vue/src/style.css.stub +26 -0
- package/stubs/themes/corporate/react/src/components/layout/app-sidebar.tsx.stub +74 -0
- package/stubs/themes/corporate/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
- package/stubs/themes/corporate/react/src/pages/Dashboard.tsx.stub +310 -0
- package/stubs/themes/corporate/react/src/pages/Login.tsx.stub +122 -0
- package/stubs/themes/corporate/react/src/pages/Register.tsx.stub +144 -0
- package/stubs/themes/corporate/react/src/style.css.stub +135 -0
- package/stubs/themes/corporate/svelte/src/pages/Dashboard.svelte.stub +271 -0
- package/stubs/themes/corporate/svelte/src/pages/Login.svelte.stub +117 -0
- package/stubs/themes/corporate/svelte/src/pages/Register.svelte.stub +138 -0
- package/stubs/themes/corporate/svelte/src/style.css.stub +134 -0
- package/stubs/themes/corporate/vue/src/pages/Dashboard.vue.stub +325 -0
- package/stubs/themes/corporate/vue/src/pages/Login.vue.stub +118 -0
- package/stubs/themes/corporate/vue/src/pages/Register.vue.stub +139 -0
- package/stubs/themes/corporate/vue/src/style.css.stub +134 -0
- package/stubs/themes/minimal/react/src/components/layout/app-sidebar.tsx.stub +68 -0
- package/stubs/themes/minimal/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
- package/stubs/themes/minimal/react/src/pages/Dashboard.tsx.stub +166 -0
- package/stubs/themes/minimal/react/src/pages/Login.tsx.stub +95 -0
- package/stubs/themes/minimal/react/src/pages/Register.tsx.stub +73 -0
- package/stubs/themes/minimal/react/src/style.css.stub +142 -0
- package/stubs/themes/minimal/svelte/src/pages/Dashboard.svelte.stub +149 -0
- package/stubs/themes/minimal/svelte/src/pages/Login.svelte.stub +90 -0
- package/stubs/themes/minimal/svelte/src/pages/Register.svelte.stub +70 -0
- package/stubs/themes/minimal/svelte/src/style.css.stub +142 -0
- package/stubs/themes/minimal/vue/src/pages/Dashboard.vue.stub +163 -0
- package/stubs/themes/minimal/vue/src/pages/Login.vue.stub +91 -0
- package/stubs/themes/minimal/vue/src/pages/Register.vue.stub +73 -0
- package/stubs/themes/minimal/vue/src/style.css.stub +142 -0
- package/stubs/themes/starter/react/src/components/layout/app-sidebar.tsx.stub +74 -0
- package/stubs/themes/starter/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
- package/stubs/themes/starter/react/src/pages/Dashboard.tsx.stub +236 -0
- package/stubs/themes/starter/react/src/pages/Login.tsx.stub +131 -0
- package/stubs/themes/starter/react/src/pages/Register.tsx.stub +145 -0
- package/stubs/themes/starter/react/src/style.css.stub +141 -0
- package/stubs/themes/starter/svelte/src/pages/Dashboard.svelte.stub +212 -0
- package/stubs/themes/starter/svelte/src/pages/Login.svelte.stub +126 -0
- package/stubs/themes/starter/svelte/src/pages/Register.svelte.stub +139 -0
- package/stubs/themes/starter/svelte/src/style.css.stub +141 -0
- package/stubs/themes/starter/vue/src/pages/Dashboard.vue.stub +228 -0
- package/stubs/themes/starter/vue/src/pages/Login.vue.stub +127 -0
- package/stubs/themes/starter/vue/src/pages/Register.vue.stub +140 -0
- package/stubs/themes/starter/vue/src/style.css.stub +141 -0
- package/stubs/themes/workspace/react/src/components/layout/app-sidebar.tsx.stub +97 -0
- package/stubs/themes/workspace/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
- package/stubs/themes/workspace/react/src/pages/Dashboard.tsx.stub +304 -0
- package/stubs/themes/workspace/react/src/pages/Login.tsx.stub +131 -0
- package/stubs/themes/workspace/react/src/pages/Register.tsx.stub +82 -0
- package/stubs/themes/workspace/react/src/style.css.stub +138 -0
- package/stubs/themes/workspace/svelte/src/pages/Dashboard.svelte.stub +215 -0
- package/stubs/themes/workspace/svelte/src/pages/Login.svelte.stub +124 -0
- package/stubs/themes/workspace/svelte/src/pages/Register.svelte.stub +76 -0
- package/stubs/themes/workspace/svelte/src/style.css.stub +134 -0
- package/stubs/themes/workspace/vue/src/pages/Dashboard.vue.stub +220 -0
- package/stubs/themes/workspace/vue/src/pages/Login.vue.stub +128 -0
- package/stubs/themes/workspace/vue/src/pages/Register.vue.stub +80 -0
- package/stubs/themes/workspace/vue/src/style.css.stub +134 -0
- package/stubs/vue/src/App.vue.stub +2 -1
- package/stubs/vue/src/lib/api.ts.stub +30 -6
- package/stubs/vue/src/main.ts.stub +3 -1
- package/stubs/vue/src/pages/Login.vue.stub +3 -3
- 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,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>
|
|
@@ -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' ?
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
39
|
+
{ title: 'GitHub', url: 'https://github.com/mantiqjs/mantiq', icon: ExternalLink, external: true },
|
|
40
40
|
],
|
|
41
41
|
},
|
|
42
42
|
{
|
|
@@ -1,17 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
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
|
|
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:
|
|
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
|
|
16
|
-
return api(url, { method: '
|
|
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('
|
|
15
|
-
const [password, setPassword] = useState('
|
|
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="
|
|
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
|
|
54
|
-
|
|
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
|
|
169
|
-
|
|
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
|
|
256
|
-
|
|
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: [
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
27
|
-
email:
|
|
28
|
-
password:
|
|
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
|
|
18
|
+
await manager.login(user)
|
|
34
19
|
|
|
35
|
-
return
|
|
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:
|
|
50
|
-
|
|
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
|
|
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
|
|
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:
|
|
14
|
-
name:
|
|
15
|
-
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
|
+
}
|