@nubase/create 0.1.6 → 0.1.8

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.
Files changed (35) hide show
  1. package/dist/index.js +5 -3
  2. package/package.json +4 -2
  3. package/templates/backend/db/schema.sql +1 -1
  4. package/templates/backend/docker/dev/postgresql-init/dump.sql +1 -1
  5. package/templates/backend/docker/test/postgresql-init/dump.sql +1 -1
  6. package/templates/backend/package.json +1 -1
  7. package/templates/backend/src/api/routes/auth.ts +19 -29
  8. package/templates/backend/src/api/routes/test-utils.ts +6 -6
  9. package/templates/backend/src/auth/index.ts +8 -8
  10. package/templates/backend/src/db/schema/user.ts +1 -1
  11. package/templates/backend/src/db/seed.ts +2 -2
  12. package/templates/frontend/e2e/auth.spec.ts +5 -5
  13. package/templates/frontend/e2e/fixtures/base.ts +3 -3
  14. package/templates/frontend/e2e/fixtures/test-api.ts +6 -6
  15. package/templates/frontend/src/auth/__PROJECT_NAME_PASCAL__AuthController.ts +5 -5
  16. package/templates/schema/src/api-endpoints.ts +7 -7
  17. package/templates/schema/src/index.ts +1 -1
  18. package/templates/schema/src/schema/auth/get-me.ts +15 -0
  19. package/templates/schema/src/schema/auth/index.ts +8 -0
  20. package/templates/schema/src/schema/auth/login-complete.ts +25 -0
  21. package/templates/schema/src/schema/auth/login-start.ts +28 -0
  22. package/templates/schema/src/schema/auth/login.ts +24 -0
  23. package/templates/schema/src/schema/auth/logout.ts +14 -0
  24. package/templates/schema/src/schema/auth/signup.ts +32 -0
  25. package/templates/schema/src/schema/auth/user.ts +10 -0
  26. package/templates/schema/src/schema/auth/workspace.ts +10 -0
  27. package/templates/schema/src/schema/ticket/delete-ticket.ts +12 -0
  28. package/templates/schema/src/schema/ticket/get-ticket.ts +9 -0
  29. package/templates/schema/src/schema/ticket/get-tickets.ts +9 -0
  30. package/templates/schema/src/schema/ticket/index.ts +6 -0
  31. package/templates/schema/src/schema/ticket/patch-ticket.ts +10 -0
  32. package/templates/schema/src/schema/ticket/post-ticket.ts +10 -0
  33. package/templates/schema/src/schema/ticket/ticket-base.ts +28 -0
  34. package/templates/schema/src/schema/auth.ts +0 -144
  35. package/templates/schema/src/schema/ticket.ts +0 -71
package/dist/index.js CHANGED
@@ -26,7 +26,8 @@ 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)).replace(/__TEST_BACKEND_PORT__/g, String(options.testBackendPort)).replace(/__TEST_FRONTEND_PORT__/g, String(options.testFrontendPort));
29
+ const nubaseVersion = options.nubaseTag === "latest" ? "*" : options.nubaseTag;
30
+ 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)).replace(/"@nubase\/core": "\*"/g, `"@nubase/core": "${nubaseVersion}"`).replace(/"@nubase\/frontend": "\*"/g, `"@nubase/frontend": "${nubaseVersion}"`).replace(/"@nubase\/backend": "\*"/g, `"@nubase/backend": "${nubaseVersion}"`).replace(/"@nubase\/create": "\*"/g, `"@nubase/create": "${nubaseVersion}"`);
30
31
  }
31
32
  function copyTemplateDir(src, dest, options) {
32
33
  fse.ensureDirSync(dest);
@@ -48,7 +49,7 @@ function copyTemplateDir(src, dest, options) {
48
49
  }
49
50
  }
