@nubase/create 0.1.6 → 0.1.7
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 +5 -3
- package/package.json +4 -2
- package/templates/backend/db/schema.sql +1 -1
- package/templates/backend/docker/dev/postgresql-init/dump.sql +1 -1
- package/templates/backend/docker/test/postgresql-init/dump.sql +1 -1
- package/templates/backend/package.json +1 -1
- package/templates/backend/src/api/routes/auth.ts +19 -29
- package/templates/backend/src/api/routes/test-utils.ts +6 -6
- package/templates/backend/src/auth/index.ts +8 -8
- package/templates/backend/src/db/schema/user.ts +1 -1
- package/templates/backend/src/db/seed.ts +2 -2
- package/templates/frontend/e2e/auth.spec.ts +5 -5
- package/templates/frontend/e2e/fixtures/base.ts +3 -3
- package/templates/frontend/e2e/fixtures/test-api.ts +6 -6
- package/templates/frontend/src/auth/__PROJECT_NAME_PASCAL__AuthController.ts +5 -5
- package/templates/schema/src/api-endpoints.ts +7 -7
- package/templates/schema/src/index.ts +1 -1
- package/templates/schema/src/schema/auth/get-me.ts +15 -0
- package/templates/schema/src/schema/auth/index.ts +8 -0
- package/templates/schema/src/schema/auth/login-complete.ts +25 -0
- package/templates/schema/src/schema/auth/login-start.ts +28 -0
- package/templates/schema/src/schema/auth/login.ts +24 -0
- package/templates/schema/src/schema/auth/logout.ts +14 -0
- package/templates/schema/src/schema/auth/signup.ts +32 -0
- package/templates/schema/src/schema/auth/user.ts +10 -0
- package/templates/schema/src/schema/auth/workspace.ts +10 -0
- package/templates/schema/src/schema/ticket/delete-ticket.ts +12 -0
- package/templates/schema/src/schema/ticket/get-ticket.ts +9 -0
- package/templates/schema/src/schema/ticket/get-tickets.ts +9 -0
- package/templates/schema/src/schema/ticket/index.ts +6 -0
- package/templates/schema/src/schema/ticket/patch-ticket.ts +10 -0
- package/templates/schema/src/schema/ticket/post-ticket.ts +10 -0
- package/templates/schema/src/schema/ticket/ticket-base.ts +28 -0
- package/templates/schema/src/schema/auth.ts +0 -144
- 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
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.7",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()
|
|
@@ -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
|
-
|
|
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
|
|
33
|
+
// Find user by email
|
|
34
34
|
const [user] = await getAdminDb()
|
|
35
35
|
.select()
|
|
36
36
|
.from(users)
|
|
37
|
-
.where(eq(users.
|
|
37
|
+
.where(eq(users.email, body.email));
|
|
38
38
|
|
|
39
39
|
if (!user) {
|
|
40
|
-
throw new HttpError(401, "Invalid
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
192
|
+
// Find user by email
|
|
193
193
|
const [dbUser] = await getAdminDb()
|
|
194
194
|
.select()
|
|
195
195
|
.from(users)
|
|
196
|
-
.where(eq(users.
|
|
196
|
+
.where(eq(users.email, body.email));
|
|
197
197
|
|
|
198
198
|
if (!dbUser) {
|
|
199
|
-
throw new HttpError(401, "Invalid
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
182
|
+
// Find user by email
|
|
183
183
|
const [dbUser] = await getAdminDb()
|
|
184
184
|
.select()
|
|
185
185
|
.from(users)
|
|
186
|
-
.where(eq(users.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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("#
|
|
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("#
|
|
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
|
|
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 (
|
|
89
|
-
await expect(userAvatar).toContainText("
|
|
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
|
|
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
|
-
|
|
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("#
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
@@ -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,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;
|