@nubase/create 0.1.3 → 0.1.5

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/dist/index.js CHANGED
@@ -26,7 +26,7 @@ function replaceInContent(content, options) {
26
26
  const kebabName = toKebabCase(options.name);
27
27
  const pascalName = toPascalCase(options.name);
28
28
  const camelName = toCamelCase(options.name);
29
- return content.replace(/__PROJECT_NAME__/g, kebabName).replace(/__PROJECT_NAME_PASCAL__/g, pascalName).replace(/__PROJECT_NAME_CAMEL__/g, camelName).replace(/__DB_NAME__/g, options.dbName).replace(/__DB_USER__/g, options.dbUser).replace(/__DB_PASSWORD__/g, options.dbPassword).replace(/__DEV_PORT__/g, String(options.devPort)).replace(/__TEST_PORT__/g, String(options.testPort)).replace(/__BACKEND_PORT__/g, String(options.backendPort)).replace(/__FRONTEND_PORT__/g, String(options.frontendPort));
29
+ return content.replace(/__PROJECT_NAME__/g, kebabName).replace(/__PROJECT_NAME_PASCAL__/g, pascalName).replace(/__PROJECT_NAME_CAMEL__/g, camelName).replace(/__DB_NAME__/g, options.dbName).replace(/__DB_USER__/g, options.dbUser).replace(/__DB_PASSWORD__/g, options.dbPassword).replace(/__DEV_PORT__/g, String(options.devPort)).replace(/__TEST_PORT__/g, String(options.testPort)).replace(/__BACKEND_PORT__/g, String(options.backendPort)).replace(/__FRONTEND_PORT__/g, String(options.frontendPort)).replace(/__TEST_BACKEND_PORT__/g, String(options.testBackendPort)).replace(/__TEST_FRONTEND_PORT__/g, String(options.testFrontendPort));
30
30
  }
31
31
  function copyTemplateDir(src, dest, options) {
32
32
  fse.ensureDirSync(dest);
@@ -48,7 +48,7 @@ function copyTemplateDir(src, dest, options) {
48
48
  }
49
49
  }
