@nubase/create 0.1.23 → 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 +20 -13
- package/templates/backend/src/api/routes/user.ts +21 -15
- package/templates/frontend/src/resources/user.ts +2 -1
- package/templates/schema/src/endpoints/ticket/get-tickets.ts +2 -2
- package/templates/schema/src/endpoints/user/get-users.ts +2 -2
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,19 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HttpError } from "@nubase/backend";
|
|
2
2
|
import type { SQL } from "drizzle-orm";
|
|
3
|
-
import { and, eq, ilike, inArray } from "drizzle-orm";
|
|
4
|
-
import { apiEndpoints } from "schema";
|
|
3
|
+
import { and, eq, ilike, inArray, or } from "drizzle-orm";
|
|
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
|
|
|
14
13
|
// Build filter conditions
|
|
15
14
|
const conditions: SQL[] = [];
|
|
16
15
|
|
|
16
|
+
// Global text search - OR across searchable text fields
|
|
17
|
+
if (params.q) {
|
|
18
|
+
const searchTerm = `%${params.q}%`;
|
|
19
|
+
const searchCondition = or(
|
|
20
|
+
ilike(tickets.title, searchTerm),
|
|
21
|
+
ilike(tickets.description, searchTerm),
|
|
22
|
+
);
|
|
23
|
+
if (searchCondition) {
|
|
24
|
+
conditions.push(searchCondition);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
// Filter by title (case-insensitive partial match)
|
|
18
29
|
if (params.title) {
|
|
19
30
|
conditions.push(ilike(tickets.title, `%${params.title}%`));
|
|
@@ -52,8 +63,7 @@ export const ticketHandlers = {
|
|
|
52
63
|
},
|
|
53
64
|
}),
|
|
54
65
|
|
|
55
|
-
getTicket:
|
|
56
|
-
endpoint: apiEndpoints.getTicket,
|
|
66
|
+
getTicket: createHandler((e) => e.getTicket, {
|
|
57
67
|
handler: async ({ params }) => {
|
|
58
68
|
const [ticket] = await getDb()
|
|
59
69
|
.select()
|
|
@@ -73,8 +83,7 @@ export const ticketHandlers = {
|
|
|
73
83
|
},
|
|
74
84
|
}),
|
|
75
85
|
|
|
76
|
-
postTicket:
|
|
77
|
-
endpoint: apiEndpoints.postTicket,
|
|
86
|
+
postTicket: createHandler((e) => e.postTicket, {
|
|
78
87
|
handler: async ({ body }) => {
|
|
79
88
|
const [ticket] = await getDb()
|
|
80
89
|
.insert(tickets)
|
|
@@ -99,8 +108,7 @@ export const ticketHandlers = {
|
|
|
99
108
|
},
|
|
100
109
|
}),
|
|
101
110
|
|
|
102
|
-
patchTicket:
|
|
103
|
-
endpoint: apiEndpoints.patchTicket,
|
|
111
|
+
patchTicket: createHandler((e) => e.patchTicket, {
|
|
104
112
|
handler: async ({ params, body }) => {
|
|
105
113
|
const updateData: {
|
|
106
114
|
title?: string;
|
|
@@ -140,8 +148,7 @@ export const ticketHandlers = {
|
|
|
140
148
|
},
|
|
141
149
|
}),
|
|
142
150
|
|
|
143
|
-
deleteTicket:
|
|
144
|
-
endpoint: apiEndpoints.deleteTicket,
|
|
151
|
+
deleteTicket: createHandler((e) => e.deleteTicket, {
|
|
145
152
|
handler: async ({ params }) => {
|
|
146
153
|
const [deleted] = await getDb()
|
|
147
154
|
.delete(tickets)
|
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HttpError } from "@nubase/backend";
|
|
2
2
|
import type { SQL } from "drizzle-orm";
|
|
3
|
-
import { and, eq, ilike } from "drizzle-orm";
|
|
4
|
-
import { apiEndpoints } from "schema";
|
|
3
|
+
import { and, eq, ilike, or } from "drizzle-orm";
|
|
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
|
|
|
15
14
|
// Build filter conditions
|
|
16
15
|
const conditions: SQL[] = [];
|
|
17
16
|
|
|
17
|
+
// Global text search - OR across searchable text fields
|
|
18
|
+
if (params.q) {
|
|
19
|
+
const searchTerm = `%${params.q}%`;
|
|
20
|
+
const searchCondition = or(
|
|
21
|
+
ilike(users.displayName, searchTerm),
|
|
22
|
+
ilike(users.email, searchTerm),
|
|
23
|
+
);
|
|
24
|
+
if (searchCondition) {
|
|
25
|
+
conditions.push(searchCondition);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
// Filter by displayName (case-insensitive partial match)
|
|
19
30
|
if (params.displayName) {
|
|
20
31
|
conditions.push(ilike(users.displayName, `%${params.displayName}%`));
|
|
@@ -41,8 +52,7 @@ export const userHandlers = {
|
|
|
41
52
|
}),
|
|
42
53
|
|
|
43
54
|
/** Get a single user by ID. */
|
|
44
|
-
getUser:
|
|
45
|
-
endpoint: apiEndpoints.getUser,
|
|
55
|
+
getUser: createHandler((e) => e.getUser, {
|
|
46
56
|
handler: async ({ params }) => {
|
|
47
57
|
const db = getDb();
|
|
48
58
|
const [user] = await db.select().from(users).where(eq(users.id, params.id));
|
|
@@ -60,8 +70,7 @@ export const userHandlers = {
|
|
|
60
70
|
}),
|
|
61
71
|
|
|
62
72
|
/** Create a new user. */
|
|
63
|
-
postUser:
|
|
64
|
-
endpoint: apiEndpoints.postUser,
|
|
73
|
+
postUser: createHandler((e) => e.postUser, {
|
|
65
74
|
handler: async ({ body }) => {
|
|
66
75
|
const db = getDb();
|
|
67
76
|
|
|
@@ -94,8 +103,7 @@ export const userHandlers = {
|
|
|
94
103
|
}),
|
|
95
104
|
|
|
96
105
|
/** Update a user by ID. */
|
|
97
|
-
patchUser:
|
|
98
|
-
endpoint: apiEndpoints.patchUser,
|
|
106
|
+
patchUser: createHandler((e) => e.patchUser, {
|
|
99
107
|
handler: async ({ params, body }) => {
|
|
100
108
|
const db = getDb();
|
|
101
109
|
|
|
@@ -126,8 +134,7 @@ export const userHandlers = {
|
|
|
126
134
|
}),
|
|
127
135
|
|
|
128
136
|
/** Delete a user by ID. */
|
|
129
|
-
deleteUser:
|
|
130
|
-
endpoint: apiEndpoints.deleteUser,
|
|
137
|
+
deleteUser: createHandler((e) => e.deleteUser, {
|
|
131
138
|
handler: async ({ params }) => {
|
|
132
139
|
const db = getDb();
|
|
133
140
|
|
|
@@ -142,8 +149,7 @@ export const userHandlers = {
|
|
|
142
149
|
}),
|
|
143
150
|
|
|
144
151
|
/** Lookup users for select/autocomplete fields. */
|
|
145
|
-
lookupUsers:
|
|
146
|
-
endpoint: apiEndpoints.lookupUsers,
|
|
152
|
+
lookupUsers: createHandler((e) => e.lookupUsers, {
|
|
147
153
|
handler: async ({ params }) => {
|
|
148
154
|
const db = getDb();
|
|
149
155
|
|
|
@@ -100,12 +100,13 @@ export const userResource = createResource("user")
|
|
|
100
100
|
id: "search-users",
|
|
101
101
|
title: "Users",
|
|
102
102
|
schemaGet: (api) => api.getUsers.responseBody,
|
|
103
|
+
schemaFilter: (api) => api.getUsers.requestParams,
|
|
103
104
|
breadcrumbs: () => [{ label: "Users", to: "/r/user/search" }],
|
|
104
105
|
tableActions: ["delete"],
|
|
105
106
|
rowActions: ["delete"],
|
|
106
107
|
onLoad: async ({ context }) => {
|
|
107
108
|
return context.http.getUsers({
|
|
108
|
-
params: {},
|
|
109
|
+
params: context.params || {},
|
|
109
110
|
});
|
|
110
111
|
},
|
|
111
112
|
},
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { nu, type RequestSchema } from "@nubase/core";
|
|
1
|
+
import { nu, type RequestSchema, withSearchParams } from "@nubase/core";
|
|
2
2
|
import { ticketSchema } from "../../resources/ticket";
|
|
3
3
|
|
|
4
4
|
export const getTicketsSchema = {
|
|
5
5
|
method: "GET" as const,
|
|
6
6
|
path: "/tickets",
|
|
7
|
-
requestParams: ticketSchema.omit("id").partial(),
|
|
7
|
+
requestParams: withSearchParams(ticketSchema.omit("id").partial()),
|
|
8
8
|
responseBody: nu.array(ticketSchema),
|
|
9
9
|
} satisfies RequestSchema;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { nu, type RequestSchema } from "@nubase/core";
|
|
1
|
+
import { nu, type RequestSchema, withSearchParams } from "@nubase/core";
|
|
2
2
|
import { userSchema } from "../../resources/user";
|
|
3
3
|
|
|
4
4
|
export const getUsersSchema = {
|
|
5
5
|
method: "GET" as const,
|
|
6
6
|
path: "/users",
|
|
7
|
-
requestParams: userSchema.omit("id").partial(),
|
|
7
|
+
requestParams: withSearchParams(userSchema.omit("id").partial()),
|
|
8
8
|
responseBody: nu.array(userSchema),
|
|
9
9
|
} satisfies RequestSchema;
|