@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.
Files changed (69) hide show
  1. package/index.js +229 -0
  2. package/package.json +26 -0
  3. package/postpublish.js +14 -0
  4. package/prepublish.js +68 -0
  5. package/template/.env.example +13 -0
  6. package/template/README.md +1 -0
  7. package/template/THUNDER_STACK.md +120 -0
  8. package/template/client/assets/logo.svg +36 -0
  9. package/template/client/expo/App.test.tsx +20 -0
  10. package/template/client/expo/App.tsx +16 -0
  11. package/template/client/expo/app.json +30 -0
  12. package/template/client/expo/babel.config.js +12 -0
  13. package/template/client/expo/global.css +3 -0
  14. package/template/client/expo/jest.config.js +7 -0
  15. package/template/client/expo/metro.config.js +21 -0
  16. package/template/client/expo/package.json +33 -0
  17. package/template/client/expo/tsconfig.json +6 -0
  18. package/template/client/next/app/globals.css +14 -0
  19. package/template/client/next/app/layout.tsx +28 -0
  20. package/template/client/next/app/page.test.tsx +13 -0
  21. package/template/client/next/app/page.tsx +10 -0
  22. package/template/client/next/next-env.d.ts +5 -0
  23. package/template/client/next/next.config.js +21 -0
  24. package/template/client/next/package.json +39 -0
  25. package/template/client/next/playwright.config.ts +17 -0
  26. package/template/client/next/postcss.config.js +6 -0
  27. package/template/client/next/tsconfig.json +30 -0
  28. package/template/client/next/vitest.config.ts +11 -0
  29. package/template/client/next/vitest.setup.ts +1 -0
  30. package/template/client/shared/index.ts +2 -0
  31. package/template/client/shared/package.json +24 -0
  32. package/template/client/shared/src/components/Button.tsx +38 -0
  33. package/template/client/shared/src/components/Card.tsx +17 -0
  34. package/template/client/shared/src/features/auth/login.tsx +139 -0
  35. package/template/client/shared/src/features/dashboard/index.tsx +232 -0
  36. package/template/client/shared/src/features/home/screen.tsx +212 -0
  37. package/template/client/shared/src/features/management/roles.tsx +356 -0
  38. package/template/client/shared/src/features/management/users.tsx +245 -0
  39. package/template/client/shared/src/nativewind-env.d.ts +1 -0
  40. package/template/client/shared/src/utils/auth.ts +21 -0
  41. package/template/client/shared/tailwind.config.js +19 -0
  42. package/template/client/shared/tsconfig.json +9 -0
  43. package/template/package.json +47 -0
  44. package/template/packages/tsconfig/base.json +20 -0
  45. package/template/packages/tsconfig/package.json +11 -0
  46. package/template/pnpm-workspace.yaml +4 -0
  47. package/template/scripts/clean.js +56 -0
  48. package/template/server/db/drizzle.config.ts +10 -0
  49. package/template/server/db/migrations/0000_loving_mindworm.sql +86 -0
  50. package/template/server/db/migrations/meta/0000_snapshot.json +549 -0
  51. package/template/server/db/migrations/meta/_journal.json +13 -0
  52. package/template/server/db/package.json +28 -0
  53. package/template/server/db/src/index.ts +16 -0
  54. package/template/server/db/src/migrate.ts +28 -0
  55. package/template/server/db/src/schema/auth.ts +73 -0
  56. package/template/server/db/src/schema/index.ts +2 -0
  57. package/template/server/db/src/schema/rbac.ts +72 -0
  58. package/template/server/db/src/schema.ts +1 -0
  59. package/template/server/db/src/seed.ts +204 -0
  60. package/template/server/db/src/services/rbac.ts +144 -0
  61. package/template/server/db/src/services/user.ts +34 -0
  62. package/template/server/db/src/types/index.ts +34 -0
  63. package/template/server/db/tsconfig.json +8 -0
  64. package/template/server/hono/package.json +22 -0
  65. package/template/server/hono/src/auth.ts +29 -0
  66. package/template/server/hono/src/index.ts +215 -0
  67. package/template/server/hono/tsconfig.json +8 -0
  68. package/template/server/hono/wrangler.toml +12 -0
  69. 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,8 @@
1
+ {
2
+ "extends": "@thunder/tsconfig/base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*", "wrangler.toml"]
8
+ }
@@ -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
+ }