@nubase/create 0.1.1 → 0.1.3

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
@@ -107,10 +107,7 @@ Creating project in ${chalk.bold(targetDir)}...
107
107
  if (template === "root") {
108
108
  copyTemplateDir(templatePath, targetDir, options);
109
109
  } else {
110
- const destPath = path.join(
111
- targetDir,
112
- `${toKebabCase(projectName)}-${template}`
113
- );
110
+ const destPath = path.join(targetDir, template);
114
111
  copyTemplateDir(templatePath, destPath, options);
115
112
  }
116
113
  console.log(chalk.green(` \u2713 Created ${template}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubase/create",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Create a new Nubase application",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "__PROJECT_NAME__-backend",
2
+ "name": "backend",
3
3
  "version": "0.1.0",
4
4
  "type": "module",
5
5
  "scripts": {
@@ -31,7 +31,7 @@
31
31
  "hono": "^4.6.14",
32
32
  "jsonwebtoken": "^9.0.2",
33
33
  "pg": "^8.13.1",
34
- "__PROJECT_NAME__-schema": "*"
34
+ "schema": "*"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@faker-js/faker": "^9.3.0",
@@ -62,7 +62,7 @@ export const authHandlers = {
62
62
  const token = authController.generateToken({
63
63
  userId: user.id,
64
64
  workspaceId: workspace.id,
65
- email: user.email,
65
+ username: user.username,
66
66
  });
67
67
 
68
68
  return c.json({
@@ -104,7 +104,7 @@ export const authHandlers = {
104
104
  const token = authController.generateToken({
105
105
  userId: user.id,
106
106
  workspaceId: userWs.id,
107
- email: user.email,
107
+ username: user.username,
108
108
  });
109
109
 
110
110
  return c.json({
@@ -197,7 +197,7 @@ export const authHandlers = {
197
197
  const token = authController.generateToken({
198
198
  userId: user.id,
199
199
  workspaceId: workspace.id,
200
- email: user.email,
200
+ username: user.username,
201
201
  });
202
202
 
203
203
  return c.json({
@@ -1,15 +1,10 @@
1
1
  import bcrypt from "bcrypt";
2
2
  import jwt from "jsonwebtoken";
3
- import type { User, Workspace } from "__PROJECT_NAME__-schema";
4
-
5
- export interface __PROJECT_NAME_PASCAL__User extends User {
6
- workspace: Workspace;
7
- }
8
3
 
9
4
  export interface TokenPayload {
10
5
  userId: number;
11
6
  workspaceId: number;
12
- email: string;
7
+ username: string;
13
8
  }
14
9
 
15
10
  const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "__PROJECT_NAME__-frontend",
2
+ "name": "frontend",
3
3
  "private": true,
4
4
  "version": "0.1.0",
5
5
  "type": "module",
@@ -15,10 +15,11 @@
15
15
  "@nubase/frontend": "*",
16
16
  "@tailwindcss/vite": "^4.1.18",
17
17
  "@tanstack/react-router": "^1.141.8",
18
+ "lucide-react": "^0.511.0",
18
19
  "react": "^19.0.0",
19
20
  "react-dom": "^19.0.0",
20
21
  "tailwindcss": "^4.1.18",
21
- "__PROJECT_NAME__-schema": "*"
22
+ "schema": "*"
22
23
  },
23
24
  "devDependencies": {
24
25
  "@types/react": "^19.0.10",
@@ -1,59 +1,269 @@
1
- import type { AuthController, AuthState } from "@nubase/frontend";
2
- import type { ApiEndpoints } from "__PROJECT_NAME__-schema";
1
+ import type {
2
+ AuthenticatedUser,
3
+ AuthenticationController,
4
+ AuthenticationState,
5
+ AuthenticationStateListener,
6
+ LoginCompleteCredentials,
7
+ LoginCredentials,
8
+ LoginStartResponse,
9
+ SignupCredentials,
10
+ WorkspaceInfo,
11
+ } from "@nubase/frontend";
3
12
 
4
- const TOKEN_KEY = "__PROJECT_NAME___auth_token";
13
+ /**
14
+ * Application-specific implementation of AuthenticationController.
15
+ * Communicates with the backend auth endpoints using HttpOnly cookies.
16
+ */
17
+ export class __PROJECT_NAME_PASCAL__AuthController
18
+ implements AuthenticationController
19
+ {
20
+ private state: AuthenticationState = {
21
+ status: "loading",
22
+ user: null,
23
+ error: null,
24
+ };
5
25
 
6
- export class __PROJECT_NAME_PASCAL__AuthController implements AuthController<ApiEndpoints> {
7
- private token: string | null = null;
26
+ private listeners = new Set<AuthenticationStateListener>();
27
+ private apiBaseUrl: string;
8
28
 
9
- constructor() {
10
- this.token = localStorage.getItem(TOKEN_KEY);
29
+ constructor(apiBaseUrl: string) {
30
+ this.apiBaseUrl = apiBaseUrl;
11
31
  }
12
32
 
13
- async getAuthState(
14
- httpClient: {
15
- request: <E extends keyof ApiEndpoints>(
16
- endpoint: E,
17
- options?: { body?: unknown; params?: unknown },
18
- ) => Promise<ApiEndpoints[E]["responseBody"]>;
19
- },
20
- workspace: string,
21
- ): Promise<AuthState> {
22
- if (!this.token) {
23
- return { isAuthenticated: false };
33
+ getState(): AuthenticationState {
34
+ return this.state;
35
+ }
36
+
37
+ subscribe(listener: AuthenticationStateListener): () => void {
38
+ this.listeners.add(listener);
39
+ return () => {
40
+ this.listeners.delete(listener);
41
+ };
42
+ }
43
+
44
+ private setState(newState: Partial<AuthenticationState>): void {
45
+ this.state = { ...this.state, ...newState };
46
+ for (const listener of this.listeners) {
47
+ listener(this.state);
48
+ }
49
+ }
50
+
51
+ async login(credentials: LoginCredentials): Promise<void> {
52
+ this.setState({ status: "loading", error: null });
53
+
54
+ try {
55
+ // Include workspace in the login request body for path-based multi-workspace
56
+ const response = await fetch(`${this.apiBaseUrl}/auth/login`, {
57
+ method: "POST",
58
+ headers: {
59
+ "Content-Type": "application/json",
60
+ },
61
+ credentials: "include", // Important for cookies
62
+ body: JSON.stringify({
63
+ username: credentials.username,
64
+ password: credentials.password,
65
+ workspace: credentials.workspace,
66
+ }),
67
+ });
68
+
69
+ if (!response.ok) {
70
+ const errorData = await response.json().catch(() => ({}));
71
+ throw new Error(errorData.error || "Invalid username or password");
72
+ }
73
+
74
+ const data = await response.json();
75
+ const user: AuthenticatedUser = data.user;
76
+
77
+ this.setState({
78
+ status: "authenticated",
79
+ user,
80
+ error: null,
81
+ });
82
+ } catch (error) {
83
+ const err = error instanceof Error ? error : new Error("Login failed");
84
+ this.setState({
85
+ status: "unauthenticated",
86
+ user: null,
87
+ error: err,
88
+ });
89
+ throw err;
24
90
  }
91
+ }
25
92
 
93
+ async logout(): Promise<void> {
26
94
  try {
27
- const response = await httpClient.request("getMe", {});
28
- return {
29
- isAuthenticated: true,
30
- user: response.user,
31
- workspace: response.workspace,
32
- };
95
+ await fetch(`${this.apiBaseUrl}/auth/logout`, {
96
+ method: "POST",
97
+ credentials: "include",
98
+ });
33
99
  } catch {
34
- this.clearToken();
35
- return { isAuthenticated: false };
100
+ // Logout errors are not critical - clear local state anyway
36
101
  }
102
+
103
+ this.setState({
104
+ status: "unauthenticated",
105
+ user: null,
106
+ error: null,
107
+ });
37
108
  }
38
109
 
39
- getAuthHeaders(): Record<string, string> {
40
- if (!this.token) {
41
- return {};
110
+ async initialize(): Promise<void> {
111
+ this.setState({ status: "loading", error: null });
112
+
113
+ try {
114
+ const response = await fetch(`${this.apiBaseUrl}/auth/me`, {
115
+ method: "GET",
116
+ credentials: "include",
117
+ });
118
+
119
+ if (!response.ok) {
120
+ this.setState({
121
+ status: "unauthenticated",
122
+ user: null,
123
+ error: null,
124
+ });
125
+ return;
126
+ }
127
+
128
+ const data = await response.json();
129
+
130
+ if (data.user) {
131
+ this.setState({
132
+ status: "authenticated",
133
+ user: data.user,
134
+ error: null,
135
+ });
136
+ } else {
137
+ this.setState({
138
+ status: "unauthenticated",
139
+ user: null,
140
+ error: null,
141
+ });
142
+ }
143
+ } catch {
144
+ // Network error or server unavailable - treat as unauthenticated
145
+ this.setState({
146
+ status: "unauthenticated",
147
+ user: null,
148
+ error: null,
149
+ });
42
150
  }
43
- return { Authorization: `Bearer ${this.token}` };
44
151
  }
45
152
 
46
- setToken(token: string): void {
47
- this.token = token;
48
- localStorage.setItem(TOKEN_KEY, token);
153
+ /**
154
+ * Start the two-step login process.
155
+ * Step 1: Validates credentials and returns list of workspaces user belongs to.
156
+ */
157
+ async loginStart(credentials: {
158
+ username: string;
159
+ password: string;
160
+ }): Promise<LoginStartResponse> {
161
+ const response = await fetch(`${this.apiBaseUrl}/auth/login/start`, {
162
+ method: "POST",
163
+ headers: {
164
+ "Content-Type": "application/json",
165
+ },
166
+ credentials: "include",
167
+ body: JSON.stringify({
168
+ username: credentials.username,
169
+ password: credentials.password,
170
+ }),
171
+ });
172
+
173
+ if (!response.ok) {
174
+ const errorData = await response.json().catch(() => ({}));
175
+ throw new Error(errorData.error || "Invalid username or password");
176
+ }
177
+
178
+ return response.json();
49
179
  }
50
180
 
51
- clearToken(): void {
52
- this.token = null;
53
- localStorage.removeItem(TOKEN_KEY);
181
+ /**
182
+ * Complete the two-step login process.
183
+ * Step 2: Select a workspace and complete authentication.
184
+ */
185
+ async loginComplete(
186
+ credentials: LoginCompleteCredentials,
187
+ ): Promise<WorkspaceInfo> {
188
+ this.setState({ status: "loading", error: null });
189
+
190
+ try {
191
+ const response = await fetch(`${this.apiBaseUrl}/auth/login/complete`, {
192
+ method: "POST",
193
+ headers: {
194
+ "Content-Type": "application/json",
195
+ },
196
+ credentials: "include",
197
+ body: JSON.stringify({
198
+ loginToken: credentials.loginToken,
199
+ workspace: credentials.workspace,
200
+ }),
201
+ });
202
+
203
+ if (!response.ok) {
204
+ const errorData = await response.json().catch(() => ({}));
205
+ throw new Error(errorData.error || "Login failed");
206
+ }
207
+
208
+ const data = await response.json();
209
+ const user: AuthenticatedUser = data.user;
210
+
211
+ this.setState({
212
+ status: "authenticated",
213
+ user,
214
+ error: null,
215
+ });
216
+
217
+ return data.workspace;
218
+ } catch (error) {
219
+ const err = error instanceof Error ? error : new Error("Login failed");
220
+ this.setState({
221
+ status: "unauthenticated",
222
+ user: null,
223
+ error: err,
224
+ });
225
+ throw err;
226
+ }
54
227
  }
55
228
 
56
- getToken(): string | null {
57
- return this.token;
229
+ /**
230
+ * Sign up a new user and create a new workspace.
231
+ * After successful signup, the user is automatically logged in.
232
+ */
233
+ async signup(credentials: SignupCredentials): Promise<void> {
234
+ this.setState({ status: "loading", error: null });
235
+
236
+ try {
237
+ const response = await fetch(`${this.apiBaseUrl}/auth/signup`, {
238
+ method: "POST",
239
+ headers: {
240
+ "Content-Type": "application/json",
241
+ },
242
+ credentials: "include",
243
+ body: JSON.stringify(credentials),
244
+ });
245
+
246
+ if (!response.ok) {
247
+ const errorData = await response.json().catch(() => ({}));
248
+ throw new Error(errorData.error || "Signup failed");
249
+ }
250
+
251
+ const data = await response.json();
252
+ const user: AuthenticatedUser = data.user;
253
+
254
+ this.setState({
255
+ status: "authenticated",
256
+ user,
257
+ error: null,
258
+ });
259
+ } catch (error) {
260
+ const err = error instanceof Error ? error : new Error("Signup failed");
261
+ this.setState({
262
+ status: "unauthenticated",
263
+ user: null,
264
+ error: err,
265
+ });
266
+ throw err;
267
+ }
58
268
  }
59
269
  }
@@ -1,58 +1,20 @@
1
- import {
2
- type NubaseFrontendConfig,
3
- defaultKeybindings,
4
- resourceLink,
5
- } from "@nubase/frontend";
6
- import { apiEndpoints } from "__PROJECT_NAME__-schema";
1
+ import type { NubaseFrontendConfig } from "@nubase/frontend";
2
+ import { defaultKeybindings, resourceLink } from "@nubase/frontend";
3
+ import { Home, TicketIcon } from "lucide-react";
4
+ import { apiEndpoints } from "schema";
7
5
  import { __PROJECT_NAME_PASCAL__AuthController } from "./auth/__PROJECT_NAME_PASCAL__AuthController";
8
6
  import { ticketResource } from "./resources/ticket";
9
7
 
10
- // Icons (inline SVG from Tabler Icons)
11
- const HomeIcon = () => (
12
- <svg
13
- xmlns="http://www.w3.org/2000/svg"
14
- width="20"
15
- height="20"
16
- viewBox="0 0 24 24"
17
- fill="none"
18
- stroke="currentColor"
19
- strokeWidth="2"
20
- strokeLinecap="round"
21
- strokeLinejoin="round"
22
- >
23
- <path d="M5 12l-2 0l9 -9l9 9l-2 0" />
24
- <path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7" />
25
- <path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6" />
26
- </svg>
27
- );
28
-
29
- const TicketIcon = () => (
30
- <svg
31
- xmlns="http://www.w3.org/2000/svg"
32
- width="20"
33
- height="20"
34
- viewBox="0 0 24 24"
35
- fill="none"
36
- stroke="currentColor"
37
- strokeWidth="2"
38
- strokeLinecap="round"
39
- strokeLinejoin="round"
40
- >
41
- <path d="M15 5l0 2" />
42
- <path d="M15 11l0 2" />
43
- <path d="M15 17l0 2" />
44
- <path d="M5 5h14a2 2 0 0 1 2 2v3a2 2 0 0 0 0 4v3a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-3a2 2 0 0 0 0 -4v-3a2 2 0 0 1 2 -2" />
45
- </svg>
46
- );
47
-
48
- const authController = new __PROJECT_NAME_PASCAL__AuthController();
8
+ const apiBaseUrl =
9
+ import.meta.env.VITE_API_BASE_URL || "http://localhost:__BACKEND_PORT__";
10
+ const authController = new __PROJECT_NAME_PASCAL__AuthController(apiBaseUrl);
49
11
 
50
12
  export const config: NubaseFrontendConfig<typeof apiEndpoints> = {
51
13
  appName: "__PROJECT_NAME_PASCAL__",
52
14
  mainMenu: [
53
15
  {
54
16
  id: "home",
55
- icon: HomeIcon,
17
+ icon: Home,
56
18
  label: "Home",
57
19
  href: "/",
58
20
  },
@@ -66,8 +28,8 @@ export const config: NubaseFrontendConfig<typeof apiEndpoints> = {
66
28
  resources: {
67
29
  [ticketResource.id]: ticketResource,
68
30
  },
69
- keybindings: defaultKeybindings,
70
- apiBaseUrl: import.meta.env.VITE_API_BASE_URL || "http://localhost:__BACKEND_PORT__",
31
+ keybindings: defaultKeybindings.extend(),
32
+ apiBaseUrl: apiBaseUrl,
71
33
  apiEndpoints,
72
34
  themeIds: ["dark", "light"],
73
35
  defaultThemeId: "dark",
@@ -1,32 +1,56 @@
1
1
  import { createResource } from "@nubase/frontend";
2
- import { apiEndpoints, ticketBaseSchema } from "__PROJECT_NAME__-schema";
2
+ import { apiEndpoints } from "schema";
3
3
 
4
4
  export const ticketResource = createResource("ticket")
5
5
  .withApiEndpoints(apiEndpoints)
6
6
  .withViews({
7
7
  create: {
8
8
  type: "resource-create",
9
+ id: "create-ticket",
9
10
  title: "Create Ticket",
10
- schema: ticketBaseSchema.omit("id", "createdAt", "updatedAt"),
11
- submitEndpoint: "postTicket",
11
+ schemaPost: (api) => api.postTicket.requestBody,
12
+ breadcrumbs: [
13
+ { label: "Tickets", to: "/r/ticket/search" },
14
+ "Create Ticket",
15
+ ],
16
+ onSubmit: async ({ data, context }) => {
17
+ return context.http.postTicket({ data });
18
+ },
12
19
  },
13
20
  view: {
14
21
  type: "resource-view",
22
+ id: "view-ticket",
15
23
  title: "View Ticket",
16
- schema: ticketBaseSchema,
17
- fetchEndpoint: "getTicket",
18
- },
19
- edit: {
20
- type: "resource-edit",
21
- title: "Edit Ticket",
22
- schema: ticketBaseSchema,
23
- fetchEndpoint: "getTicket",
24
- submitEndpoint: "patchTicket",
24
+ schemaGet: (api) => api.getTicket.responseBody.omit("id"),
25
+ schemaParams: (api) => api.getTicket.requestParams,
26
+ breadcrumbs: ({ context, data }) => [
27
+ { label: "Tickets", to: "/r/ticket/search" },
28
+ {
29
+ label: data?.title || `Ticket #${context.params?.id || "Unknown"}`,
30
+ },
31
+ ],
32
+ onLoad: async ({ context }) => {
33
+ return context.http.getTicket({
34
+ params: { id: context.params.id },
35
+ });
36
+ },
37
+ onPatch: async ({ data, context }) => {
38
+ return context.http.patchTicket({
39
+ params: { id: context.params.id },
40
+ data: data,
41
+ });
42
+ },
25
43
  },