50
51
  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("--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
+ 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").option("--tag <tag>", "npm tag for @nubase/* packages (latest, dev)", "latest").parse();
52
53
  const args = program.args;
53
54
  const opts = program.opts();
54
55
  console.log(chalk.bold.cyan("\n Welcome to Nubase!\n"));
@@ -80,7 +81,8 @@ async function main() {
80
81
  backendPort: Number.parseInt(opts.backendPort, 10),
81
82
  frontendPort: Number.parseInt(opts.frontendPort, 10),
82
83
  testBackendPort: Number.parseInt(opts.testBackendPort, 10),
83
- testFrontendPort: Number.parseInt(opts.testFrontendPort, 10)
84
+ testFrontendPort: Number.parseInt(opts.testFrontendPort, 10),
85
+ nubaseTag: opts.tag
84
86
  };
85
87
  const targetDir = path.join(process.cwd(), projectName);
86
88
  if (fs.existsSync(targetDir)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubase/create",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Create a new Nubase application",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,9 @@
16
16
  "typecheck": "tsc --noEmit",
17
17
  "lint": "biome lint .",
18
18
  "lint:fix": "biome lint --write .",
19
- "prepublishOnly": "npm run build"
19
+ "prepublishOnly": "npm run build",
20
+ "publish:latest": "npm publish --access public",
21
+ "publish:dev": "npm publish --access public --tag dev"
20
22
  },
21
23
  "dependencies": {
22
24
  "chalk": "^5.3.0",
@@ -34,7 +34,7 @@ GRANT USAGE, SELECT ON SEQUENCE workspaces_id_seq TO __DB_NAME___app;
34
34
  CREATE TABLE IF NOT EXISTS users (
35
35
  id SERIAL PRIMARY KEY,
36
36
  email VARCHAR(255) UNIQUE NOT NULL,
37
- username VARCHAR(100) UNIQUE NOT NULL,
37
+ display_name VARCHAR(100) NOT NULL,
38
38
  password_hash VARCHAR(255) NOT NULL,
39
39
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
40
40
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
@@ -34,7 +34,7 @@ GRANT USAGE, SELECT ON SEQUENCE workspaces_id_seq TO __DB_NAME___app;
34
34
  CREATE TABLE IF NOT EXISTS users (
35
35
  id SERIAL PRIMARY KEY,
36
36
  email VARCHAR(255) UNIQUE NOT NULL,
37
- username VARCHAR(100) UNIQUE NOT NULL,
37
+ display_name VARCHAR(100) NOT NULL,
38
38
  password_hash VARCHAR(255) NOT NULL,
39
39
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
40
40
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
@@ -34,7 +34,7 @@ GRANT USAGE, SELECT ON SEQUENCE workspaces_id_seq TO __DB_NAME___app;
34
34
  CREATE TABLE IF NOT EXISTS users (
35
35
  id SERIAL PRIMARY KEY,
36
36
  email VARCHAR(255) UNIQUE NOT NULL,
37
- username VARCHAR(100) UNIQUE NOT NULL,
37
+ display_name VARCHAR(100) NOT NULL,
38
38
  password_hash VARCHAR(255) NOT NULL,
39
39
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
40
40
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
@@ -29,7 +29,7 @@
29
29
  "bcrypt": "^5.1.1",
30
30
  "dotenv": "^16.4.7",
31
31
  "drizzle-orm": "^0.38.3",
32
- "hono": "^4.6.14",
32
+ "hono": "^4.11.1",
33
33
  "jsonwebtoken": "^9.0.2",
34
34
  "pg": "^8.13.1",
35
35
  "schema": "*"
@@ -19,7 +19,7 @@ const LOGIN_TOKEN_EXPIRY = "5m"; // 5 minutes to complete workspace selection
19
19
 
20
20
  interface LoginTokenPayload {
21
21
  userId: number;
22
- username: string;
22
+ email: string;
23
23
  }
24
24
 
25
25
  export const authHandlers = {
@@ -30,14 +30,14 @@ export const authHandlers = {
30
30
  loginStart: createHttpHandler({
31
31
  endpoint: apiEndpoints.loginStart,
32
32
  handler: async ({ body }) => {
33
- // Find user by username
33
+ // Find user by email
34
34
  const [user] = await getAdminDb()
35
35
  .select()
36
36
  .from(users)
37
- .where(eq(users.username, body.username));
37
+ .where(eq(users.email, body.email));
38
38
 
39
39
  if (!user) {
40
- throw new HttpError(401, "Invalid username or password");
40
+ throw new HttpError(401, "Invalid email or password");
41
41
  }
42
42
 
43
43
  // Verify password
@@ -46,7 +46,7 @@ export const authHandlers = {
46
46
  user.passwordHash,
47
47
  );
48
48
  if (!isValidPassword) {
49
- throw new HttpError(401, "Invalid username or password");
49
+ throw new HttpError(401, "Invalid email or password");
50
50
  }
51
51
 
52
52
  // Get all workspaces this user belongs to
@@ -70,7 +70,7 @@ export const authHandlers = {
70
70
  const loginToken = jwt.sign(
71
71
  {
72
72
  userId: user.id,
73
- username: body.username,
73
+ email: body.email,
74
74
  } satisfies LoginTokenPayload,
75
75
  LOGIN_TOKEN_SECRET,
76
76
  { expiresIn: LOGIN_TOKEN_EXPIRY },
@@ -78,7 +78,7 @@ export const authHandlers = {
78
78
 
79
79
  return {
80
80
  loginToken,
81
- username: body.username,
81
+ email: body.email,
82
82
  workspaces: workspaceList.map((w) => ({
83
83
  id: w.id,
84
84
  slug: w.slug,
@@ -147,7 +147,7 @@ export const authHandlers = {
147
147
  const user: __PROJECT_NAME_PASCAL__User = {
148
148
  id: dbUser.id,
149
149
  email: dbUser.email,
150
- username: dbUser.username,
150
+ displayName: dbUser.displayName,
151
151
  workspaceId: workspace.id,
152
152
  };
153
153
 
@@ -159,7 +159,7 @@ export const authHandlers = {
159
159
  user: {
160
160
  id: user.id,
161
161
  email: user.email,
162
- username: user.username,
162
+ displayName: user.displayName,
163
163
  },
164
164
  workspace: {
165
165
  id: workspace.id,
@@ -189,14 +189,14 @@ export const authHandlers = {
189
189
  throw new HttpError(404, `Workspace not found: ${body.workspace}`);
190
190
  }
191
191
 
192
- // Find user by username
192
+ // Find user by email
193
193
  const [dbUser] = await getAdminDb()
194
194
  .select()
195
195
  .from(users)
196
- .where(eq(users.username, body.username));
196
+ .where(eq(users.email, body.email));
197
197
 
198
198
  if (!dbUser) {
199
- throw new HttpError(401, "Invalid username or password");
199
+ throw new HttpError(401, "Invalid email or password");
200
200
  }
201
201
 
202
202
  // Verify password
@@ -205,7 +205,7 @@ export const authHandlers = {
205
205
  dbUser.passwordHash,
206
206
  );
207
207
  if (!isValidPassword) {
208
- throw new HttpError(401, "Invalid username or password");
208
+ throw new HttpError(401, "Invalid email or password");
209
209
  }
210
210
 
211
211
  // Verify user has access to this workspace
@@ -227,7 +227,7 @@ export const authHandlers = {
227
227
  const user: __PROJECT_NAME_PASCAL__User = {
228
228
  id: dbUser.id,
229
229
  email: dbUser.email,
230
- username: dbUser.username,
230
+ displayName: dbUser.displayName,
231
231
  workspaceId: workspace.id,
232
232
  };
233
233
 
@@ -239,7 +239,7 @@ export const authHandlers = {
239
239
  user: {
240
240
  id: user.id,
241
241
  email: user.email,
242
- username: user.username,
242
+ displayName: user.displayName,
243
243
  },
244
244
  };
245
245
  },
@@ -272,7 +272,7 @@ export const authHandlers = {
272
272
  user: {
273
273
  id: user.id,
274
274
  email: user.email,
275
- username: user.username,
275
+ displayName: user.displayName,
276
276
  },
277
277
  };
278
278
  },
@@ -302,16 +302,6 @@ export const authHandlers = {
302
302
  throw new HttpError(409, "Organization slug is already taken");
303
303
  }
304
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
305
  // Check if email already exists
316
306
  const [existingEmail] = await getAdminDb()
317
307
  .select()
@@ -344,7 +334,7 @@ export const authHandlers = {
344
334
  .insert(users)
345
335
  .values({
346
336
  email: body.email,
347
- username: body.username,
337
+ displayName: body.displayName,
348
338
  passwordHash,
349
339
  })
350
340
  .returning();
@@ -359,7 +349,7 @@ export const authHandlers = {
359
349
  const user: __PROJECT_NAME_PASCAL__User = {
360
350
  id: newUser.id,
361
351
  email: newUser.email,
362
- username: newUser.username,
352
+ displayName: newUser.displayName,
363
353
  workspaceId: newWorkspace.id,
364
354
  };
365
355
 
@@ -371,7 +361,7 @@ export const authHandlers = {
371
361
  user: {
372
362
  id: user.id,
373
363
  email: user.email,
374
- username: user.username,
364
+ displayName: user.displayName,
375
365
  },
376
366
  workspace: {
377
367
  id: newWorkspace.id,
@@ -75,7 +75,7 @@ export const handleClearDatabase = createHttpHandler({
75
75
  .insert(users)
76
76
  .values({
77
77
  email: "admin@example.com",
78
- username: "admin",
78
+ displayName: "Admin User",
79
79
  passwordHash,
80
80
  })
81
81
  .returning();
@@ -274,9 +274,9 @@ export const handleSeedMultiWorkspaceUser = createHttpHandler({
274
274
  path: "/api/test/seed-multi-workspace-user",
275
275
  requestParams: emptySchema,
276
276
  requestBody: nu.object({
277
- username: nu.string(),
278
- password: nu.string(),
279
277
  email: nu.string(),
278
+ password: nu.string(),
279
+ displayName: nu.string(),
280
280
  workspaces: nu.array(
281
281
  nu.object({
282
282
  slug: nu.string(),
@@ -313,7 +313,7 @@ export const handleSeedMultiWorkspaceUser = createHttpHandler({
313
313
  const existingUsers = await db
314
314
  .select()
315
315
  .from(users)
316
- .where(eq(users.username, body.username));
316
+ .where(eq(users.email, body.email));
317
317
 
318
318
  if (existingUsers.length > 0) {
319
319
  userId = existingUsers[0].id;
@@ -323,7 +323,7 @@ export const handleSeedMultiWorkspaceUser = createHttpHandler({
323
323
  .insert(users)
324
324
  .values({
325
325
  email: body.email,
326
- username: body.username,
326
+ displayName: body.displayName,
327
327
  passwordHash,
328
328
  })
329
329
  .returning();
@@ -374,7 +374,7 @@ export const handleSeedMultiWorkspaceUser = createHttpHandler({
374
374
 
375
375
  return {
376
376
  success: true,
377
- message: `User ${body.username} seeded in ${createdWorkspaces.length} workspaces`,
377
+ message: `User ${body.email} seeded in ${createdWorkspaces.length} workspaces`,
378
378
  workspaces: createdWorkspaces,
379
379
  };
380
380
  },
@@ -18,7 +18,7 @@ import { users, userWorkspaces } from "../db/schema";
18
18
  export interface __PROJECT_NAME_PASCAL__User extends BackendUser {
19
19
  id: number;
20
20
  email: string;
21
- username: string;
21
+ displayName: string;
22
22
  workspaceId: number;
23
23
  }
24
24
 
@@ -27,7 +27,7 @@ export interface __PROJECT_NAME_PASCAL__User extends BackendUser {
27
27
  */
28
28
  export interface __PROJECT_NAME_PASCAL__TokenPayload extends TokenPayload {
29
29
  userId: number;
30
- username: string;
30
+ email: string;
31
31
  workspaceId: number;
32
32
  }
33
33
 
@@ -109,7 +109,7 @@ export class __PROJECT_NAME_PASCAL__AuthController
109
109
  user: {
110
110
  id: dbUser.id,
111
111
  email: dbUser.email,
112
- username: dbUser.username,
112
+ displayName: dbUser.displayName,
113
113
  workspaceId: decoded.workspaceId,
114
114
  },
115
115
  };
