create-mantiq 0.5.23 → 0.6.1
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 +1 -1
- package/src/index.ts +188 -12
- package/src/templates.ts +23 -5
- package/src/terminal.ts +62 -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 +582 -0
- 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/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/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, statSync, readFileSync } from 'node:fs'
|
|
3
3
|
import { dirname, resolve, join, relative } from 'node:path'
|
|
4
4
|
import { randomBytes } from 'node:crypto'
|
|
5
|
-
import { getTemplates } from './templates.ts'
|
|
5
|
+
import { getTemplates, type Theme } from './templates.ts'
|
|
6
6
|
import { Terminal } from './terminal.ts'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Recursively copy a directory, preserving structure.
|
|
10
10
|
* Skips node_modules, .git, bun.lock, *.sqlite files.
|
|
11
|
+
* `skipRelPaths` contains relative paths (from root src) to skip.
|
|
11
12
|
*/
|
|
12
|
-
async function copyDirectory(src: string, dest: string): Promise<number> {
|
|
13
|
+
async function copyDirectory(src: string, dest: string, skipRelPaths?: Set<string>, rootSrc?: string): Promise<number> {
|
|
13
14
|
let count = 0
|
|
15
|
+
const root = rootSrc ?? src
|
|
14
16
|
const entries = readdirSync(src, { withFileTypes: true })
|
|
15
17
|
for (const entry of entries) {
|
|
16
18
|
const srcPath = join(src, entry.name)
|
|
@@ -20,9 +22,15 @@ async function copyDirectory(src: string, dest: string): Promise<number> {
|
|
|
20
22
|
if (['node_modules', '.git', 'bun.lock', 'README.md'].includes(entry.name)) continue
|
|
21
23
|
if (entry.name.endsWith('.sqlite') || entry.name.endsWith('.sqlite-wal') || entry.name.endsWith('.sqlite-shm')) continue
|
|
22
24
|
|
|
25
|
+
// Skip paths in the conditional skip set
|
|
26
|
+
if (skipRelPaths) {
|
|
27
|
+
const rel = relative(root, srcPath)
|
|
28
|
+
if (skipRelPaths.has(rel)) continue
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
if (entry.isDirectory()) {
|
|
24
32
|
mkdirSync(destPath, { recursive: true })
|
|
25
|
-
count += await copyDirectory(srcPath, destPath)
|
|
33
|
+
count += await copyDirectory(srcPath, destPath, skipRelPaths, root)
|
|
26
34
|
} else {
|
|
27
35
|
mkdirSync(dirname(destPath), { recursive: true })
|
|
28
36
|
await Bun.write(destPath, Bun.file(srcPath))
|
|
@@ -74,7 +82,11 @@ if (!projectName) {
|
|
|
74
82
|
|
|
75
83
|
${bold('Options:')}
|
|
76
84
|
--kit=${emerald('react|vue|svelte')} Frontend framework
|
|
77
|
-
--ui=${emerald('shadcn')}
|
|
85
|
+
--ui=${emerald('shadcn|tailwind')} UI component library
|
|
86
|
+
--theme=${emerald('default|minimal|workspace|corporate|starter')}
|
|
87
|
+
Dashboard theme (shadcn only)
|
|
88
|
+
--auth=${emerald('builtin|none')} Authentication setup
|
|
89
|
+
--with=${emerald('ai')} Optional packages (comma-separated)
|
|
78
90
|
--no-git Skip git initialization
|
|
79
91
|
--yes Accept defaults (non-interactive)
|
|
80
92
|
|
|
@@ -82,6 +94,8 @@ if (!projectName) {
|
|
|
82
94
|
bun create mantiq my-app
|
|
83
95
|
bun create mantiq my-app --kit=react
|
|
84
96
|
bun create mantiq my-app --kit=react --ui=shadcn
|
|
97
|
+
bun create mantiq my-app --kit=react --auth=none
|
|
98
|
+
bun create mantiq my-app --kit=react --with=ai
|
|
85
99
|
`)
|
|
86
100
|
process.exit(1)
|
|
87
101
|
}
|
|
@@ -97,7 +111,15 @@ if (existsSync(projectDir)) {
|
|
|
97
111
|
const term = new Terminal()
|
|
98
112
|
|
|
99
113
|
let kit: Kit | undefined = flags['kit'] as Kit | undefined
|
|
100
|
-
let ui: 'shadcn' | '
|
|
114
|
+
let ui: 'shadcn' | 'tailwind' = (flags['ui'] as string) === 'tailwind' ? 'tailwind' : 'shadcn'
|
|
115
|
+
const validThemes = ['default', 'minimal', 'workspace', 'corporate', 'starter'] as const
|
|
116
|
+
let theme: Theme = (validThemes as readonly string[]).includes(flags['theme'] as string)
|
|
117
|
+
? (flags['theme'] as Theme)
|
|
118
|
+
: 'default'
|
|
119
|
+
let auth: 'builtin' | 'none' = (flags['auth'] as string) === 'none' ? 'none' : 'builtin'
|
|
120
|
+
let optionalPackages: string[] = typeof flags['with'] === 'string'
|
|
121
|
+
? flags['with'].split(',').map(s => s.trim()).filter(Boolean)
|
|
122
|
+
: []
|
|
101
123
|
|
|
102
124
|
if (!isCI && !kit) {
|
|
103
125
|
// Show branded header
|
|
@@ -108,11 +130,48 @@ if (!isCI && !kit) {
|
|
|
108
130
|
{ value: 'react', label: 'React' },
|
|
109
131
|
{ value: 'vue', label: 'Vue' },
|
|
110
132
|
{ value: 'svelte', label: 'Svelte' },
|
|
111
|
-
{ value: 'none', label: '
|
|
133
|
+
{ value: 'none', label: 'Vanilla', hint: 'API only' },
|
|
112
134
|
])
|
|
113
135
|
|
|
114
136
|
kit = framework === 'none' ? undefined : framework as Kit
|
|
115
137
|
|
|
138
|
+
// UI kit selection (only if a frontend framework was chosen)
|
|
139
|
+
if (kit && !flags['ui']) {
|
|
140
|
+
const uiChoice = await term.select('Choose a UI kit', [
|
|
141
|
+
{ value: 'shadcn', label: 'shadcn + Tailwind' },
|
|
142
|
+
{ value: 'tailwind', label: 'Tailwind only' },
|
|
143
|
+
])
|
|
144
|
+
ui = uiChoice as 'shadcn' | 'tailwind'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Theme selection (only for shadcn)
|
|
148
|
+
if (kit && ui === 'shadcn' && !flags['theme']) {
|
|
149
|
+
const themeChoice = await term.select('Choose a theme', [
|
|
150
|
+
{ value: 'default', label: 'Default', hint: 'emerald, classic admin' },
|
|
151
|
+
{ value: 'minimal', label: 'Minimal', hint: 'clean & focused' },
|
|
152
|
+
{ value: 'workspace', label: 'Workspace', hint: 'warm & approachable' },
|
|
153
|
+
{ value: 'corporate', label: 'Corporate', hint: 'professional & data-rich' },
|
|
154
|
+
{ value: 'starter', label: 'Starter', hint: 'bold & marketing-ready' },
|
|
155
|
+
])
|
|
156
|
+
theme = themeChoice as Theme
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Authentication selection
|
|
160
|
+
if (!flags['auth']) {
|
|
161
|
+
const authChoice = await term.select('Authentication', [
|
|
162
|
+
{ value: 'builtin', label: 'Built-in', hint: 'session + token auth' },
|
|
163
|
+
{ value: 'none', label: 'None' },
|
|
164
|
+
])
|
|
165
|
+
auth = authChoice as 'builtin' | 'none'
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Optional packages selection
|
|
169
|
+
if (!flags['with']) {
|
|
170
|
+
optionalPackages = await term.multiSelect('Optional packages', [
|
|
171
|
+
{ value: 'ai', label: 'AI', hint: '@mantiq/ai' },
|
|
172
|
+
])
|
|
173
|
+
}
|
|
174
|
+
|
|
116
175
|
} else {
|
|
117
176
|
// Validate flags
|
|
118
177
|
if (kit && !validKits.includes(kit as Kit)) {
|
|
@@ -130,7 +189,16 @@ let fileCount = 0
|
|
|
130
189
|
// Step 1: Copy the skeleton directory as the base
|
|
131
190
|
const skeletonDir = resolve(import.meta.dir, '..', 'skeleton')
|
|
132
191
|
if (existsSync(skeletonDir)) {
|
|
133
|
-
|
|
192
|
+
// Build conditional skip set for skeleton files
|
|
193
|
+
const skeletonSkips = new Set<string>()
|
|
194
|
+
if (auth === 'none') {
|
|
195
|
+
skeletonSkips.add('config/auth.ts')
|
|
196
|
+
skeletonSkips.add('database/migrations/002_create_personal_access_tokens_table.ts')
|
|
197
|
+
}
|
|
198
|
+
if (!optionalPackages.includes('ai')) {
|
|
199
|
+
skeletonSkips.add('config/ai.ts')
|
|
200
|
+
}
|
|
201
|
+
fileCount += await copyDirectory(skeletonDir, projectDir, skeletonSkips)
|
|
134
202
|
} else {
|
|
135
203
|
// Fallback: skeleton not bundled (shouldn't happen in published package)
|
|
136
204
|
console.error(' Skeleton directory not found')
|
|
@@ -138,7 +206,7 @@ if (existsSync(skeletonDir)) {
|
|
|
138
206
|
|
|
139
207
|
// Step 2: Generate dynamic files (package.json, .env — overwrites skeleton versions)
|
|
140
208
|
const appKey = `base64:${randomBytes(32).toString('base64')}`
|
|
141
|
-
const templates = getTemplates({ name: projectName, appKey, kit, ui })
|
|
209
|
+
const templates = getTemplates({ name: projectName, appKey, kit, ui, theme, auth, optionalPackages })
|
|
142
210
|
for (const [relativePath, content] of Object.entries(templates)) {
|
|
143
211
|
const fullPath = `${projectDir}/${relativePath}`
|
|
144
212
|
mkdirSync(dirname(fullPath), { recursive: true })
|
|
@@ -156,9 +224,32 @@ if (kit) {
|
|
|
156
224
|
const kitManifest = manifest[kit]
|
|
157
225
|
const sharedManifest = manifest.shared
|
|
158
226
|
|
|
227
|
+
// Auth-related shared stubs to skip when auth === 'none'
|
|
228
|
+
const authSharedTargets = new Set([
|
|
229
|
+
'app/Http/Controllers/AuthController.ts',
|
|
230
|
+
'app/Http/Controllers/PageController.ts',
|
|
231
|
+
'app/Http/Requests/LoginRequest.ts',
|
|
232
|
+
'app/Http/Requests/RegisterRequest.ts',
|
|
233
|
+
'routes/web.ts',
|
|
234
|
+
'tests/feature/auth.test.ts',
|
|
235
|
+
])
|
|
236
|
+
|
|
237
|
+
// Targets that tailwind-only stubs will override — skip from shadcn kit
|
|
238
|
+
const tailwindOnlyManifest = manifest['tailwind-only']?.[kit]
|
|
239
|
+
const tailwindOverrideTargets = new Set<string>()
|
|
240
|
+
if (ui === 'tailwind' && tailwindOnlyManifest?.files) {
|
|
241
|
+
for (const { target } of tailwindOnlyManifest.files) {
|
|
242
|
+
tailwindOverrideTargets.add(target)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
159
246
|
// Kit-specific stubs (src/, vite.config.ts, etc.)
|
|
160
247
|
if (kitManifest?.files) {
|
|
161
248
|
for (const { stub, target } of kitManifest.files) {
|
|
249
|
+
// Skip components.json when ui === 'tailwind'
|
|
250
|
+
if (ui === 'tailwind' && stub === 'components.json.stub') continue
|
|
251
|
+
// Skip files that will be overridden by tailwind-only stubs
|
|
252
|
+
if (ui === 'tailwind' && tailwindOverrideTargets.has(target)) continue
|
|
162
253
|
const src = resolve(stubsDir, kit, stub)
|
|
163
254
|
const dest = resolve(projectDir, target)
|
|
164
255
|
mkdirSync(dirname(dest), { recursive: true })
|
|
@@ -167,6 +258,31 @@ if (kit) {
|
|
|
167
258
|
}
|
|
168
259
|
}
|
|
169
260
|
|
|
261
|
+
// Tailwind-only overlay: replace shadcn-dependent components with plain Tailwind versions
|
|
262
|
+
if (ui === 'tailwind' && tailwindOnlyManifest?.files) {
|
|
263
|
+
for (const { stub, target } of tailwindOnlyManifest.files) {
|
|
264
|
+
const src = resolve(stubsDir, 'tailwind-only', kit, stub)
|
|
265
|
+
const dest = resolve(projectDir, target)
|
|
266
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
267
|
+
await Bun.write(dest, Bun.file(src))
|
|
268
|
+
fileCount++
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Theme overlay: replace pages/layouts/styles with theme-specific variants
|
|
273
|
+
if (ui === 'shadcn' && theme !== 'default' && kit) {
|
|
274
|
+
const themeManifest = manifest.themes?.[theme]?.[kit]
|
|
275
|
+
if (themeManifest?.files) {
|
|
276
|
+
for (const { stub, target } of themeManifest.files) {
|
|
277
|
+
const src = resolve(stubsDir, 'themes', theme, kit, stub)
|
|
278
|
+
const dest = resolve(projectDir, target)
|
|
279
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
280
|
+
await Bun.write(dest, Bun.file(src))
|
|
281
|
+
fileCount++
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
170
286
|
// Shared stubs (routes, controllers, config — overwrites skeleton versions)
|
|
171
287
|
if (sharedManifest?.files) {
|
|
172
288
|
// Build placeholder map from manifest
|
|
@@ -178,6 +294,8 @@ if (kit) {
|
|
|
178
294
|
}
|
|
179
295
|
|
|
180
296
|
for (const { stub, target } of sharedManifest.files) {
|
|
297
|
+
// Skip auth-related stubs when auth === 'none'
|
|
298
|
+
if (auth === 'none' && authSharedTargets.has(target)) continue
|
|
181
299
|
const src = resolve(stubsDir, 'shared', stub)
|
|
182
300
|
const dest = resolve(projectDir, target)
|
|
183
301
|
mkdirSync(dirname(dest), { recursive: true })
|
|
@@ -189,10 +307,43 @@ if (kit) {
|
|
|
189
307
|
fileCount++
|
|
190
308
|
}
|
|
191
309
|
}
|
|
310
|
+
|
|
311
|
+
// Noauth overlay: replace auth-aware routes/controllers/models when auth === 'none'
|
|
312
|
+
if (auth === 'none') {
|
|
313
|
+
const noauthManifest = manifest.noauth
|
|
314
|
+
if (noauthManifest?.files) {
|
|
315
|
+
for (const { stub, target } of noauthManifest.files) {
|
|
316
|
+
const src = resolve(stubsDir, 'noauth', stub)
|
|
317
|
+
if (existsSync(src)) {
|
|
318
|
+
const dest = resolve(projectDir, target)
|
|
319
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
320
|
+
let content = await Bun.file(src).text()
|
|
321
|
+
// Apply shared placeholders
|
|
322
|
+
if (sharedManifest?.placeholders) {
|
|
323
|
+
for (const [key, values] of Object.entries(sharedManifest.placeholders)) {
|
|
324
|
+
const value = (values as Record<string, string>)[kit!] ?? ''
|
|
325
|
+
content = content.replaceAll(key, value)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
await Bun.write(dest, content)
|
|
329
|
+
fileCount++
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
192
334
|
}
|
|
193
335
|
} else {
|
|
194
336
|
// API-only: overlay token-based auth stubs
|
|
195
337
|
const stubsDir = resolve(import.meta.dir, '..', 'stubs')
|
|
338
|
+
|
|
339
|
+
// Auth-related API-only stubs to skip when auth === 'none'
|
|
340
|
+
const authApiOnlyTargets = new Set([
|
|
341
|
+
'app/Http/Controllers/ApiAuthController.ts',
|
|
342
|
+
'app/Http/Requests/RegisterRequest.ts',
|
|
343
|
+
'app/Http/Requests/LoginRequest.ts',
|
|
344
|
+
'tests/feature/token-auth.test.ts',
|
|
345
|
+
])
|
|
346
|
+
|
|
196
347
|
const apiOnlyFiles = [
|
|
197
348
|
{ stub: 'api-only/routes/api.ts.stub', target: 'routes/api.ts' },
|
|
198
349
|
{ stub: 'shared/app/Http/Controllers/ApiAuthController.ts.stub', target: 'app/Http/Controllers/ApiAuthController.ts' },
|
|
@@ -206,6 +357,8 @@ if (kit) {
|
|
|
206
357
|
{ stub: 'api-only/tests/feature/token-auth.test.ts.stub', target: 'tests/feature/token-auth.test.ts' },
|
|
207
358
|
]
|
|
208
359
|
for (const { stub, target } of apiOnlyFiles) {
|
|
360
|
+
// Skip auth-related stubs when auth === 'none'
|
|
361
|
+
if (auth === 'none' && authApiOnlyTargets.has(target)) continue
|
|
209
362
|
const src = resolve(stubsDir, stub)
|
|
210
363
|
if (existsSync(src)) {
|
|
211
364
|
const dest = resolve(projectDir, target)
|
|
@@ -214,6 +367,26 @@ if (kit) {
|
|
|
214
367
|
fileCount++
|
|
215
368
|
}
|
|
216
369
|
}
|
|
370
|
+
|
|
371
|
+
// Noauth overlay for API-only
|
|
372
|
+
if (auth === 'none') {
|
|
373
|
+
const manifestPath = resolve(stubsDir, 'manifest.json')
|
|
374
|
+
if (existsSync(manifestPath)) {
|
|
375
|
+
const manifest = JSON.parse(await Bun.file(manifestPath).text())
|
|
376
|
+
const noauthManifest = manifest.noauth
|
|
377
|
+
if (noauthManifest?.files) {
|
|
378
|
+
for (const { stub, target } of noauthManifest.files) {
|
|
379
|
+
const src = resolve(stubsDir, 'noauth', stub)
|
|
380
|
+
if (existsSync(src)) {
|
|
381
|
+
const dest = resolve(projectDir, target)
|
|
382
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
383
|
+
await Bun.write(dest, Bun.file(src))
|
|
384
|
+
fileCount++
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
217
390
|
}
|
|
218
391
|
console.log(` ${dim(`${fileCount} files created`)}`)
|
|
219
392
|
|
|
@@ -228,8 +401,8 @@ const install = Bun.spawn(['bun', 'install'], {
|
|
|
228
401
|
await install.exited
|
|
229
402
|
spin.stop('Dependencies installed')
|
|
230
403
|
|
|
231
|
-
// ── Install shadcn components
|
|
232
|
-
if (kit === 'react') {
|
|
404
|
+
// ── Install shadcn components ────────────────────────────────────────────────
|
|
405
|
+
if (kit === 'react' && ui === 'shadcn') {
|
|
233
406
|
const shadcnSpin = term.spinner('Installing shadcn/ui components')
|
|
234
407
|
|
|
235
408
|
const run = async (args: string[]) => {
|
|
@@ -287,8 +460,11 @@ if (!noGit) {
|
|
|
287
460
|
|
|
288
461
|
// ── Done ─────────────────────────────────────────────────────────────────────
|
|
289
462
|
console.log(`\n ${emerald('✓')} ${bold(projectName)} created\n`)
|
|
290
|
-
console.log(` ${dim('Framework')} ${kit ? bold(kit.charAt(0).toUpperCase() + kit.slice(1)) : dim('
|
|
291
|
-
if (kit
|
|
463
|
+
console.log(` ${dim('Framework')} ${kit ? bold(kit.charAt(0).toUpperCase() + kit.slice(1)) : dim('Vanilla (API only)')}`)
|
|
464
|
+
if (kit) console.log(` ${dim('UI Kit')} ${ui === 'shadcn' ? bold('shadcn/ui') : bold('Tailwind')}`)
|
|
465
|
+
if (kit && ui === 'shadcn') console.log(` ${dim('Theme')} ${bold(theme.charAt(0).toUpperCase() + theme.slice(1))}`)
|
|
466
|
+
console.log(` ${dim('Auth')} ${auth === 'builtin' ? bold('Built-in') : dim('None')}`)
|
|
467
|
+
if (optionalPackages.length > 0) console.log(` ${dim('Extras')} ${bold(optionalPackages.join(', '))}`)
|
|
292
468
|
console.log(`\n ${dim('Next steps:')}\n`)
|
|
293
469
|
console.log(` cd ${projectName}`)
|
|
294
470
|
console.log(` bun mantiq migrate`)
|
package/src/templates.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
export type Theme = 'default' | 'minimal' | 'workspace' | 'corporate' | 'starter'
|
|
2
|
+
|
|
1
3
|
export interface TemplateContext {
|
|
2
4
|
name: string
|
|
3
5
|
appKey: string
|
|
4
6
|
kit?: 'react' | 'vue' | 'svelte' | undefined
|
|
5
|
-
ui
|
|
7
|
+
ui: 'shadcn' | 'tailwind'
|
|
8
|
+
theme: Theme
|
|
9
|
+
auth: 'builtin' | 'none'
|
|
10
|
+
optionalPackages: string[]
|
|
6
11
|
}
|
|
7
12
|
|
|
8
13
|
/**
|
|
@@ -21,7 +26,7 @@ export function getTemplates(ctx: TemplateContext): Record<string, string> {
|
|
|
21
26
|
|
|
22
27
|
// ── package.json (always dynamic — name + deps) ────────────────────────
|
|
23
28
|
const baseDeps: Record<string, string> = {
|
|
24
|
-
'@mantiq/auth': '^0.5.0',
|
|
29
|
+
...(ctx.auth !== 'none' ? { '@mantiq/auth': '^0.5.0' } : {}),
|
|
25
30
|
'@mantiq/cli': '^0.5.0',
|
|
26
31
|
'@mantiq/core': '^0.5.0',
|
|
27
32
|
'@mantiq/database': '^0.5.0',
|
|
@@ -37,6 +42,7 @@ export function getTemplates(ctx: TemplateContext): Record<string, string> {
|
|
|
37
42
|
'@mantiq/notify': '^0.5.0',
|
|
38
43
|
'@mantiq/search': '^0.5.0',
|
|
39
44
|
'@mantiq/health': '^0.5.0',
|
|
45
|
+
...(ctx.optionalPackages.includes('ai') ? { '@mantiq/ai': '^0.5.0' } : {}),
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
const baseDevDeps: Record<string, string> = {
|
|
@@ -60,11 +66,23 @@ export function getTemplates(ctx: TemplateContext): Record<string, string> {
|
|
|
60
66
|
? { 'vue': '^3.5.0', '@vitejs/plugin-vue': '^6.0.0' }
|
|
61
67
|
: { 'svelte': '^5.0.0', '@sveltejs/vite-plugin-svelte': '^7.0.0' }
|
|
62
68
|
|
|
69
|
+
const shadcnOnly = ctx.ui === 'shadcn'
|
|
70
|
+
|
|
63
71
|
const uiDeps: Record<string, string> = ctx.kit === 'react'
|
|
64
|
-
? {
|
|
72
|
+
? {
|
|
73
|
+
'clsx': '^2.1.0', 'tailwind-merge': '^2.6.0',
|
|
74
|
+
...(shadcnOnly ? { 'class-variance-authority': '^0.7.1', 'lucide-react': '^0.577.0', 'radix-ui': '^1.4.0' } : {}),
|
|
75
|
+
}
|
|
65
76
|
: ctx.kit === 'vue'
|
|
66
|
-
? {
|
|
67
|
-
|
|
77
|
+
? {
|
|
78
|
+
'clsx': '^2.1.0', 'tailwind-merge': '^3.5.0',
|
|
79
|
+
...(shadcnOnly ? { 'class-variance-authority': '^0.7.1', 'lucide-vue-next': '^0.577.0', 'reka-ui': '^2.9.0' } : {}),
|
|
80
|
+
'tw-animate-css': '^1.4.0', '@tanstack/vue-table': '^8.0.0',
|
|
81
|
+
}
|
|
82
|
+
: {
|
|
83
|
+
'clsx': '^2.1.0', 'tailwind-merge': '^2.6.0',
|
|
84
|
+
...(shadcnOnly ? { 'tailwind-variants': '^3.2.0', 'lucide-svelte': '^0.577.0', '@lucide/svelte': '^0.577.0', 'bits-ui': '^2.16.0' } : {}),
|
|
85
|
+
}
|
|
68
86
|
|
|
69
87
|
Object.assign(baseDeps, {
|
|
70
88
|
'@mantiq/vite': '^0.5.0',
|
package/src/terminal.ts
CHANGED
|
@@ -96,6 +96,68 @@ export class Terminal {
|
|
|
96
96
|
})
|
|
97
97
|
}
|
|
98
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
|
+
|
|
99
161
|
/** Yes/No confirm prompt */
|
|
100
162
|
async confirm(label: string, defaultVal = false): Promise<boolean> {
|
|
101
163
|
const options: SelectOption[] = [
|
|
@@ -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
|
+
})
|