@nubase/create 0.1.1
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.d.ts +1 -0
- package/dist/index.js +148 -0
- package/package.json +55 -0
- package/templates/backend/.env.development.template +4 -0
- package/templates/backend/.env.test.template +4 -0
- package/templates/backend/db/schema.sql +95 -0
- package/templates/backend/docker/dev/docker-compose.yml +21 -0
- package/templates/backend/docker/dev/postgresql-init/dump.sql +95 -0
- package/templates/backend/docker/test/docker-compose.yml +21 -0
- package/templates/backend/docker/test/postgresql-init/dump.sql +95 -0
- package/templates/backend/drizzle.config.ts +10 -0
- package/templates/backend/package.json +47 -0
- package/templates/backend/src/api/routes/auth.ts +209 -0
- package/templates/backend/src/api/routes/index.ts +2 -0
- package/templates/backend/src/api/routes/ticket.ts +62 -0
- package/templates/backend/src/auth/index.ts +37 -0
- package/templates/backend/src/db/helpers/drizzle.ts +34 -0
- package/templates/backend/src/db/schema/index.ts +4 -0
- package/templates/backend/src/db/schema/ticket.ts +13 -0
- package/templates/backend/src/db/schema/user-workspace.ts +17 -0
- package/templates/backend/src/db/schema/user.ts +10 -0
- package/templates/backend/src/db/schema/workspace.ts +9 -0
- package/templates/backend/src/db/seed.ts +71 -0
- package/templates/backend/src/helpers/env.ts +10 -0
- package/templates/backend/src/index.ts +55 -0
- package/templates/backend/src/middleware/workspace-middleware.ts +11 -0
- package/templates/backend/tsconfig.json +15 -0
- package/templates/backend/vitest.config.ts +8 -0
- package/templates/frontend/.env.development.template +1 -0
- package/templates/frontend/index.html +13 -0
- package/templates/frontend/package.json +30 -0
- package/templates/frontend/postcss.config.js +5 -0
- package/templates/frontend/src/auth/__PROJECT_NAME_PASCAL__AuthController.ts +59 -0
- package/templates/frontend/src/config.tsx +76 -0
- package/templates/frontend/src/main.tsx +6 -0
- package/templates/frontend/src/resources/ticket.ts +32 -0
- package/templates/frontend/src/styles/theme.css +3 -0
- package/templates/frontend/src/vite-env.d.ts +9 -0
- package/templates/frontend/tsconfig.json +17 -0
- package/templates/frontend/vite.config.ts +14 -0
- package/templates/root/README.md +94 -0
- package/templates/root/biome.json.template +40 -0
- package/templates/root/package.json +30 -0
- package/templates/root/turbo.json +18 -0
- package/templates/schema/package.json +28 -0
- package/templates/schema/src/api-endpoints.ts +34 -0
- package/templates/schema/src/index.ts +3 -0
- package/templates/schema/src/schema/auth.ts +89 -0
- package/templates/schema/src/schema/ticket.ts +68 -0
- package/templates/schema/tsconfig.json +14 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import bcrypt from "bcrypt";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
import type { Context } from "hono";
|
|
4
|
+
import { __PROJECT_NAME_PASCAL__AuthController } from "../../auth";
|
|
5
|
+
import { adminDb, db } from "../../db/helpers/drizzle";
|
|
6
|
+
import { userWorkspaces, users, workspaces } from "../../db/schema";
|
|
7
|
+
|
|
8
|
+
const authController = new __PROJECT_NAME_PASCAL__AuthController();
|
|
9
|
+
|
|
10
|
+
export const authHandlers = {
|
|
11
|
+
async loginStart(c: Context) {
|
|
12
|
+
const { email, password } = await c.req.json();
|
|
13
|
+
|
|
14
|
+
const [user] = await db.select().from(users).where(eq(users.email, email));
|
|
15
|
+
|
|
16
|
+
if (!user) {
|
|
17
|
+
return c.json({ error: "Invalid credentials" }, 401);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const isValid = await bcrypt.compare(password, user.passwordHash);
|
|
21
|
+
if (!isValid) {
|
|
22
|
+
return c.json({ error: "Invalid credentials" }, 401);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Get user's workspaces
|
|
26
|
+
const userWs = await db
|
|
27
|
+
.select({
|
|
28
|
+
id: workspaces.id,
|
|
29
|
+
slug: workspaces.slug,
|
|
30
|
+
name: workspaces.name,
|
|
31
|
+
})
|
|
32
|
+
.from(userWorkspaces)
|
|
33
|
+
.innerJoin(workspaces, eq(userWorkspaces.workspaceId, workspaces.id))
|
|
34
|
+
.where(eq(userWorkspaces.userId, user.id));
|
|
35
|
+
|
|
36
|
+
return c.json({ workspaces: userWs });
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
async loginComplete(c: Context) {
|
|
40
|
+
const { email, password, workspaceId } = await c.req.json();
|
|
41
|
+
|
|
42
|
+
const [user] = await db.select().from(users).where(eq(users.email, email));
|
|
43
|
+
|
|
44
|
+
if (!user) {
|
|
45
|
+
return c.json({ error: "Invalid credentials" }, 401);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isValid = await bcrypt.compare(password, user.passwordHash);
|
|
49
|
+
if (!isValid) {
|
|
50
|
+
return c.json({ error: "Invalid credentials" }, 401);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const [workspace] = await db
|
|
54
|
+
.select()
|
|
55
|
+
.from(workspaces)
|
|
56
|
+
.where(eq(workspaces.id, workspaceId));
|
|
57
|
+
|
|
58
|
+
if (!workspace) {
|
|
59
|
+
return c.json({ error: "Workspace not found" }, 404);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const token = authController.generateToken({
|
|
63
|
+
userId: user.id,
|
|
64
|
+
workspaceId: workspace.id,
|
|
65
|
+
email: user.email,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return c.json({
|
|
69
|
+
token,
|
|
70
|
+
user: { id: user.id, email: user.email, username: user.username },
|
|
71
|
+
workspace: { id: workspace.id, slug: workspace.slug, name: workspace.name },
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
async login(c: Context) {
|
|
76
|
+
const { email, password } = await c.req.json();
|
|
77
|
+
|
|
78
|
+
const [user] = await db.select().from(users).where(eq(users.email, email));
|
|
79
|
+
|
|
80
|
+
if (!user) {
|
|
81
|
+
return c.json({ error: "Invalid credentials" }, 401);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const isValid = await bcrypt.compare(password, user.passwordHash);
|
|
85
|
+
if (!isValid) {
|
|
86
|
+
return c.json({ error: "Invalid credentials" }, 401);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Get first workspace for simple login
|
|
90
|
+
const [userWs] = await db
|
|
91
|
+
.select({
|
|
92
|
+
id: workspaces.id,
|
|
93
|
+
slug: workspaces.slug,
|
|
94
|
+
name: workspaces.name,
|
|
95
|
+
})
|
|
96
|
+
.from(userWorkspaces)
|
|
97
|
+
.innerJoin(workspaces, eq(userWorkspaces.workspaceId, workspaces.id))
|
|
98
|
+
.where(eq(userWorkspaces.userId, user.id));
|
|
99
|
+
|
|
100
|
+
if (!userWs) {
|
|
101
|
+
return c.json({ error: "No workspace found" }, 404);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const token = authController.generateToken({
|
|
105
|
+
userId: user.id,
|
|
106
|
+
workspaceId: userWs.id,
|
|
107
|
+
email: user.email,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return c.json({
|
|
111
|
+
token,
|
|
112
|
+
user: { id: user.id, email: user.email, username: user.username },
|
|
113
|
+
workspace: userWs,
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async logout(c: Context) {
|
|
118
|
+
// Client-side token removal
|
|
119
|
+
return c.json({ success: true });
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
async getMe(c: Context) {
|
|
123
|
+
const authHeader = c.req.header("Authorization");
|
|
124
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
125
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const token = authHeader.slice(7);
|
|
129
|
+
const payload = authController.verifyToken(token);
|
|
130
|
+
|
|
131
|
+
if (!payload) {
|
|
132
|
+
return c.json({ error: "Invalid token" }, 401);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const [user] = await db
|
|
136
|
+
.select()
|
|
137
|
+
.from(users)
|
|
138
|
+
.where(eq(users.id, payload.userId));
|
|
139
|
+
|
|
140
|
+
if (!user) {
|
|
141
|
+
return c.json({ error: "User not found" }, 404);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const [workspace] = await db
|
|
145
|
+
.select()
|
|
146
|
+
.from(workspaces)
|
|
147
|
+
.where(eq(workspaces.id, payload.workspaceId));
|
|
148
|
+
|
|
149
|
+
return c.json({
|
|
150
|
+
user: { id: user.id, email: user.email, username: user.username },
|
|
151
|
+
workspace: workspace
|
|
152
|
+
? { id: workspace.id, slug: workspace.slug, name: workspace.name }
|
|
153
|
+
: null,
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
async signup(c: Context) {
|
|
158
|
+
const { email, username, password, workspaceName } = await c.req.json();
|
|
159
|
+
|
|
160
|
+
// Check if user exists
|
|
161
|
+
const [existingUser] = await db
|
|
162
|
+
.select()
|
|
163
|
+
.from(users)
|
|
164
|
+
.where(eq(users.email, email));
|
|
165
|
+
|
|
166
|
+
if (existingUser) {
|
|
167
|
+
return c.json({ error: "Email already registered" }, 400);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Create user
|
|
171
|
+
const passwordHash = await bcrypt.hash(password, 10);
|
|
172
|
+
const [user] = await adminDb
|
|
173
|
+
.insert(users)
|
|
174
|
+
.values({ email, username, passwordHash })
|
|
175
|
+
.returning();
|
|
176
|
+
|
|
177
|
+
// Create or get workspace
|
|
178
|
+
const wsSlug = workspaceName?.toLowerCase().replace(/\s+/g, "-") || "default";
|
|
179
|
+
let [workspace] = await db
|
|
180
|
+
.select()
|
|
181
|
+
.from(workspaces)
|
|
182
|
+
.where(eq(workspaces.slug, wsSlug));
|
|
183
|
+
|
|
184
|
+
if (!workspace) {
|
|
185
|
+
[workspace] = await adminDb
|
|
186
|
+
.insert(workspaces)
|
|
187
|
+
.values({ slug: wsSlug, name: workspaceName || "Default Workspace" })
|
|
188
|
+
.returning();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Link user to workspace
|
|
192
|
+
await adminDb.insert(userWorkspaces).values({
|
|
193
|
+
userId: user.id,
|
|
194
|
+
workspaceId: workspace.id,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const token = authController.generateToken({
|
|
198
|
+
userId: user.id,
|
|
199
|
+
workspaceId: workspace.id,
|
|
200
|
+
email: user.email,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return c.json({
|
|
204
|
+
token,
|
|
205
|
+
user: { id: user.id, email: user.email, username: user.username },
|
|
206
|
+
workspace: { id: workspace.id, slug: workspace.slug, name: workspace.name },
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import type { Context } from "hono";
|
|
3
|
+
import { db } from "../../db/helpers/drizzle";
|
|
4
|
+
import { tickets } from "../../db/schema";
|
|
5
|
+
|
|
6
|
+
export const ticketHandlers = {
|
|
7
|
+
async getTickets(c: Context) {
|
|
8
|
+
const allTickets = await db.select().from(tickets);
|
|
9
|
+
return c.json(allTickets);
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
async getTicket(c: Context) {
|
|
13
|
+
const id = Number(c.req.param("id"));
|
|
14
|
+
const [ticket] = await db.select().from(tickets).where(eq(tickets.id, id));
|
|
15
|
+
|
|
16
|
+
if (!ticket) {
|
|
17
|
+
return c.json({ error: "Ticket not found" }, 404);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return c.json(ticket);
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
async postTicket(c: Context) {
|
|
24
|
+
const body = await c.req.json();
|
|
25
|
+
const [ticket] = await db
|
|
26
|
+
.insert(tickets)
|
|
27
|
+
.values({
|
|
28
|
+
workspaceId: 1, // TODO: Get from context
|
|
29
|
+
title: body.title,
|
|
30
|
+
description: body.description,
|
|
31
|
+
})
|
|
32
|
+
.returning();
|
|
33
|
+
|
|
34
|
+
return c.json(ticket, 201);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async patchTicket(c: Context) {
|
|
38
|
+
const id = Number(c.req.param("id"));
|
|
39
|
+
const body = await c.req.json();
|
|
40
|
+
|
|
41
|
+
const [ticket] = await db
|
|
42
|
+
.update(tickets)
|
|
43
|
+
.set({
|
|
44
|
+
...body,
|
|
45
|
+
updatedAt: new Date(),
|
|
46
|
+
})
|
|
47
|
+
.where(eq(tickets.id, id))
|
|
48
|
+
.returning();
|
|
49
|
+
|
|
50
|
+
if (!ticket) {
|
|
51
|
+
return c.json({ error: "Ticket not found" }, 404);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return c.json(ticket);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async deleteTicket(c: Context) {
|
|
58
|
+
const id = Number(c.req.param("id"));
|
|
59
|
+
await db.delete(tickets).where(eq(tickets.id, id));
|
|
60
|
+
return c.json({ success: true });
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import bcrypt from "bcrypt";
|
|
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
|
+
|
|
9
|
+
export interface TokenPayload {
|
|
10
|
+
userId: number;
|
|
11
|
+
workspaceId: number;
|
|
12
|
+
email: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
|
|
16
|
+
|
|
17
|
+
export class __PROJECT_NAME_PASCAL__AuthController {
|
|
18
|
+
async hashPassword(password: string): Promise<string> {
|
|
19
|
+
return bcrypt.hash(password, 10);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async verifyPassword(password: string, hash: string): Promise<boolean> {
|
|
23
|
+
return bcrypt.compare(password, hash);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
generateToken(payload: TokenPayload): string {
|
|
27
|
+
return jwt.sign(payload, JWT_SECRET, { expiresIn: "7d" });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
verifyToken(token: string): TokenPayload | null {
|
|
31
|
+
try {
|
|
32
|
+
return jwt.verify(token, JWT_SECRET) as TokenPayload;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
2
|
+
import pg from "pg";
|
|
3
|
+
import * as schema from "../schema";
|
|
4
|
+
|
|
5
|
+
const { Pool } = pg;
|
|
6
|
+
|
|
7
|
+
// Main app pool (uses RLS-restricted user)
|
|
8
|
+
const appPool = new Pool({
|
|
9
|
+
connectionString: process.env.DATABASE_URL,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Admin pool (bypasses RLS for setup operations)
|
|
13
|
+
const adminPool = new Pool({
|
|
14
|
+
connectionString: process.env.DATABASE_URL_ADMIN,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const db = drizzle(appPool, { schema });
|
|
18
|
+
export const adminDb = drizzle(adminPool, { schema });
|
|
19
|
+
|
|
20
|
+
// Set workspace context for RLS
|
|
21
|
+
export async function withWorkspaceContext<T>(
|
|
22
|
+
workspaceId: number,
|
|
23
|
+
fn: () => Promise<T>,
|
|
24
|
+
): Promise<T> {
|
|
25
|
+
const client = await appPool.connect();
|
|
26
|
+
try {
|
|
27
|
+
await client.query(`SET app.current_workspace_id = '${workspaceId}'`);
|
|
28
|
+
const result = await fn();
|
|
29
|
+
return result;
|
|
30
|
+
} finally {
|
|
31
|
+
await client.query("RESET app.current_workspace_id");
|
|
32
|
+
client.release();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { integer, pgTable, serial, text, timestamp, varchar } from "drizzle-orm/pg-core";
|
|
2
|
+
import { workspaces } from "./workspace";
|
|
3
|
+
|
|
4
|
+
export const tickets = pgTable("tickets", {
|
|
5
|
+
id: serial("id").primaryKey(),
|
|
6
|
+
workspaceId: integer("workspace_id")
|
|
7
|
+
.notNull()
|
|
8
|
+
.references(() => workspaces.id, { onDelete: "cascade" }),
|
|
9
|
+
title: varchar("title", { length: 255 }).notNull(),
|
|
10
|
+
description: text("description"),
|
|
11
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
|
12
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
|
13
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { integer, pgTable, primaryKey, timestamp } from "drizzle-orm/pg-core";
|
|
2
|
+
import { users } from "./user";
|
|
3
|
+
import { workspaces } from "./workspace";
|
|
4
|
+
|
|
5
|
+
export const userWorkspaces = pgTable(
|
|
6
|
+
"user_workspaces",
|
|
7
|
+
{
|
|
8
|
+
userId: integer("user_id")
|
|
9
|
+
.notNull()
|
|
10
|
+
.references(() => users.id, { onDelete: "cascade" }),
|
|
11
|
+
workspaceId: integer("workspace_id")
|
|
12
|
+
.notNull()
|
|
13
|
+
.references(() => workspaces.id, { onDelete: "cascade" }),
|
|
14
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
|
15
|
+
},
|
|
16
|
+
(table) => [primaryKey({ columns: [table.userId, table.workspaceId] })],
|
|
17
|
+
);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { pgTable, serial, timestamp, varchar } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
export const users = pgTable("users", {
|
|
4
|
+
id: serial("id").primaryKey(),
|
|
5
|
+
email: varchar("email", { length: 255 }).unique().notNull(),
|
|
6
|
+
username: varchar("username", { length: 100 }).unique().notNull(),
|
|
7
|
+
passwordHash: varchar("password_hash", { length: 255 }).notNull(),
|
|
8
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
|
9
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
|
10
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { pgTable, serial, timestamp, varchar } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
export const workspaces = pgTable("workspaces", {
|
|
4
|
+
id: serial("id").primaryKey(),
|
|
5
|
+
slug: varchar("slug", { length: 100 }).unique().notNull(),
|
|
6
|
+
name: varchar("name", { length: 255 }).notNull(),
|
|
7
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
|
8
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
|
9
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { faker } from "@faker-js/faker";
|
|
2
|
+
import bcrypt from "bcrypt";
|
|
3
|
+
import { loadEnv } from "../helpers/env";
|
|
4
|
+
import { adminDb } from "./helpers/drizzle";
|
|
5
|
+
import { tickets, userWorkspaces, users, workspaces } from "./schema";
|
|
6
|
+
|
|
7
|
+
loadEnv();
|
|
8
|
+
|
|
9
|
+
async function seed() {
|
|
10
|
+
console.log("Seeding database...");
|
|
11
|
+
|
|
12
|
+
// Create default workspace
|
|
13
|
+
const [workspace] = await adminDb
|
|
14
|
+
.insert(workspaces)
|
|
15
|
+
.values({
|
|
16
|
+
slug: "default",
|
|
17
|
+
name: "Default Workspace",
|
|
18
|
+
})
|
|
19
|
+
.onConflictDoNothing()
|
|
20
|
+
.returning();
|
|
21
|
+
|
|
22
|
+
const workspaceId = workspace?.id ?? 1;
|
|
23
|
+
console.log(`Created/found workspace: ${workspaceId}`);
|
|
24
|
+
|
|
25
|
+
// Create test user
|
|
26
|
+
const passwordHash = await bcrypt.hash("password123", 10);
|
|
27
|
+
const [user] = await adminDb
|
|
28
|
+
.insert(users)
|
|
29
|
+
.values({
|
|
30
|
+
email: "demo@example.com",
|
|
31
|
+
username: "demo",
|
|
32
|
+
passwordHash,
|
|
33
|
+
})
|
|
34
|
+
.onConflictDoNothing()
|
|
35
|
+
.returning();
|
|
36
|
+
|
|
37
|
+
if (user) {
|
|
38
|
+
console.log(`Created user: ${user.email}`);
|
|
39
|
+
|
|
40
|
+
// Link user to workspace
|
|
41
|
+
await adminDb
|
|
42
|
+
.insert(userWorkspaces)
|
|
43
|
+
.values({
|
|
44
|
+
userId: user.id,
|
|
45
|
+
workspaceId,
|
|
46
|
+
})
|
|
47
|
+
.onConflictDoNothing();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Create sample tickets
|
|
51
|
+
const ticketData = Array.from({ length: 10 }, () => ({
|
|
52
|
+
workspaceId,
|
|
53
|
+
title: faker.lorem.sentence(),
|
|
54
|
+
description: faker.lorem.paragraph(),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
await adminDb.insert(tickets).values(ticketData).onConflictDoNothing();
|
|
58
|
+
console.log(`Created ${ticketData.length} sample tickets`);
|
|
59
|
+
|
|
60
|
+
console.log("Database seeded successfully!");
|
|
61
|
+
console.log("\nTest credentials:");
|
|
62
|
+
console.log(" Email: demo@example.com");
|
|
63
|
+
console.log(" Password: password123");
|
|
64
|
+
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
seed().catch((error) => {
|
|
69
|
+
console.error("Seed failed:", error);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function loadEnv(): void {
|
|
5
|
+
const nodeEnv = process.env.NODE_ENV || "development";
|
|
6
|
+
const envFile = `.env.${nodeEnv}`;
|
|
7
|
+
|
|
8
|
+
dotenv.config({ path: path.resolve(process.cwd(), envFile) });
|
|
9
|
+
dotenv.config({ path: path.resolve(process.cwd(), ".env") });
|
|
10
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { cors } from "hono/cors";
|
|
4
|
+
import { ticketHandlers } from "./api/routes/ticket";
|
|
5
|
+
import { authHandlers } from "./api/routes/auth";
|
|
6
|
+
import { workspaceMiddleware } from "./middleware/workspace-middleware";
|
|
7
|
+
import { __PROJECT_NAME_PASCAL__AuthController } from "./auth";
|
|
8
|
+
import { loadEnv } from "./helpers/env";
|
|
9
|
+
|
|
10
|
+
// Load environment variables
|
|
11
|
+
loadEnv();
|
|
12
|
+
|
|
13
|
+
const app = new Hono();
|
|
14
|
+
|
|
15
|
+
// CORS configuration
|
|
16
|
+
app.use(
|
|
17
|
+
"*",
|
|
18
|
+
cors({
|
|
19
|
+
origin: ["http://localhost:__FRONTEND_PORT__"],
|
|
20
|
+
credentials: true,
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Workspace middleware (extracts workspace from path)
|
|
25
|
+
app.use("/:workspace/*", workspaceMiddleware);
|
|
26
|
+
|
|
27
|
+
// Auth controller
|
|
28
|
+
const authController = new __PROJECT_NAME_PASCAL__AuthController();
|
|
29
|
+
|
|
30
|
+
// Routes
|
|
31
|
+
app.get("/:workspace", (c) => c.json({ message: "Welcome to __PROJECT_NAME_PASCAL__ API" }));
|
|
32
|
+
|
|
33
|
+
// Auth routes
|
|
34
|
+
app.post("/:workspace/auth/login/start", authHandlers.loginStart);
|
|
35
|
+
app.post("/:workspace/auth/login/complete", authHandlers.loginComplete);
|
|
36
|
+
app.post("/:workspace/auth/login", authHandlers.login);
|
|
37
|
+
app.post("/:workspace/auth/logout", authHandlers.logout);
|
|
38
|
+
app.get("/:workspace/auth/me", authHandlers.getMe);
|
|
39
|
+
app.post("/:workspace/auth/signup", authHandlers.signup);
|
|
40
|
+
|
|
41
|
+
// Ticket routes
|
|
42
|
+
app.get("/:workspace/tickets", ticketHandlers.getTickets);
|
|
43
|
+
app.get("/:workspace/tickets/:id", ticketHandlers.getTicket);
|
|
44
|
+
app.post("/:workspace/tickets", ticketHandlers.postTicket);
|
|
45
|
+
app.patch("/:workspace/tickets/:id", ticketHandlers.patchTicket);
|
|
46
|
+
app.delete("/:workspace/tickets/:id", ticketHandlers.deleteTicket);
|
|
47
|
+
|
|
48
|
+
const port = Number(process.env.PORT) || __BACKEND_PORT__;
|
|
49
|
+
|
|
50
|
+
console.log(`Server is running on http://localhost:${port}`);
|
|
51
|
+
|
|
52
|
+
serve({
|
|
53
|
+
fetch: app.fetch,
|
|
54
|
+
port,
|
|
55
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"rootDir": "./src"
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VITE_API_BASE_URL=http://localhost:__BACKEND_PORT__
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>__PROJECT_NAME_PASCAL__</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__-frontend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"lint": "biome check .",
|
|
12
|
+
"lint:fix": "biome check . --write --unsafe"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@nubase/frontend": "*",
|
|
16
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
17
|
+
"@tanstack/react-router": "^1.141.8",
|
|
18
|
+
"react": "^19.0.0",
|
|
19
|
+
"react-dom": "^19.0.0",
|
|
20
|
+
"tailwindcss": "^4.1.18",
|
|
21
|
+
"__PROJECT_NAME__-schema": "*"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/react": "^19.0.10",
|
|
25
|
+
"@types/react-dom": "^19.0.4",
|
|
26
|
+
"@vitejs/plugin-react": "^5.1.2",
|
|
27
|
+
"typescript": "^5.9.3",
|
|
28
|
+
"vite": "^7.3.0"
|
|
29
|
+
}
|
|
30
|
+
}
|