@@ -137,7 +137,7 @@ export class __PROJECT_NAME_PASCAL__AuthController
137
137
  ): Promise<string> {
138
138
  const payload: __PROJECT_NAME_PASCAL__TokenPayload = {
139
139
  userId: user.id,
140
- username: user.username,
140
+ email: user.email,
141
141
  workspaceId: user.workspaceId,
142
142
  ...additionalPayload,
143
143
  };
@@ -169,7 +169,7 @@ export class __PROJECT_NAME_PASCAL__AuthController
169
169
  * Validate user credentials during login.
170
170
  */
171
171
  async validateCredentials(
172
- username: string,
172
+ email: string,
173
173
  password: string,
174
174
  workspaceId?: number,
175
175
  ): Promise<__PROJECT_NAME_PASCAL__User | null> {
@@ -179,11 +179,11 @@ export class __PROJECT_NAME_PASCAL__AuthController
179
179
  );
180
180
  }
181
181
 
182
- // Find user by username
182
+ // Find user by email
183
183
  const [dbUser] = await getAdminDb()
184
184
  .select()
185
185
  .from(users)
186
- .where(eq(users.username, username));
186
+ .where(eq(users.email, email));
187
187
 
188
188
  if (!dbUser) {
189
189
  return null;
@@ -213,7 +213,7 @@ export class __PROJECT_NAME_PASCAL__AuthController
213
213
  return {
214
214
  id: dbUser.id,
215
215
  email: dbUser.email,
216
- username: dbUser.username,
216
+ displayName: dbUser.displayName,
217
217
  workspaceId: workspaceId,
218
218
  };
219
219
  }
