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