26
44
  search: {
27
45
  type: "resource-search",
28
- title: "Tickets",
29
- schema: ticketBaseSchema,
30
- fetchEndpoint: "getTickets",
46
+ id: "search-tickets",
47
+ title: "Search Tickets",
48
+ schemaGet: (api) => api.getTickets.responseBody,
49
+ breadcrumbs: () => [{ label: "Tickets", to: "/r/ticket/search" }],
50
+ onLoad: async ({ context }) => {
51
+ return context.http.getTickets({
52
+ params: {},
53
+ });
54
+ },
31
55
  },
32
56
  });
@@ -49,10 +49,10 @@ The application will be available at:
49
49
 
50
50
  ```
51
51
  __PROJECT_NAME__/
52
- ├── __PROJECT_NAME__-schema/ # Shared API schemas and types
53
- ├── __PROJECT_NAME__-backend/ # Node.js backend (Hono + PostgreSQL)
54
- ├── __PROJECT_NAME__-frontend/ # React frontend (Vite + Nubase)
55
- └── package.json # Root workspace configuration
52
+ ├── schema/ # Shared API schemas and types
53
+ ├── backend/ # Node.js backend (Hono + PostgreSQL)
54
+ ├── frontend/ # React frontend (Vite + Nubase)
55
+ └── package.json # Root workspace configuration
56
56
  ```
