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,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,24 @@
|
|
|
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
|
+
// 下发 XSRF-TOKEN Cookie(前端首次发起修改类请求前调用,见 src/api.ts)
|
|
12
|
+
r.get('/csrf-cookie', ctx => ctx.noContent())
|
|
13
|
+
r.post('/hello', [GreetingController, 'store']).validate({ body: GreetSchema })
|
|
14
|
+
r.resource('/users', UserController).validateMember({ params: UserIdParams })
|
|
15
|
+
|
|
16
|
+
r.group({ prefix: '/auth', as: 'auth.' }, (r) => {
|
|
17
|
+
r.post('/register', [AuthController, 'register']).name('register')
|
|
18
|
+
// 登录限流:同一 IP 每分钟最多 10 次
|
|
19
|
+
r.post('/login', [AuthController, 'login']).middleware('throttle:10,60').name('login')
|
|
20
|
+
r.get('/me', [AuthController, 'me']).middleware('auth').name('me')
|
|
21
|
+
r.post('/avatar', [AuthController, 'avatar']).middleware(['auth', 'bodyLimit:3mb']).name('avatar')
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineSchedule } from '@hazejs/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 定时任务:haze schedule:work(开发 / 常驻),或系统 cron 每分钟执行 haze schedule:run;
|
|
5
|
+
* 构建产物:node dist/…/worker.mjs schedule,Workers 为 Cron Triggers,Lambda 为 index.schedule。
|
|
6
|
+
*/
|
|
7
|
+
export default defineSchedule((s) => {
|
|
8
|
+
s.call('heartbeat', () => {
|
|
9
|
+
// 示例:替换为清理过期数据、生成报表等;onOneServer 保证多实例时只执行一次
|
|
10
|
+
}).dailyAt('03:00').onOneServer()
|
|
11
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.page {
|
|
2
|
+
max-width: 560px;
|
|
3
|
+
margin: 64px auto;
|
|
4
|
+
padding: 0 16px;
|
|
5
|
+
font-family: system-ui, sans-serif;
|
|
6
|
+
}
|
|
7
|
+
.form {
|
|
8
|
+
display: flex;
|
|
9
|
+
gap: 8px;
|
|
10
|
+
}
|
|
11
|
+
.form input {
|
|
12
|
+
flex: 1;
|
|
13
|
+
padding: 6px 10px;
|
|
14
|
+
}
|
|
15
|
+
.error {
|
|
16
|
+
color: #c0392b;
|
|
17
|
+
}
|
|
18
|
+
.tip {
|
|
19
|
+
margin-top: 32px;
|
|
20
|
+
color: #666;
|
|
21
|
+
font-size: 14px;
|
|
22
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { FormEvent } from 'react'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
3
|
+
import { api } from '@/api'
|
|
4
|
+
import { chat } from '@/socket'
|
|
5
|
+
import './App.css'
|
|
6
|
+
|
|
7
|
+
const title = import.meta.env.VITE_APP_TITLE
|
|
8
|
+
|
|
9
|
+
export function App() {
|
|
10
|
+
const [hello, setHello] = useState('')
|
|
11
|
+
const [name, setName] = useState('')
|
|
12
|
+
const [reply, setReply] = useState('')
|
|
13
|
+
const [error, setError] = useState('')
|
|
14
|
+
const [online, setOnline] = useState(0)
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
api<{ message: string }>('/api/hello').then(data => setHello(data.message))
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
chat.on('online', setOnline)
|
|
22
|
+
chat.connect()
|
|
23
|
+
return () => {
|
|
24
|
+
chat.off('online', setOnline)
|
|
25
|
+
chat.disconnect()
|
|
26
|
+
}
|
|
27
|
+
}, [])
|
|
28
|
+
|
|
29
|
+
async function greet(event: FormEvent) {
|
|
30
|
+
event.preventDefault()
|
|
31
|
+
setError('')
|
|
32
|
+
try {
|
|
33
|
+
const data = await api<{ message: string }>('/api/hello', {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
body: JSON.stringify({ name }),
|
|
36
|
+
})
|
|
37
|
+
setReply(data.message)
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
setReply('')
|
|
41
|
+
setError((e as Error).message)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<main className="page">
|
|
47
|
+
<h1>{title}</h1>
|
|
48
|
+
<p className="hello">{hello || '加载中…'}</p>
|
|
49
|
+
<p className="online">
|
|
50
|
+
在线
|
|
51
|
+
{' '}
|
|
52
|
+
{online}
|
|
53
|
+
{' '}
|
|
54
|
+
人(Socket.IO)
|
|
55
|
+
</p>
|
|
56
|
+
|
|
57
|
+
<form className="form" onSubmit={greet}>
|
|
58
|
+
<input value={name} onChange={e => setName(e.target.value)} placeholder="输入名字" aria-label="名字" />
|
|
59
|
+
<button type="submit">打招呼</button>
|
|
60
|
+
</form>
|
|
61
|
+
{reply && <p className="reply">{reply}</p>}
|
|
62
|
+
{error && <p className="error">{error}</p>}
|
|
63
|
+
|
|
64
|
+
<p className="tip">
|
|
65
|
+
修改
|
|
66
|
+
{' '}
|
|
67
|
+
<code>server/app/service/GreetingService.ts</code>
|
|
68
|
+
,刷新页面即可看到后端变化,无需重启。
|
|
69
|
+
</p>
|
|
70
|
+
</main>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** 后端统一响应格式 */
|
|
2
|
+
export interface ApiResponse<T> {
|
|
3
|
+
code: number | string
|
|
4
|
+
message: string
|
|
5
|
+
data: T
|
|
6
|
+
errors?: Array<{ path: string, message: string }>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class ApiError extends Error {
|
|
10
|
+
constructor(readonly response: ApiResponse<unknown>) {
|
|
11
|
+
super(response.errors?.[0]?.message ?? response.message)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readCookie(name: string): string | undefined {
|
|
16
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`))
|
|
17
|
+
return match
|
|
18
|
+
? decodeURIComponent(match[1])
|
|
19
|
+
: undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** CSRF:修改类请求带上 XSRF-TOKEN(没有时先向后端取一次) */
|
|
23
|
+
async function csrfHeader(method: string): Promise<Record<string, string>> {
|
|
24
|
+
if (['GET', 'HEAD', 'OPTIONS'].includes(method))
|
|
25
|
+
return {}
|
|
26
|
+
if (!readCookie('XSRF-TOKEN'))
|
|
27
|
+
await fetch('/api/csrf-cookie')
|
|
28
|
+
const token = readCookie('XSRF-TOKEN')
|
|
29
|
+
return token
|
|
30
|
+
? { 'x-xsrf-token': token }
|
|
31
|
+
: {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 开发时同源请求由 Vite 转给后端,生产环境由同一个 Node 进程处理,无需配置代理 */
|
|
35
|
+
export async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
36
|
+
const res = await fetch(path, {
|
|
37
|
+
...init,
|
|
38
|
+
headers: {
|
|
39
|
+
'content-type': 'application/json',
|
|
40
|
+
...await csrfHeader((init.method ?? 'GET').toUpperCase()),
|
|
41
|
+
...init.headers,
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
const body = await res.json() as ApiResponse<T>
|
|
45
|
+
if (body.code !== 0)
|
|
46
|
+
throw new ApiError(body)
|
|
47
|
+
return body.data
|
|
48
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Events, Mail, MailFake, Queue, Storage } from '@hazejs/core'
|
|
2
|
+
import { refreshDatabase } from '@hazejs/orm'
|
|
3
|
+
import { useTestApp } from '@hazejs/testing'
|
|
4
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import UserRegistered from '~/app/event/UserRegistered'
|
|
6
|
+
import SendWelcomeEmail from '~/app/job/SendWelcomeEmail'
|
|
7
|
+
import WelcomeMail from '~/app/mail/WelcomeMail'
|
|
8
|
+
import { User } from '~/app/model/User'
|
|
9
|
+
|
|
10
|
+
const t = useTestApp()
|
|
11
|
+
let fake: ReturnType<typeof Queue.fake>
|
|
12
|
+
// 队列改为记录投递,断言后可用 fake.drain() 执行
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
fake = Queue.fake()
|
|
15
|
+
})
|
|
16
|
+
beforeEach(() => refreshDatabase({ migrations: 'server/database/migrations' }))
|
|
17
|
+
|
|
18
|
+
const ada = { name: 'Ada', email: 'ada@example.com', password: 'correct horse' }
|
|
19
|
+
|
|
20
|
+
describe('auth', () => {
|
|
21
|
+
it('registers, logs in and reads the current user with a Bearer token', async () => {
|
|
22
|
+
const registered = await t.post('/api/auth/register', ada)
|
|
23
|
+
expect(registered.status).toBe(201)
|
|
24
|
+
expect(registered.body.data.user).toMatchObject({ name: 'Ada', email: 'ada@example.com' })
|
|
25
|
+
// 密码哈希不会出现在响应中
|
|
26
|
+
expect(registered.body.data.user).not.toHaveProperty('password')
|
|
27
|
+
// 注册事件的监听器投递欢迎邮件任务(app/listener/QueueWelcomeEmail.ts)
|
|
28
|
+
fake.assertDispatched(SendWelcomeEmail, payload => payload.userId === registered.body.data.user.id)
|
|
29
|
+
// 执行任务:发送欢迎邮件(Mail.fake() 只记录不投递)
|
|
30
|
+
const mail = Mail.fake()
|
|
31
|
+
await fake.drain()
|
|
32
|
+
mail.assertSent(WelcomeMail, sent => MailFake.hasRecipient(sent, ada.email) && sent.message.subject === '欢迎加入,Ada')
|
|
33
|
+
|
|
34
|
+
const login = await t.post('/api/auth/login', { email: ada.email, password: ada.password })
|
|
35
|
+
expect(login.body.data).toMatchObject({ type: 'Bearer', expiresIn: 7200 })
|
|
36
|
+
|
|
37
|
+
const me = await t.withHeaders({ authorization: `Bearer ${login.body.data.token}` }).get('/api/auth/me')
|
|
38
|
+
expect(me.body.data).toMatchObject({ email: 'ada@example.com' })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('emits UserRegistered on registration', async () => {
|
|
42
|
+
const events = Events.fake([UserRegistered])
|
|
43
|
+
const registered = await t.post('/api/auth/register', ada)
|
|
44
|
+
events.assertEmitted(UserRegistered, payload => payload.userId === registered.body.data.user.id && payload.email === ada.email)
|
|
45
|
+
// 事件被 fake 后监听器不执行,任务也不会投递
|
|
46
|
+
fake.assertNotDispatched(SendWelcomeEmail)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('rejects wrong passwords and anonymous requests with 401', async () => {
|
|
50
|
+
await t.post('/api/auth/register', ada)
|
|
51
|
+
const wrong = await t.post('/api/auth/login', { email: ada.email, password: 'nope' })
|
|
52
|
+
expect([wrong.status, wrong.body.message]).toEqual([401, '邮箱或密码错误'])
|
|
53
|
+
expect((await t.get('/api/auth/me')).status).toBe(401)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('actingAs() logs a user in without going through the login endpoint', async () => {
|
|
57
|
+
const user = await User.create({ name: 'Bob', email: 'bob@example.com' })
|
|
58
|
+
const me = await (await t.actingAs(user)).get('/api/auth/me')
|
|
59
|
+
expect(me.body.data).toMatchObject({ id: user.id, email: 'bob@example.com' })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('uploads an avatar to the public disk', async () => {
|
|
63
|
+
const disk = Storage.fake('public')
|
|
64
|
+
const user = await User.create({ name: 'Bob', email: 'bob@example.com' })
|
|
65
|
+
const form = new FormData()
|
|
66
|
+
form.set('avatar', new File([
|
|
67
|
+
new Uint8Array([137, 80, 78, 71]),
|
|
68
|
+
], 'me.png', { type: 'image/png' }))
|
|
69
|
+
const res = await (await t.actingAs(user)).post('/api/auth/avatar', form)
|
|
70
|
+
expect(res.status).toBe(201)
|
|
71
|
+
expect(res.body.data.url).toBe(`/storage/${res.body.data.path}`)
|
|
72
|
+
await disk.assertExists(res.body.data.path)
|
|
73
|
+
|
|
74
|
+
form.set('avatar', new File(['text'], 'a.txt', { type: 'text/plain' }))
|
|
75
|
+
expect((await (await t.actingAs(user)).post('/api/auth/avatar', form)).status).toBe(422)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useTestApp } from '@hazejs/testing'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
const t = useTestApp()
|
|
5
|
+
|
|
6
|
+
describe('greeting', () => {
|
|
7
|
+
it('says hello', async () => {
|
|
8
|
+
const res = await t.get('/api/hello')
|
|
9
|
+
expect(res.status).toBe(200)
|
|
10
|
+
expect(res.body).toEqual({
|
|
11
|
+
code: 0,
|
|
12
|
+
message: 'ok',
|
|
13
|
+
data: { app: '{{name}}', message: 'Hello, hazejs!' },
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('greets by name', async () => {
|
|
18
|
+
const res = await t.post('/api/hello', { name: ' Ada ' })
|
|
19
|
+
expect(res.body.data).toEqual({ message: 'Hello, Ada!' })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('validates the request body', async () => {
|
|
23
|
+
const res = await t.post('/api/hello', { name: '' })
|
|
24
|
+
expect(res.status).toBe(422)
|
|
25
|
+
expect(res.body.errors).toEqual([
|
|
26
|
+
{ target: 'body', path: 'name', message: 'name 不能为空' },
|
|
27
|
+
])
|
|
28
|
+
})
|
|
29
|
+
})
|