create-hazejs 0.0.0-stage → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +17 -2
- package/bin/create-hazejs.mjs +2 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +74 -0
- package/dist/scaffold.d.mts +29 -0
- package/dist/scaffold.mjs +76 -0
- package/package.json +77 -3
- package/templates/api/.env +4 -0
- package/templates/api/.env.test +1 -0
- package/templates/api/README.md +383 -0
- package/templates/api/_gitignore +9 -0
- package/templates/api/app/controller/AuthController.ts +45 -0
- package/templates/api/app/controller/GreetingController.ts +23 -0
- package/templates/api/app/controller/UserController.ts +38 -0
- package/templates/api/app/event/UserRegistered.ts +6 -0
- package/templates/api/app/exception/Handler.ts +13 -0
- package/templates/api/app/job/SendWelcomeEmail.ts +19 -0
- package/templates/api/app/listener/BroadcastRegistration.ts +7 -0
- package/templates/api/app/listener/QueueWelcomeEmail.ts +6 -0
- package/templates/api/app/mail/WelcomeMail.ts +17 -0
- package/templates/api/app/model/User.ts +11 -0
- package/templates/api/app/policy/UserPolicy.ts +14 -0
- package/templates/api/app/provider/AppProvider.ts +9 -0
- package/templates/api/app/provider/DatabaseProvider.ts +4 -0
- package/templates/api/app/response/Formatter.ts +17 -0
- package/templates/api/app/service/GreetingService.ts +6 -0
- package/templates/api/app/validate/auth.ts +20 -0
- package/templates/api/app/validate/greeting.ts +5 -0
- package/templates/api/app/validate/user.ts +12 -0
- package/templates/api/config/app.ts +7 -0
- package/templates/api/config/auth.ts +23 -0
- package/templates/api/config/cache.ts +23 -0
- package/templates/api/config/cors.ts +11 -0
- package/templates/api/config/database.ts +17 -0
- package/templates/api/config/filesystem.ts +32 -0
- package/templates/api/config/mail.ts +31 -0
- package/templates/api/config/middleware.ts +10 -0
- package/templates/api/config/queue.ts +21 -0
- package/templates/api/config/throttle.ts +12 -0
- package/templates/api/config/websocket.ts +12 -0
- package/templates/api/database/migrations/0000_create_users.sql +9 -0
- package/templates/api/database/migrations/0001_add_user_password.sql +1 -0
- package/templates/api/database/migrations/0002_add_user_role.sql +1 -0
- package/templates/api/database/migrations/meta/0000_snapshot.json +71 -0
- package/templates/api/database/migrations/meta/0001_snapshot.json +78 -0
- package/templates/api/database/migrations/meta/0002_snapshot.json +86 -0
- package/templates/api/database/migrations/meta/_journal.json +27 -0
- package/templates/api/database/schema/users.ts +17 -0
- package/templates/api/database/seeders/01-users.ts +15 -0
- package/templates/api/haze.config.ts +5 -0
- package/templates/api/package.json +38 -0
- package/templates/api/route/api.ts +22 -0
- package/templates/api/route/ws.ts +18 -0
- package/templates/api/schedule/app.ts +11 -0
- package/templates/api/tests/auth.test.ts +77 -0
- package/templates/api/tests/greeting.test.ts +29 -0
- package/templates/api/tests/user.test.ts +58 -0
- package/templates/api/tsconfig.json +34 -0
- package/templates/api/vitest.config.ts +4 -0
- package/templates/react/.env +8 -0
- package/templates/react/.env.test +1 -0
- package/templates/react/README.md +374 -0
- package/templates/react/_gitignore +9 -0
- package/templates/react/haze.config.ts +8 -0
- package/templates/react/index.html +12 -0
- package/templates/react/package.json +46 -0
- package/templates/react/server/app/controller/AuthController.ts +45 -0
- package/templates/react/server/app/controller/GreetingController.ts +23 -0
- package/templates/react/server/app/controller/UserController.ts +38 -0
- package/templates/react/server/app/event/UserRegistered.ts +6 -0
- package/templates/react/server/app/exception/Handler.ts +13 -0
- package/templates/react/server/app/job/SendWelcomeEmail.ts +19 -0
- package/templates/react/server/app/listener/BroadcastRegistration.ts +7 -0
- package/templates/react/server/app/listener/QueueWelcomeEmail.ts +6 -0
- package/templates/react/server/app/mail/WelcomeMail.ts +17 -0
- package/templates/react/server/app/model/User.ts +11 -0
- package/templates/react/server/app/policy/UserPolicy.ts +14 -0
- package/templates/react/server/app/provider/AppProvider.ts +9 -0
- package/templates/react/server/app/provider/DatabaseProvider.ts +4 -0
- package/templates/react/server/app/provider/SocketProvider.ts +4 -0
- package/templates/react/server/app/response/Formatter.ts +17 -0
- package/templates/react/server/app/service/GreetingService.ts +6 -0
- package/templates/react/server/app/socket/chat.ts +17 -0
- package/templates/react/server/app/validate/auth.ts +20 -0
- package/templates/react/server/app/validate/greeting.ts +5 -0
- package/templates/react/server/app/validate/user.ts +12 -0
- package/templates/react/server/config/app.ts +7 -0
- package/templates/react/server/config/auth.ts +23 -0
- package/templates/react/server/config/cache.ts +23 -0
- package/templates/react/server/config/database.ts +17 -0
- package/templates/react/server/config/filesystem.ts +32 -0
- package/templates/react/server/config/mail.ts +31 -0
- package/templates/react/server/config/middleware.ts +11 -0
- package/templates/react/server/config/queue.ts +21 -0
- package/templates/react/server/config/socketio.ts +13 -0
- package/templates/react/server/config/throttle.ts +12 -0
- package/templates/react/server/config/websocket.ts +12 -0
- package/templates/react/server/database/migrations/0000_create_users.sql +9 -0
- package/templates/react/server/database/migrations/0001_add_user_password.sql +1 -0
- package/templates/react/server/database/migrations/0002_add_user_role.sql +1 -0
- package/templates/react/server/database/migrations/meta/0000_snapshot.json +71 -0
- package/templates/react/server/database/migrations/meta/0001_snapshot.json +78 -0
- package/templates/react/server/database/migrations/meta/0002_snapshot.json +86 -0
- package/templates/react/server/database/migrations/meta/_journal.json +27 -0
- package/templates/react/server/database/schema/users.ts +17 -0
- package/templates/react/server/database/seeders/01-users.ts +15 -0
- package/templates/react/server/route/api.ts +24 -0
- package/templates/react/server/route/ws.ts +18 -0
- package/templates/react/server/schedule/app.ts +11 -0
- package/templates/react/src/App.css +22 -0
- package/templates/react/src/App.tsx +72 -0
- package/templates/react/src/api.ts +48 -0
- package/templates/react/src/main.tsx +9 -0
- package/templates/react/src/socket.ts +5 -0
- package/templates/react/src/vite-env.d.ts +5 -0
- package/templates/react/tests/auth.test.ts +77 -0
- package/templates/react/tests/greeting.test.ts +29 -0
- package/templates/react/tests/user.test.ts +58 -0
- package/templates/react/tsconfig.app.json +22 -0
- package/templates/react/tsconfig.json +7 -0
- package/templates/react/tsconfig.server.json +34 -0
- package/templates/react/vite.config.ts +18 -0
- package/templates/react/vitest.config.ts +8 -0
- package/templates/vue/.env +8 -0
- package/templates/vue/.env.test +1 -0
- package/templates/vue/README.md +374 -0
- package/templates/vue/_gitignore +9 -0
- package/templates/vue/haze.config.ts +8 -0
- package/templates/vue/index.html +12 -0
- package/templates/vue/package.json +44 -0
- package/templates/vue/server/app/controller/AuthController.ts +45 -0
- package/templates/vue/server/app/controller/GreetingController.ts +23 -0
- package/templates/vue/server/app/controller/UserController.ts +38 -0
- package/templates/vue/server/app/event/UserRegistered.ts +6 -0
- package/templates/vue/server/app/exception/Handler.ts +13 -0
- package/templates/vue/server/app/job/SendWelcomeEmail.ts +19 -0
- package/templates/vue/server/app/listener/BroadcastRegistration.ts +7 -0
- package/templates/vue/server/app/listener/QueueWelcomeEmail.ts +6 -0
- package/templates/vue/server/app/mail/WelcomeMail.ts +17 -0
- package/templates/vue/server/app/model/User.ts +11 -0
- package/templates/vue/server/app/policy/UserPolicy.ts +14 -0
- package/templates/vue/server/app/provider/AppProvider.ts +9 -0
- package/templates/vue/server/app/provider/DatabaseProvider.ts +4 -0
- package/templates/vue/server/app/provider/SocketProvider.ts +4 -0
- package/templates/vue/server/app/response/Formatter.ts +17 -0
- package/templates/vue/server/app/service/GreetingService.ts +6 -0
- package/templates/vue/server/app/socket/chat.ts +17 -0
- package/templates/vue/server/app/validate/auth.ts +20 -0
- package/templates/vue/server/app/validate/greeting.ts +5 -0
- package/templates/vue/server/app/validate/user.ts +12 -0
- package/templates/vue/server/config/app.ts +7 -0
- package/templates/vue/server/config/auth.ts +23 -0
- package/templates/vue/server/config/cache.ts +23 -0
- package/templates/vue/server/config/database.ts +17 -0
- package/templates/vue/server/config/filesystem.ts +32 -0
- package/templates/vue/server/config/mail.ts +31 -0
- package/templates/vue/server/config/middleware.ts +11 -0
- package/templates/vue/server/config/queue.ts +21 -0
- package/templates/vue/server/config/socketio.ts +13 -0
- package/templates/vue/server/config/throttle.ts +12 -0
- package/templates/vue/server/config/websocket.ts +12 -0
- package/templates/vue/server/database/migrations/0000_create_users.sql +9 -0
- package/templates/vue/server/database/migrations/0001_add_user_password.sql +1 -0
- package/templates/vue/server/database/migrations/0002_add_user_role.sql +1 -0
- package/templates/vue/server/database/migrations/meta/0000_snapshot.json +71 -0
- package/templates/vue/server/database/migrations/meta/0001_snapshot.json +78 -0
- package/templates/vue/server/database/migrations/meta/0002_snapshot.json +86 -0
- package/templates/vue/server/database/migrations/meta/_journal.json +27 -0
- package/templates/vue/server/database/schema/users.ts +17 -0
- package/templates/vue/server/database/seeders/01-users.ts +15 -0
- package/templates/vue/server/route/api.ts +24 -0
- package/templates/vue/server/route/ws.ts +18 -0
- package/templates/vue/server/schedule/app.ts +11 -0
- package/templates/vue/src/App.vue +92 -0
- package/templates/vue/src/api.ts +48 -0
- package/templates/vue/src/env.d.ts +5 -0
- package/templates/vue/src/main.ts +4 -0
- package/templates/vue/src/socket.ts +5 -0
- package/templates/vue/tests/auth.test.ts +77 -0
- package/templates/vue/tests/greeting.test.ts +29 -0
- package/templates/vue/tests/user.test.ts +58 -0
- package/templates/vue/tsconfig.app.json +22 -0
- package/templates/vue/tsconfig.json +7 -0
- package/templates/vue/tsconfig.server.json +34 -0
- package/templates/vue/vite.config.ts +18 -0
- package/templates/vue/vitest.config.ts +8 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "haze dev",
|
|
8
|
+
"build": "haze build",
|
|
9
|
+
"start": "haze start",
|
|
10
|
+
"preview": "haze preview",
|
|
11
|
+
"queue": "haze queue:work",
|
|
12
|
+
"schedule": "haze schedule:work",
|
|
13
|
+
"test": "haze test",
|
|
14
|
+
"routes": "haze route:list",
|
|
15
|
+
"typecheck": "tsc --build",
|
|
16
|
+
"typegen": "haze typegen",
|
|
17
|
+
"db:generate": "haze db:generate",
|
|
18
|
+
"db:migrate": "haze db:migrate",
|
|
19
|
+
"db:seed": "haze db:seed"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@hazejs/core": "{{version}}",
|
|
23
|
+
"@hazejs/orm": "{{version}}",
|
|
24
|
+
"@hazejs/socket.io": "{{version}}",
|
|
25
|
+
"@libsql/client": "^0.18.0",
|
|
26
|
+
"drizzle-orm": "^0.45.4",
|
|
27
|
+
"react": "^19.3.0",
|
|
28
|
+
"react-dom": "^19.3.0",
|
|
29
|
+
"socket.io": "^4.8.0",
|
|
30
|
+
"socket.io-client": "^4.8.0",
|
|
31
|
+
"valibot": "^1.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@hazejs/cli": "{{version}}",
|
|
35
|
+
"@hazejs/testing": "{{version}}",
|
|
36
|
+
"@hazejs/vite": "{{version}}",
|
|
37
|
+
"@types/node": "^24.0.0",
|
|
38
|
+
"@types/react": "^19.3.0",
|
|
39
|
+
"@types/react-dom": "^19.3.0",
|
|
40
|
+
"@vitejs/plugin-react": "^6.1.0",
|
|
41
|
+
"drizzle-kit": "^0.31.11",
|
|
42
|
+
"typescript": "~6.0.3",
|
|
43
|
+
"vite": "^8.3.5",
|
|
44
|
+
"vitest": "^5.0.5"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { HttpContext } from '@hazejs/core'
|
|
2
|
+
import { Hash, Storage, UnauthorizedException, validate } from '@hazejs/core'
|
|
3
|
+
import UserRegistered from '~/app/event/UserRegistered'
|
|
4
|
+
import { User } from '~/app/model/User'
|
|
5
|
+
import { AvatarSchema, LoginSchema, RegisterSchema } from '~/app/validate/auth'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* JWT 登录示例(config/auth.ts 的 api Guard):
|
|
9
|
+
* 客户端保存 login / register 返回的 token,之后请求带上 `Authorization: Bearer <token>`
|
|
10
|
+
*/
|
|
11
|
+
export class AuthController {
|
|
12
|
+
async register(ctx: HttpContext) {
|
|
13
|
+
const { password, ...input } = await validate(RegisterSchema, await ctx.body())
|
|
14
|
+
const user = await User.create({ ...input, password: await Hash.make(password) })
|
|
15
|
+
// 事件:监听器投递欢迎邮件任务、推送实时消息(app/listener)
|
|
16
|
+
await UserRegistered.emit({ userId: user.id, email: user.email })
|
|
17
|
+
return ctx.created({
|
|
18
|
+
user,
|
|
19
|
+
...await ctx.auth.jwt().login(user),
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async login(ctx: HttpContext) {
|
|
24
|
+
const issued = await ctx.auth.jwt().attempt(await validate(LoginSchema, await ctx.body()))
|
|
25
|
+
if (!issued)
|
|
26
|
+
throw new UnauthorizedException('邮箱或密码错误')
|
|
27
|
+
return issued
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 需要 auth 中间件:未登录时返回 401 */
|
|
31
|
+
me(ctx: HttpContext) {
|
|
32
|
+
return ctx.auth.user()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 上传头像(multipart 字段 avatar):保存到 public 磁盘,经 /storage/<path> 访问 */
|
|
36
|
+
async avatar(ctx: HttpContext) {
|
|
37
|
+
const { avatar } = await validate(AvatarSchema, await ctx.body())
|
|
38
|
+
const path = await (await ctx.file('avatar'))!.store('avatars', 'public')
|
|
39
|
+
return ctx.created({
|
|
40
|
+
path,
|
|
41
|
+
url: Storage.disk('public').url(path),
|
|
42
|
+
size: avatar.size,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { HttpContext } from '@hazejs/core'
|
|
2
|
+
import type { GreetSchema } from '~/app/validate/greeting'
|
|
3
|
+
import { Config, inject } from '@hazejs/core'
|
|
4
|
+
import { GreetingService } from '~/app/service/GreetingService'
|
|
5
|
+
|
|
6
|
+
/** 控制层:接收参数 → 调用服务 → 返回数据(框架统一包装为 { code, message, data }) */
|
|
7
|
+
export class GreetingController {
|
|
8
|
+
private greeting = inject(GreetingService)
|
|
9
|
+
private config = inject(Config)
|
|
10
|
+
|
|
11
|
+
index() {
|
|
12
|
+
return {
|
|
13
|
+
app: this.config.get('app.name'),
|
|
14
|
+
message: this.greeting.greet('hazejs'),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
store(ctx: HttpContext<{ body: typeof GreetSchema }>) {
|
|
19
|
+
return {
|
|
20
|
+
message: this.greeting.greet(ctx.validated.body.name),
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { HttpContext } from '@hazejs/core'
|
|
2
|
+
import type { UserIdParams } from '~/app/validate/user'
|
|
3
|
+
import { validate } from '@hazejs/core'
|
|
4
|
+
import { User } from '~/app/model/User'
|
|
5
|
+
import { CreateUserSchema, UpdateUserSchema } from '~/app/validate/user'
|
|
6
|
+
|
|
7
|
+
type WithId = HttpContext<{ params: typeof UserIdParams }>
|
|
8
|
+
|
|
9
|
+
/** RESTful 资源控制器:路由见 route/api.ts 中的 r.resource('/users', UserController) */
|
|
10
|
+
export class UserController {
|
|
11
|
+
index(ctx: HttpContext) {
|
|
12
|
+
return User.order('id', 'desc').paginate(Number(ctx.query('page') ?? 1), 20)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
show(ctx: WithId) {
|
|
16
|
+
return User.findOrFail(ctx.validated.params.id)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async store(ctx: HttpContext) {
|
|
20
|
+
const input = await validate(CreateUserSchema, await ctx.body())
|
|
21
|
+
return ctx.created(await User.create(input))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async update(ctx: WithId) {
|
|
25
|
+
const user = await User.findOrFail(ctx.validated.params.id)
|
|
26
|
+
// 授权:app/policy/UserPolicy.ts
|
|
27
|
+
await ctx.authorize('update', user)
|
|
28
|
+
user.fill(await validate(UpdateUserSchema, await ctx.body()))
|
|
29
|
+
return user.save()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async destroy(ctx: WithId) {
|
|
33
|
+
const user = await User.findOrFail(ctx.validated.params.id)
|
|
34
|
+
await ctx.authorize('delete', user)
|
|
35
|
+
await user.delete()
|
|
36
|
+
return ctx.noContent()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HttpContext } from '@hazejs/core'
|
|
2
|
+
import { ExceptionHandler } from '@hazejs/core'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 全局异常处理:report() 负责记录 / 上报,render() 负责生成响应。
|
|
6
|
+
* 开发环境中浏览器访问出错的地址会看到带源码定位的错误页。
|
|
7
|
+
*/
|
|
8
|
+
export default class Handler extends ExceptionHandler {
|
|
9
|
+
override report(error: unknown, ctx: HttpContext) {
|
|
10
|
+
// ctx.logger 已携带 requestId;可在此接入 Sentry 等上报服务
|
|
11
|
+
return super.report(error, ctx)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineJob, Mail } from '@hazejs/core'
|
|
2
|
+
import WelcomeMail from '~/app/mail/WelcomeMail'
|
|
3
|
+
import { User } from '~/app/model/User'
|
|
4
|
+
|
|
5
|
+
/** 注册成功后投递(app/listener/QueueWelcomeEmail.ts):发送欢迎邮件,失败时按 backoff 重试 */
|
|
6
|
+
const SendWelcomeEmail = defineJob({
|
|
7
|
+
tries: 3,
|
|
8
|
+
backoff: [10, 60],
|
|
9
|
+
timeout: '30s',
|
|
10
|
+
async handle(payload: { userId: number }, job) {
|
|
11
|
+
const user = await User.find(payload.userId)
|
|
12
|
+
if (!user)
|
|
13
|
+
return
|
|
14
|
+
await Mail.to({ address: user.email, name: user.name }).send(WelcomeMail, { name: user.name, url: 'http://localhost:5173' })
|
|
15
|
+
job.logger.info(`Welcome email sent to ${user.email}`)
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export default SendWelcomeEmail
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineListener, Socket } from '@hazejs/core'
|
|
2
|
+
import UserRegistered from '~/app/event/UserRegistered'
|
|
3
|
+
|
|
4
|
+
/** 实时推送:订阅了 users 的 WebSocket 连接(route/ws.ts)收到新用户注册的消息 */
|
|
5
|
+
export default defineListener(UserRegistered, ({ userId }) => {
|
|
6
|
+
Socket.publish('users', { type: 'user.registered', userId })
|
|
7
|
+
})
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { defineListener } from '@hazejs/core'
|
|
2
|
+
import UserRegistered from '~/app/event/UserRegistered'
|
|
3
|
+
import SendWelcomeEmail from '~/app/job/SendWelcomeEmail'
|
|
4
|
+
|
|
5
|
+
/** 注册后投递欢迎邮件任务(耗时操作放到队列,不阻塞注册响应) */
|
|
6
|
+
export default defineListener(UserRegistered, ({ userId }) => SendWelcomeEmail.dispatch({ userId }))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { button, defineMail, html, mailLayout } from '@hazejs/core'
|
|
2
|
+
|
|
3
|
+
/** 欢迎邮件(app/job/SendWelcomeEmail.ts 发送):函数返回主题与 html,纯文本版本自动生成 */
|
|
4
|
+
export default defineMail((payload: { name: string, url: string }) => ({
|
|
5
|
+
subject: `欢迎加入,${payload.name}`,
|
|
6
|
+
html: mailLayout({
|
|
7
|
+
brand: '{{name}}',
|
|
8
|
+
preheader: '感谢注册,开始使用吧',
|
|
9
|
+
body: html`
|
|
10
|
+
<h1 style="margin:0 0 16px;font-size:22px">你好,${payload.name}</h1>
|
|
11
|
+
<p>感谢注册!点击下方按钮开始使用。</p>
|
|
12
|
+
${button('开始使用', payload.url)}
|
|
13
|
+
<p style="color:#71717a">如果这不是你本人的操作,请忽略这封邮件。</p>
|
|
14
|
+
`,
|
|
15
|
+
footer: '此邮件由系统自动发送,请勿直接回复',
|
|
16
|
+
}),
|
|
17
|
+
}), { name: 'WelcomeMail' })
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Model } from '@hazejs/orm'
|
|
2
|
+
import { users } from '~/database/schema/users'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 用户模型:字段与类型来自 database/schema/users.ts。
|
|
6
|
+
* 常用:User.findOrFail(id)、User.where('name', 'Ada').paginate(1, 20)、User.create(data)、user.save()
|
|
7
|
+
*/
|
|
8
|
+
export class User extends Model.define(users) {
|
|
9
|
+
// 序列化(接口返回)时隐藏密码哈希
|
|
10
|
+
static override hidden = ['password']
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { definePolicy, deny } from '@hazejs/core'
|
|
2
|
+
import { User } from '~/app/model/User'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 用户的授权策略(app/policy 下自动注册):
|
|
6
|
+
* 控制器中 await ctx.authorize('update', user);路由上也可用 .middleware('can:update,User,id')
|
|
7
|
+
* 未登录用户默认被拒绝(authorize 返回 401),已登录但无权限返回 403 与 deny() 的消息。
|
|
8
|
+
*/
|
|
9
|
+
export default definePolicy(User, {
|
|
10
|
+
// 管理员拥有全部权限
|
|
11
|
+
before: (actor: User | undefined) => actor?.role === 'admin' || undefined,
|
|
12
|
+
update: (actor: User, user: User) => actor.id === user.id || deny('只能修改自己的资料'),
|
|
13
|
+
delete: (actor: User, user: User) => actor.id === user.id || deny('只能删除自己的账号'),
|
|
14
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineProvider } from '@hazejs/core'
|
|
2
|
+
import { GreetingService } from '~/app/service/GreetingService'
|
|
3
|
+
|
|
4
|
+
/** 服务提供者:register() 中注册绑定,boot() 中执行启动逻辑 */
|
|
5
|
+
export default defineProvider({
|
|
6
|
+
register(container) {
|
|
7
|
+
container.singleton(GreetingService)
|
|
8
|
+
},
|
|
9
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HttpContext, NormalizedError } from '@hazejs/core'
|
|
2
|
+
import { ResponseFormatter } from '@hazejs/core'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 统一响应格式:在这里修改即可全局调整成功 / 失败时出站数据的结构。
|
|
6
|
+
* 默认:成功 { code: 0, message: 'ok', data },失败 { code, message, data: null, errors? }。
|
|
7
|
+
* 单个路由跳过格式化:.meta({ envelope: false }) 或 return raw(value)。
|
|
8
|
+
*/
|
|
9
|
+
export default class Formatter extends ResponseFormatter {
|
|
10
|
+
override success(data: unknown, ctx: HttpContext) {
|
|
11
|
+
return super.success(data, ctx)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override error(error: NormalizedError, ctx: HttpContext) {
|
|
15
|
+
return super.error(error, ctx)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineNamespace } from '@hazejs/socket.io'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Socket.IO 命名空间 /chat(前端:src/socket.ts)。
|
|
5
|
+
* 需要登录时加 middleware: [authenticate()],客户端 io('/chat', { auth: { token } });
|
|
6
|
+
* 在接口、任务、监听器中推送:SocketIO.of('/chat').emit('chat:message', text)
|
|
7
|
+
*/
|
|
8
|
+
export default defineNamespace({
|
|
9
|
+
connection(socket, _io, chat) {
|
|
10
|
+
chat.emit('online', chat.sockets.size)
|
|
11
|
+
socket.on('chat:send', (text: string, ack?: (result: string) => void) => {
|
|
12
|
+
chat.emit('chat:message', text)
|
|
13
|
+
ack?.('ok')
|
|
14
|
+
})
|
|
15
|
+
socket.on('disconnect', () => chat.emit('online', chat.sockets.size))
|
|
16
|
+
},
|
|
17
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as v from 'valibot'
|
|
2
|
+
|
|
3
|
+
export const RegisterSchema = v.object({
|
|
4
|
+
name: v.pipe(v.string('名称必填'), v.trim(), v.minLength(1, '名称不能为空')),
|
|
5
|
+
email: v.pipe(v.string('邮箱必填'), v.email('邮箱格式不正确')),
|
|
6
|
+
password: v.pipe(v.string('密码必填'), v.minLength(8, '密码至少 8 位')),
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export const LoginSchema = v.object({
|
|
10
|
+
email: v.pipe(v.string('邮箱必填'), v.email('邮箱格式不正确')),
|
|
11
|
+
password: v.pipe(v.string('密码必填'), v.minLength(1, '密码必填')),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export const AvatarSchema = v.object({
|
|
15
|
+
avatar: v.pipe(
|
|
16
|
+
v.file('请选择图片'),
|
|
17
|
+
v.mimeType(['image/png', 'image/jpeg', 'image/webp'], '只支持 png / jpeg / webp'),
|
|
18
|
+
v.maxSize(2 * 1024 * 1024, '图片不能超过 2MB'),
|
|
19
|
+
),
|
|
20
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as v from 'valibot'
|
|
2
|
+
|
|
3
|
+
export const UserIdParams = v.object({
|
|
4
|
+
id: v.pipe(v.string(), v.transform(Number), v.integer('用户 ID 必须是整数')),
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
export const CreateUserSchema = v.object({
|
|
8
|
+
name: v.pipe(v.string('名称必填'), v.trim(), v.minLength(1, '名称不能为空')),
|
|
9
|
+
email: v.pipe(v.string('邮箱必填'), v.email('邮箱格式不正确')),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export const UpdateUserSchema = v.partial(CreateUserSchema)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineAuthConfig, env } from '@hazejs/core'
|
|
2
|
+
import { modelUserProvider } from '@hazejs/orm'
|
|
3
|
+
import { User } from '~/app/model/User'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 认证:guards 决定“怎么认”(jwt:Authorization: Bearer;session:会话 Cookie),
|
|
7
|
+
* providers 决定“用户从哪来”。路由上用 .middleware('auth') / 'auth:web,api' 保护。
|
|
8
|
+
*/
|
|
9
|
+
export default defineAuthConfig(() => ({
|
|
10
|
+
default: 'api',
|
|
11
|
+
guards: {
|
|
12
|
+
// 密钥默认为 APP_KEY;也可配置 algorithm: 'ES256' 与 privateKey / publicKey
|
|
13
|
+
api: {
|
|
14
|
+
driver: 'jwt',
|
|
15
|
+
provider: 'users',
|
|
16
|
+
expiresIn: env('JWT_TTL', '2h'),
|
|
17
|
+
},
|
|
18
|
+
// 浏览器同源场景可改用 session:{ driver: 'session', provider: 'users' }
|
|
19
|
+
},
|
|
20
|
+
providers: {
|
|
21
|
+
users: modelUserProvider(() => User, { username: 'email', password: 'password' }),
|
|
22
|
+
},
|
|
23
|
+
}))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineCacheConfig, env } from '@hazejs/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 缓存:Cache.remember('key', '10m', () => ...)、Cache.increment()、Cache.lock()。
|
|
5
|
+
* 多实例部署时设置 CACHE_STORE=redis(REDIS_URL),限流(config/throttle.ts)与 Session 也会共享计数 / 数据。
|
|
6
|
+
*/
|
|
7
|
+
export default defineCacheConfig(() => ({
|
|
8
|
+
default: env('CACHE_STORE', 'memory'),
|
|
9
|
+
// 多个应用共用一个 Redis 时用前缀区分
|
|
10
|
+
prefix: env('CACHE_PREFIX', '{{name}}:'),
|
|
11
|
+
stores: {
|
|
12
|
+
memory: { driver: 'memory' },
|
|
13
|
+
// 需要安装 ioredis
|
|
14
|
+
redis: {
|
|
15
|
+
driver: 'redis',
|
|
16
|
+
url: env('REDIS_URL', 'redis://127.0.0.1:6379'),
|
|
17
|
+
},
|
|
18
|
+
// Edge / Serverless:Upstash(HTTP,需要安装 @upstash/redis)
|
|
19
|
+
// upstash: { driver: 'upstash', url: env('UPSTASH_REDIS_REST_URL', ''), token: env('UPSTASH_REDIS_REST_TOKEN', '') },
|
|
20
|
+
// Cloudflare Workers:KV 绑定(最终一致,适合缓存与 Session,不适合精确限流)
|
|
21
|
+
// kv: { driver: 'kv', binding: 'CACHE' },
|
|
22
|
+
},
|
|
23
|
+
}))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { env } from '@hazejs/core'
|
|
2
|
+
import { defineDatabaseConfig } from '@hazejs/orm'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 数据库连接。driver:sqlite(libsql)| turso(HTTP)| d1(Cloudflare)| postgres | pglite | mysql
|
|
6
|
+
* 切换数据库:安装对应驱动(postgres / mysql2),修改 driver 与 url,并改用对应方言的表结构。
|
|
7
|
+
*/
|
|
8
|
+
export default defineDatabaseConfig(() => ({
|
|
9
|
+
default: 'main',
|
|
10
|
+
connections: {
|
|
11
|
+
main: {
|
|
12
|
+
driver: 'sqlite',
|
|
13
|
+
url: env('DATABASE_URL', 'file:./data/app.db'),
|
|
14
|
+
logging: env('DB_LOGGING', false),
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
}))
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineFilesystemConfig, env } from '@hazejs/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 文件存储:Storage.put('reports/a.csv', csv)、Storage.disk('public').putFile('avatars', file)、
|
|
5
|
+
* 上传文件 `await file.store('avatars', 'public')`。
|
|
6
|
+
* public 磁盘的文件经 /storage/<path> 访问;私有文件用 Storage.temporaryUrl(path, '10m') 生成签名链接。
|
|
7
|
+
*/
|
|
8
|
+
export default defineFilesystemConfig(() => ({
|
|
9
|
+
default: env('FILESYSTEM_DISK', 'local'),
|
|
10
|
+
disks: {
|
|
11
|
+
local: { driver: 'local', root: 'storage/app' },
|
|
12
|
+
public: {
|
|
13
|
+
driver: 'local',
|
|
14
|
+
root: 'storage/app/public',
|
|
15
|
+
url: '/storage',
|
|
16
|
+
serve: true,
|
|
17
|
+
},
|
|
18
|
+
// AWS S3 与 S3 兼容服务(R2 的 S3 接口、MinIO、RustFS 等),需要 pnpm add @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
|
|
19
|
+
s3: {
|
|
20
|
+
driver: 's3',
|
|
21
|
+
bucket: env('S3_BUCKET', ''),
|
|
22
|
+
region: env('S3_REGION', 'us-east-1'),
|
|
23
|
+
endpoint: env('S3_ENDPOINT', '') || undefined,
|
|
24
|
+
accessKeyId: env('S3_ACCESS_KEY_ID', ''),
|
|
25
|
+
secretAccessKey: env('S3_SECRET_ACCESS_KEY', ''),
|
|
26
|
+
// 公共读的桶 / CDN 地址;私有桶用 temporaryUrl() 预签名
|
|
27
|
+
url: env('S3_URL', '') || undefined,
|
|
28
|
+
},
|
|
29
|
+
// Cloudflare Workers:R2 绑定(wrangler.json 的 r2_buckets)
|
|
30
|
+
// r2: { driver: 'r2', binding: 'BUCKET', url: 'https://files.example.com' },
|
|
31
|
+
},
|
|
32
|
+
}))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineMailConfig, env } from '@hazejs/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 邮件:await Mail.to(user.email).send(WelcomeMail, { name }),排队发送用 .queue() / .later('10m', ...)。
|
|
5
|
+
* 开发环境默认 log(只写日志);本地查看真实邮件可运行 Mailpit 并设置 MAIL_MAILER=smtp MAIL_PORT=1025。
|
|
6
|
+
*/
|
|
7
|
+
export default defineMailConfig(() => ({
|
|
8
|
+
default: env('MAIL_MAILER', 'log'),
|
|
9
|
+
from: {
|
|
10
|
+
address: env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
|
11
|
+
name: env('MAIL_FROM_NAME', '{{name}}'),
|
|
12
|
+
},
|
|
13
|
+
mailers: {
|
|
14
|
+
// 需要 pnpm add nodemailer(Cloudflare Workers 不支持 SMTP,请用下面的 HTTP 接口驱动)
|
|
15
|
+
smtp: {
|
|
16
|
+
driver: 'smtp',
|
|
17
|
+
host: env('MAIL_HOST', '127.0.0.1'),
|
|
18
|
+
port: env('MAIL_PORT', 1025),
|
|
19
|
+
username: env('MAIL_USERNAME', ''),
|
|
20
|
+
password: env('MAIL_PASSWORD', ''),
|
|
21
|
+
},
|
|
22
|
+
resend: {
|
|
23
|
+
driver: 'resend',
|
|
24
|
+
key: env('RESEND_API_KEY', ''),
|
|
25
|
+
},
|
|
26
|
+
// postmark: { driver: 'postmark', token: env('POSTMARK_TOKEN', '') },
|
|
27
|
+
// mailgun: { driver: 'mailgun', key: env('MAILGUN_SECRET', ''), domain: env('MAILGUN_DOMAIN', '') },
|
|
28
|
+
// 需要 pnpm add @aws-sdk/client-sesv2
|
|
29
|
+
// ses: { driver: 'ses', region: env('AWS_REGION', 'us-east-1') },
|
|
30
|
+
},
|
|
31
|
+
}))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineMiddlewareConfig } from '@hazejs/core'
|
|
2
|
+
|
|
3
|
+
// 请求 ID(x-request-id)与访问日志由框架内置,无需注册
|
|
4
|
+
// 内置中间件:cors、secureHeaders、compress、throttle:60,60、bodyLimit:10mb、session、auth / auth:api、guest
|
|
5
|
+
export default defineMiddlewareConfig({
|
|
6
|
+
// 全局中间件,按顺序作用于所有请求
|
|
7
|
+
// csrf:修改类请求须带 X-XSRF-TOKEN(src/api.ts 自动处理),Bearer 请求不受影响
|
|
8
|
+
global: ['secureHeaders', 'compress', 'csrf'],
|
|
9
|
+
// 别名,在路由中按名引用:.middleware('auth');用 haze make:middleware 生成
|
|
10
|
+
alias: {},
|
|
11
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineQueueConfig, env } from '@hazejs/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 队列:app/job 下的任务,await SendWelcomeEmail.dispatch({ userId })。
|
|
5
|
+
* memory 在本进程消费(开发 / 单实例);生产多实例设置 QUEUE_CONNECTION=redis(需要 pnpm add bullmq ioredis),
|
|
6
|
+
* 并运行 haze queue:work(构建产物:node dist/…/worker.mjs queue)。
|
|
7
|
+
*/
|
|
8
|
+
export default defineQueueConfig(() => ({
|
|
9
|
+
default: env('QUEUE_CONNECTION', 'memory'),
|
|
10
|
+
connections: {
|
|
11
|
+
memory: { driver: 'memory', concurrency: 2 },
|
|
12
|
+
sync: { driver: 'sync' },
|
|
13
|
+
redis: {
|
|
14
|
+
driver: 'redis',
|
|
15
|
+
url: env('REDIS_URL', 'redis://127.0.0.1:6379'),
|
|
16
|
+
queues: ['default'],
|
|
17
|
+
},
|
|
18
|
+
// Cloudflare Workers:Queues 绑定(wrangler.json 的 queues.producers / consumers)
|
|
19
|
+
// cloudflare: { driver: 'cloudflare', binding: 'QUEUE' },
|
|
20
|
+
},
|
|
21
|
+
}))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { env } from '@hazejs/core'
|
|
2
|
+
import { defineSocketIOConfig } from '@hazejs/socket.io'
|
|
3
|
+
|
|
4
|
+
// 多实例部署时设置 SOCKET_ADAPTER=redis:房间与广播经 Redis 同步到所有实例(需要 pnpm add @socket.io/redis-adapter ioredis)
|
|
5
|
+
export default defineSocketIOConfig(() => env('SOCKET_ADAPTER', '') === 'redis'
|
|
6
|
+
? {
|
|
7
|
+
adapter: {
|
|
8
|
+
driver: 'redis',
|
|
9
|
+
url: env('REDIS_URL', 'redis://127.0.0.1:6379'),
|
|
10
|
+
key: '{{name}}',
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
: {})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineThrottleConfig, env } from '@hazejs/core'
|
|
2
|
+
|
|
3
|
+
// 限流计数存在 config/cache.ts 的存储中(CACHE_STORE=redis 时多实例共享)
|
|
4
|
+
export default defineThrottleConfig(() => ({
|
|
5
|
+
store: env('CACHE_STORE', 'memory'),
|
|
6
|
+
// 命名规则:.middleware('throttle:api')
|
|
7
|
+
limiters: {
|
|
8
|
+
api: { max: 60, window: '1m' },
|
|
9
|
+
// Cloudflare Workers:使用 Rate Limiting 绑定(wrangler.json 的 ratelimits)
|
|
10
|
+
// api: { max: 60, window: '1m', binding: 'API_RATE_LIMITER' },
|
|
11
|
+
},
|
|
12
|
+
}))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineWebSocketConfig, env } from '@hazejs/core'
|
|
2
|
+
|
|
3
|
+
// 多实例部署时设置 WS_BROADCAST=redis:Socket.publish / peer.publish 经 Redis 转发到所有实例(需要 ioredis)
|
|
4
|
+
export default defineWebSocketConfig(() => env('WS_BROADCAST', '') === 'redis'
|
|
5
|
+
? {
|
|
6
|
+
broadcast: {
|
|
7
|
+
driver: 'redis',
|
|
8
|
+
url: env('REDIS_URL', 'redis://127.0.0.1:6379'),
|
|
9
|
+
channel: '{{name}}:ws',
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
: {})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
CREATE TABLE `users` (
|
|
2
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
3
|
+
`name` text NOT NULL,
|
|
4
|
+
`email` text NOT NULL,
|
|
5
|
+
`created_at` integer,
|
|
6
|
+
`updated_at` integer
|
|
7
|
+
);
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `users` ADD `password` text;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `users` ADD `role` text DEFAULT 'member' NOT NULL;
|