57
57
 
58
58
  ## Available Scripts
@@ -4,17 +4,17 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "workspaces": [
7
- "__PROJECT_NAME__-schema",
8
- "__PROJECT_NAME__-backend",
9
- "__PROJECT_NAME__-frontend"
7
+ "schema",
8
+ "backend",
9
+ "frontend"
10
10
  ],
11
11
  "scripts": {
12
12
  "dev": "turbo run dev",
13
13
  "build": "turbo run build",
14
- "db:up": "cd __PROJECT_NAME__-backend && npm run db:dev:up",
15
- "db:down": "cd __PROJECT_NAME__-backend && npm run db:dev:down",
16
- "db:kill": "cd __PROJECT_NAME__-backend && npm run db:dev:kill",
17
- "db:seed": "cd __PROJECT_NAME__-backend && npm run db:seed",
14
+ "db:up": "cd backend && npm run db:dev:up",
15
+ "db:down": "cd backend && npm run db:dev:down",
16
+ "db:kill": "cd backend && npm run db:dev:kill",
17
+ "db:seed": "cd backend && npm run db:seed",
18
18
  "typecheck": "turbo run typecheck",
19
19
  "lint": "turbo run lint",
20
20
  "lint:fix": "turbo run lint:fix"
@@ -25,6 +25,8 @@
25
25
  "typescript": "^5.7.2"
26
26
  },
