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,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
|
+
}
|
|
@@ -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
|
|
9
|
-
const hasher = new HashManager({ bcrypt: { rounds: 10 } })
|
|
7
|
+
override definition(index: number, fake: Faker): Record<string, any> {
|
|
10
8
|
return {
|
|
11
|
-
name:
|
|
12
|
-
email:
|
|
13
|
-
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 {
|
|
3
|
-
import {
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
router.
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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 =
|
|
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('
|
|
17
|
-
let password = $state('
|
|
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="
|
|
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: [
|
|
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
|
+
}
|