@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,66 @@
|
|
|
1
|
+
import { validate, rules } from '@basicbenframework/core/validation'
|
|
2
|
+
import { User } from '../models/User.js'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
function hashPassword(password) {
|
|
6
|
+
return createHash('sha256').update(password).digest('hex')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const ProfileController = {
|
|
10
|
+
async show(req, res) {
|
|
11
|
+
const user = await User.find(req.userId)
|
|
12
|
+
if (!user) {
|
|
13
|
+
return res.json({ error: 'User not found' }, 404)
|
|
14
|
+
}
|
|
15
|
+
res.json({
|
|
16
|
+
user: { id: user.id, name: user.name, email: user.email, created_at: user.created_at }
|
|
17
|
+
})
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
async update(req, res) {
|
|
21
|
+
const result = await validate(req.body, {
|
|
22
|
+
name: [rules.required, rules.string, rules.min(2)],
|
|
23
|
+
email: [rules.required, rules.email]
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if (result.fails()) {
|
|
27
|
+
return res.json({ errors: result.errors }, 422)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { name, email } = req.body
|
|
31
|
+
const user = await User.find(req.userId)
|
|
32
|
+
|
|
33
|
+
if (email !== user.email) {
|
|
34
|
+
const existing = await User.findByEmail(email)
|
|
35
|
+
if (existing) {
|
|
36
|
+
return res.json({ error: 'Email already taken' }, 400)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const updated = await User.update(req.userId, { name, email })
|
|
41
|
+
res.json({
|
|
42
|
+
user: { id: updated.id, name: updated.name, email: updated.email }
|
|
43
|
+
})
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async changePassword(req, res) {
|
|
47
|
+
const result = await validate(req.body, {
|
|
48
|
+
currentPassword: [rules.required],
|
|
49
|
+
newPassword: [rules.required, rules.min(8)]
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (result.fails()) {
|
|
53
|
+
return res.json({ errors: result.errors }, 422)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { currentPassword, newPassword } = req.body
|
|
57
|
+
const user = await User.find(req.userId)
|
|
58
|
+
|
|
59
|
+
if (user.password !== hashPassword(currentPassword)) {
|
|
60
|
+
return res.json({ error: 'Current password is incorrect' }, 400)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await User.update(req.userId, { password: hashPassword(newPassword) })
|
|
64
|
+
res.json({ message: 'Password updated successfully' })
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const api = async (path, options = {}) => {
|
|
2
|
+
const token = localStorage.getItem('token')
|
|
3
|
+
let res
|
|
4
|
+
try {
|
|
5
|
+
res = await fetch(path, {
|
|
6
|
+
...options,
|
|
7
|
+
headers: {
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
10
|
+
...options.headers
|
|
11
|
+
}
|
|
12
|
+
})
|
|
13
|
+
} catch {
|
|
14
|
+
throw new Error('Unable to connect to server')
|
|
15
|
+
}
|
|
16
|
+
let data
|
|
17
|
+
try {
|
|
18
|
+
data = await res.json()
|
|
19
|
+
} catch {
|
|
20
|
+
throw new Error(res.ok ? 'Invalid response from server' : `Server error (${res.status})`)
|
|
21
|
+
}
|
|
22
|
+
if (!res.ok) throw new Error(data.error || Object.values(data.errors || {})[0]?.[0] || 'Request failed')
|
|
23
|
+
return data
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { verifyJwt } from '@basicbenframework/core/auth'
|
|
2
|
+
|
|
3
|
+
export const auth = async (req, res, next) => {
|
|
4
|
+
const token = req.headers.authorization?.replace('Bearer ', '')
|
|
5
|
+
if (!token) {
|
|
6
|
+
return res.json({ error: 'Unauthorized' }, 401)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const payload = verifyJwt(token, process.env.APP_KEY)
|
|
10
|
+
if (!payload) {
|
|
11
|
+
return res.json({ error: 'Invalid token' }, 401)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
req.userId = payload.userId
|
|
15
|
+
next()
|
|
16
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getDb } from '@basicbenframework/core/db'
|
|
2
|
+
|
|
3
|
+
export const Post = {
|
|
4
|
+
async all() {
|
|
5
|
+
const db = await getDb()
|
|
6
|
+
return db.all('SELECT * FROM posts ORDER BY created_at DESC')
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
async find(id) {
|
|
10
|
+
const db = await getDb()
|
|
11
|
+
return db.get('SELECT * FROM posts WHERE id = ?', [id])
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
async findByUser(userId) {
|
|
15
|
+
const db = await getDb()
|
|
16
|
+
return db.all('SELECT * FROM posts WHERE user_id = ? ORDER BY created_at DESC', [userId])
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
async findPublished() {
|
|
20
|
+
const db = await getDb()
|
|
21
|
+
return db.all(`
|
|
22
|
+
SELECT posts.*, users.name as author_name
|
|
23
|
+
FROM posts
|
|
24
|
+
JOIN users ON posts.user_id = users.id
|
|
25
|
+
WHERE posts.published = 1
|
|
26
|
+
ORDER BY posts.created_at DESC
|
|
27
|
+
`)
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async findPublishedById(id) {
|
|
31
|
+
const db = await getDb()
|
|
32
|
+
return db.get(`
|
|
33
|
+
SELECT posts.*, users.name as author_name
|
|
34
|
+
FROM posts
|
|
35
|
+
JOIN users ON posts.user_id = users.id
|
|
36
|
+
WHERE posts.id = ? AND posts.published = 1
|
|
37
|
+
`, [id])
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async create(data) {
|
|
41
|
+
const db = await getDb()
|
|
42
|
+
const result = await db.run(
|
|
43
|
+
'INSERT INTO posts (user_id, title, content, published) VALUES (?, ?, ?, ?)',
|
|
44
|
+
[data.user_id, data.title, data.content, data.published ? 1 : 0]
|
|
45
|
+
)
|
|
46
|
+
return { id: result.lastInsertRowid, ...data }
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async update(id, data) {
|
|
50
|
+
const db = await getDb()
|
|
51
|
+
const fields = Object.keys(data).map(k => `${k} = ?`).join(', ')
|
|
52
|
+
await db.run(
|
|
53
|
+
`UPDATE posts SET ${fields}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
|
54
|
+
[...Object.values(data), id]
|
|
55
|
+
)
|
|
56
|
+
return this.find(id)
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
async delete(id) {
|
|
60
|
+
const db = await getDb()
|
|
61
|
+
return db.run('DELETE FROM posts WHERE id = ?', [id])
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getDb } from '@basicbenframework/core/db'
|
|
2
|
+
|
|
3
|
+
export const User = {
|
|
4
|
+
async all() {
|
|
5
|
+
const db = await getDb()
|
|
6
|
+
return db.all('SELECT * FROM users')
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
async find(id) {
|
|
10
|
+
const db = await getDb()
|
|
11
|
+
return db.get('SELECT * FROM users WHERE id = ?', [id])
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
async findByEmail(email) {
|
|
15
|
+
const db = await getDb()
|
|
16
|
+
return db.get('SELECT * FROM users WHERE email = ?', [email])
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
async create(data) {
|
|
20
|
+
const db = await getDb()
|
|
21
|
+
const result = await db.run(
|
|
22
|
+
'INSERT INTO users (name, email, password) VALUES (?, ?, ?)',
|
|
23
|
+
[data.name, data.email, data.password]
|
|
24
|
+
)
|
|
25
|
+
return { id: result.lastInsertRowid, ...data }
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
async update(id, data) {
|
|
29
|
+
const db = await getDb()
|
|
30
|
+
const fields = Object.keys(data).map(k => `${k} = ?`).join(', ')
|
|
31
|
+
await db.run(
|
|
32
|
+
`UPDATE users SET ${fields} WHERE id = ?`,
|
|
33
|
+
[...Object.values(data), id]
|
|
34
|
+
)
|
|
35
|
+
return this.find(id)
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async delete(id) {
|
|
39
|
+
const db = await getDb()
|
|
40
|
+
return db.run('DELETE FROM users WHERE id = ?', [id])
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createClientApp } from '@basicbenframework/core/client'
|
|
2
|
+
import { AppLayout } from '../client/layouts/AppLayout'
|
|
3
|
+
import { AuthLayout } from '../client/layouts/AuthLayout'
|
|
4
|
+
import { DocsLayout } from '../client/layouts/DocsLayout'
|
|
5
|
+
import { Home } from '../client/pages/Home'
|
|
6
|
+
import { Auth } from '../client/pages/Auth'
|
|
7
|
+
import { Feed } from '../client/pages/Feed'
|
|
8
|
+
import { FeedPost } from '../client/pages/FeedPost'
|
|
9
|
+
import { Posts } from '../client/pages/Posts'
|
|
10
|
+
import { PostForm } from '../client/pages/PostForm'
|
|
11
|
+
import { Profile } from '../client/pages/Profile'
|
|
12
|
+
import { GettingStarted } from '../client/pages/GettingStarted'
|
|
13
|
+
import { Database } from '../client/pages/Database'
|
|
14
|
+
import { Routing } from '../client/pages/Routing'
|
|
15
|
+
import { Authentication } from '../client/pages/Authentication'
|
|
16
|
+
import { Validation } from '../client/pages/Validation'
|
|
17
|
+
import { Testing } from '../client/pages/Testing'
|
|
18
|
+
|
|
19
|
+
export default createClientApp({
|
|
20
|
+
layout: AppLayout,
|
|
21
|
+
routes: {
|
|
22
|
+
'/': Home,
|
|
23
|
+
'/login': { component: Auth, layout: AuthLayout, guest: true },
|
|
24
|
+
'/register': { component: Auth, layout: AuthLayout, guest: true },
|
|
25
|
+
'/feed': Feed,
|
|
26
|
+
'/feed/:id': FeedPost,
|
|
27
|
+
'/posts': { component: Posts, auth: true },
|
|
28
|
+
'/posts/new': { component: PostForm, auth: true },
|
|
29
|
+
'/posts/:id/edit': { component: PostForm, auth: true },
|
|
30
|
+
'/profile': { component: Profile, auth: true },
|
|
31
|
+
'/docs': { component: GettingStarted, layout: DocsLayout },
|
|
32
|
+
'/docs/routing': { component: Routing, layout: DocsLayout },
|
|
33
|
+
'/docs/database': { component: Database, layout: DocsLayout },
|
|
34
|
+
'/docs/authentication': { component: Authentication, layout: DocsLayout },
|
|
35
|
+
'/docs/validation': { component: Validation, layout: DocsLayout },
|
|
36
|
+
'/docs/testing': { component: Testing, layout: DocsLayout },
|
|
37
|
+
}
|
|
38
|
+
})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { AuthController } from '../../controllers/AuthController.js'
|
|
2
|
+
|
|
3
|
+
export default (router) => {
|
|
4
|
+
router.post('/api/auth/register', AuthController.register)
|
|
5
|
+
router.post('/api/auth/login', AuthController.login)
|
|
6
|
+
router.get('/api/user', AuthController.user)
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PostController } from '../../controllers/PostController.js'
|
|
2
|
+
import { auth } from '../../middleware/auth.js'
|
|
3
|
+
|
|
4
|
+
export default (router) => {
|
|
5
|
+
// Public feed routes
|
|
6
|
+
router.get('/api/feed', PostController.feed)
|
|
7
|
+
router.get('/api/feed/:id', PostController.feedShow)
|
|
8
|
+
|
|
9
|
+
// Authenticated post routes
|
|
10
|
+
router.get('/api/posts', auth, PostController.index)
|
|
11
|
+
router.post('/api/posts', auth, PostController.store)
|
|
12
|
+
router.get('/api/posts/:id', auth, PostController.show)
|
|
13
|
+
router.put('/api/posts/:id', auth, PostController.update)
|
|
14
|
+
router.delete('/api/posts/:id', auth, PostController.destroy)
|
|
15
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ProfileController } from '../../controllers/ProfileController.js'
|
|
2
|
+
import { auth } from '../../middleware/auth.js'
|
|
3
|
+
|
|
4
|
+
export default (router) => {
|
|
5
|
+
router.get('/api/profile', auth, ProfileController.show)
|
|
6
|
+
router.put('/api/profile', auth, ProfileController.update)
|
|
7
|
+
router.put('/api/profile/password', auth, ProfileController.changePassword)
|
|
8
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server entry point (optional)
|
|
3
|
+
*
|
|
4
|
+
* Delete this file to use the default server.
|
|
5
|
+
* Customize here for websockets, custom middleware, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createServer } from '@basicbenframework/core/server'
|
|
9
|
+
|
|
10
|
+
const app = await createServer()
|
|
11
|
+
|
|
12
|
+
const port = process.env.PORT || 3001
|
|
13
|
+
|
|
14
|
+
app.listen(port, () => {
|
|
15
|
+
console.log(`Server running at http://localhost:${port}`)
|
|
16
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [react()],
|
|
6
|
+
server: {
|
|
7
|
+
port: parseInt(process.env.VITE_PORT || 3000),
|
|
8
|
+
proxy: {
|
|
9
|
+
'/api': {
|
|
10
|
+
target: `http://localhost:${process.env.PORT || 3001}`,
|
|
11
|
+
changeOrigin: true
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
build: {
|
|
16
|
+
outDir: 'dist/client'
|
|
17
|
+
}
|
|
18
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@basicbenframework/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A full-stack framework for React. Minimal dependencies, maximum clarity.",
|
|
5
|
+
"author": "ctmakes",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/BasicBenFramework/basicben-framework.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/BasicBenFramework/basicben-framework#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/BasicBenFramework/basicben-framework/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"basicben": "./bin/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./src/index.js",
|
|
20
|
+
"./server": "./src/server/index.js",
|
|
21
|
+
"./client": "./src/client/index.js",
|
|
22
|
+
"./validation": "./src/validation/index.js",
|
|
23
|
+
"./auth": "./src/auth/jwt.js",
|
|
24
|
+
"./db": "./src/db/index.js"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=24.14.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node --test src/**/*.test.js",
|
|
31
|
+
"test:db": "node --test src/db/**/*.test.js",
|
|
32
|
+
"test:validation": "node --test src/validation/**/*.test.js",
|
|
33
|
+
"test:auth": "node --test src/auth/**/*.test.js",
|
|
34
|
+
"test:server": "node --test src/server/**/*.test.js",
|
|
35
|
+
"test:cli": "node --test src/cli/**/*.test.js",
|
|
36
|
+
"test:scaffolding": "node --test src/scaffolding/**/*.test.js",
|
|
37
|
+
"test:app": "./scripts/test-app.sh"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"react",
|
|
41
|
+
"framework",
|
|
42
|
+
"fullstack",
|
|
43
|
+
"vite",
|
|
44
|
+
"minimal"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"dependencies": {},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"react": ">=18",
|
|
50
|
+
"react-dom": ">=18",
|
|
51
|
+
"vite": ">=7",
|
|
52
|
+
"@vitejs/plugin-react": ">=5"
|
|
53
|
+
},
|
|
54
|
+
"optionalDependencies": {
|
|
55
|
+
"better-sqlite3": ">=12",
|
|
56
|
+
"pg": ">=8",
|
|
57
|
+
"@libsql/client": ">=0.17",
|
|
58
|
+
"@planetscale/database": ">=1",
|
|
59
|
+
"@neondatabase/serverless": ">=1.0"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Test script for BasicBen framework
|
|
4
|
+
# Deletes my-test-app, creates a fresh one, and runs migrations
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
10
|
+
|
|
11
|
+
echo "=== BasicBen Test App Setup ==="
|
|
12
|
+
echo ""
|
|
13
|
+
|
|
14
|
+
# Step 1: Delete existing my-test-app
|
|
15
|
+
if [ -d "$ROOT_DIR/my-test-app" ]; then
|
|
16
|
+
echo "Removing existing my-test-app..."
|
|
17
|
+
rm -rf "$ROOT_DIR/my-test-app"
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Step 2: Create new test app with --local flag
|
|
21
|
+
echo "Creating new test app..."
|
|
22
|
+
cd "$ROOT_DIR"
|
|
23
|
+
node create-basicben-app/index.js my-test-app --local
|
|
24
|
+
|
|
25
|
+
# Step 3: Configure .env with port 3002 for frontend
|
|
26
|
+
echo "Configuring .env..."
|
|
27
|
+
APP_KEY=$(openssl rand -base64 32)
|
|
28
|
+
cat > "$ROOT_DIR/my-test-app/.env" << EOF
|
|
29
|
+
# Application
|
|
30
|
+
APP_KEY=$APP_KEY
|
|
31
|
+
|
|
32
|
+
# Server Ports
|
|
33
|
+
PORT=3001 # API server
|
|
34
|
+
VITE_PORT=3002 # Frontend dev server
|
|
35
|
+
|
|
36
|
+
# Database
|
|
37
|
+
DATABASE_URL=./database.sqlite
|
|
38
|
+
EOF
|
|
39
|
+
|
|
40
|
+
# Step 4: Install dependencies
|
|
41
|
+
echo ""
|
|
42
|
+
echo "Installing dependencies..."
|
|
43
|
+
cd "$ROOT_DIR/my-test-app"
|
|
44
|
+
npm install
|
|
45
|
+
|
|
46
|
+
# Step 5: Run migrations
|
|
47
|
+
echo ""
|
|
48
|
+
echo "Running migrations..."
|
|
49
|
+
npm run migrate
|
|
50
|
+
|
|
51
|
+
echo ""
|
|
52
|
+
echo "=== Setup Complete ==="
|
|
53
|
+
echo ""
|
|
54
|
+
echo "To start the dev server:"
|
|
55
|
+
echo " cd my-test-app"
|
|
56
|
+
echo " npm run dev"
|
|
57
|
+
echo ""
|
|
58
|
+
echo "Frontend: http://localhost:3002"
|
|
59
|
+
echo "API: http://localhost:3001"
|
package/src/auth/jwt.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth helpers using node:crypto.
|
|
3
|
+
* No jsonwebtoken or bcrypt dependencies needed.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createHmac, timingSafeEqual } from 'node:crypto'
|
|
7
|
+
|
|
8
|
+
// Re-export password helpers
|
|
9
|
+
export { hashPassword, verifyPassword, needsRehash } from './password.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Sign a JWT token
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} payload - Data to encode in the token
|
|
15
|
+
* @param {string} secret - Secret key for signing
|
|
16
|
+
* @param {Object} options - Options (expiresIn)
|
|
17
|
+
* @returns {string} - JWT token
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const token = signJwt({ userId: 1 }, process.env.APP_KEY, { expiresIn: '7d' })
|
|
21
|
+
*/
|
|
22
|
+
export function signJwt(payload, secret, options = {}) {
|
|
23
|
+
if (!secret) {
|
|
24
|
+
throw new Error('JWT secret is required')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const header = {
|
|
28
|
+
alg: 'HS256',
|
|
29
|
+
typ: 'JWT'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const now = Math.floor(Date.now() / 1000)
|
|
33
|
+
|
|
34
|
+
const tokenPayload = {
|
|
35
|
+
...payload,
|
|
36
|
+
iat: now
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Handle expiration
|
|
40
|
+
if (options.expiresIn) {
|
|
41
|
+
tokenPayload.exp = now + parseExpiry(options.expiresIn)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const encodedHeader = base64UrlEncode(JSON.stringify(header))
|
|
45
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(tokenPayload))
|
|
46
|
+
|
|
47
|
+
const signature = sign(`${encodedHeader}.${encodedPayload}`, secret)
|
|
48
|
+
|
|
49
|
+
return `${encodedHeader}.${encodedPayload}.${signature}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Verify and decode a JWT token
|
|
54
|
+
*
|
|
55
|
+
* @param {string} token - JWT token to verify
|
|
56
|
+
* @param {string} secret - Secret key used for signing
|
|
57
|
+
* @returns {Object|null} - Decoded payload or null if invalid
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* const payload = verifyJwt(token, process.env.APP_KEY)
|
|
61
|
+
* if (!payload) {
|
|
62
|
+
* // Invalid or expired token
|
|
63
|
+
* }
|
|
64
|
+
*/
|
|
65
|
+
export function verifyJwt(token, secret) {
|
|
66
|
+
if (!token || !secret) {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const parts = token.split('.')
|
|
71
|
+
if (parts.length !== 3) {
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const [encodedHeader, encodedPayload, signature] = parts
|
|
76
|
+
|
|
77
|
+
// Verify signature
|
|
78
|
+
const expectedSignature = sign(`${encodedHeader}.${encodedPayload}`, secret)
|
|
79
|
+
|
|
80
|
+
if (!safeCompare(signature, expectedSignature)) {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Decode payload
|
|
85
|
+
try {
|
|
86
|
+
const payload = JSON.parse(base64UrlDecode(encodedPayload))
|
|
87
|
+
|
|
88
|
+
// Check expiration
|
|
89
|
+
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return payload
|
|
94
|
+
} catch {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Decode a JWT token without verification
|
|
101
|
+
* Useful for debugging or reading claims before verification
|
|
102
|
+
*
|
|
103
|
+
* @param {string} token - JWT token
|
|
104
|
+
* @returns {Object|null} - Decoded payload or null if malformed
|
|
105
|
+
*/
|
|
106
|
+
export function decodeJwt(token) {
|
|
107
|
+
if (!token) return null
|
|
108
|
+
|
|
109
|
+
const parts = token.split('.')
|
|
110
|
+
if (parts.length !== 3) return null
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(base64UrlDecode(parts[1]))
|
|
114
|
+
} catch {
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Create HMAC-SHA256 signature
|
|
121
|
+
*/
|
|
122
|
+
function sign(data, secret) {
|
|
123
|
+
return base64UrlEncode(
|
|
124
|
+
createHmac('sha256', secret).update(data).digest()
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Base64URL encode (URL-safe base64)
|
|
130
|
+
*/
|
|
131
|
+
function base64UrlEncode(data) {
|
|
132
|
+
const base64 = Buffer.isBuffer(data)
|
|
133
|
+
? data.toString('base64')
|
|
134
|
+
: Buffer.from(data).toString('base64')
|
|
135
|
+
|
|
136
|
+
return base64
|
|
137
|
+
.replace(/=/g, '')
|
|
138
|
+
.replace(/\+/g, '-')
|
|
139
|
+
.replace(/\//g, '_')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Base64URL decode
|
|
144
|
+
*/
|
|
145
|
+
function base64UrlDecode(str) {
|
|
146
|
+
// Add padding if needed
|
|
147
|
+
const pad = str.length % 4
|
|
148
|
+
const padded = pad ? str + '='.repeat(4 - pad) : str
|
|
149
|
+
|
|
150
|
+
// Convert URL-safe chars back
|
|
151
|
+
const base64 = padded.replace(/-/g, '+').replace(/_/g, '/')
|
|
152
|
+
|
|
153
|
+
return Buffer.from(base64, 'base64').toString('utf8')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Timing-safe string comparison
|
|
158
|
+
*/
|
|
159
|
+
function safeCompare(a, b) {
|
|
160
|
+
if (a.length !== b.length) {
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const bufA = Buffer.from(a)
|
|
165
|
+
const bufB = Buffer.from(b)
|
|
166
|
+
|
|
167
|
+
return timingSafeEqual(bufA, bufB)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Parse expiry string to seconds
|
|
172
|
+
* Supports: 60 (seconds), '60s', '5m', '2h', '7d'
|
|
173
|
+
*/
|
|
174
|
+
function parseExpiry(expiry) {
|
|
175
|
+
if (typeof expiry === 'number') {
|
|
176
|
+
return expiry
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const match = expiry.match(/^(\d+)(s|m|h|d)?$/)
|
|
180
|
+
if (!match) {
|
|
181
|
+
throw new Error(`Invalid expiry format: ${expiry}`)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const value = parseInt(match[1], 10)
|
|
185
|
+
const unit = match[2] || 's'
|
|
186
|
+
|
|
187
|
+
const multipliers = {
|
|
188
|
+
s: 1,
|
|
189
|
+
m: 60,
|
|
190
|
+
h: 60 * 60,
|
|
191
|
+
d: 60 * 60 * 24
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return value * multipliers[unit]
|
|
195
|
+
}
|