27
27
  "engines": {
28
- "node": ">=18"
29
- }
28
+ "node": ">=18",
29
+ "npm": ">=10.9.2"
30
+ },
31
+ "packageManager": "npm@10.9.2"
30
32
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "__PROJECT_NAME__-schema",
2
+ "name": "schema",
3
3
  "version": "0.1.0",
4
4
  "description": "Schema for __PROJECT_NAME_PASCAL__",
5
5
  "type": "module",
@@ -1,89 +1,144 @@
1
- import { type RequestSchema, nu } from "@nubase/core";
2
-
3
- export const workspaceSchema = nu.object({
4
- id: nu.number(),
5
- slug: nu.string(),
6
- name: nu.string(),
7
- });
8
-
9
- export type Workspace = (typeof workspaceSchema)["_output"];
1
+ import { emptySchema, nu, type RequestSchema } from "@nubase/core";
10
2
 
3
+ /**
4
+ * User schema for authenticated user data
5
+ */
11
6
  export const userSchema = nu.object({
12
7
  id: nu.number(),
13
8
  email: nu.string(),
14
9
  username: nu.string(),
15
10
  });
16
11
 
17
- export type User = (typeof userSchema)["_output"];
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
+ });
18
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
+ */
19
29
  export const loginStartSchema = {
20
- method: "POST",
30
+ method: "POST" as const,
21
31
  path: "/auth/login/start",
32
+ requestParams: emptySchema,
22
33
  requestBody: nu.object({
23
- email: nu.string(),
34
+ username: nu.string(),
24
35
  password: nu.string(),
25
36
  }),
26
37
  responseBody: nu.object({
27
- workspaces: nu.array(workspaceSchema),
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),
28
44
  }),
