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,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,11 @@
|
|
|
1
|
+
import { defineCorsConfig, env } from '@hazejs/core'
|
|
2
|
+
|
|
3
|
+
// 跨域:CORS_ORIGIN 为逗号分隔的来源列表,默认允许任意来源(不携带 Cookie)
|
|
4
|
+
export default defineCorsConfig(() => {
|
|
5
|
+
const origin = env('CORS_ORIGIN', '*')
|
|
6
|
+
return {
|
|
7
|
+
origin: origin === '*'
|
|
8
|
+
? '*'
|
|
9
|
+
: origin.split(',').map(item => item.trim()),
|
|
10
|
+
}
|
|
11
|
+
})
|
|
@@ -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,10 @@
|
|
|
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
|
+
global: ['cors', 'secureHeaders', 'compress'],
|
|
8
|
+
// 别名,在路由中按名引用:.middleware('auth');用 haze make:middleware 生成
|
|
9
|
+
alias: {},
|
|
10
|
+
})
|
|
@@ -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,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;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "6",
|
|
3
|
+
"dialect": "sqlite",
|
|
4
|
+
"id": "1b28388a-9186-40de-9d67-bb08f562b58c",
|
|
5
|
+
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
6
|
+
"tables": {
|
|
7
|
+
"users": {
|
|
8
|
+
"name": "users",
|
|
9
|
+
"columns": {
|
|
10
|
+
"id": {
|
|
11
|
+
"name": "id",
|
|
12
|
+
"type": "integer",
|
|
13
|
+
"primaryKey": true,
|
|
14
|
+
"notNull": true,
|
|
15
|
+
"autoincrement": true
|
|
16
|
+
},
|
|
17
|
+
"name": {
|
|
18
|
+
"name": "name",
|
|
19
|
+
"type": "text",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": true,
|
|
22
|
+
"autoincrement": false
|
|
23
|
+
},
|
|
24
|
+
"email": {
|
|
25
|
+
"name": "email",
|
|
26
|
+
"type": "text",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": true,
|
|
29
|
+
"autoincrement": false
|
|
30
|
+
},
|
|
31
|
+
"created_at": {
|
|
32
|
+
"name": "created_at",
|
|
33
|
+
"type": "integer",
|
|
34
|
+
"primaryKey": false,
|
|
35
|
+
"notNull": false,
|
|
36
|
+
"autoincrement": false
|
|
37
|
+
},
|
|
38
|
+
"updated_at": {
|
|
39
|
+
"name": "updated_at",
|
|
40
|
+
"type": "integer",
|
|
41
|
+
"primaryKey": false,
|
|
42
|
+
"notNull": false,
|
|
43
|
+
"autoincrement": false
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"indexes": {
|
|
47
|
+
"users_email_unique": {
|
|
48
|
+
"name": "users_email_unique",
|
|
49
|
+
"columns": [
|
|
50
|
+
"email"
|
|
51
|
+
],
|
|
52
|
+
"isUnique": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"foreignKeys": {},
|
|
56
|
+
"compositePrimaryKeys": {},
|
|
57
|
+
"uniqueConstraints": {},
|
|
58
|
+
"checkConstraints": {}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"views": {},
|
|
62
|
+
"enums": {},
|
|
63
|
+
"_meta": {
|
|
64
|
+
"schemas": {},
|
|
65
|
+
"tables": {},
|
|
66
|
+
"columns": {}
|
|
67
|
+
},
|
|
68
|
+
"internal": {
|
|
69
|
+
"indexes": {}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "6",
|
|
3
|
+
"dialect": "sqlite",
|
|
4
|
+
"id": "1138ad71-fa8c-4b78-a94a-5dbce374d676",
|
|
5
|
+
"prevId": "1b28388a-9186-40de-9d67-bb08f562b58c",
|
|
6
|
+
"tables": {
|
|
7
|
+
"users": {
|
|
8
|
+
"name": "users",
|
|
9
|
+
"columns": {
|
|
10
|
+
"id": {
|
|
11
|
+
"name": "id",
|
|
12
|
+
"type": "integer",
|
|
13
|
+
"primaryKey": true,
|
|
14
|
+
"notNull": true,
|
|
15
|
+
"autoincrement": true
|
|
16
|
+
},
|
|
17
|
+
"name": {
|
|
18
|
+
"name": "name",
|
|
19
|
+
"type": "text",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": true,
|
|
22
|
+
"autoincrement": false
|
|
23
|
+
},
|
|
24
|
+
"email": {
|
|
25
|
+
"name": "email",
|
|
26
|
+
"type": "text",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": true,
|
|
29
|
+
"autoincrement": false
|
|
30
|
+
},
|
|
31
|
+
"password": {
|
|
32
|
+
"name": "password",
|
|
33
|
+
"type": "text",
|
|
34
|
+
"primaryKey": false,
|
|
35
|
+
"notNull": false,
|
|
36
|
+
"autoincrement": false
|
|
37
|
+
},
|
|
38
|
+
"created_at": {
|
|
39
|
+
"name": "created_at",
|
|
40
|
+
"type": "integer",
|
|
41
|
+
"primaryKey": false,
|
|
42
|
+
"notNull": false,
|
|
43
|
+
"autoincrement": false
|
|
44
|
+
},
|
|
45
|
+
"updated_at": {
|
|
46
|
+
"name": "updated_at",
|
|
47
|
+
"type": "integer",
|
|
48
|
+
"primaryKey": false,
|
|
49
|
+
"notNull": false,
|
|
50
|
+
"autoincrement": false
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"indexes": {
|
|
54
|
+
"users_email_unique": {
|
|
55
|
+
"name": "users_email_unique",
|
|
56
|
+
"columns": [
|
|
57
|
+
"email"
|
|
58
|
+
],
|
|
59
|
+
"isUnique": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"foreignKeys": {},
|
|
63
|
+
"compositePrimaryKeys": {},
|
|
64
|
+
"uniqueConstraints": {},
|
|
65
|
+
"checkConstraints": {}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"views": {},
|
|
69
|
+
"enums": {},
|
|
70
|
+
"_meta": {
|
|
71
|
+
"schemas": {},
|
|
72
|
+
"tables": {},
|
|
73
|
+
"columns": {}
|
|
74
|
+
},
|
|
75
|
+
"internal": {
|
|
76
|
+
"indexes": {}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "6",
|
|
3
|
+
"dialect": "sqlite",
|
|
4
|
+
"id": "d41d3fc5-5742-40b2-9f99-4f4870f40ebf",
|
|
5
|
+
"prevId": "1138ad71-fa8c-4b78-a94a-5dbce374d676",
|
|
6
|
+
"tables": {
|
|
7
|
+
"users": {
|
|
8
|
+
"name": "users",
|
|
9
|
+
"columns": {
|
|
10
|
+
"id": {
|
|
11
|
+
"name": "id",
|
|
12
|
+
"type": "integer",
|
|
13
|
+
"primaryKey": true,
|
|
14
|
+
"notNull": true,
|
|
15
|
+
"autoincrement": true
|
|
16
|
+
},
|
|
17
|
+
"name": {
|
|
18
|
+
"name": "name",
|
|
19
|
+
"type": "text",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": true,
|
|
22
|
+
"autoincrement": false
|
|
23
|
+
},
|
|
24
|
+
"email": {
|
|
25
|
+
"name": "email",
|
|
26
|
+
"type": "text",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": true,
|
|
29
|
+
"autoincrement": false
|
|
30
|
+
},
|
|
31
|
+
"password": {
|
|
32
|
+
"name": "password",
|
|
33
|
+
"type": "text",
|
|
34
|
+
"primaryKey": false,
|
|
35
|
+
"notNull": false,
|
|
36
|
+
"autoincrement": false
|
|
37
|
+
},
|
|
38
|
+
"role": {
|
|
39
|
+
"name": "role",
|
|
40
|
+
"type": "text",
|
|
41
|
+
"primaryKey": false,
|
|
42
|
+
"notNull": true,
|
|
43
|
+
"autoincrement": false,
|
|
44
|
+
"default": "'member'"
|
|
45
|
+
},
|
|
46
|
+
"created_at": {
|
|
47
|
+
"name": "created_at",
|
|
48
|
+
"type": "integer",
|
|
49
|
+
"primaryKey": false,
|
|
50
|
+
"notNull": false,
|
|
51
|
+
"autoincrement": false
|
|
52
|
+
},
|
|
53
|
+
"updated_at": {
|
|
54
|
+
"name": "updated_at",
|
|
55
|
+
"type": "integer",
|
|
56
|
+
"primaryKey": false,
|
|
57
|
+
"notNull": false,
|
|
58
|
+
"autoincrement": false
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"indexes": {
|
|
62
|
+
"users_email_unique": {
|
|
63
|
+
"name": "users_email_unique",
|
|
64
|
+
"columns": [
|
|
65
|
+
"email"
|
|
66
|
+
],
|
|
67
|
+
"isUnique": true
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"foreignKeys": {},
|
|
71
|
+
"compositePrimaryKeys": {},
|
|
72
|
+
"uniqueConstraints": {},
|
|
73
|
+
"checkConstraints": {}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"views": {},
|
|
77
|
+
"enums": {},
|
|
78
|
+
"_meta": {
|
|
79
|
+
"schemas": {},
|
|
80
|
+
"tables": {},
|
|
81
|
+
"columns": {}
|
|
82
|
+
},
|
|
83
|
+
"internal": {
|
|
84
|
+
"indexes": {}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "7",
|
|
3
|
+
"dialect": "sqlite",
|
|
4
|
+
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"idx": 0,
|
|
7
|
+
"version": "6",
|
|
8
|
+
"when": 1793072281689,
|
|
9
|
+
"tag": "0000_create_users",
|
|
10
|
+
"breakpoints": true
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"idx": 1,
|
|
14
|
+
"version": "6",
|
|
15
|
+
"when": 1793084099573,
|
|
16
|
+
"tag": "0001_add_user_password",
|
|
17
|
+
"breakpoints": true
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"idx": 2,
|
|
21
|
+
"version": "6",
|
|
22
|
+
"when": 1793086076027,
|
|
23
|
+
"tag": "0002_add_user_role",
|
|
24
|
+
"breakpoints": true
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
|
2
|
+
|
|
3
|
+
// 表结构定义(drizzle):此文件只依赖 drizzle-orm,不要使用 ~ 别名(drizzle-kit 需要直接读取)
|
|
4
|
+
// 修改后运行 haze db:generate 生成迁移,再 haze db:migrate
|
|
5
|
+
export const users = sqliteTable('users', {
|
|
6
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
7
|
+
name: text('name').notNull(),
|
|
8
|
+
email: text('email').notNull().unique(),
|
|
9
|
+
// 密码哈希(Hash.make()),通过 /api/auth/register 注册的用户才有
|
|
10
|
+
password: text('password'),
|
|
11
|
+
// 角色:admin 可以管理所有用户(app/policy/UserPolicy.ts)
|
|
12
|
+
role: text('role', {
|
|
13
|
+
enum: ['member', 'admin'],
|
|
14
|
+
}).notNull().default('member'),
|
|
15
|
+
createdAt: integer('created_at', { mode: 'timestamp' }),
|
|
16
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }),
|
|
17
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Hash } from '@hazejs/core'
|
|
2
|
+
import { User } from '~/app/model/User'
|
|
3
|
+
|
|
4
|
+
/** haze db:seed:按文件名顺序执行 database/seeders 下的文件 */
|
|
5
|
+
export default async function seed(): Promise<void> {
|
|
6
|
+
if (await User.where('email', 'admin@example.com').exists())
|
|
7
|
+
return
|
|
8
|
+
// 开发用账号,上线前请修改密码
|
|
9
|
+
await User.create({
|
|
10
|
+
name: 'Admin',
|
|
11
|
+
email: 'admin@example.com',
|
|
12
|
+
password: await Hash.make('password'),
|
|
13
|
+
role: 'admin',
|
|
14
|
+
})
|
|
15
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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 --noEmit",
|
|
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
|
+
"@libsql/client": "^0.18.0",
|
|
25
|
+
"drizzle-orm": "^0.45.4",
|
|
26
|
+
"valibot": "^1.1.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@hazejs/cli": "{{version}}",
|
|
30
|
+
"@hazejs/testing": "{{version}}",
|
|
31
|
+
"@hazejs/vite": "{{version}}",
|
|
32
|
+
"@types/node": "^24.0.0",
|
|
33
|
+
"drizzle-kit": "^0.31.11",
|
|
34
|
+
"typescript": "~6.0.3",
|
|
35
|
+
"vite": "^8.3.5",
|
|
36
|
+
"vitest": "^5.0.5"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineRoutes } from '@hazejs/core'
|
|
2
|
+
import { AuthController } from '~/app/controller/AuthController'
|
|
3
|
+
import { GreetingController } from '~/app/controller/GreetingController'
|
|
4
|
+
import { UserController } from '~/app/controller/UserController'
|
|
5
|
+
import { GreetSchema } from '~/app/validate/greeting'
|
|
6
|
+
import { UserIdParams } from '~/app/validate/user'
|
|
7
|
+
|
|
8
|
+
export default defineRoutes((r) => {
|
|
9
|
+
r.group({ prefix: '/api', as: 'api.' }, (r) => {
|
|
10
|
+
r.get('/hello', [GreetingController, 'index']).name('hello')
|
|
11
|
+
r.post('/hello', [GreetingController, 'store']).validate({ body: GreetSchema })
|
|
12
|
+
r.resource('/users', UserController).validateMember({ params: UserIdParams })
|
|
13
|
+
|
|
14
|
+
r.group({ prefix: '/auth', as: 'auth.' }, (r) => {
|
|
15
|
+
r.post('/register', [AuthController, 'register']).name('register')
|
|
16
|
+
// 登录限流:同一 IP 每分钟最多 10 次
|
|
17
|
+
r.post('/login', [AuthController, 'login']).middleware('throttle:10,60').name('login')
|
|
18
|
+
r.get('/me', [AuthController, 'me']).middleware('auth').name('me')
|
|
19
|
+
r.post('/avatar', [AuthController, 'avatar']).middleware(['auth', 'bodyLimit:3mb']).name('avatar')
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineRoutes } from '@hazejs/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WebSocket:ws://<host>/api/ws。升级请求同样经过中间件,需要登录时加 .middleware('auth')。
|
|
5
|
+
* 服务端任意位置用 Socket.publish('users', data) 推送给订阅者(多实例见 config/websocket.ts)
|
|
6
|
+
*/
|
|
7
|
+
export default defineRoutes((r) => {
|
|
8
|
+
r.ws('/api/ws', {
|
|
9
|
+
open(peer) {
|
|
10
|
+
peer.subscribe('users')
|
|
11
|
+
peer.send(JSON.stringify({ type: 'connected' }))
|
|
12
|
+
},
|
|
13
|
+
message(peer, message) {
|
|
14
|
+
if (message.text() === 'ping')
|
|
15
|
+
peer.send(JSON.stringify({ type: 'pong' }))
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
})
|