50
50
  async function main() {
51
- program.name("@nubase/create").description("Create a new Nubase application").argument("[project-name]", "Name of the project").option("--db-name <name>", "Database name").option("--db-user <user>", "Database user").option("--db-password <password>", "Database password").option("--dev-port <port>", "Development database port", "5434").option("--test-port <port>", "Test database port", "5435").option("--backend-port <port>", "Backend server port", "3001").option("--frontend-port <port>", "Frontend dev server port", "3002").option("--skip-install", "Skip npm install").parse();
51
+ program.name("@nubase/create").description("Create a new Nubase application").argument("[project-name]", "Name of the project").option("--db-name <name>", "Database name").option("--db-user <user>", "Database user").option("--db-password <password>", "Database password").option("--dev-port <port>", "Development database port", "5434").option("--test-port <port>", "Test database port", "5435").option("--backend-port <port>", "Backend server port", "3001").option("--frontend-port <port>", "Frontend dev server port", "3002").option("--test-backend-port <port>", "Test backend server port", "4001").option("--test-frontend-port <port>", "Test frontend dev server port", "4002").option("--skip-install", "Skip npm install").parse();
52
52
  const args = program.args;
53
53
  const opts = program.opts();
54
54
  console.log(chalk.bold.cyan("\n Welcome to Nubase!\n"));
@@ -78,7 +78,9 @@ async function main() {
78
78
  devPort: Number.parseInt(opts.devPort, 10),
79
79
  testPort: Number.parseInt(opts.testPort, 10),
80
80
  backendPort: Number.parseInt(opts.backendPort, 10),
81
- frontendPort: Number.parseInt(opts.frontendPort, 10)
81
+ frontendPort: Number.parseInt(opts.frontendPort, 10),
82
+ testBackendPort: Number.parseInt(opts.testBackendPort, 10),
83
+ testFrontendPort: Number.parseInt(opts.testFrontendPort, 10)
82
84
  };
83
85
  const targetDir = path.join(process.cwd(), projectName);
84
86
  if (fs.existsSync(targetDir)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubase/create",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Create a new Nubase application",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,17 +4,18 @@
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "NODE_ENV=development tsx watch src/index.ts",
7
- "dev:test": "PORT=__BACKEND_PORT__ DB_PORT=__TEST_PORT__ NODE_ENV=test tsx watch src/index.ts",
7
+ "dev:test": "PORT=__TEST_BACKEND_PORT__ DB_PORT=__TEST_PORT__ NODE_ENV=test tsx watch src/index.ts",
8
8
  "build": "tsc",
9
9
  "start": "node dist/index.js",
10
10
  "db:dev:up": "docker compose -f docker/dev/docker-compose.yml up -d",
11
11
  "db:dev:down": "docker compose -f docker/dev/docker-compose.yml down",
12
- "db:dev:kill": "docker compose -f docker/dev/docker-compose.yml down -v",
12
+ "db:dev:kill": "docker compose -f docker/dev/docker-compose.yml down -v && rm -rf docker/dev/postgresql-data",
13
13
  "db:dev:seed": "NODE_ENV=development tsx src/db/seed.ts",
14
+ "db:dev:reset": "npm run db:dev:kill && npm run db:schema-sync && npm run db:dev:up && sleep 5 && npm run db:dev:seed",
14
15
  "db:test:up": "docker compose -f docker/test/docker-compose.yml up -d",
15
16
  "db:test:down": "docker compose -f docker/test/docker-compose.yml down",
16
- "db:test:kill": "docker compose -f docker/test/docker-compose.yml down -v",
17
- "db:seed": "tsx src/db/seed.ts",
17
+ "db:test:kill": "docker compose -f docker/test/docker-compose.yml down -v && rm -rf docker/test/postgresql-data",
18
+ "db:test:reset": "npm run db:test:kill && npm run db:schema-sync && npm run db:test:up",
18
19
  "db:schema-sync": "cp db/schema.sql docker/dev/postgresql-init/dump.sql && cp db/schema.sql docker/test/postgresql-init/dump.sql",
19
20
  "typecheck": "tsc --noEmit",
20
21
  "lint": "biome check .",
@@ -1,209 +1,384 @@
1
+ import {
2
+ createHttpHandler,
3
+ getAuthController,
4
+ HttpError,
5
+ } from "@nubase/backend";
1
6
  import bcrypt from "bcrypt";
2
- import { eq } from "drizzle-orm";
3
- import type { Context } from "hono";
4
- import { __PROJECT_NAME_PASCAL__AuthController } from "../../auth";
5
- import { adminDb, db } from "../../db/helpers/drizzle";
6
- import { userWorkspaces, users, workspaces } from "../../db/schema";
7
-
8
- const authController = new __PROJECT_NAME_PASCAL__AuthController();
7
+ import { and, eq, inArray } from "drizzle-orm";
8
+ import { apiEndpoints } from "schema";
9
+ import jwt from "jsonwebtoken";
10
+ import type { __PROJECT_NAME_PASCAL__User } from "../../auth";
11
+ import { getAdminDb } from "../../db/helpers/drizzle";
12
+ import { users, userWorkspaces, workspaces } from "../../db/schema";
13
+
14
+ // Short-lived secret for login tokens (in production, use a proper secret)
15
+ const LOGIN_TOKEN_SECRET =
16
+ process.env.LOGIN_TOKEN_SECRET ||
17
+ "nubase-login-token-secret-change-in-production";
18
+ const LOGIN_TOKEN_EXPIRY = "5m"; // 5 minutes to complete workspace selection
19
+
20
+ interface LoginTokenPayload {
21
+ userId: number;
22
+ username: string;
23
+ }
9
24
 
10
25
  export const authHandlers = {
11
- async loginStart(c: Context) {
12
- const { email, password } = await c.req.json();
13
-
14
- const [user] = await db.select().from(users).where(eq(users.email, email));
15
-
16
- if (!user) {
17
- return c.json({ error: "Invalid credentials" }, 401);
18
- }
19
-
20
- const isValid = await bcrypt.compare(password, user.passwordHash);
21
- if (!isValid) {
22
- return c.json({ error: "Invalid credentials" }, 401);
23
- }
24
-
25
- // Get user's workspaces
26
- const userWs = await db
27
- .select({
28
- id: workspaces.id,
29
- slug: workspaces.slug,
30
- name: workspaces.name,
31
- })
32
- .from(userWorkspaces)
33
- .innerJoin(workspaces, eq(userWorkspaces.workspaceId, workspaces.id))
34
- .where(eq(userWorkspaces.userId, user.id));
35
-
36
- return c.json({ workspaces: userWs });
37
- },
38
-
39
- async loginComplete(c: Context) {
40
- const { email, password, workspaceId } = await c.req.json();
41
-
42
- const [user] = await db.select().from(users).where(eq(users.email, email));
43
-
44
- if (!user) {
45
- return c.json({ error: "Invalid credentials" }, 401);
46
- }
47
-
48
- const isValid = await bcrypt.compare(password, user.passwordHash);
49
- if (!isValid) {
50
- return c.json({ error: "Invalid credentials" }, 401);
51
- }
52
-
53
- const [workspace] = await db
54
- .select()
55
- .from(workspaces)
56
- .where(eq(workspaces.id, workspaceId));
57
-
58
- if (!workspace) {
59
- return c.json({ error: "Workspace not found" }, 404);
60
- }
61
-
62
- const token = authController.generateToken({
63
- userId: user.id,
64
- workspaceId: workspace.id,
65
- username: user.username,
66
- });
67
-
68
- return c.json({
69
- token,
70
- user: { id: user.id, email: user.email, username: user.username },
71
- workspace: { id: workspace.id, slug: workspace.slug, name: workspace.name },
72
- });
73
- },
74
-
75
- async login(c: Context) {
76
- const { email, password } = await c.req.json();
77
-
78
- const [user] = await db.select().from(users).where(eq(users.email, email));
79
-
80
- if (!user) {
81
- return c.json({ error: "Invalid credentials" }, 401);
82
- }
83
-
84
- const isValid = await bcrypt.compare(password, user.passwordHash);
85
- if (!isValid) {
86
- return c.json({ error: "Invalid credentials" }, 401);
87
- }
88
-
89
- // Get first workspace for simple login
90
- const [userWs] = await db
91
- .select({
92
- id: workspaces.id,
93
- slug: workspaces.slug,
94
- name: workspaces.name,
95
- })
96
- .from(userWorkspaces)
97
- .innerJoin(workspaces, eq(userWorkspaces.workspaceId, workspaces.id))
98
- .where(eq(userWorkspaces.userId, user.id));
99
-
100
- if (!userWs) {
101
- return c.json({ error: "No workspace found" }, 404);
102
- }
103
-
104
- const token = authController.generateToken({
105
- userId: user.id,
106
- workspaceId: userWs.id,
107
- username: user.username,
108
- });
109
-
110
- return c.json({
111
- token,
112
- user: { id: user.id, email: user.email, username: user.username },
113
- workspace: userWs,
114
- });
115
- },
116
-
117
- async logout(c: Context) {
118
- // Client-side token removal
119
- return c.json({ success: true });
120
- },
121
-
122
- async getMe(c: Context) {
123
- const authHeader = c.req.header("Authorization");
124
- if (!authHeader?.startsWith("Bearer ")) {
125
- return c.json({ error: "Unauthorized" }, 401);
126
- }
127
-
128
- const token = authHeader.slice(7);
129
- const payload = authController.verifyToken(token);
130
-
131
- if (!payload) {
132
- return c.json({ error: "Invalid token" }, 401);
133
- }
134
-
135
- const [user] = await db
136
- .select()
137
- .from(users)
138
- .where(eq(users.id, payload.userId));
139
-
140
- if (!user) {
141
- return c.json({ error: "User not found" }, 404);
142
- }
143
-
144
- const [workspace] = await db
145
- .select()
146
- .from(workspaces)
147
- .where(eq(workspaces.id, payload.workspaceId));
148
-
149
- return c.json({
150
- user: { id: user.id, email: user.email, username: user.username },
151
- workspace: workspace
152
- ? { id: workspace.id, slug: workspace.slug, name: workspace.name }
153
- : null,
154
- });
155
- },
156
-
157
- async signup(c: Context) {
158
- const { email, username, password, workspaceName } = await c.req.json();
159
-
160
- // Check if user exists
161
- const [existingUser] = await db
162
- .select()
163
- .from(users)
164
- .where(eq(users.email, email));
165
-
166
- if (existingUser) {
167
- return c.json({ error: "Email already registered" }, 400);
168
- }
169
-
170
- // Create user
171
- const passwordHash = await bcrypt.hash(password, 10);
172
- const [user] = await adminDb
173
- .insert(users)
174
- .values({ email, username, passwordHash })
175
- .returning();
176
-
177
- // Create or get workspace
178
- const wsSlug = workspaceName?.toLowerCase().replace(/\s+/g, "-") || "default";
179
- let [workspace] = await db
180
- .select()
181
- .from(workspaces)
182
- .where(eq(workspaces.slug, wsSlug));
183
-
184
- if (!workspace) {
185
- [workspace] = await adminDb
26
+ /**
27
+ * Login Start handler - Step 1 of two-step auth.
28
+ * Validates credentials and returns list of workspaces.
29
+ */
30
+ loginStart: createHttpHandler({
31
+ endpoint: apiEndpoints.loginStart,
32
+ handler: async ({ body }) => {
33
+ // Find user by username
34
+ const [user] = await getAdminDb()
35
+ .select()
36
+ .from(users)
37
+ .where(eq(users.username, body.username));
38
+
39
+ if (!user) {
40
+ throw new HttpError(401, "Invalid username or password");
41
+ }
42
+
43
+ // Verify password
44
+ const isValidPassword = await bcrypt.compare(
45
+ body.password,
46
+ user.passwordHash,
47
+ );
48
+ if (!isValidPassword) {
49
+ throw new HttpError(401, "Invalid username or password");
50
+ }
51
+
52
+ // Get all workspaces this user belongs to
53
+ const userWorkspaceRows = await getAdminDb()
54
+ .select()
55
+ .from(userWorkspaces)
56
+ .where(eq(userWorkspaces.userId, user.id));
57
+
58
+ if (userWorkspaceRows.length === 0) {
59
+ throw new HttpError(401, "User has no workspace access");
60
+ }
61
+
62
+ // Fetch workspace details
63
+ const workspaceIds = userWorkspaceRows.map((uw) => uw.workspaceId);
64
+ const workspaceList = await getAdminDb()
65
+ .select()
66
+ .from(workspaces)
67
+ .where(inArray(workspaces.id, workspaceIds));
68
+
69
+ // Create a short-lived login token
70
+ const loginToken = jwt.sign(
71
+ {
72
+ userId: user.id,
73
+ username: body.username,
74
+ } satisfies LoginTokenPayload,
75
+ LOGIN_TOKEN_SECRET,
76
+ { expiresIn: LOGIN_TOKEN_EXPIRY },
77
+ );
78
+
79
+ return {
80
+ loginToken,
81
+ username: body.username,
82
+ workspaces: workspaceList.map((w) => ({
83
+ id: w.id,
84
+ slug: w.slug,
85
+ name: w.name,
86
+ })),
87
+ };
88
+ },
89
+ }),
90
+
91
+ /**
92
+ * Login Complete handler - Step 2 of two-step auth.
93
+ * Validates the login token and selected workspace.
94
+ */
95
+ loginComplete: createHttpHandler({
96
+ endpoint: apiEndpoints.loginComplete,
97
+ handler: async ({ body, ctx }) => {
98
+ const authController = getAuthController<__PROJECT_NAME_PASCAL__User>(ctx);
99
+
100
+ // Verify the login token
101
+ let decoded: LoginTokenPayload;
102
+ try {
103
+ decoded = jwt.verify(
104
+ body.loginToken,
105
+ LOGIN_TOKEN_SECRET,
106
+ ) as LoginTokenPayload;
107
+ } catch {
108
+ throw new HttpError(401, "Invalid or expired login token");
109
+ }
110
+
111
+ // Look up the selected workspace
112
+ const [workspace] = await getAdminDb()
113
+ .select()
114
+ .from(workspaces)
115
+ .where(eq(workspaces.slug, body.workspace));
116
+
117
+ if (!workspace) {
118
+ throw new HttpError(404, `Workspace not found: ${body.workspace}`);
119
+ }
120
+
121
+ // Verify user has access to this workspace
122
+ const [access] = await getAdminDb()
123
+ .select()
124
+ .from(userWorkspaces)
125
+ .where(
126
+ and(
127
+ eq(userWorkspaces.userId, decoded.userId),
128
+ eq(userWorkspaces.workspaceId, workspace.id),
129
+ ),
130
+ );
131
+
132
+ if (!access) {
133
+ throw new HttpError(403, "You do not have access to this workspace");
134
+ }
135
+
136
+ // Fetch the user
137
+ const [dbUser] = await getAdminDb()
138
+ .select()
139
+ .from(users)
140
+ .where(eq(users.id, decoded.userId));
141
+
142
+ if (!dbUser) {
143
+ throw new HttpError(401, "User not found");
144
+ }
145
+
146
+ // Create user object for token
147
+ const user: __PROJECT_NAME_PASCAL__User = {
148
+ id: dbUser.id,
149
+ email: dbUser.email,
150
+ username: dbUser.username,
151
+ workspaceId: workspace.id,
152
+ };
153
+
154
+ // Create and set the auth token
155
+ const token = await authController.createToken(user);
156
+ authController.setTokenInResponse(ctx, token);
157
+
158
+ return {
159
+ user: {
160
+ id: user.id,
161
+ email: user.email,
162
+ username: user.username,
163
+ },
164
+ workspace: {
165
+ id: workspace.id,
166
+ slug: workspace.slug,
167
+ name: workspace.name,
168
+ },
169
+ };
170
+ },
171
+ }),
172
+
173
+ /**
174
+ * Legacy Login handler - validates credentials and sets HttpOnly cookie.
175
+ * @deprecated Use loginStart and loginComplete for two-step flow
176
+ */
177
+ login: createHttpHandler({
178
+ endpoint: apiEndpoints.login,
179
+ handler: async ({ body, ctx }) => {
180
+ const authController = getAuthController<__PROJECT_NAME_PASCAL__User>(ctx);
181
+
182
+ // Look up workspace
183
+ const [workspace] = await getAdminDb()
184
+ .select()
185
+ .from(workspaces)
186
+ .where(eq(workspaces.slug, body.workspace));
187
+
188
+ if (!workspace) {
189
+ throw new HttpError(404, `Workspace not found: ${body.workspace}`);
190
+ }
191
+
192
+ // Find user by username
193
+ const [dbUser] = await getAdminDb()
194
+ .select()
195
+ .from(users)
196
+ .where(eq(users.username, body.username));
197
+
198
+ if (!dbUser) {
199
+ throw new HttpError(401, "Invalid username or password");
200
+ }
201
+
202
+ // Verify password
203
+ const isValidPassword = await bcrypt.compare(
204
+ body.password,
205
+ dbUser.passwordHash,
206
+ );
207
+ if (!isValidPassword) {
208
+ throw new HttpError(401, "Invalid username or password");
209
+ }
210
+
211
+ // Verify user has access to this workspace
212
+ const [access] = await getAdminDb()
213
+ .select()
214
+ .from(userWorkspaces)
215
+ .where(
216
+ and(
217
+ eq(userWorkspaces.userId, dbUser.id),
218
+ eq(userWorkspaces.workspaceId, workspace.id),
219
+ ),
220
+ );
221
+
222
+ if (!access) {
223
+ throw new HttpError(403, "You do not have access to this workspace");
224
+ }
225
+
226
+ // Create user object for token
227
+ const user: __PROJECT_NAME_PASCAL__User = {
228
+ id: dbUser.id,
229
+ email: dbUser.email,
230
+ username: dbUser.username,
231
+ workspaceId: workspace.id,
232
+ };
233
+
234
+ // Create and set token
235
+ const token = await authController.createToken(user);
236
+ authController.setTokenInResponse(ctx, token);
237
+
238
+ return {
239
+ user: {
240
+ id: user.id,
241
+ email: user.email,
242
+ username: user.username,
243
+ },
244
+ };
245
+ },
246
+ }),
247
+
248
+ /** Logout handler - clears the auth cookie. */
249
+ logout: createHttpHandler({
250
+ endpoint: apiEndpoints.logout,
251
+ handler: async ({ ctx }) => {
252
+ const authController = getAuthController(ctx);
253
+ authController.clearTokenFromResponse(ctx);
254
+ return { success: true };
255
+ },
256
+ }),
257
+
258
+ /** Get current user handler. */
259
+ getMe: createHttpHandler<
260
+ typeof apiEndpoints.getMe,
261
+ "optional",
262
+ __PROJECT_NAME_PASCAL__User
263
+ >({
264
+ endpoint: apiEndpoints.getMe,
265
+ auth: "optional",
266
+ handler: async ({ user }) => {
267
+ if (!user) {
268
+ return { user: undefined };
269
+ }
270
+
271
+ return {
272
+ user: {
273
+ id: user.id,
274
+ email: user.email,
275
+ username: user.username,
276
+ },
277
+ };
278
+ },
279
+ }),
280
+
281
+ /** Signup handler - creates a new workspace and admin user. */
282
+ signup: createHttpHandler({
283
+ endpoint: apiEndpoints.signup,
284
+ handler: async ({ body, ctx }) => {
285
+ const authController = getAuthController<__PROJECT_NAME_PASCAL__User>(ctx);
286
+
287
+ // Validate workspace slug format
288
+ if (!/^[a-z0-9-]+$/.test(body.workspace)) {
289
+ throw new HttpError(
290
+ 400,
291
+ "Workspace slug must be lowercase and contain only letters, numbers, and hyphens",
292
+ );
293
+ }
294
+
295
+ // Check if workspace slug already exists
296
+ const [existingWorkspace] = await getAdminDb()
297
+ .select()
298
+ .from(workspaces)
299
+ .where(eq(workspaces.slug, body.workspace));
300
+
301
+ if (existingWorkspace) {
302
+ throw new HttpError(409, "Organization slug is already taken");
303
+ }
304
+
305
+ // Check if username already exists
306
+ const [existingUser] = await getAdminDb()
307
+ .select()
308
+ .from(users)
309
+ .where(eq(users.username, body.username));
310
+
311
+ if (existingUser) {
312
+ throw new HttpError(409, "Username is already taken");
313
+ }
314
+
315
+ // Check if email already exists
316
+ const [existingEmail] = await getAdminDb()
317
+ .select()
318
+ .from(users)
319
+ .where(eq(users.email, body.email));
320
+
321
+ if (existingEmail) {
322
+ throw new HttpError(409, "Email is already registered");
323
+ }
324
+
325
+ // Validate password length
326
+ if (body.password.length < 8) {
327
+ throw new HttpError(400, "Password must be at least 8 characters long");
328
+ }
329
+
330
+ // Create the workspace
331
+ const [newWorkspace] = await getAdminDb()
186
332
  .insert(workspaces)
187
- .values({ slug: wsSlug, name: workspaceName || "Default Workspace" })
333
+ .values({
334
+ slug: body.workspace,
335
+ name: body.workspaceName,
336
+ })
337
+ .returning();
338
+
339
+ // Hash the password
340
+ const passwordHash = await bcrypt.hash(body.password, 10);
341
+
342
+ // Create the admin user
343
+ const [newUser] = await getAdminDb()
344
+ .insert(users)
345
+ .values({
346
+ email: body.email,
347
+ username: body.username,
348
+ passwordHash,
349
+ })
188
350
  .returning();
189
- }
190
-
191
- // Link user to workspace
192
- await adminDb.insert(userWorkspaces).values({
193
- userId: user.id,
194
- workspaceId: workspace.id,
195
- });
196
-
197
- const token = authController.generateToken({
198
- userId: user.id,
199
- workspaceId: workspace.id,
200
- username: user.username,
201
- });
202
-
203
- return c.json({
204
- token,
205
- user: { id: user.id, email: user.email, username: user.username },
206
- workspace: { id: workspace.id, slug: workspace.slug, name: workspace.name },
207
- });
208
- },
351
+
352
+ // Link user to workspace
353
+ await getAdminDb().insert(userWorkspaces).values({
354
+ userId: newUser.id,
355
+ workspaceId: newWorkspace.id,
356
+ });
357
+
358
+ // Create user object for token
359
+ const user: __PROJECT_NAME_PASCAL__User = {
360
+ id: newUser.id,
361
+ email: newUser.email,
362
+ username: newUser.username,
363
+ workspaceId: newWorkspace.id,
364
+ };
365
+
366
+ // Create and set the auth token
367
+ const token = await authController.createToken(user);
368
+ authController.setTokenInResponse(ctx, token);
369
+
370
+ return {
371
+ user: {
372
+ id: user.id,
373
+ email: user.email,
374
+ username: user.username,
375
+ },
376
+ workspace: {
377
+ id: newWorkspace.id,
378
+ slug: newWorkspace.slug,
379
+ name: newWorkspace.name,
380
+ },
381
+ };
382
+ },
383
+ }),
209
384
  };
@@ -1,2 +1,3 @@
1
1
  export * from "./ticket";
2
2
  export * from "./auth";
3
+ export * from "./test-utils";