@@ -3,7 +3,7 @@ import { pgTable, serial, timestamp, varchar } from "drizzle-orm/pg-core";
3
3
  export const users = pgTable("users", {
4
4
  id: serial("id").primaryKey(),
5
5
  email: varchar("email", { length: 255 }).unique().notNull(),
6
- username: varchar("username", { length: 100 }).unique().notNull(),
6
+ displayName: varchar("display_name", { length: 100 }).notNull(),
7
7
  passwordHash: varchar("password_hash", { length: 255 }).notNull(),
8
8
  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
9
9
  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
@@ -32,7 +32,7 @@ async function seed() {
32
32
  .insert(users)
33
33
  .values({
34
34
  email: "admin@example.com",
35
- username: "admin",
35
+ displayName: "Admin User",
36
36
  passwordHash,
37
37
  })
38
38
  .onConflictDoNothing()
@@ -63,7 +63,7 @@ async function seed() {
63
63
 
64
64
  console.log("Database seeded successfully!");
65
65
  console.log("\nTest credentials:");
66
- console.log(" Username: admin");
66
+ console.log(" Email: admin@example.com");
67
67
  console.log(" Password: password123");
68
68
 
69
69
  process.exit(0);
@@ -12,7 +12,7 @@ test.describe("Authentication", () => {
12
12
  await expect(page.locator("h1")).toContainText("Sign In");
13
13
 
14
14
  // Fill in valid credentials
15
- await page.fill("#username", TEST_USER.username);
15
+ await page.fill("#email", TEST_USER.email);
16
16
  await page.fill("#password", TEST_USER.password);
17
17
 
18
18
  // Submit the form
@@ -35,7 +35,7 @@ test.describe("Authentication", () => {
35
35
  await page.goto("/signin");
36
36
 
37
37
  // Fill in invalid credentials
38
- await page.fill("#username", "wronguser");
38
+ await page.fill("#email", "wrong@example.com");
39
39
  await page.fill("#password", "wrongpassword");
40
40
 
41
41
  // Submit the form
@@ -44,7 +44,7 @@ test.describe("Authentication", () => {
44
44
  // Wait for error message to appear
45
45
  const errorMessage = page.getByTestId("signin-error");
46
46
  await expect(errorMessage).toBeVisible();
47
- await expect(errorMessage).toContainText("Invalid username or password");
47
+ await expect(errorMessage).toContainText("Invalid email or password");
48
48
 
49
49
  // Should still be on signin page
50
50
  expect(page.url()).toContain("/signin");
@@ -85,8 +85,8 @@ test.describe("Authentication", () => {
85
85
  const userAvatar = authenticatedPage.getByTestId("user-avatar");
86
86
  await expect(userAvatar).toBeVisible();
87
87
 
88
- // Avatar should show the user's initials (AD for "admin")
89
- await expect(userAvatar).toContainText("AD");
88
+ // Avatar should show the user's initials (AU for "Admin User")
89
+ await expect(userAvatar).toContainText("AU");
90
90
  });
91
91
 
92
92
  test("should sign out successfully when clicking sign out", async ({
@@ -11,7 +11,7 @@ const WORKSPACE_PREFIX = `/${TEST_WORKSPACE}`;
11
11
  * Can be used directly in tests that need to test login functionality.
12
12
  *
13
13
  * With the two-step login flow:
14
- * 1. User enters username + password at /signin
14
+ * 1. User enters email + password at /signin
15
15
  * 2. If user belongs to one workspace, auto-completes and redirects to /$workspace
16
16
  * 3. If user belongs to multiple workspaces, shows selection screen
17
17
  *
@@ -19,14 +19,14 @@ const WORKSPACE_PREFIX = `/${TEST_WORKSPACE}`;
19
19
  */
20
20
  export async function performLogin(
21
21
  page: Page,
22
- username: string = TEST_USER.username,
22
+ email: string = TEST_USER.email,
23
23
  password: string = TEST_USER.password,
24
24
  ) {
25
25
  // Navigate to root-level signin page
26
26
  await page.goto("/signin");
27
27
 
28
28
  // Fill in credentials
29
- await page.fill("#username", username);
29
+ await page.fill("#email", email);
30
30
  await page.fill("#password", password);
31
31
 
32
32
  // Submit the form
@@ -2,9 +2,9 @@ import type { APIRequestContext } from "@playwright/test";
2
2
 
3
3
  // Test user credentials - matches what test-utils.ts seeds
4
4
  export const TEST_USER = {
5
- username: "admin",
6
- password: "password123",
7
5
  email: "admin@example.com",
6
+ password: "password123",
7
+ displayName: "Admin User",
8
8
  };
9
9
 
10
10
  // Test workspace for path-based multi-workspace
@@ -41,12 +41,12 @@ export class TestAPI {
41
41
  * Login as the test user and return cookies for authenticated requests
42
42
  */
43
43
  async login(
44
- username: string = TEST_USER.username,
44
+ email: string = TEST_USER.email,
45
45
  password: string = TEST_USER.password,
46
46
  workspace: string = TEST_WORKSPACE,
47
47
  ) {
48
48
  const response = await this.request.post(`${API_BASE_URL}/auth/login`, {
49
- data: { username, password, workspace },
49
+ data: { email, password, workspace },
50
50
  });
51
51
 
52
52
  if (!response.ok()) {
@@ -108,9 +108,9 @@ export class TestAPI {
108
108
  * Seed a user with multiple workspaces for testing workspace selection flow
109
109
  */
110
110
  async seedMultiWorkspaceUser(data: {
111
- username: string;
112
- password: string;
113
111
  email: string;
112
+ password: string;
113
+ displayName: string;
114
114
  workspaces: Array<{ slug: string; name: string }>;
115
115
  }) {
116
116
  const response = await this.request.post(
@@ -60,7 +60,7 @@ export class __PROJECT_NAME_PASCAL__AuthController
60
60
  },
61
61
  credentials: "include", // Important for cookies
62
62
  body: JSON.stringify({
63
- username: credentials.username,
63
+ email: credentials.email,
64
64
  password: credentials.password,
65
65
  workspace: credentials.workspace,
66
66
  }),
@@ -68,7 +68,7 @@ export class __PROJECT_NAME_PASCAL__AuthController
68
68
 
69
69
  if (!response.ok) {
70
70
  const errorData = await response.json().catch(() => ({}));
71
- throw new Error(errorData.error || "Invalid username or password");
71
+ throw new Error(errorData.error || "Invalid email or password");
72
72
  }
73
73
 
74
74
  const data = await response.json();
@@ -155,7 +155,7 @@ export class __PROJECT_NAME_PASCAL__AuthController
155
155
  * Step 1: Validates credentials and returns list of workspaces user belongs to.
156
156
  */
157
157
  async loginStart(credentials: {
158
- username: string;
158
+ email: string;
159
159
  password: string;
160
160
  }): Promise<LoginStartResponse> {
161
161
  const response = await fetch(`${this.apiBaseUrl}/auth/login/start`, {
@@ -165,14 +165,14 @@ export class __PROJECT_NAME_PASCAL__AuthController
165
165
  },
166
166
  credentials: "include",
167
167
  body: JSON.stringify({
168
- username: credentials.username,
168
+ email: credentials.email,
169
169
  password: credentials.password,
170
170
  }),
171
171
  });
172
172
 
173
173
  if (!response.ok) {
174
174
  const errorData = await response.json().catch(() => ({}));
175
- throw new Error(errorData.error || "Invalid username or password");
175
+ throw new Error(errorData.error || "Invalid email or password");
176
176
  }
177
177
 
178
178
  return response.json();
@@ -1,10 +1,3 @@
1
- import {
2
- deleteTicketSchema,
3
- getTicketSchema,
4
- getTicketsSchema,
5
- patchTicketSchema,
6
- postTicketSchema,
7
- } from "./schema/ticket";
8
1
  import {
9
2
  getMeSchema,
10
3
  loginCompleteSchema,
@@ -13,6 +6,13 @@ import {
13
6
  logoutSchema,
14
7
  signupSchema,
15
8
  } from "./schema/auth";
9
+ import {
10
+ deleteTicketSchema,
11
+ getTicketSchema,
12
+ getTicketsSchema,
13
+ patchTicketSchema,
14
+ postTicketSchema,
15
+ } from "./schema/ticket";
16
16
 
17
17
  export const apiEndpoints = {
18
18
  // Tickets
@@ -1,3 +1,3 @@
1
1
  export * from "./api-endpoints";
2
- export * from "./schema/ticket";
3
2
  export * from "./schema/auth";
3
+ export * from "./schema/ticket";
@@ -0,0 +1,15 @@
1
+ import { emptySchema, nu, type RequestSchema } from "@nubase/core";
2
+ import { userSchema } from "./user";
3
+
4
+ /**
5
+ * Get current user schema
6
+ * GET /auth/me
7
+ */
8
+ export const getMeSchema = {
9
+ method: "GET" as const,
10
+ path: "/auth/me",
11
+ requestParams: emptySchema,
12
+ responseBody: nu.object({
13
+ user: userSchema.optional(),
14
+ }),
15
+ } satisfies RequestSchema;
@@ -0,0 +1,8 @@
1
+ export { getMeSchema } from "./get-me";
2
+ export { loginSchema } from "./login";
3
+ export { loginCompleteSchema } from "./login-complete";
4
+ export { loginStartSchema } from "./login-start";
5
+ export { logoutSchema } from "./logout";
6
+ export { signupSchema } from "./signup";
7
+ export { userSchema } from "./user";
8
+ export { workspaceInfoSchema } from "./workspace";
@@ -0,0 +1,25 @@
1
+ import { emptySchema, nu, type RequestSchema } from "@nubase/core";
2
+ import { userSchema } from "./user";
3
+ import { workspaceInfoSchema } from "./workspace";
4
+
5
+ /**
6
+ * Login complete request schema - Step 2 of two-step auth
7
+ * POST /auth/login/complete
8
+ *
9
+ * Completes login by selecting a workspace and issuing the full auth token.
10
+ */
11
+ export const loginCompleteSchema = {
12
+ method: "POST" as const,
13
+ path: "/auth/login/complete",
14
+ requestParams: emptySchema,
15
+ requestBody: nu.object({
16
+ /** Temporary login token from login/start */
17
+ loginToken: nu.string(),
18
+ /** Selected workspace slug */
19
+ workspace: nu.string(),
20
+ }),
21
+ responseBody: nu.object({
22
+ user: userSchema,
23
+ workspace: workspaceInfoSchema,
24
+ }),
25
+ } satisfies RequestSchema;
@@ -0,0 +1,28 @@
1
+ import { emptySchema, nu, type RequestSchema } from "@nubase/core";
2
+ import { workspaceInfoSchema } from "./workspace";
3
+
4
+ /**
5
+ * Login start request schema - Step 1 of two-step auth
6
+ * POST /auth/login/start
7
+ *
8
+ * Validates credentials and returns list of workspaces the user belongs to.
9
+ * If user has multiple workspaces, frontend should show selection.
10
+ * If user has one workspace, frontend can auto-complete login.
11
+ */
12
+ export const loginStartSchema = {
13
+ method: "POST" as const,
14
+ path: "/auth/login/start",
15
+ requestParams: emptySchema,
16
+ requestBody: nu.object({
17
+ email: nu.string(),
18
+ password: nu.string(),
19
+ }),
20
+ responseBody: nu.object({
21
+ /** Temporary token for completing login (short-lived) */
22
+ loginToken: nu.string(),
23
+ /** User's email (for display) */
24
+ email: nu.string(),
25
+ /** List of workspaces the user belongs to */
26
+ workspaces: nu.array(workspaceInfoSchema),
27
+ }),
28
+ } satisfies RequestSchema;
@@ -0,0 +1,24 @@
1
+ import { emptySchema, nu, type RequestSchema } from "@nubase/core";
2
+ import { userSchema } from "./user";
3
+
4
+ /**
5
+ * Legacy login request schema (kept for backwards compatibility)
6
+ * POST /auth/login
7
+ *
8
+ * For path-based multi-workspace, the workspace slug is required in the request body.
9
+ * @deprecated Use loginStartSchema and loginCompleteSchema for two-step flow
10
+ */
11
+ export const loginSchema = {
12
+ method: "POST" as const,
13
+ path: "/auth/login",
14
+ requestParams: emptySchema,
15
+ requestBody: nu.object({
16
+ email: nu.string(),
17
+ password: nu.string(),
18
+ /** Workspace slug for path-based multi-workspace */
19
+ workspace: nu.string(),
20
+ }),
21
+ responseBody: nu.object({
22
+ user: userSchema,
23
+ }),
24
+ } satisfies RequestSchema;
@@ -0,0 +1,14 @@
1
+ import { emptySchema, nu, type RequestSchema } from "@nubase/core";
2
+
3
+ /**
4
+ * Logout request schema
5
+ * POST /auth/logout
6
+ */
7
+ export const logoutSchema = {
8
+ method: "POST" as const,
9
+ path: "/auth/logout",
10
+ requestParams: emptySchema,
11
+ responseBody: nu.object({
12
+ success: nu.boolean(),
13
+ }),
14
+ } satisfies RequestSchema;
@@ -0,0 +1,32 @@
1
+ import { emptySchema, nu, type RequestSchema } from "@nubase/core";
2
+ import { userSchema } from "./user";
3
+ import { workspaceInfoSchema } from "./workspace";
4
+
5
+ /**
6
+ * Signup request schema
7
+ * POST /auth/signup
8
+ *
9
+ * Creates a new workspace and the initial admin user.
10
+ * After successful signup, the user is automatically logged in.
11
+ */
12
+ export const signupSchema = {
13
+ method: "POST" as const,
14
+ path: "/auth/signup",
15
+ requestParams: emptySchema,
16
+ requestBody: nu.object({
17
+ /** Workspace slug (unique identifier) */
18
+ workspace: nu.string(),
19
+ /** Display name for the workspace */
20
+ workspaceName: nu.string(),
21
+ /** Email for the admin user */
22
+ email: nu.string(),
23
+ /** Display name for the admin user */
24
+ displayName: nu.string(),
25
+ /** Password for the admin user */
26
+ password: nu.string(),
27
+ }),
28
+ responseBody: nu.object({
29
+ user: userSchema,
30
+ workspace: workspaceInfoSchema,
31
+ }),
32
+ } satisfies RequestSchema;
@@ -0,0 +1,10 @@
1
+ import { nu } from "@nubase/core";
2
+
3
+ /**
4
+ * User schema for authenticated user data
5
+ */
6
+ export const userSchema = nu.object({
7
+ id: nu.number(),
8
+ email: nu.string(),
9
+ displayName: nu.string(),
10
+ });
@@ -0,0 +1,10 @@
1
+ import { nu } from "@nubase/core";
2
+
3
+ /**
4
+ * Workspace info returned during login
5
+ */
6
+ export const workspaceInfoSchema = nu.object({
7
+ id: nu.number(),
8
+ slug: nu.string(),
9
+ name: nu.string(),
10
+ });
@@ -0,0 +1,12 @@
1
+ import {
2
+ idNumberSchema,
3
+ type RequestSchema,
4
+ successSchema,
5
+ } from "@nubase/core";
6
+
7
+ export const deleteTicketSchema = {
8
+ method: "DELETE" as const,
9
+ path: "/tickets/:id",
10
+ requestParams: idNumberSchema,
11
+ responseBody: successSchema,
12
+ } satisfies RequestSchema;
@@ -0,0 +1,9 @@
1
+ import { idNumberSchema, type RequestSchema } from "@nubase/core";
2
+ import { ticketBaseSchema } from "./ticket-base";
3
+
4
+ export const getTicketSchema = {
5
+ method: "GET" as const,
6
+ path: "/tickets/:id",
7
+ requestParams: idNumberSchema,
8
+ responseBody: ticketBaseSchema,
9
+ } satisfies RequestSchema;
@@ -0,0 +1,9 @@
1
+ import { nu, type RequestSchema } from "@nubase/core";
2
+ import { ticketBaseSchema } from "./ticket-base";
3
+
4
+ export const getTicketsSchema = {
5
+ method: "GET" as const,
6
+ path: "/tickets",
7
+ requestParams: ticketBaseSchema.omit("id").partial(),
8
+ responseBody: nu.array(ticketBaseSchema),
9
+ } satisfies RequestSchema;
@@ -0,0 +1,6 @@
1
+ export { deleteTicketSchema } from "./delete-ticket";
2
+ export { getTicketSchema } from "./get-ticket";
3
+ export { getTicketsSchema } from "./get-tickets";
4
+ export { patchTicketSchema } from "./patch-ticket";
5
+ export { postTicketSchema } from "./post-ticket";
6
+ export { ticketBaseSchema } from "./ticket-base";
@@ -0,0 +1,10 @@
1
+ import { idNumberSchema, type RequestSchema } from "@nubase/core";
2
+ import { ticketBaseSchema } from "./ticket-base";
3
+
4
+ export const patchTicketSchema = {
5
+ method: "PATCH" as const,
6
+ path: "/tickets/:id",
7
+ requestParams: idNumberSchema,
8
+ requestBody: ticketBaseSchema.omit("id").partial(),
9
+ responseBody: ticketBaseSchema,
10
+ } satisfies RequestSchema;
@@ -0,0 +1,10 @@
1
+ import { emptySchema, type RequestSchema } from "@nubase/core";
2
+ import { ticketBaseSchema } from "./ticket-base";
3
+
4
+ export const postTicketSchema = {
5
+ method: "POST" as const,
6
+ path: "/tickets",
7
+ requestParams: emptySchema,
8
+ requestBody: ticketBaseSchema.omit("id"),
9
+ responseBody: ticketBaseSchema,
10
+ } satisfies RequestSchema;
@@ -0,0 +1,28 @@
1
+ import { nu } from "@nubase/core";
2
+
3
+ export const ticketBaseSchema = nu
4
+ .object({
5
+ id: nu.number(),
6
+ title: nu.string().withMeta({
7
+ label: "Title",
8
+ description: "Enter the title of the ticket",
9
+ }),
10
+ description: nu.string().optional().withMeta({
11
+ label: "Description",
12
+ description: "Enter the description of the ticket",
13
+ renderer: "multiline",
14
+ }),
15
+ })
16
+ .withId("id")
17
+ .withTableLayouts({
18
+ default: {
19
+ fields: [
20
+ { name: "id", columnWidthPx: 80, pinned: true },
21
+ { name: "title", columnWidthPx: 300, pinned: true },
22
+ { name: "description", columnWidthPx: 400 },
23
+ ],
24
+ metadata: {
25
+ linkFields: ["title"],
26
+ },
27
+ },
28
+ });
@@ -1,144 +0,0 @@
1
- import { emptySchema, nu, type RequestSchema } from "@nubase/core";
2
-
3
- /**
4
- * User schema for authenticated user data
5
- */
6
- export const userSchema = nu.object({
7
- id: nu.number(),
8
- email: nu.string(),
9
- username: nu.string(),
10
- });
11
-
12
- /**
13
- * Workspace info returned during login
14
- */
15
- export const workspaceInfoSchema = nu.object({
16
- id: nu.number(),
17
- slug: nu.string(),
18
- name: nu.string(),
19
- });
20
-
21
- /**
22
- * Login start request schema - Step 1 of two-step auth
23
- * POST /auth/login/start
24
- *
25
- * Validates credentials and returns list of workspaces the user belongs to.
26
- * If user has multiple workspaces, frontend should show selection.
27
- * If user has one workspace, frontend can auto-complete login.
28
- */
29
- export const loginStartSchema = {
30
- method: "POST" as const,
31
- path: "/auth/login/start",
32
- requestParams: emptySchema,
33
- requestBody: nu.object({
34
- username: nu.string(),
35
- password: nu.string(),
36
- }),
37
- responseBody: nu.object({
38
- /** Temporary token for completing login (short-lived) */
39
- loginToken: nu.string(),
40
- /** User's username (for display) */
41
- username: nu.string(),
42
- /** List of workspaces the user belongs to */
43
- workspaces: nu.array(workspaceInfoSchema),
44
- }),
45
- } satisfies RequestSchema;
46
-
47
- /**
48
- * Login complete request schema - Step 2 of two-step auth
49
- * POST /auth/login/complete
50
- *
51
- * Completes login by selecting a workspace and issuing the full auth token.
52
- */
53
- export const loginCompleteSchema = {
54
- method: "POST" as const,
55
- path: "/auth/login/complete",
56
- requestParams: emptySchema,
57
- requestBody: nu.object({
58
- /** Temporary login token from login/start */
59
- loginToken: nu.string(),
60
- /** Selected workspace slug */
61
- workspace: nu.string(),
62
- }),
63
- responseBody: nu.object({
64
- user: userSchema,
65
- workspace: workspaceInfoSchema,
66
- }),
67
- } satisfies RequestSchema;
68
-
69
- /**
70
- * Legacy login request schema (kept for backwards compatibility)
71
- * POST /auth/login
72
- *
73
- * For path-based multi-workspace, the workspace slug is required in the request body.
74
- * @deprecated Use loginStartSchema and loginCompleteSchema for two-step flow
75
- */
76
- export const loginSchema = {
77
- method: "POST" as const,
78
- path: "/auth/login",
79
- requestParams: emptySchema,
80
- requestBody: nu.object({
81
- username: nu.string(),
82
- password: nu.string(),
83
- /** Workspace slug for path-based multi-workspace */
84
- workspace: nu.string(),
85
- }),
86
- responseBody: nu.object({
87
- user: userSchema,
88
- }),
89
- } satisfies RequestSchema;
90
-
91
- /**
92
- * Logout request schema
93
- * POST /auth/logout
94
- */
95
- export const logoutSchema = {
96
- method: "POST" as const,
97
- path: "/auth/logout",
98
- requestParams: emptySchema,
99
- responseBody: nu.object({
100
- success: nu.boolean(),
101
- }),
102
- } satisfies RequestSchema;
103
-
104
- /**
105
- * Get current user schema
106
- * GET /auth/me
107
- */
108
- export const getMeSchema = {
109
- method: "GET" as const,
110
- path: "/auth/me",
111
- requestParams: emptySchema,
112
- responseBody: nu.object({
113
- user: userSchema.optional(),
114
- }),
115
- } satisfies RequestSchema;
116
-
117
- /**
118
- * Signup request schema
119
- * POST /auth/signup
120
- *
121
- * Creates a new workspace and the initial admin user.
122
- * After successful signup, the user is automatically logged in.
123
- */
124
- export const signupSchema = {
125
- method: "POST" as const,
126
- path: "/auth/signup",
127
- requestParams: emptySchema,
128
- requestBody: nu.object({
129
- /** Workspace slug (unique identifier) */
130
- workspace: nu.string(),
131
- /** Display name for the workspace */
132
- workspaceName: nu.string(),
133
- /** Username for the admin user */
134
- username: nu.string(),
135
- /** Email for the admin user */
136
- email: nu.string(),
137
- /** Password for the admin user */
138
- password: nu.string(),
139
- }),
140
- responseBody: nu.object({
141
- user: userSchema,
142
- workspace: workspaceInfoSchema,
143
- }),
144
- } satisfies RequestSchema;
@@ -1,71 +0,0 @@
1
- import {
2
- emptySchema,
3
- idNumberSchema,
4
- nu,
5
- type RequestSchema,
6
- successSchema,
7
- } from "@nubase/core";
8
-
9
- export const ticketBaseSchema = nu
10
- .object({
11
- id: nu.number(),
12
- title: nu.string().withMeta({
13
- label: "Title",
14
- description: "Enter the title of the ticket",
15
- }),
16
- description: nu.string().optional().withMeta({
17
- label: "Description",
18
- description: "Enter the description of the ticket",
19
- renderer: "multiline",
20
- }),
21
- })
22
- .withId("id")
23
- .withTableLayouts({
24
- default: {
25
- fields: [
26
- { name: "id", columnWidthPx: 80, pinned: true },
27
- { name: "title", columnWidthPx: 300, pinned: true },
28
- { name: "description", columnWidthPx: 400 },
29
- ],
30
- metadata: {
31
- linkFields: ["title"],
32
- },
33
- },
34
- });
35
-
36
- export const getTicketsSchema = {
37
- method: "GET" as const,
38
- path: "/tickets",
39
- requestParams: ticketBaseSchema.omit("id").partial(),
40
- responseBody: nu.array(ticketBaseSchema),
41
- } satisfies RequestSchema;
42
-
43
- export const getTicketSchema = {
44
- method: "GET" as const,
45
- path: "/tickets/:id",
46
- requestParams: idNumberSchema,
47
- responseBody: ticketBaseSchema,
48
- } satisfies RequestSchema;
49
-
50
- export const postTicketSchema = {
51
- method: "POST" as const,
52
- path: "/tickets",
53
- requestParams: emptySchema,
54
- requestBody: ticketBaseSchema.omit("id"),
55
- responseBody: ticketBaseSchema,
56
- } satisfies RequestSchema;
57
-
58
- export const patchTicketSchema = {
59
- method: "PATCH" as const,
60
- path: "/tickets/:id",
61
- requestParams: idNumberSchema,
62
- requestBody: ticketBaseSchema.omit("id").partial(),
63
- responseBody: ticketBaseSchema,
64
- } satisfies RequestSchema;
65
-
66
- export const deleteTicketSchema = {
67
- method: "DELETE" as const,
68
- path: "/tickets/:id",
69
- requestParams: idNumberSchema,
70
- responseBody: successSchema,
71
- } satisfies RequestSchema;