@nubase/create 0.1.24 → 0.1.25
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/package.json +1 -1
- package/templates/backend/src/api/handler-factory.ts +34 -0
- package/templates/backend/src/api/routes/auth.ts +12 -36
- package/templates/backend/src/api/routes/dashboard.ts +7 -14
- package/templates/backend/src/api/routes/ticket.ts +7 -12
- package/templates/backend/src/api/routes/user.ts +8 -14
package/package.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createHandlerFactory } from "@nubase/backend";
|
|
2
|
+
import { apiEndpoints, type ApiEndpoints } from "schema";
|
|
3
|
+
import type { __PROJECT_NAME_PASCAL__User } from "../auth";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pre-configured handler factory for __PROJECT_NAME_PASCAL__.
|
|
7
|
+
*
|
|
8
|
+
* This factory pre-binds:
|
|
9
|
+
* - The apiEndpoints object for endpoint schema inference
|
|
10
|
+
* - The __PROJECT_NAME_PASCAL__User type for authenticated handlers
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // Required auth - user is guaranteed to be __PROJECT_NAME_PASCAL__User
|
|
15
|
+
* getTickets: createHandler((e) => e.getTickets, {
|
|
16
|
+
* auth: "required",
|
|
17
|
+
* handler: async ({ params, user, ctx }) => { ... },
|
|
18
|
+
* }),
|
|
19
|
+
*
|
|
20
|
+
* // No auth (default) - no user in handler
|
|
21
|
+
* loginStart: createHandler((e) => e.loginStart, {
|
|
22
|
+
* handler: async ({ body }) => { ... },
|
|
23
|
+
* }),
|
|
24
|
+
*
|
|
25
|
+
* // Optional auth - user may be null
|
|
26
|
+
* getMe: createHandler((e) => e.getMe, {
|
|
27
|
+
* auth: "optional",
|
|
28
|
+
* handler: async ({ user }) => { ... },
|
|
29
|
+
* }),
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export const createHandler = createHandlerFactory<ApiEndpoints, __PROJECT_NAME_PASCAL__User>({
|
|
33
|
+
endpoints: apiEndpoints,
|
|
34
|
+
});
|
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createHttpHandler,
|
|
3
|
-
getAuthController,
|
|
4
|
-
HttpError,
|
|
5
|
-
} from "@nubase/backend";
|
|
1
|
+
import { getAuthController, HttpError } from "@nubase/backend";
|
|
6
2
|
import bcrypt from "bcrypt";
|
|
7
3
|
import { and, eq, inArray } from "drizzle-orm";
|
|
8
|
-
import { apiEndpoints } from "schema";
|
|
9
4
|
import jwt from "jsonwebtoken";
|
|
10
5
|
import type { __PROJECT_NAME_PASCAL__User } from "../../auth";
|
|
11
6
|
import { getAdminDb } from "../../db/helpers/drizzle";
|
|
12
7
|
import { users, userWorkspaces, workspaces } from "../../db/schema";
|
|
8
|
+
import { createHandler } from "../handler-factory";
|
|
13
9
|
|
|
14
10
|
// Short-lived secret for login tokens (in production, use a proper secret)
|
|
15
11
|
const LOGIN_TOKEN_SECRET =
|
|
16
|
-
process.env.LOGIN_TOKEN_SECRET ||
|
|
17
|
-
"nubase-login-token-secret-change-in-production";
|
|
12
|
+
process.env.LOGIN_TOKEN_SECRET || "nubase-login-token-secret-change-in-production";
|
|
18
13
|
const LOGIN_TOKEN_EXPIRY = "5m"; // 5 minutes to complete workspace selection
|
|
19
14
|
|
|
20
15
|
interface LoginTokenPayload {
|
|
@@ -27,8 +22,7 @@ export const authHandlers = {
|
|
|
27
22
|
* Login Start handler - Step 1 of two-step auth.
|
|
28
23
|
* Validates credentials and returns list of workspaces.
|
|
29
24
|
*/
|
|
30
|
-
loginStart:
|
|
31
|
-
endpoint: apiEndpoints.loginStart,
|
|
25
|
+
loginStart: createHandler((e) => e.loginStart, {
|
|
32
26
|
handler: async ({ body }) => {
|
|
33
27
|
// Find user by email
|
|
34
28
|
const [user] = await getAdminDb()
|
|
@@ -41,10 +35,7 @@ export const authHandlers = {
|
|
|
41
35
|
}
|
|
42
36
|
|
|
43
37
|
// Verify password
|
|
44
|
-
const isValidPassword = await bcrypt.compare(
|
|
45
|
-
body.password,
|
|
46
|
-
user.passwordHash,
|
|
47
|
-
);
|
|
38
|
+
const isValidPassword = await bcrypt.compare(body.password, user.passwordHash);
|
|
48
39
|
if (!isValidPassword) {
|
|
49
40
|
throw new HttpError(401, "Invalid email or password");
|
|
50
41
|
}
|
|
@@ -92,18 +83,14 @@ export const authHandlers = {
|
|
|
92
83
|
* Login Complete handler - Step 2 of two-step auth.
|
|
93
84
|
* Validates the login token and selected workspace.
|
|
94
85
|
*/
|
|
95
|
-
loginComplete:
|
|
96
|
-
endpoint: apiEndpoints.loginComplete,
|
|
86
|
+
loginComplete: createHandler((e) => e.loginComplete, {
|
|
97
87
|
handler: async ({ body, ctx }) => {
|
|
98
88
|
const authController = getAuthController<__PROJECT_NAME_PASCAL__User>(ctx);
|
|
99
89
|
|
|
100
90
|
// Verify the login token
|
|
101
91
|
let decoded: LoginTokenPayload;
|
|
102
92
|
try {
|
|
103
|
-
decoded = jwt.verify(
|
|
104
|
-
body.loginToken,
|
|
105
|
-
LOGIN_TOKEN_SECRET,
|
|
106
|
-
) as LoginTokenPayload;
|
|
93
|
+
decoded = jwt.verify(body.loginToken, LOGIN_TOKEN_SECRET) as LoginTokenPayload;
|
|
107
94
|
} catch {
|
|
108
95
|
throw new HttpError(401, "Invalid or expired login token");
|
|
109
96
|
}
|
|
@@ -174,8 +161,7 @@ export const authHandlers = {
|
|
|
174
161
|
* Legacy Login handler - validates credentials and sets HttpOnly cookie.
|
|
175
162
|
* @deprecated Use loginStart and loginComplete for two-step flow
|
|
176
163
|
*/
|
|
177
|
-
login:
|
|
178
|
-
endpoint: apiEndpoints.login,
|
|
164
|
+
login: createHandler((e) => e.login, {
|
|
179
165
|
handler: async ({ body, ctx }) => {
|
|
180
166
|
const authController = getAuthController<__PROJECT_NAME_PASCAL__User>(ctx);
|
|
181
167
|
|
|
@@ -200,10 +186,7 @@ export const authHandlers = {
|
|
|
200
186
|
}
|
|
201
187
|
|
|
202
188
|
// Verify password
|
|
203
|
-
const isValidPassword = await bcrypt.compare(
|
|
204
|
-
body.password,
|
|
205
|
-
dbUser.passwordHash,
|
|
206
|
-
);
|
|
189
|
+
const isValidPassword = await bcrypt.compare(body.password, dbUser.passwordHash);
|
|
207
190
|
if (!isValidPassword) {
|
|
208
191
|
throw new HttpError(401, "Invalid email or password");
|
|
209
192
|
}
|
|
@@ -246,8 +229,7 @@ export const authHandlers = {
|
|
|
246
229
|
}),
|
|
247
230
|
|
|
248
231
|
/** Logout handler - clears the auth cookie. */
|
|
249
|
-
logout:
|
|
250
|
-
endpoint: apiEndpoints.logout,
|
|
232
|
+
logout: createHandler((e) => e.logout, {
|
|
251
233
|
handler: async ({ ctx }) => {
|
|
252
234
|
const authController = getAuthController(ctx);
|
|
253
235
|
authController.clearTokenFromResponse(ctx);
|
|
@@ -256,12 +238,7 @@ export const authHandlers = {
|
|
|
256
238
|
}),
|
|
257
239
|
|
|
258
240
|
/** Get current user handler. */
|
|
259
|
-
getMe:
|
|
260
|
-
typeof apiEndpoints.getMe,
|
|
261
|
-
"optional",
|
|
262
|
-
__PROJECT_NAME_PASCAL__User
|
|
263
|
-
>({
|
|
264
|
-
endpoint: apiEndpoints.getMe,
|
|
241
|
+
getMe: createHandler((e) => e.getMe, {
|
|
265
242
|
auth: "optional",
|
|
266
243
|
handler: async ({ user }) => {
|
|
267
244
|
if (!user) {
|
|
@@ -279,8 +256,7 @@ export const authHandlers = {
|
|
|
279
256
|
}),
|
|
280
257
|
|
|
281
258
|
/** Signup handler - creates a new workspace and admin user. */
|
|
282
|
-
signup:
|
|
283
|
-
endpoint: apiEndpoints.signup,
|
|
259
|
+
signup: createHandler((e) => e.signup, {
|
|
284
260
|
handler: async ({ body, ctx }) => {
|
|
285
261
|
const authController = getAuthController<__PROJECT_NAME_PASCAL__User>(ctx);
|
|
286
262
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { apiEndpoints } from "schema";
|
|
1
|
+
import { createHandler } from "../handler-factory";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Dashboard widget endpoints.
|
|
@@ -7,8 +6,7 @@ import { apiEndpoints } from "schema";
|
|
|
7
6
|
*/
|
|
8
7
|
export const dashboardHandlers = {
|
|
9
8
|
/** Revenue chart - returns series data for area/line/bar charts. */
|
|
10
|
-
getRevenueChart:
|
|
11
|
-
endpoint: apiEndpoints.getRevenueChart,
|
|
9
|
+
getRevenueChart: createHandler((e) => e.getRevenueChart, {
|
|
12
10
|
handler: async () => ({
|
|
13
11
|
type: "series",
|
|
14
12
|
config: {
|
|
@@ -26,8 +24,7 @@ export const dashboardHandlers = {
|
|
|
26
24
|
}),
|
|
27
25
|
|
|
28
26
|
/** Browser stats - returns proportional data for pie/donut charts. */
|
|
29
|
-
getBrowserStats:
|
|
30
|
-
endpoint: apiEndpoints.getBrowserStats,
|
|
27
|
+
getBrowserStats: createHandler((e) => e.getBrowserStats, {
|
|
31
28
|
handler: async () => ({
|
|
32
29
|
type: "proportional",
|
|
33
30
|
data: [
|
|
@@ -41,8 +38,7 @@ export const dashboardHandlers = {
|
|
|
41
38
|
}),
|
|
42
39
|
|
|
43
40
|
/** Total revenue KPI - returns single value with trend. */
|
|
44
|
-
getTotalRevenue:
|
|
45
|
-
endpoint: apiEndpoints.getTotalRevenue,
|
|
41
|
+
getTotalRevenue: createHandler((e) => e.getTotalRevenue, {
|
|
46
42
|
handler: async () => ({
|
|
47
43
|
type: "kpi",
|
|
48
44
|
value: "$45,231.89",
|
|
@@ -53,8 +49,7 @@ export const dashboardHandlers = {
|
|
|
53
49
|
}),
|
|
54
50
|
|
|
55
51
|
/** Active users KPI - returns single value with trend. */
|
|
56
|
-
getActiveUsers:
|
|
57
|
-
endpoint: apiEndpoints.getActiveUsers,
|
|
52
|
+
getActiveUsers: createHandler((e) => e.getActiveUsers, {
|
|
58
53
|
handler: async () => ({
|
|
59
54
|
type: "kpi",
|
|
60
55
|
value: "+2,350",
|
|
@@ -65,8 +60,7 @@ export const dashboardHandlers = {
|
|
|
65
60
|
}),
|
|
66
61
|
|
|
67
62
|
/** Sales chart - returns series data for bar charts. */
|
|
68
|
-
getSalesChart:
|
|
69
|
-
endpoint: apiEndpoints.getSalesChart,
|
|
63
|
+
getSalesChart: createHandler((e) => e.getSalesChart, {
|
|
70
64
|
handler: async () => ({
|
|
71
65
|
type: "series",
|
|
72
66
|
config: {
|
|
@@ -85,8 +79,7 @@ export const dashboardHandlers = {
|
|
|
85
79
|
}),
|
|
86
80
|
|
|
87
81
|
/** Recent activity - returns table data. */
|
|
88
|
-
getRecentActivity:
|
|
89
|
-
endpoint: apiEndpoints.getRecentActivity,
|
|
82
|
+
getRecentActivity: createHandler((e) => e.getRecentActivity, {
|
|
90
83
|
handler: async () => ({
|
|
91
84
|
type: "table",
|
|
92
85
|
columns: [
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HttpError } from "@nubase/backend";
|
|
2
2
|
import type { SQL } from "drizzle-orm";
|
|
3
3
|
import { and, eq, ilike, inArray, or } from "drizzle-orm";
|
|
4
|
-
import { apiEndpoints } from "schema";
|
|
5
4
|
import { getDb } from "../../db/helpers/drizzle";
|
|
6
5
|
import { tickets } from "../../db/schema";
|
|
6
|
+
import { createHandler } from "../handler-factory";
|
|
7
7
|
|
|
8
8
|
export const ticketHandlers = {
|
|
9
|
-
getTickets:
|
|
10
|
-
endpoint: apiEndpoints.getTickets,
|
|
9
|
+
getTickets: createHandler((e) => e.getTickets, {
|
|
11
10
|
handler: async ({ params }) => {
|
|
12
11
|
const db = getDb();
|
|
13
12
|
|
|
@@ -64,8 +63,7 @@ export const ticketHandlers = {
|
|
|
64
63
|
},
|
|
65
64
|
}),
|
|
66
65
|
|
|
67
|
-
getTicket:
|
|
68
|
-
endpoint: apiEndpoints.getTicket,
|
|
66
|
+
getTicket: createHandler((e) => e.getTicket, {
|
|
69
67
|
handler: async ({ params }) => {
|
|
70
68
|
const [ticket] = await getDb()
|
|
71
69
|
.select()
|
|
@@ -85,8 +83,7 @@ export const ticketHandlers = {
|
|
|
85
83
|
},
|
|
86
84
|
}),
|
|
87
85
|
|
|
88
|
-
postTicket:
|
|
89
|
-
endpoint: apiEndpoints.postTicket,
|
|
86
|
+
postTicket: createHandler((e) => e.postTicket, {
|
|
90
87
|
handler: async ({ body }) => {
|
|
91
88
|
const [ticket] = await getDb()
|
|
92
89
|
.insert(tickets)
|
|
@@ -111,8 +108,7 @@ export const ticketHandlers = {
|
|
|
111
108
|
},
|
|
112
109
|
}),
|
|
113
110
|
|
|
114
|
-
patchTicket:
|
|
115
|
-
endpoint: apiEndpoints.patchTicket,
|
|
111
|
+
patchTicket: createHandler((e) => e.patchTicket, {
|
|
116
112
|
handler: async ({ params, body }) => {
|
|
117
113
|
const updateData: {
|
|
118
114
|
title?: string;
|
|
@@ -152,8 +148,7 @@ export const ticketHandlers = {
|
|
|
152
148
|
},
|
|
153
149
|
}),
|
|
154
150
|
|
|
155
|
-
deleteTicket:
|
|
156
|
-
endpoint: apiEndpoints.deleteTicket,
|
|
151
|
+
deleteTicket: createHandler((e) => e.deleteTicket, {
|
|
157
152
|
handler: async ({ params }) => {
|
|
158
153
|
const [deleted] = await getDb()
|
|
159
154
|
.delete(tickets)
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HttpError } from "@nubase/backend";
|
|
2
2
|
import type { SQL } from "drizzle-orm";
|
|
3
3
|
import { and, eq, ilike, or } from "drizzle-orm";
|
|
4
|
-
import { apiEndpoints } from "schema";
|
|
5
4
|
import { getDb } from "../../db/helpers/drizzle";
|
|
6
5
|
import { users } from "../../db/schema";
|
|
6
|
+
import { createHandler } from "../handler-factory";
|
|
7
7
|
|
|
8
8
|
export const userHandlers = {
|
|
9
9
|
/** Get all users with optional filters. */
|
|
10
|
-
getUsers:
|
|
11
|
-
endpoint: apiEndpoints.getUsers,
|
|
10
|
+
getUsers: createHandler((e) => e.getUsers, {
|
|
12
11
|
handler: async ({ params }) => {
|
|
13
12
|
const db = getDb();
|
|
14
13
|
|
|
@@ -53,8 +52,7 @@ export const userHandlers = {
|
|
|
53
52
|
}),
|
|
54
53
|
|
|
55
54
|
/** Get a single user by ID. */
|
|
56
|
-
getUser:
|
|
57
|
-
endpoint: apiEndpoints.getUser,
|
|
55
|
+
getUser: createHandler((e) => e.getUser, {
|
|
58
56
|
handler: async ({ params }) => {
|
|
59
57
|
const db = getDb();
|
|
60
58
|
const [user] = await db.select().from(users).where(eq(users.id, params.id));
|
|
@@ -72,8 +70,7 @@ export const userHandlers = {
|
|
|
72
70
|
}),
|
|
73
71
|
|
|
74
72
|
/** Create a new user. */
|
|
75
|
-
postUser:
|
|
76
|
-
endpoint: apiEndpoints.postUser,
|
|
73
|
+
postUser: createHandler((e) => e.postUser, {
|
|
77
74
|
handler: async ({ body }) => {
|
|
78
75
|
const db = getDb();
|
|
79
76
|
|
|
@@ -106,8 +103,7 @@ export const userHandlers = {
|
|
|
106
103
|
}),
|
|
107
104
|
|
|
108
105
|
/** Update a user by ID. */
|
|
109
|
-
patchUser:
|
|
110
|
-
endpoint: apiEndpoints.patchUser,
|
|
106
|
+
patchUser: createHandler((e) => e.patchUser, {
|
|
111
107
|
handler: async ({ params, body }) => {
|
|
112
108
|
const db = getDb();
|
|
113
109
|
|
|
@@ -138,8 +134,7 @@ export const userHandlers = {
|
|
|
138
134
|
}),
|
|
139
135
|
|
|
140
136
|
/** Delete a user by ID. */
|
|
141
|
-
deleteUser:
|
|
142
|
-
endpoint: apiEndpoints.deleteUser,
|
|
137
|
+
deleteUser: createHandler((e) => e.deleteUser, {
|
|
143
138
|
handler: async ({ params }) => {
|
|
144
139
|
const db = getDb();
|
|
145
140
|
|
|
@@ -154,8 +149,7 @@ export const userHandlers = {
|
|
|
154
149
|
}),
|
|
155
150
|
|
|
156
151
|
/** Lookup users for select/autocomplete fields. */
|
|
157
|
-
lookupUsers:
|
|
158
|
-
endpoint: apiEndpoints.lookupUsers,
|
|
152
|
+
lookupUsers: createHandler((e) => e.lookupUsers, {
|
|
159
153
|
handler: async ({ params }) => {
|
|
160
154
|
const db = getDb();
|
|
161
155
|
|