29
45
  } satisfies RequestSchema;
30
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
+ */
31
53
  export const loginCompleteSchema = {
32
- method: "POST",
54
+ method: "POST" as const,
33
55
  path: "/auth/login/complete",
56
+ requestParams: emptySchema,
34
57
  requestBody: nu.object({
35
- email: nu.string(),
36
- password: nu.string(),
37
- workspaceId: nu.number(),
58
+ /** Temporary login token from login/start */
59
+ loginToken: nu.string(),
60
+ /** Selected workspace slug */
61
+ workspace: nu.string(),
38
62
  }),
39
63
  responseBody: nu.object({
40
- token: nu.string(),
41
64
  user: userSchema,
42
- workspace: workspaceSchema,
65
+ workspace: workspaceInfoSchema,
43
66
  }),
44
67
  } satisfies RequestSchema;
45
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
+ */
46
76
  export const loginSchema = {
47
- method: "POST",
77
+ method: "POST" as const,
48
78
  path: "/auth/login",
79
+ requestParams: emptySchema,
49
80
  requestBody: nu.object({
50
- email: nu.string(),
81
+ username: nu.string(),
51
82
  password: nu.string(),
83
+ /** Workspace slug for path-based multi-workspace */
84
+ workspace: nu.string(),
52
85
  }),
53
86
  responseBody: nu.object({
54
- token: nu.string(),
55
87
  user: userSchema,
56
- workspace: workspaceSchema,
57
88
  }),
58
89
  } satisfies RequestSchema;
