@thunder-stack/create-thunder-app 0.0.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/index.js +229 -0
- package/package.json +26 -0
- package/postpublish.js +14 -0
- package/prepublish.js +68 -0
- package/template/.env.example +13 -0
- package/template/README.md +1 -0
- package/template/THUNDER_STACK.md +120 -0
- package/template/client/assets/logo.svg +36 -0
- package/template/client/expo/App.test.tsx +20 -0
- package/template/client/expo/App.tsx +16 -0
- package/template/client/expo/app.json +30 -0
- package/template/client/expo/babel.config.js +12 -0
- package/template/client/expo/global.css +3 -0
- package/template/client/expo/jest.config.js +7 -0
- package/template/client/expo/metro.config.js +21 -0
- package/template/client/expo/package.json +33 -0
- package/template/client/expo/tsconfig.json +6 -0
- package/template/client/next/app/globals.css +14 -0
- package/template/client/next/app/layout.tsx +28 -0
- package/template/client/next/app/page.test.tsx +13 -0
- package/template/client/next/app/page.tsx +10 -0
- package/template/client/next/next-env.d.ts +5 -0
- package/template/client/next/next.config.js +21 -0
- package/template/client/next/package.json +39 -0
- package/template/client/next/playwright.config.ts +17 -0
- package/template/client/next/postcss.config.js +6 -0
- package/template/client/next/tsconfig.json +30 -0
- package/template/client/next/vitest.config.ts +11 -0
- package/template/client/next/vitest.setup.ts +1 -0
- package/template/client/shared/index.ts +2 -0
- package/template/client/shared/package.json +24 -0
- package/template/client/shared/src/components/Button.tsx +38 -0
- package/template/client/shared/src/components/Card.tsx +17 -0
- package/template/client/shared/src/features/auth/login.tsx +139 -0
- package/template/client/shared/src/features/dashboard/index.tsx +232 -0
- package/template/client/shared/src/features/home/screen.tsx +212 -0
- package/template/client/shared/src/features/management/roles.tsx +356 -0
- package/template/client/shared/src/features/management/users.tsx +245 -0
- package/template/client/shared/src/nativewind-env.d.ts +1 -0
- package/template/client/shared/src/utils/auth.ts +21 -0
- package/template/client/shared/tailwind.config.js +19 -0
- package/template/client/shared/tsconfig.json +9 -0
- package/template/package.json +47 -0
- package/template/packages/tsconfig/base.json +20 -0
- package/template/packages/tsconfig/package.json +11 -0
- package/template/pnpm-workspace.yaml +4 -0
- package/template/scripts/clean.js +56 -0
- package/template/server/db/drizzle.config.ts +10 -0
- package/template/server/db/migrations/0000_loving_mindworm.sql +86 -0
- package/template/server/db/migrations/meta/0000_snapshot.json +549 -0
- package/template/server/db/migrations/meta/_journal.json +13 -0
- package/template/server/db/package.json +28 -0
- package/template/server/db/src/index.ts +16 -0
- package/template/server/db/src/migrate.ts +28 -0
- package/template/server/db/src/schema/auth.ts +73 -0
- package/template/server/db/src/schema/index.ts +2 -0
- package/template/server/db/src/schema/rbac.ts +72 -0
- package/template/server/db/src/schema.ts +1 -0
- package/template/server/db/src/seed.ts +204 -0
- package/template/server/db/src/services/rbac.ts +144 -0
- package/template/server/db/src/services/user.ts +34 -0
- package/template/server/db/src/types/index.ts +34 -0
- package/template/server/db/tsconfig.json +8 -0
- package/template/server/hono/package.json +22 -0
- package/template/server/hono/src/auth.ts +29 -0
- package/template/server/hono/src/index.ts +215 -0
- package/template/server/hono/tsconfig.json +8 -0
- package/template/server/hono/wrangler.toml +12 -0
- package/template/turbo.json +68 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { cors } from "hono/cors";
|
|
3
|
+
import { createDbClient, RbacService, UserService } from "@thunder/db";
|
|
4
|
+
import { getAuthInstance } from "./auth";
|
|
5
|
+
|
|
6
|
+
type Bindings = {
|
|
7
|
+
DATABASE_URL: string;
|
|
8
|
+
BETTER_AUTH_SECRET: string;
|
|
9
|
+
BETTER_AUTH_URL: string;
|
|
10
|
+
GOOGLE_CLIENT_ID: string;
|
|
11
|
+
GOOGLE_CLIENT_SECRET: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const app = new Hono<{ Bindings: Bindings }>();
|
|
15
|
+
|
|
16
|
+
// Enable CORS for frontend applications (localhost and production web/app origins)
|
|
17
|
+
app.use("*", async (c, next) => {
|
|
18
|
+
const corsMiddleware = cors({
|
|
19
|
+
origin: (origin) => origin, // Dynamically reflect requested origin
|
|
20
|
+
allowHeaders: ["Content-Type", "Authorization"],
|
|
21
|
+
allowMethods: ["POST", "GET", "OPTIONS", "PUT", "DELETE"],
|
|
22
|
+
exposeHeaders: ["Content-Length"],
|
|
23
|
+
maxAge: 600,
|
|
24
|
+
credentials: true,
|
|
25
|
+
});
|
|
26
|
+
return corsMiddleware(c, next);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Better Auth Mounting Route handlers
|
|
30
|
+
app.on(["POST", "GET"], "/api/auth/*", async (c) => {
|
|
31
|
+
const db = createDbClient(c.env.DATABASE_URL);
|
|
32
|
+
const auth = getAuthInstance(db, {
|
|
33
|
+
BETTER_AUTH_SECRET: c.env.BETTER_AUTH_SECRET,
|
|
34
|
+
BETTER_AUTH_URL: c.env.BETTER_AUTH_URL,
|
|
35
|
+
GOOGLE_CLIENT_ID: c.env.GOOGLE_CLIENT_ID,
|
|
36
|
+
GOOGLE_CLIENT_SECRET: c.env.GOOGLE_CLIENT_SECRET,
|
|
37
|
+
});
|
|
38
|
+
return auth.handler(c.req.raw);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Helper middleware to verify permissions
|
|
42
|
+
const checkPermission = (...requiredPermissions: string[]) => {
|
|
43
|
+
return async (c: any, next: any) => {
|
|
44
|
+
const db = createDbClient(c.env.DATABASE_URL);
|
|
45
|
+
const auth = getAuthInstance(db, {
|
|
46
|
+
BETTER_AUTH_SECRET: c.env.BETTER_AUTH_SECRET,
|
|
47
|
+
BETTER_AUTH_URL: c.env.BETTER_AUTH_URL,
|
|
48
|
+
GOOGLE_CLIENT_ID: c.env.GOOGLE_CLIENT_ID,
|
|
49
|
+
GOOGLE_CLIENT_SECRET: c.env.GOOGLE_CLIENT_SECRET,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const session = await auth.api.getSession({
|
|
53
|
+
headers: c.req.raw.headers,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!session) {
|
|
57
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const rbacService = new RbacService(db);
|
|
61
|
+
const userPerms = await rbacService.getUserPermissions(session.user.id);
|
|
62
|
+
const hasPerm = userPerms.some((p) => requiredPermissions.includes(p.name));
|
|
63
|
+
|
|
64
|
+
if (!hasPerm) {
|
|
65
|
+
return c.json({ error: `Forbidden: Requires one of permissions [${requiredPermissions.join(", ")}]` }, 403);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
c.set("userId", session.user.id);
|
|
69
|
+
c.set("user", session.user);
|
|
70
|
+
await next();
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Authenticated hello route
|
|
75
|
+
app.get("/hello", async (c) => {
|
|
76
|
+
const db = createDbClient(c.env.DATABASE_URL);
|
|
77
|
+
const auth = getAuthInstance(db, {
|
|
78
|
+
BETTER_AUTH_SECRET: c.env.BETTER_AUTH_SECRET,
|
|
79
|
+
BETTER_AUTH_URL: c.env.BETTER_AUTH_URL,
|
|
80
|
+
GOOGLE_CLIENT_ID: c.env.GOOGLE_CLIENT_ID,
|
|
81
|
+
GOOGLE_CLIENT_SECRET: c.env.GOOGLE_CLIENT_SECRET,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const session = await auth.api.getSession({
|
|
85
|
+
headers: c.req.raw.headers,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!session) {
|
|
89
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return c.json({
|
|
93
|
+
message: `Hello ${session.user.name}! Welcomed by Hono on Cloudflare Edge.`,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
app.get("/api/dashboard/stats", checkPermission("view_dashboard"), async (c) => {
|
|
98
|
+
const db = createDbClient(c.env.DATABASE_URL);
|
|
99
|
+
const rbacService = new RbacService(db);
|
|
100
|
+
const stats = await rbacService.getStats();
|
|
101
|
+
return c.json(stats);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Get current user details, active roles, and permissions
|
|
105
|
+
app.get("/api/me", async (c) => {
|
|
106
|
+
const db = createDbClient(c.env.DATABASE_URL);
|
|
107
|
+
const auth = getAuthInstance(db, {
|
|
108
|
+
BETTER_AUTH_SECRET: c.env.BETTER_AUTH_SECRET,
|
|
109
|
+
BETTER_AUTH_URL: c.env.BETTER_AUTH_URL,
|
|
110
|
+
GOOGLE_CLIENT_ID: c.env.GOOGLE_CLIENT_ID,
|
|
111
|
+
GOOGLE_CLIENT_SECRET: c.env.GOOGLE_CLIENT_SECRET,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const session = await auth.api.getSession({
|
|
115
|
+
headers: c.req.raw.headers,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!session) {
|
|
119
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const rbacService = new RbacService(db);
|
|
123
|
+
const roles = await rbacService.getUserRoles(session.user.id);
|
|
124
|
+
const permissions = await rbacService.getUserPermissions(session.user.id);
|
|
125
|
+
|
|
126
|
+
return c.json({
|
|
127
|
+
user: session.user,
|
|
128
|
+
roles: roles.map((r) => r.name),
|
|
129
|
+
permissions: permissions.map((p) => p.name),
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Management sub-router
|
|
134
|
+
const management = new Hono<{ Bindings: Bindings }>();
|
|
135
|
+
|
|
136
|
+
// 1. User Management
|
|
137
|
+
management.get("/users", checkPermission("manage:user"), async (c) => {
|
|
138
|
+
const db = createDbClient(c.env.DATABASE_URL);
|
|
139
|
+
const userService = new UserService(db);
|
|
140
|
+
const users = await userService.getUsersWithRoles();
|
|
141
|
+
return c.json(users);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
management.post("/users/:id/roles", checkPermission("manage:user"), async (c) => {
|
|
145
|
+
const userId = c.req.param("id");
|
|
146
|
+
const { roleId } = await c.req.json<{ roleId: string }>();
|
|
147
|
+
const db = createDbClient(c.env.DATABASE_URL);
|
|
148
|
+
const rbacService = new RbacService(db);
|
|
149
|
+
await rbacService.assignRoleToUser(userId, roleId);
|
|
150
|
+
return c.json({ success: true });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
management.delete("/users/:id/roles/:roleId", checkPermission("manage:user"), async (c) => {
|
|
154
|
+
const userId = c.req.param("id");
|
|
155
|
+
const roleId = c.req.param("roleId");
|
|
156
|
+
const db = createDbClient(c.env.DATABASE_URL);
|
|
157
|
+
const rbacService = new RbacService(db);
|
|
158
|
+
await rbacService.removeRoleFromUser(userId, roleId);
|
|
159
|
+
return c.json({ success: true });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// 2. Role & Permission Management
|
|
163
|
+
management.get("/roles", checkPermission("manage:role", "manage:role-perm"), async (c) => {
|
|
164
|
+
const db = createDbClient(c.env.DATABASE_URL);
|
|
165
|
+
const rbacService = new RbacService(db);
|
|
166
|
+
const roles = await rbacService.getRoles();
|
|
167
|
+
return c.json(roles);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
management.post("/roles", checkPermission("manage:role"), async (c) => {
|
|
171
|
+
const { name, description } = await c.req.json<{ name: string; description?: string }>();
|
|
172
|
+
const db = createDbClient(c.env.DATABASE_URL);
|
|
173
|
+
const rbacService = new RbacService(db);
|
|
174
|
+
const role = await rbacService.createRole(name, description);
|
|
175
|
+
return c.json(role);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
management.delete("/roles/:id", checkPermission("manage:role"), async (c) => {
|
|
179
|
+
const id = c.req.param("id");
|
|
180
|
+
const db = createDbClient(c.env.DATABASE_URL);
|
|
181
|
+
const rbacService = new RbacService(db);
|
|
182
|
+
await rbacService.deleteRole(id);
|
|
183
|
+
return c.json({ success: true });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
management.get("/permissions", checkPermission("manage:role", "manage:role-perm"), async (c) => {
|
|
187
|
+
const db = createDbClient(c.env.DATABASE_URL);
|
|
188
|
+
const rbacService = new RbacService(db);
|
|
189
|
+
const permissions = await rbacService.getPermissions();
|
|
190
|
+
return c.json(permissions);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
management.post("/roles/:id/permissions", checkPermission("manage:role-perm"), async (c) => {
|
|
194
|
+
const roleId = c.req.param("id");
|
|
195
|
+
const { permissionId } = await c.req.json<{ permissionId: string }>();
|
|
196
|
+
const db = createDbClient(c.env.DATABASE_URL);
|
|
197
|
+
const rbacService = new RbacService(db);
|
|
198
|
+
await rbacService.assignPermissionToRole(roleId, permissionId);
|
|
199
|
+
return c.json({ success: true });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
management.delete("/roles/:id/permissions/:permissionId", checkPermission("manage:role-perm"), async (c) => {
|
|
203
|
+
const roleId = c.req.param("id");
|
|
204
|
+
const permissionId = c.req.param("permissionId");
|
|
205
|
+
const db = createDbClient(c.env.DATABASE_URL);
|
|
206
|
+
const rbacService = new RbacService(db);
|
|
207
|
+
await rbacService.removePermissionFromRole(roleId, permissionId);
|
|
208
|
+
return c.json({ success: true });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Mount management routes
|
|
212
|
+
app.route("/api/management", management);
|
|
213
|
+
|
|
214
|
+
export default app;
|
|
215
|
+
export type AppType = typeof app;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
name = "thunder-api"
|
|
2
|
+
main = "src/index.ts"
|
|
3
|
+
compatibility_date = "2024-03-01"
|
|
4
|
+
|
|
5
|
+
[vars]
|
|
6
|
+
# wrangler dev reads variables from the local shell environment or .dev.vars.
|
|
7
|
+
# When deploying to production on Cloudflare Workers, set these secret bindings:
|
|
8
|
+
# wrangler secret put DATABASE_URL
|
|
9
|
+
# wrangler secret put BETTER_AUTH_SECRET
|
|
10
|
+
# wrangler secret put BETTER_AUTH_URL
|
|
11
|
+
# wrangler secret put GOOGLE_CLIENT_ID
|
|
12
|
+
# wrangler secret put GOOGLE_CLIENT_SECRET
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://turbo.build/schema.json",
|
|
3
|
+
"globalEnv": [
|
|
4
|
+
"DATABASE_URL",
|
|
5
|
+
"BETTER_AUTH_SECRET",
|
|
6
|
+
"BETTER_AUTH_URL",
|
|
7
|
+
"NEXT_PUBLIC_API_URL",
|
|
8
|
+
"GOOGLE_CLIENT_ID",
|
|
9
|
+
"GOOGLE_CLIENT_SECRET"
|
|
10
|
+
],
|
|
11
|
+
"tasks": {
|
|
12
|
+
"build": {
|
|
13
|
+
"dependsOn": [
|
|
14
|
+
"^build"
|
|
15
|
+
],
|
|
16
|
+
"inputs": [
|
|
17
|
+
"src/**",
|
|
18
|
+
"tsconfig.json",
|
|
19
|
+
"package.json"
|
|
20
|
+
],
|
|
21
|
+
"outputs": [
|
|
22
|
+
".next/**",
|
|
23
|
+
".open-next/**",
|
|
24
|
+
"dist/**"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"dev": {
|
|
28
|
+
"cache": false,
|
|
29
|
+
"persistent": true
|
|
30
|
+
},
|
|
31
|
+
"typecheck": {
|
|
32
|
+
"dependsOn": [
|
|
33
|
+
"^typecheck"
|
|
34
|
+
],
|
|
35
|
+
"inputs": [
|
|
36
|
+
"src/**",
|
|
37
|
+
"tsconfig.json"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"lint": {
|
|
41
|
+
"dependsOn": [
|
|
42
|
+
"^lint"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"db:generate": {
|
|
46
|
+
"cache": false
|
|
47
|
+
},
|
|
48
|
+
"db:migrate": {
|
|
49
|
+
"cache": false
|
|
50
|
+
},
|
|
51
|
+
"db:seed": {
|
|
52
|
+
"cache": false
|
|
53
|
+
},
|
|
54
|
+
"test": {
|
|
55
|
+
"cache": false
|
|
56
|
+
},
|
|
57
|
+
"test:unit": {
|
|
58
|
+
"dependsOn": [
|
|
59
|
+
"^build"
|
|
60
|
+
],
|
|
61
|
+
"inputs": [
|
|
62
|
+
"src/**",
|
|
63
|
+
"tests/**",
|
|
64
|
+
"vitest.config.*"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|