@basicbenframework/core 0.1.0
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/.github/workflows/publish.yml +35 -0
- package/README.md +588 -0
- package/bin/cli.js +8 -0
- package/create-basicben-app/index.js +205 -0
- package/create-basicben-app/package.json +30 -0
- package/create-basicben-app/template/.env.example +24 -0
- package/create-basicben-app/template/README.md +59 -0
- package/create-basicben-app/template/basicben.config.js +33 -0
- package/create-basicben-app/template/index.html +54 -0
- package/create-basicben-app/template/migrations/001_create_users.js +15 -0
- package/create-basicben-app/template/migrations/002_create_posts.js +18 -0
- package/create-basicben-app/template/public/.gitkeep +0 -0
- package/create-basicben-app/template/seeds/01_users.js +29 -0
- package/create-basicben-app/template/seeds/02_posts.js +43 -0
- package/create-basicben-app/template/src/client/components/Alert.jsx +11 -0
- package/create-basicben-app/template/src/client/components/Avatar.jsx +11 -0
- package/create-basicben-app/template/src/client/components/BackLink.jsx +10 -0
- package/create-basicben-app/template/src/client/components/Button.jsx +19 -0
- package/create-basicben-app/template/src/client/components/Card.jsx +10 -0
- package/create-basicben-app/template/src/client/components/Empty.jsx +6 -0
- package/create-basicben-app/template/src/client/components/Input.jsx +12 -0
- package/create-basicben-app/template/src/client/components/Loading.jsx +6 -0
- package/create-basicben-app/template/src/client/components/Logo.jsx +40 -0
- package/create-basicben-app/template/src/client/components/Nav/DarkModeToggle.jsx +23 -0
- package/create-basicben-app/template/src/client/components/Nav/DesktopNav.jsx +32 -0
- package/create-basicben-app/template/src/client/components/Nav/MobileNav.jsx +107 -0
- package/create-basicben-app/template/src/client/components/NavLink.jsx +10 -0
- package/create-basicben-app/template/src/client/components/PageHeader.jsx +8 -0
- package/create-basicben-app/template/src/client/components/PostCard.jsx +19 -0
- package/create-basicben-app/template/src/client/components/Textarea.jsx +12 -0
- package/create-basicben-app/template/src/client/components/ThemeContext.jsx +5 -0
- package/create-basicben-app/template/src/client/contexts/ToastContext.jsx +94 -0
- package/create-basicben-app/template/src/client/layouts/AppLayout.jsx +60 -0
- package/create-basicben-app/template/src/client/layouts/AuthLayout.jsx +33 -0
- package/create-basicben-app/template/src/client/layouts/DocsLayout.jsx +60 -0
- package/create-basicben-app/template/src/client/layouts/RootLayout.jsx +25 -0
- package/create-basicben-app/template/src/client/pages/Auth.jsx +55 -0
- package/create-basicben-app/template/src/client/pages/Authentication.jsx +236 -0
- package/create-basicben-app/template/src/client/pages/Database.jsx +426 -0
- package/create-basicben-app/template/src/client/pages/Feed.jsx +34 -0
- package/create-basicben-app/template/src/client/pages/FeedPost.jsx +37 -0
- package/create-basicben-app/template/src/client/pages/GettingStarted.jsx +136 -0
- package/create-basicben-app/template/src/client/pages/Home.jsx +206 -0
- package/create-basicben-app/template/src/client/pages/PostForm.jsx +69 -0
- package/create-basicben-app/template/src/client/pages/Posts.jsx +59 -0
- package/create-basicben-app/template/src/client/pages/Profile.jsx +68 -0
- package/create-basicben-app/template/src/client/pages/Routing.jsx +207 -0
- package/create-basicben-app/template/src/client/pages/Testing.jsx +251 -0
- package/create-basicben-app/template/src/client/pages/Validation.jsx +210 -0
- package/create-basicben-app/template/src/controllers/AuthController.js +81 -0
- package/create-basicben-app/template/src/controllers/HomeController.js +17 -0
- package/create-basicben-app/template/src/controllers/PostController.js +86 -0
- package/create-basicben-app/template/src/controllers/ProfileController.js +66 -0
- package/create-basicben-app/template/src/helpers/api.js +24 -0
- package/create-basicben-app/template/src/main.jsx +9 -0
- package/create-basicben-app/template/src/middleware/auth.js +16 -0
- package/create-basicben-app/template/src/models/Post.js +63 -0
- package/create-basicben-app/template/src/models/User.js +42 -0
- package/create-basicben-app/template/src/routes/App.jsx +38 -0
- package/create-basicben-app/template/src/routes/api/auth.js +7 -0
- package/create-basicben-app/template/src/routes/api/posts.js +15 -0
- package/create-basicben-app/template/src/routes/api/profile.js +8 -0
- package/create-basicben-app/template/src/server/index.js +16 -0
- package/create-basicben-app/template/vite.config.js +18 -0
- package/database.sqlite +0 -0
- package/my-test-app/.env.example +24 -0
- package/my-test-app/README.md +59 -0
- package/my-test-app/basicben.config.js +33 -0
- package/my-test-app/database.sqlite-shm +0 -0
- package/my-test-app/database.sqlite-wal +0 -0
- package/my-test-app/index.html +54 -0
- package/my-test-app/migrations/001_create_users.js +15 -0
- package/my-test-app/migrations/002_create_posts.js +18 -0
- package/my-test-app/package-lock.json +2160 -0
- package/my-test-app/package.json +29 -0
- package/my-test-app/public/.gitkeep +0 -0
- package/my-test-app/seeds/01_users.js +29 -0
- package/my-test-app/seeds/02_posts.js +43 -0
- package/my-test-app/src/client/components/Alert.jsx +11 -0
- package/my-test-app/src/client/components/Avatar.jsx +11 -0
- package/my-test-app/src/client/components/BackLink.jsx +10 -0
- package/my-test-app/src/client/components/Button.jsx +19 -0
- package/my-test-app/src/client/components/Card.jsx +10 -0
- package/my-test-app/src/client/components/Empty.jsx +6 -0
- package/my-test-app/src/client/components/Input.jsx +12 -0
- package/my-test-app/src/client/components/Loading.jsx +6 -0
- package/my-test-app/src/client/components/Logo.jsx +40 -0
- package/my-test-app/src/client/components/Nav/DarkModeToggle.jsx +23 -0
- package/my-test-app/src/client/components/Nav/DesktopNav.jsx +32 -0
- package/my-test-app/src/client/components/Nav/MobileNav.jsx +107 -0
- package/my-test-app/src/client/components/NavLink.jsx +10 -0
- package/my-test-app/src/client/components/PageHeader.jsx +8 -0
- package/my-test-app/src/client/components/PostCard.jsx +19 -0
- package/my-test-app/src/client/components/Textarea.jsx +12 -0
- package/my-test-app/src/client/components/ThemeContext.jsx +5 -0
- package/my-test-app/src/client/contexts/AppContext.jsx +13 -0
- package/my-test-app/src/client/contexts/ToastContext.jsx +94 -0
- package/my-test-app/src/client/layouts/AppLayout.jsx +60 -0
- package/my-test-app/src/client/layouts/AuthLayout.jsx +33 -0
- package/my-test-app/src/client/layouts/DocsLayout.jsx +60 -0
- package/my-test-app/src/client/layouts/RootLayout.jsx +25 -0
- package/my-test-app/src/client/pages/Auth.jsx +55 -0
- package/my-test-app/src/client/pages/Authentication.jsx +236 -0
- package/my-test-app/src/client/pages/Database.jsx +426 -0
- package/my-test-app/src/client/pages/Feed.jsx +34 -0
- package/my-test-app/src/client/pages/FeedPost.jsx +37 -0
- package/my-test-app/src/client/pages/GettingStarted.jsx +136 -0
- package/my-test-app/src/client/pages/Home.jsx +206 -0
- package/my-test-app/src/client/pages/PostForm.jsx +69 -0
- package/my-test-app/src/client/pages/Posts.jsx +59 -0
- package/my-test-app/src/client/pages/Profile.jsx +68 -0
- package/my-test-app/src/client/pages/Routing.jsx +207 -0
- package/my-test-app/src/client/pages/Testing.jsx +251 -0
- package/my-test-app/src/client/pages/Validation.jsx +210 -0
- package/my-test-app/src/controllers/AuthController.js +81 -0
- package/my-test-app/src/controllers/HomeController.js +17 -0
- package/my-test-app/src/controllers/PostController.js +86 -0
- package/my-test-app/src/controllers/ProfileController.js +66 -0
- package/my-test-app/src/helpers/api.js +24 -0
- package/my-test-app/src/main.jsx +9 -0
- package/my-test-app/src/middleware/auth.js +16 -0
- package/my-test-app/src/models/Post.js +63 -0
- package/my-test-app/src/models/User.js +42 -0
- package/my-test-app/src/routes/App.jsx +38 -0
- package/my-test-app/src/routes/api/auth.js +7 -0
- package/my-test-app/src/routes/api/posts.js +15 -0
- package/my-test-app/src/routes/api/profile.js +8 -0
- package/my-test-app/src/server/index.js +16 -0
- package/my-test-app/vite.config.js +18 -0
- package/package.json +61 -0
- package/scripts/test-app.sh +59 -0
- package/src/auth/jwt.js +195 -0
- package/src/auth/password.js +132 -0
- package/src/cli/colors.js +31 -0
- package/src/cli/dispatcher.js +168 -0
- package/src/cli/parser.js +91 -0
- package/src/client/context.js +4 -0
- package/src/client/hooks.js +50 -0
- package/src/client/index.js +3 -0
- package/src/client/router.js +184 -0
- package/src/commands/build.js +155 -0
- package/src/commands/dev.js +206 -0
- package/src/commands/help.js +84 -0
- package/src/commands/make-controller.js +36 -0
- package/src/commands/make-middleware.js +44 -0
- package/src/commands/make-migration.js +51 -0
- package/src/commands/make-model.js +38 -0
- package/src/commands/make-route.js +36 -0
- package/src/commands/make-seed.js +38 -0
- package/src/commands/migrate-fresh.js +32 -0
- package/src/commands/migrate-rollback.js +30 -0
- package/src/commands/migrate-status.js +41 -0
- package/src/commands/migrate.js +30 -0
- package/src/commands/seed.js +47 -0
- package/src/commands/start.js +69 -0
- package/src/commands/test.js +46 -0
- package/src/db/Grammar.js +125 -0
- package/src/db/QueryBuilder.js +476 -0
- package/src/db/adapters/neon.js +170 -0
- package/src/db/adapters/planetscale.js +146 -0
- package/src/db/adapters/postgres.js +166 -0
- package/src/db/adapters/sqlite.js +125 -0
- package/src/db/adapters/turso.js +165 -0
- package/src/db/index.js +156 -0
- package/src/db/migrator.js +250 -0
- package/src/db/seeder.js +124 -0
- package/src/index.js +12 -0
- package/src/scaffolding/index.js +152 -0
- package/src/server/body-parser.js +159 -0
- package/src/server/cors.js +63 -0
- package/src/server/default-entry.js +13 -0
- package/src/server/http.js +221 -0
- package/src/server/index.js +168 -0
- package/src/server/loader.js +128 -0
- package/src/server/router.js +281 -0
- package/src/server/static.js +139 -0
- package/src/validation/index.js +436 -0
- package/src/vite/config.js +49 -0
- package/stubs/controller.stub +48 -0
- package/stubs/middleware-auth.stub +29 -0
- package/stubs/middleware.stub +9 -0
- package/stubs/migration.stub +17 -0
- package/stubs/model.stub +77 -0
- package/stubs/route.stub +13 -0
- package/stubs/seed.stub +16 -0
- package/stubs/vite.config.stub +18 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure password hashing using node:crypto scrypt.
|
|
3
|
+
*
|
|
4
|
+
* Comparable to bcrypt/Argon2 used by Laravel and Next.js.
|
|
5
|
+
* - Deliberately slow (configurable cost)
|
|
6
|
+
* - Automatic random salt
|
|
7
|
+
* - Timing-safe comparison
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto'
|
|
11
|
+
import { promisify } from 'node:util'
|
|
12
|
+
|
|
13
|
+
const scryptAsync = promisify(scrypt)
|
|
14
|
+
|
|
15
|
+
// Default parameters (similar to bcrypt cost factor 10)
|
|
16
|
+
const DEFAULT_KEY_LENGTH = 64
|
|
17
|
+
const DEFAULT_COST = 16384 // N - CPU/memory cost (2^14)
|
|
18
|
+
const DEFAULT_BLOCK_SIZE = 8 // r
|
|
19
|
+
const DEFAULT_PARALLELIZATION = 1 // p
|
|
20
|
+
const SALT_LENGTH = 16
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Hash a password securely
|
|
24
|
+
*
|
|
25
|
+
* @param {string} password - Plain text password
|
|
26
|
+
* @param {Object} options - Optional parameters
|
|
27
|
+
* @returns {Promise<string>} - Hash in format: salt:hash:params
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const hash = await hashPassword('mysecretpassword')
|
|
31
|
+
* // Store hash in database
|
|
32
|
+
*/
|
|
33
|
+
export async function hashPassword(password, options = {}) {
|
|
34
|
+
const {
|
|
35
|
+
keyLength = DEFAULT_KEY_LENGTH,
|
|
36
|
+
cost = DEFAULT_COST,
|
|
37
|
+
blockSize = DEFAULT_BLOCK_SIZE,
|
|
38
|
+
parallelization = DEFAULT_PARALLELIZATION
|
|
39
|
+
} = options
|
|
40
|
+
|
|
41
|
+
const salt = randomBytes(SALT_LENGTH)
|
|
42
|
+
|
|
43
|
+
const derivedKey = await scryptAsync(password, salt, keyLength, {
|
|
44
|
+
N: cost,
|
|
45
|
+
r: blockSize,
|
|
46
|
+
p: parallelization
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Format: base64(salt):base64(hash):N:r:p:keylen
|
|
50
|
+
const params = `${cost}:${blockSize}:${parallelization}:${keyLength}`
|
|
51
|
+
return `${salt.toString('base64')}:${derivedKey.toString('base64')}:${params}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Verify a password against a hash
|
|
56
|
+
*
|
|
57
|
+
* @param {string} password - Plain text password to verify
|
|
58
|
+
* @param {string} hash - Stored hash from hashPassword()
|
|
59
|
+
* @returns {Promise<boolean>} - True if password matches
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const isValid = await verifyPassword('mysecretpassword', storedHash)
|
|
63
|
+
* if (!isValid) {
|
|
64
|
+
* // Invalid password
|
|
65
|
+
* }
|
|
66
|
+
*/
|
|
67
|
+
export async function verifyPassword(password, hash) {
|
|
68
|
+
if (!password || !hash) {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const parts = hash.split(':')
|
|
74
|
+
if (parts.length !== 6) {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const [saltB64, hashB64, costStr, blockSizeStr, parallelizationStr, keyLengthStr] = parts
|
|
79
|
+
|
|
80
|
+
const salt = Buffer.from(saltB64, 'base64')
|
|
81
|
+
const storedHash = Buffer.from(hashB64, 'base64')
|
|
82
|
+
const cost = parseInt(costStr, 10)
|
|
83
|
+
const blockSize = parseInt(blockSizeStr, 10)
|
|
84
|
+
const parallelization = parseInt(parallelizationStr, 10)
|
|
85
|
+
const keyLength = parseInt(keyLengthStr, 10)
|
|
86
|
+
|
|
87
|
+
const derivedKey = await scryptAsync(password, salt, keyLength, {
|
|
88
|
+
N: cost,
|
|
89
|
+
r: blockSize,
|
|
90
|
+
p: parallelization
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Timing-safe comparison to prevent timing attacks
|
|
94
|
+
return timingSafeEqual(storedHash, derivedKey)
|
|
95
|
+
} catch {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if a hash needs to be rehashed (e.g., cost factor increased)
|
|
102
|
+
*
|
|
103
|
+
* @param {string} hash - Stored hash
|
|
104
|
+
* @param {Object} options - Current desired parameters
|
|
105
|
+
* @returns {boolean} - True if hash should be regenerated
|
|
106
|
+
*/
|
|
107
|
+
export function needsRehash(hash, options = {}) {
|
|
108
|
+
const {
|
|
109
|
+
cost = DEFAULT_COST,
|
|
110
|
+
blockSize = DEFAULT_BLOCK_SIZE,
|
|
111
|
+
parallelization = DEFAULT_PARALLELIZATION,
|
|
112
|
+
keyLength = DEFAULT_KEY_LENGTH
|
|
113
|
+
} = options
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const parts = hash.split(':')
|
|
117
|
+
if (parts.length !== 6) {
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const [, , costStr, blockSizeStr, parallelizationStr, keyLengthStr] = parts
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
parseInt(costStr, 10) !== cost ||
|
|
125
|
+
parseInt(blockSizeStr, 10) !== blockSize ||
|
|
126
|
+
parseInt(parallelizationStr, 10) !== parallelization ||
|
|
127
|
+
parseInt(keyLengthStr, 10) !== keyLength
|
|
128
|
+
)
|
|
129
|
+
} catch {
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple ANSI color helpers. No Chalk needed.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const isColorSupported = process.stdout.isTTY && !process.env.NO_COLOR
|
|
6
|
+
|
|
7
|
+
const code = (open, close) => {
|
|
8
|
+
if (!isColorSupported) return (str) => str
|
|
9
|
+
return (str) => `\x1b[${open}m${str}\x1b[${close}m`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const bold = code(1, 22)
|
|
13
|
+
export const dim = code(2, 22)
|
|
14
|
+
export const italic = code(3, 23)
|
|
15
|
+
export const underline = code(4, 24)
|
|
16
|
+
|
|
17
|
+
export const red = code(31, 39)
|
|
18
|
+
export const green = code(32, 39)
|
|
19
|
+
export const yellow = code(33, 39)
|
|
20
|
+
export const blue = code(34, 39)
|
|
21
|
+
export const magenta = code(35, 39)
|
|
22
|
+
export const cyan = code(36, 39)
|
|
23
|
+
export const white = code(37, 39)
|
|
24
|
+
export const gray = code(90, 39)
|
|
25
|
+
|
|
26
|
+
// Semantic aliases
|
|
27
|
+
export const success = green
|
|
28
|
+
export const error = red
|
|
29
|
+
export const warn = yellow
|
|
30
|
+
export const info = cyan
|
|
31
|
+
export const muted = gray
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command dispatcher. Maps command names to handlers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { bold, red, cyan, yellow, dim } from './colors.js'
|
|
6
|
+
|
|
7
|
+
// Command registry
|
|
8
|
+
const commands = {
|
|
9
|
+
// Development
|
|
10
|
+
dev: () => import('../commands/dev.js'),
|
|
11
|
+
build: () => import('../commands/build.js'),
|
|
12
|
+
start: () => import('../commands/start.js'),
|
|
13
|
+
test: () => import('../commands/test.js'),
|
|
14
|
+
|
|
15
|
+
// Scaffolding
|
|
16
|
+
'make:controller': () => import('../commands/make-controller.js'),
|
|
17
|
+
'make:model': () => import('../commands/make-model.js'),
|
|
18
|
+
'make:route': () => import('../commands/make-route.js'),
|
|
19
|
+
'make:migration': () => import('../commands/make-migration.js'),
|
|
20
|
+
'make:middleware': () => import('../commands/make-middleware.js'),
|
|
21
|
+
|
|
22
|
+
// Database
|
|
23
|
+
migrate: () => import('../commands/migrate.js'),
|
|
24
|
+
'migrate:rollback': () => import('../commands/migrate-rollback.js'),
|
|
25
|
+
'migrate:fresh': () => import('../commands/migrate-fresh.js'),
|
|
26
|
+
'migrate:status': () => import('../commands/migrate-status.js'),
|
|
27
|
+
'migrate:make': () => import('../commands/make-migration.js'), // alias
|
|
28
|
+
|
|
29
|
+
// Seeding
|
|
30
|
+
seed: () => import('../commands/seed.js'),
|
|
31
|
+
'db:seed': () => import('../commands/seed.js'), // alias
|
|
32
|
+
'make:seed': () => import('../commands/make-seed.js'),
|
|
33
|
+
|
|
34
|
+
// Help
|
|
35
|
+
help: () => import('../commands/help.js')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Command metadata for help display
|
|
39
|
+
export const commandMeta = {
|
|
40
|
+
dev: {
|
|
41
|
+
description: 'Start development server (Vite + Node with hot reload)',
|
|
42
|
+
usage: 'basicben dev',
|
|
43
|
+
options: {
|
|
44
|
+
'--port <port>': 'API server port (default: 3001)'
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
build: {
|
|
48
|
+
description: 'Build client and server for production',
|
|
49
|
+
usage: 'basicben build',
|
|
50
|
+
options: {
|
|
51
|
+
'--static': 'Build client only (for static hosts like CF Pages)'
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
start: {
|
|
55
|
+
description: 'Start production server',
|
|
56
|
+
usage: 'basicben start',
|
|
57
|
+
options: {
|
|
58
|
+
'--port <port>': 'Server port (default: 3000)'
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
test: {
|
|
62
|
+
description: 'Run tests with Vitest',
|
|
63
|
+
usage: 'basicben test [files]',
|
|
64
|
+
options: {
|
|
65
|
+
'--watch, -w': 'Watch mode (re-run on changes)',
|
|
66
|
+
'--coverage': 'Generate coverage report',
|
|
67
|
+
'--ui': 'Open Vitest UI'
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
'make:controller': {
|
|
72
|
+
description: 'Generate a controller with CRUD methods',
|
|
73
|
+
usage: 'basicben make:controller <Name>',
|
|
74
|
+
example: 'basicben make:controller User'
|
|
75
|
+
},
|
|
76
|
+
'make:model': {
|
|
77
|
+
description: 'Generate a model with common database methods',
|
|
78
|
+
usage: 'basicben make:model <Name>',
|
|
79
|
+
example: 'basicben make:model User'
|
|
80
|
+
},
|
|
81
|
+
'make:route': {
|
|
82
|
+
description: 'Generate a route file with REST endpoints',
|
|
83
|
+
usage: 'basicben make:route <name>',
|
|
84
|
+
example: 'basicben make:route users'
|
|
85
|
+
},
|
|
86
|
+
'make:migration': {
|
|
87
|
+
description: 'Generate a timestamped migration file',
|
|
88
|
+
usage: 'basicben make:migration <name>',
|
|
89
|
+
example: 'basicben make:migration create_users_table'
|
|
90
|
+
},
|
|
91
|
+
'make:middleware': {
|
|
92
|
+
description: 'Generate middleware (includes auth template)',
|
|
93
|
+
usage: 'basicben make:middleware <name>',
|
|
94
|
+
example: 'basicben make:middleware auth'
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
migrate: {
|
|
98
|
+
description: 'Run all pending migrations',
|
|
99
|
+
usage: 'basicben migrate'
|
|
100
|
+
},
|
|
101
|
+
'migrate:rollback': {
|
|
102
|
+
description: 'Roll back the last migration batch',
|
|
103
|
+
usage: 'basicben migrate:rollback'
|
|
104
|
+
},
|
|
105
|
+
'migrate:fresh': {
|
|
106
|
+
description: 'Drop all tables and re-run all migrations',
|
|
107
|
+
usage: 'basicben migrate:fresh'
|
|
108
|
+
},
|
|
109
|
+
'migrate:status': {
|
|
110
|
+
description: 'Show which migrations have run',
|
|
111
|
+
usage: 'basicben migrate:status'
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
seed: {
|
|
115
|
+
description: 'Run database seeders',
|
|
116
|
+
usage: 'basicben seed [name]',
|
|
117
|
+
example: 'basicben seed users'
|
|
118
|
+
},
|
|
119
|
+
'make:seed': {
|
|
120
|
+
description: 'Generate a new seed file',
|
|
121
|
+
usage: 'basicben make:seed <name>',
|
|
122
|
+
example: 'basicben make:seed users'
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
help: {
|
|
126
|
+
description: 'Show help for a command',
|
|
127
|
+
usage: 'basicben help [command]'
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function dispatch(command, args, flags) {
|
|
132
|
+
// Version flag (check first)
|
|
133
|
+
if (flags.version || flags.v) {
|
|
134
|
+
const pkg = await import('../../package.json', { with: { type: 'json' } })
|
|
135
|
+
console.log(pkg.default.version)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// No command or help flag
|
|
140
|
+
if (!command || flags.help || flags.h) {
|
|
141
|
+
const helpModule = await commands.help()
|
|
142
|
+
return helpModule.run(args, flags)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Look up command
|
|
146
|
+
const loader = commands[command]
|
|
147
|
+
|
|
148
|
+
if (!loader) {
|
|
149
|
+
console.error(`\n${red('Error:')} Unknown command ${bold(command)}`)
|
|
150
|
+
console.error(`\nRun ${cyan('basicben help')} to see available commands.\n`)
|
|
151
|
+
process.exit(1)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const module = await loader()
|
|
156
|
+
await module.run(args, flags)
|
|
157
|
+
} catch (err) {
|
|
158
|
+
// Handle module not found (command not implemented yet)
|
|
159
|
+
if (err.code === 'ERR_MODULE_NOT_FOUND') {
|
|
160
|
+
console.error(`\n${yellow('Not implemented:')} ${bold(command)}`)
|
|
161
|
+
console.error(`${dim('This command is coming soon.')}\n`)
|
|
162
|
+
process.exit(1)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Re-throw other errors
|
|
166
|
+
throw err
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom argument parser. No Commander.js needed.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Commands: basicben dev, basicben make:controller
|
|
6
|
+
* - Positional args: basicben make:controller UserController
|
|
7
|
+
* - Flags: --port=3000, --verbose, -v
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export function parseArgs(argv) {
|
|
11
|
+
const result = {
|
|
12
|
+
command: null,
|
|
13
|
+
args: [],
|
|
14
|
+
flags: {}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
const arg = argv[i]
|
|
19
|
+
|
|
20
|
+
// Long flag with value: --port=3000
|
|
21
|
+
if (arg.startsWith('--') && arg.includes('=')) {
|
|
22
|
+
const [key, value] = arg.slice(2).split('=')
|
|
23
|
+
result.flags[key] = value
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Long flag: --verbose (boolean) or --port 3000 (with next arg)
|
|
28
|
+
if (arg.startsWith('--')) {
|
|
29
|
+
const key = arg.slice(2)
|
|
30
|
+
const next = argv[i + 1]
|
|
31
|
+
|
|
32
|
+
// If next arg exists and isn't a flag, use it as value
|
|
33
|
+
if (next && !next.startsWith('-')) {
|
|
34
|
+
result.flags[key] = next
|
|
35
|
+
i++ // skip next
|
|
36
|
+
} else {
|
|
37
|
+
result.flags[key] = true
|
|
38
|
+
}
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Short flags: -v, -p 3000
|
|
43
|
+
if (arg.startsWith('-') && arg.length === 2) {
|
|
44
|
+
const key = arg.slice(1)
|
|
45
|
+
const next = argv[i + 1]
|
|
46
|
+
|
|
47
|
+
if (next && !next.startsWith('-')) {
|
|
48
|
+
result.flags[key] = next
|
|
49
|
+
i++
|
|
50
|
+
} else {
|
|
51
|
+
result.flags[key] = true
|
|
52
|
+
}
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Combined short flags: -abc (multiple booleans)
|
|
57
|
+
if (arg.startsWith('-') && arg.length > 2) {
|
|
58
|
+
for (const char of arg.slice(1)) {
|
|
59
|
+
result.flags[char] = true
|
|
60
|
+
}
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// First non-flag is the command
|
|
65
|
+
if (!result.command) {
|
|
66
|
+
result.command = arg
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Everything else is a positional arg
|
|
71
|
+
result.args.push(arg)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return result
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Expand flag aliases to full names
|
|
79
|
+
*/
|
|
80
|
+
export function expandAliases(flags, aliases) {
|
|
81
|
+
const expanded = { ...flags }
|
|
82
|
+
|
|
83
|
+
for (const [short, long] of Object.entries(aliases)) {
|
|
84
|
+
if (expanded[short] !== undefined && expanded[long] === undefined) {
|
|
85
|
+
expanded[long] = expanded[short]
|
|
86
|
+
delete expanded[short]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return expanded
|
|
91
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import { RouterContext, AuthContext } from './context.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Access auth state and methods
|
|
6
|
+
* @returns {{ user: object|null, setUser: function, logout: function, loading: boolean }}
|
|
7
|
+
*/
|
|
8
|
+
export function useAuth() {
|
|
9
|
+
const context = useContext(AuthContext)
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error('useAuth must be used within createClientApp')
|
|
12
|
+
}
|
|
13
|
+
return context
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get navigation function
|
|
18
|
+
* @returns {function} navigate(path, options)
|
|
19
|
+
*/
|
|
20
|
+
export function useNavigate() {
|
|
21
|
+
const context = useContext(RouterContext)
|
|
22
|
+
if (!context) {
|
|
23
|
+
throw new Error('useNavigate must be used within createClientApp')
|
|
24
|
+
}
|
|
25
|
+
return context.navigate
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get current route params
|
|
30
|
+
* @returns {object} params object (e.g. { id: '123' })
|
|
31
|
+
*/
|
|
32
|
+
export function useParams() {
|
|
33
|
+
const context = useContext(RouterContext)
|
|
34
|
+
if (!context) {
|
|
35
|
+
throw new Error('useParams must be used within createClientApp')
|
|
36
|
+
}
|
|
37
|
+
return context.params
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get current path
|
|
42
|
+
* @returns {string} current pathname
|
|
43
|
+
*/
|
|
44
|
+
export function usePath() {
|
|
45
|
+
const context = useContext(RouterContext)
|
|
46
|
+
if (!context) {
|
|
47
|
+
throw new Error('usePath must be used within createClientApp')
|
|
48
|
+
}
|
|
49
|
+
return context.path
|
|
50
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, createElement } from 'react'
|
|
2
|
+
import { RouterContext, AuthContext } from './context.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create a client-side React app with routing
|
|
6
|
+
*
|
|
7
|
+
* @param {object} config
|
|
8
|
+
* @param {object} config.routes - Route definitions { path: Component | { component, auth?, guest?, layout? } }
|
|
9
|
+
* @param {function} [config.layout] - Default layout wrapper
|
|
10
|
+
* @param {function} [config.api] - API function for auth check (default: fetch /api/user)
|
|
11
|
+
* @param {function} [config.Loading] - Loading component
|
|
12
|
+
* @returns {function} React component
|
|
13
|
+
*/
|
|
14
|
+
export function createClientApp(config) {
|
|
15
|
+
const { routes, layout: DefaultLayout, api, Loading } = config
|
|
16
|
+
|
|
17
|
+
// Normalize routes to consistent format
|
|
18
|
+
const normalizedRoutes = Object.entries(routes).map(([path, value]) => {
|
|
19
|
+
const isSimple = typeof value === 'function'
|
|
20
|
+
return {
|
|
21
|
+
path,
|
|
22
|
+
pattern: pathToRegex(path),
|
|
23
|
+
component: isSimple ? value : value.component,
|
|
24
|
+
auth: isSimple ? false : value.auth || false,
|
|
25
|
+
guest: isSimple ? false : value.guest || false,
|
|
26
|
+
layout: isSimple ? null : value.layout || null,
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
function App() {
|
|
31
|
+
const [user, setUser] = useState(null)
|
|
32
|
+
const [loading, setLoading] = useState(true)
|
|
33
|
+
const [path, setPath] = useState(window.location.pathname)
|
|
34
|
+
|
|
35
|
+
// Match current route
|
|
36
|
+
const matchRoute = useCallback((pathname) => {
|
|
37
|
+
for (const route of normalizedRoutes) {
|
|
38
|
+
const match = pathname.match(route.pattern)
|
|
39
|
+
if (match) {
|
|
40
|
+
const routeParams = extractParams(route.path, match)
|
|
41
|
+
return { route, params: routeParams }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null
|
|
45
|
+
}, [])
|
|
46
|
+
|
|
47
|
+
// Navigate function
|
|
48
|
+
const navigate = useCallback((to, options = {}) => {
|
|
49
|
+
const { replace = false } = options
|
|
50
|
+
|
|
51
|
+
if (replace) {
|
|
52
|
+
window.history.replaceState({}, '', to)
|
|
53
|
+
} else {
|
|
54
|
+
window.history.pushState({}, '', to)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setPath(to)
|
|
58
|
+
window.scrollTo(0, 0)
|
|
59
|
+
}, [])
|
|
60
|
+
|
|
61
|
+
// Logout function
|
|
62
|
+
const logout = useCallback(() => {
|
|
63
|
+
localStorage.removeItem('token')
|
|
64
|
+
setUser(null)
|
|
65
|
+
navigate('/')
|
|
66
|
+
}, [navigate])
|
|
67
|
+
|
|
68
|
+
// Auth check on mount
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const token = localStorage.getItem('token')
|
|
71
|
+
if (!token) {
|
|
72
|
+
setLoading(false)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const checkAuth = api || defaultApi
|
|
77
|
+
checkAuth('/api/user')
|
|
78
|
+
.then(data => setUser(data.user))
|
|
79
|
+
.catch(() => localStorage.removeItem('token'))
|
|
80
|
+
.finally(() => setLoading(false))
|
|
81
|
+
}, [])
|
|
82
|
+
|
|
83
|
+
// Handle browser back/forward
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const handlePopState = () => {
|
|
86
|
+
setPath(window.location.pathname)
|
|
87
|
+
}
|
|
88
|
+
window.addEventListener('popstate', handlePopState)
|
|
89
|
+
return () => window.removeEventListener('popstate', handlePopState)
|
|
90
|
+
}, [])
|
|
91
|
+
|
|
92
|
+
// Loading state
|
|
93
|
+
if (loading) {
|
|
94
|
+
if (Loading) return createElement(Loading)
|
|
95
|
+
return createElement('div', {
|
|
96
|
+
style: { display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }
|
|
97
|
+
}, 'Loading...')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Find matching route
|
|
101
|
+
const matched = matchRoute(path)
|
|
102
|
+
if (!matched) {
|
|
103
|
+
return createElement('div', null, '404 - Not Found')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const { route, params } = matched
|
|
107
|
+
|
|
108
|
+
// Route guards
|
|
109
|
+
if (route.auth && !user) {
|
|
110
|
+
navigate('/login', { replace: true })
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
if (route.guest && user) {
|
|
114
|
+
navigate('/', { replace: true })
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Build page element
|
|
119
|
+
let wrapped = createElement(route.component)
|
|
120
|
+
|
|
121
|
+
// Apply layout: route-specific layout replaces default, or use default
|
|
122
|
+
const Layout = route.layout || DefaultLayout
|
|
123
|
+
if (Layout) {
|
|
124
|
+
wrapped = createElement(Layout, null, wrapped)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Provide context
|
|
128
|
+
return createElement(
|
|
129
|
+
AuthContext.Provider,
|
|
130
|
+
{ value: { user, setUser, logout, loading } },
|
|
131
|
+
createElement(
|
|
132
|
+
RouterContext.Provider,
|
|
133
|
+
{ value: { path, params, navigate } },
|
|
134
|
+
wrapped
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return App
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Convert route path to regex
|
|
144
|
+
* /posts/:id -> /^\/posts\/([^/]+)$/
|
|
145
|
+
*/
|
|
146
|
+
function pathToRegex(path) {
|
|
147
|
+
if (path === '*') return /^.*$/
|
|
148
|
+
|
|
149
|
+
const pattern = path
|
|
150
|
+
.replace(/\*/g, '.*')
|
|
151
|
+
.replace(/:(\w+)/g, '([^/]+)')
|
|
152
|
+
.replace(/\//g, '\\/')
|
|
153
|
+
|
|
154
|
+
return new RegExp(`^${pattern}$`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Extract params from match
|
|
159
|
+
*/
|
|
160
|
+
function extractParams(path, match) {
|
|
161
|
+
const params = {}
|
|
162
|
+
const paramNames = path.match(/:(\w+)/g) || []
|
|
163
|
+
|
|
164
|
+
paramNames.forEach((name, i) => {
|
|
165
|
+
params[name.slice(1)] = match[i + 1]
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
return params
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Default API function
|
|
173
|
+
*/
|
|
174
|
+
async function defaultApi(path) {
|
|
175
|
+
const token = localStorage.getItem('token')
|
|
176
|
+
const res = await fetch(path, {
|
|
177
|
+
headers: {
|
|
178
|
+
'Content-Type': 'application/json',
|
|
179
|
+
...(token ? { Authorization: `Bearer ${token}` } : {})
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
if (!res.ok) throw new Error('Request failed')
|
|
183
|
+
return res.json()
|
|
184
|
+
}
|