59
90
 
91
+ /**
92
+ * Logout request schema
93
+ * POST /auth/logout
94
+ */
60
95
  export const logoutSchema = {
61
- method: "POST",
96
+ method: "POST" as const,
62
97
  path: "/auth/logout",
63
- responseBody: nu.object({ success: nu.boolean() }),
98
+ requestParams: emptySchema,
99
+ responseBody: nu.object({
100
+ success: nu.boolean(),
101
+ }),
64
102
  } satisfies RequestSchema;
65
103
 
104
+ /**
105
+ * Get current user schema
106
+ * GET /auth/me
107
+ */
66
108
  export const getMeSchema = {
67
- method: "GET",
109
+ method: "GET" as const,
68
110
  path: "/auth/me",
111
+ requestParams: emptySchema,
69
112
  responseBody: nu.object({
70
- user: userSchema,
71
- workspace: workspaceSchema,
113
+ user: userSchema.optional(),
72
114
  }),
73
115
  } satisfies RequestSchema;
74
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
+ */
75
124
  export const signupSchema = {
76
- method: "POST",
125
+ method: "POST" as const,
77
126
  path: "/auth/signup",
127
+ requestParams: emptySchema,
78
128
  requestBody: nu.object({
79
- email: nu.string(),
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 */
80
134
  username: nu.string(),
135
+ /** Email for the admin user */
136
+ email: nu.string(),
137
+ /** Password for the admin user */
81
138
  password: nu.string(),
82
- workspaceName: nu.string().optional(),
83
139
  }),
84
140
  responseBody: nu.object({
85
- token: nu.string(),
86
141
  user: userSchema,
87
- workspace: workspaceSchema,
142
+ workspace: workspaceInfoSchema,
88
143
  }),
89
144
  } satisfies RequestSchema;
@@ -1,68 +1,71 @@
1
- import { type RequestSchema, nu } from "@nubase/core";
1
+ import {
2
+ emptySchema,
3
+ idNumberSchema,
4
+ nu,
5
+ type RequestSchema,
6
+ successSchema,
7
+ } from "@nubase/core";
2
8
 
3
9
  export const ticketBaseSchema = nu
4
10
  .object({
5
11
  id: nu.number(),
6
12
  title: nu.string().withMeta({
7
13
  label: "Title",
8
- placeholder: "Enter ticket title",
14
+ description: "Enter the title of the ticket",
9
15
  }),
10
16
  description: nu.string().optional().withMeta({
11
17
  label: "Description",
12
- placeholder: "Enter ticket description",
18
+ description: "Enter the description of the ticket",
13
19
  renderer: "multiline",
14
20
  }),
15
- createdAt: nu.string().optional().withMeta({
16
- label: "Created At",
17
- }),
18
- updatedAt: nu.string().optional().withMeta({
19
- label: "Updated At",
20
- }),
21
21
  })
22
22
  .withId("id")
23
23
  .withTableLayouts({
24
24
  default: {
25
- fields: ["id", "title", "description", "createdAt"],
25
+ fields: [
26
+ { name: "id", columnWidthPx: 80, pinned: true },
27
+ { name: "title", columnWidthPx: 300, pinned: true },
28
+ { name: "description", columnWidthPx: 400 },
29
+ ],
26
30
  metadata: {
27
31
  linkFields: ["title"],
28
32
  },
29
33
  },
30
34
  });
31
35
 
32
- export type Ticket = (typeof ticketBaseSchema)["_output"];
33
-
34
36
  export const getTicketsSchema = {
35
- method: "GET",
37
+ method: "GET" as const,
36
38
  path: "/tickets",
37
- requestParams: ticketBaseSchema.omit("id", "createdAt", "updatedAt").partial(),
39
+ requestParams: ticketBaseSchema.omit("id").partial(),
38
40
  responseBody: nu.array(ticketBaseSchema),
39
41
  } satisfies RequestSchema;
40
42
 
41
43
  export const getTicketSchema = {
42
- method: "GET",
44
+ method: "GET" as const,
43
45
  path: "/tickets/:id",
44
- pathParams: nu.object({ id: nu.number() }),
46
+ requestParams: idNumberSchema,
45
47
  responseBody: ticketBaseSchema,
46
48
  } satisfies RequestSchema;
47
49
 
48
50
  export const postTicketSchema = {
49
- method: "POST",
51
+ method: "POST" as const,
50
52
  path: "/tickets",
51
- requestBody: ticketBaseSchema.omit("id", "createdAt", "updatedAt"),
53
+ requestParams: emptySchema,
54
+ requestBody: ticketBaseSchema.omit("id"),
52
55
  responseBody: ticketBaseSchema,
53
56
  } satisfies RequestSchema;
54
57
 
55
58
  export const patchTicketSchema = {
56
- method: "PATCH",
59
+ method: "PATCH" as const,
57
60
  path: "/tickets/:id",
58
- pathParams: nu.object({ id: nu.number() }),
59
- requestBody: ticketBaseSchema.omit("id", "createdAt", "updatedAt").partial(),
61
+ requestParams: idNumberSchema,
62
+ requestBody: ticketBaseSchema.omit("id").partial(),
60
63
  responseBody: ticketBaseSchema,
61
64
  } satisfies RequestSchema;
62
65
 
63
66
  export const deleteTicketSchema = {
64
- method: "DELETE",
67
+ method: "DELETE" as const,
65
68
  path: "/tickets/:id",
66
- pathParams: nu.object({ id: nu.number() }),
67
- responseBody: nu.object({ success: nu.boolean() }),
69
+ requestParams: idNumberSchema,
70
+ responseBody: successSchema,
68
71
  } satisfies RequestSchema;