@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,73 @@
1
+ import { relations } from "drizzle-orm";
2
+ import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
3
+ import { userRole } from "./rbac";
4
+
5
+ export const user = pgTable("user", {
6
+ id: text("id").primaryKey(),
7
+ name: text("name").notNull(),
8
+ email: text("email").notNull().unique(),
9
+ emailVerified: boolean("email_verified").notNull(),
10
+ image: text("image"),
11
+ createdAt: timestamp("created_at").notNull(),
12
+ updatedAt: timestamp("updated_at").notNull(),
13
+ });
14
+
15
+ export const userRelations = relations(user, ({ many }) => ({
16
+ sessions: many(session),
17
+ accounts: many(account),
18
+ userRoles: many(userRole),
19
+ }));
20
+
21
+ export const session = pgTable("session", {
22
+ id: text("id").primaryKey(),
23
+ expiresAt: timestamp("expires_at").notNull(),
24
+ token: text("token").notNull().unique(),
25
+ createdAt: timestamp("created_at").notNull(),
26
+ updatedAt: timestamp("updated_at").notNull(),
27
+ ipAddress: text("ip_address"),
28
+ userAgent: text("user_agent"),
29
+ userId: text("user_id")
30
+ .notNull()
31
+ .references(() => user.id, { onDelete: "cascade" }),
32
+ });
33
+
34
+ export const sessionRelations = relations(session, ({ one }) => ({
35
+ user: one(user, {
36
+ fields: [session.userId],
37
+ references: [user.id],
38
+ }),
39
+ }));
40
+
41
+ export const account = pgTable("account", {
42
+ id: text("id").primaryKey(),
43
+ accountId: text("account_id").notNull(),
44
+ providerId: text("provider_id").notNull(),
45
+ userId: text("user_id")
46
+ .notNull()
47
+ .references(() => user.id, { onDelete: "cascade" }),
48
+ accessToken: text("access_token"),
49
+ refreshToken: text("refresh_token"),
50
+ idToken: text("id_token"),
51
+ accessTokenExpiresAt: timestamp("access_token_expires_at"),
52
+ refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
53
+ scope: text("scope"),
54
+ password: text("password"),
55
+ createdAt: timestamp("created_at").notNull(),
56
+ updatedAt: timestamp("updated_at").notNull(),
57
+ });
58
+
59
+ export const accountRelations = relations(account, ({ one }) => ({
60
+ user: one(user, {
61
+ fields: [account.userId],
62
+ references: [user.id],
63
+ }),
64
+ }));
65
+
66
+ export const verification = pgTable("verification", {
67
+ id: text("id").primaryKey(),
68
+ identifier: text("identifier").notNull(),
69
+ value: text("value").notNull(),
70
+ expiresAt: timestamp("expires_at").notNull(),
71
+ createdAt: timestamp("created_at"),
72
+ updatedAt: timestamp("updated_at"),
73
+ });
@@ -0,0 +1,2 @@
1
+ export * from "./auth";
2
+ export * from "./rbac";
@@ -0,0 +1,72 @@
1
+ import { relations } from "drizzle-orm";
2
+ import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
3
+ import { user } from "./auth";
4
+
5
+ export const role = pgTable("role", {
6
+ id: text("id").primaryKey(),
7
+ name: text("name").notNull().unique(),
8
+ description: text("description"),
9
+ createdAt: timestamp("created_at").notNull(),
10
+ updatedAt: timestamp("updated_at").notNull(),
11
+ });
12
+
13
+ export const roleRelations = relations(role, ({ many }) => ({
14
+ rolePermissions: many(rolePermission),
15
+ userRoles: many(userRole),
16
+ }));
17
+
18
+ export const permission = pgTable("permission", {
19
+ id: text("id").primaryKey(),
20
+ name: text("name").notNull().unique(),
21
+ description: text("description"),
22
+ createdAt: timestamp("created_at").notNull(),
23
+ updatedAt: timestamp("updated_at").notNull(),
24
+ });
25
+
26
+ export const permissionRelations = relations(permission, ({ many }) => ({
27
+ rolePermissions: many(rolePermission),
28
+ }));
29
+
30
+ export const rolePermission = pgTable("role_permission", {
31
+ id: text("id").primaryKey(),
32
+ roleId: text("role_id")
33
+ .notNull()
34
+ .references(() => role.id, { onDelete: "cascade" }),
35
+ permissionId: text("permission_id")
36
+ .notNull()
37
+ .references(() => permission.id, { onDelete: "cascade" }),
38
+ createdAt: timestamp("created_at").notNull(),
39
+ });
40
+
41
+ export const rolePermissionRelations = relations(rolePermission, ({ one }) => ({
42
+ role: one(role, {
43
+ fields: [rolePermission.roleId],
44
+ references: [role.id],
45
+ }),
46
+ permission: one(permission, {
47
+ fields: [rolePermission.permissionId],
48
+ references: [permission.id],
49
+ }),
50
+ }));
51
+
52
+ export const userRole = pgTable("user_role", {
53
+ id: text("id").primaryKey(),
54
+ userId: text("user_id")
55
+ .notNull()
56
+ .references(() => user.id, { onDelete: "cascade" }),
57
+ roleId: text("role_id")
58
+ .notNull()
59
+ .references(() => role.id, { onDelete: "cascade" }),
60
+ createdAt: timestamp("created_at").notNull(),
61
+ });
62
+
63
+ export const userRoleRelations = relations(userRole, ({ one }) => ({
64
+ user: one(user, {
65
+ fields: [userRole.userId],
66
+ references: [user.id],
67
+ }),
68
+ role: one(role, {
69
+ fields: [userRole.roleId],
70
+ references: [role.id],
71
+ }),
72
+ }));
@@ -0,0 +1 @@
1
+ export * from "./schema/index";
@@ -0,0 +1,204 @@
1
+ import { createDbClient } from "./index";
2
+ import { user, role, permission, rolePermission, userRole } from "./schema";
3
+ import { eq } from "drizzle-orm";
4
+ import { betterAuth } from "better-auth";
5
+ import { drizzleAdapter } from "@better-auth/drizzle-adapter";
6
+
7
+ const dbUrl = process.env.DATABASE_URL;
8
+ if (!dbUrl) {
9
+ console.error("DATABASE_URL environment variable is required to run the seed script.");
10
+ process.exit(1);
11
+ }
12
+
13
+ const db = createDbClient(dbUrl);
14
+
15
+ async function main() {
16
+ console.log("Starting database seed...");
17
+
18
+ // 1. Insert permissions
19
+ const permissionsData = [
20
+ { id: "perm_view_dashboard", name: "view_dashboard", description: "Allows viewing the dashboard and stats" },
21
+ { id: "perm_manage_user", name: "manage:user", description: "Allows managing user roles and viewing user lists" },
22
+ { id: "perm_manage_role", name: "manage:role", description: "Allows creating, editing, and deleting roles" },
23
+ { id: "perm_manage_role_perm", name: "manage:role-perm", description: "Allows mapping permissions to roles" },
24
+ ];
25
+
26
+ console.log("Upserting permissions...");
27
+ for (const perm of permissionsData) {
28
+ await db
29
+ .insert(permission)
30
+ .values({
31
+ id: perm.id,
32
+ name: perm.name,
33
+ description: perm.description,
34
+ createdAt: new Date(),
35
+ updatedAt: new Date(),
36
+ })
37
+ .onConflictDoNothing();
38
+ }
39
+
40
+ // 2. Insert roles
41
+ const rolesData = [
42
+ { id: "role_admin", name: "Admin", description: "System administrator with full access permissions" },
43
+ { id: "role_user", name: "User", description: "Regular system user with dashboard access only" },
44
+ ];
45
+
46
+ console.log("Upserting roles...");
47
+ for (const r of rolesData) {
48
+ await db
49
+ .insert(role)
50
+ .values({
51
+ id: r.id,
52
+ name: r.name,
53
+ description: r.description,
54
+ createdAt: new Date(),
55
+ updatedAt: new Date(),
56
+ })
57
+ .onConflictDoNothing();
58
+ }
59
+
60
+ // 3. Link permissions to roles
61
+ console.log("Linking permissions to Admin role...");
62
+ for (const perm of permissionsData) {
63
+ await db
64
+ .insert(rolePermission)
65
+ .values({
66
+ id: `rp_admin_${perm.name}`,
67
+ roleId: "role_admin",
68
+ permissionId: perm.id,
69
+ createdAt: new Date(),
70
+ })
71
+ .onConflictDoNothing();
72
+ }
73
+
74
+ console.log("Linking permissions to User role...");
75
+ await db
76
+ .insert(rolePermission)
77
+ .values({
78
+ id: "rp_user_view_dashboard",
79
+ roleId: "role_user",
80
+ permissionId: "perm_view_dashboard",
81
+ createdAt: new Date(),
82
+ })
83
+ .onConflictDoNothing();
84
+
85
+ // 4. Create default admin user via Better Auth (so the password gets hashed correctly)
86
+ const adminEmail = "admin@thunderstack.dev";
87
+ const adminPassword = "AdminPassword123!";
88
+
89
+ console.log("Initializing Better Auth for seed...");
90
+ const auth = betterAuth({
91
+ database: drizzleAdapter(db, {
92
+ provider: "pg",
93
+ }),
94
+ secret: process.env.BETTER_AUTH_SECRET || "temp_secret_for_seeding_12345678",
95
+ baseURL: process.env.BETTER_AUTH_URL || "http://localhost:8787",
96
+ emailAndPassword: {
97
+ enabled: true,
98
+ },
99
+ });
100
+
101
+ let adminUser = await db.query.user.findFirst({
102
+ where: eq(user.email, adminEmail),
103
+ });
104
+
105
+ if (!adminUser) {
106
+ console.log(`Creating default Admin user: ${adminEmail}...`);
107
+ try {
108
+ const result = await auth.api.signUpEmail({
109
+ headers: new Headers(),
110
+ body: {
111
+ email: adminEmail,
112
+ password: adminPassword,
113
+ name: "System Admin",
114
+ },
115
+ });
116
+
117
+ if (result && result.user) {
118
+ adminUser = {
119
+ ...result.user,
120
+ image: result.user.image ?? null,
121
+ };
122
+ console.log("Admin user created.");
123
+ } else {
124
+ throw new Error("Invalid response from auth.api.signUpEmail");
125
+ }
126
+ } catch (err) {
127
+ console.error("Failed to create admin user via Better Auth API:", err);
128
+ process.exit(1);
129
+ }
130
+ } else {
131
+ console.log("Admin user already exists.");
132
+ }
133
+
134
+ if (adminUser) {
135
+ // 5. Assign Admin role to the Admin user
136
+ console.log("Assigning Admin role to Admin user...");
137
+ await db
138
+ .insert(userRole)
139
+ .values({
140
+ id: `ur_${adminUser.id}_role_admin`,
141
+ userId: adminUser.id,
142
+ roleId: "role_admin",
143
+ createdAt: new Date(),
144
+ })
145
+ .onConflictDoNothing();
146
+ }
147
+
148
+ // 6. Create extra dummy users
149
+ const dummyUsers = [
150
+ { email: "jane@thunderstack.dev", password: "UserPassword123!", name: "Jane Doe", roleId: "role_user" },
151
+ { email: "john@thunderstack.dev", password: "UserPassword123!", name: "John Smith", roleId: "role_user" },
152
+ { email: "moderator@thunderstack.dev", password: "ModPassword123!", name: "Alex Moderator", roleId: "role_user" },
153
+ ];
154
+
155
+ for (const u of dummyUsers) {
156
+ let existingUser = await db.query.user.findFirst({
157
+ where: eq(user.email, u.email),
158
+ });
159
+
160
+ if (!existingUser) {
161
+ console.log(`Creating dummy user: ${u.email}...`);
162
+ try {
163
+ const result = await auth.api.signUpEmail({
164
+ headers: new Headers(),
165
+ body: {
166
+ email: u.email,
167
+ password: u.password,
168
+ name: u.name,
169
+ },
170
+ });
171
+
172
+ if (result && result.user) {
173
+ existingUser = {
174
+ ...result.user,
175
+ image: result.user.image ?? null,
176
+ };
177
+ console.log(`User ${u.email} created.`);
178
+ }
179
+ } catch (err) {
180
+ console.error(`Failed to create user ${u.email}:`, err);
181
+ }
182
+ }
183
+
184
+ if (existingUser) {
185
+ console.log(`Assigning role ${u.roleId} to user ${u.email}...`);
186
+ await db
187
+ .insert(userRole)
188
+ .values({
189
+ id: `ur_${existingUser.id}_${u.roleId}`,
190
+ userId: existingUser.id,
191
+ roleId: u.roleId,
192
+ createdAt: new Date(),
193
+ })
194
+ .onConflictDoNothing();
195
+ }
196
+ }
197
+
198
+ console.log("Database seeding completed successfully!");
199
+ }
200
+
201
+ main().catch((err) => {
202
+ console.error("Unhandled database seed error:", err);
203
+ process.exit(1);
204
+ });
@@ -0,0 +1,144 @@
1
+ import { eq, and, sql } from "drizzle-orm";
2
+ import type { DbClient } from "../index";
3
+ import * as schema from "../schema";
4
+ import type { Role, Permission } from "../types";
5
+
6
+ export class RbacService {
7
+ constructor(private db: DbClient) {}
8
+
9
+ async getStats(): Promise<{ users: number; roles: number; permissions: number }> {
10
+ const usersRes = await this.db.select({ count: sql<number>`count(*)` }).from(schema.user);
11
+ const rolesRes = await this.db.select({ count: sql<number>`count(*)` }).from(schema.role);
12
+ const permsRes = await this.db.select({ count: sql<number>`count(*)` }).from(schema.permission);
13
+
14
+ return {
15
+ users: Number(usersRes[0]?.count || 0),
16
+ roles: Number(rolesRes[0]?.count || 0),
17
+ permissions: Number(permsRes[0]?.count || 0),
18
+ };
19
+ }
20
+
21
+ async getPermissions(): Promise<Permission[]> {
22
+ return this.db.select().from(schema.permission);
23
+ }
24
+
25
+ async getRoles(): Promise<(Role & { permissions: Permission[] })[]> {
26
+ const roles = await this.db.select().from(schema.role);
27
+ const result: (Role & { permissions: Permission[] })[] = [];
28
+
29
+ for (const r of roles) {
30
+ const perms = await this.db
31
+ .select({
32
+ id: schema.permission.id,
33
+ name: schema.permission.name,
34
+ description: schema.permission.description,
35
+ createdAt: schema.permission.createdAt,
36
+ updatedAt: schema.permission.updatedAt,
37
+ })
38
+ .from(schema.rolePermission)
39
+ .innerJoin(schema.permission, eq(schema.rolePermission.permissionId, schema.permission.id))
40
+ .where(eq(schema.rolePermission.roleId, r.id));
41
+
42
+ result.push({
43
+ ...r,
44
+ permissions: perms,
45
+ });
46
+ }
47
+
48
+ return result;
49
+ }
50
+
51
+ async createRole(name: string, description?: string): Promise<Role> {
52
+ const id = `role_${name.toLowerCase().replace(/\s+/g, "_")}`;
53
+ const now = new Date();
54
+ const [inserted] = await this.db
55
+ .insert(schema.role)
56
+ .values({
57
+ id,
58
+ name,
59
+ description: description || null,
60
+ createdAt: now,
61
+ updatedAt: now,
62
+ })
63
+ .returning();
64
+ if (!inserted) {
65
+ throw new Error("Failed to create role");
66
+ }
67
+ return inserted;
68
+ }
69
+
70
+ async deleteRole(id: string): Promise<void> {
71
+ await this.db.delete(schema.role).where(eq(schema.role.id, id));
72
+ }
73
+
74
+ async assignPermissionToRole(roleId: string, permissionId: string): Promise<void> {
75
+ await this.db
76
+ .insert(schema.rolePermission)
77
+ .values({
78
+ id: `rp_${roleId}_${permissionId}`,
79
+ roleId,
80
+ permissionId,
81
+ createdAt: new Date(),
82
+ })
83
+ .onConflictDoNothing();
84
+ }
85
+
86
+ async removePermissionFromRole(roleId: string, permissionId: string): Promise<void> {
87
+ await this.db
88
+ .delete(schema.rolePermission)
89
+ .where(
90
+ and(
91
+ eq(schema.rolePermission.roleId, roleId),
92
+ eq(schema.rolePermission.permissionId, permissionId)
93
+ )
94
+ );
95
+ }
96
+
97
+ async getUserRoles(userId: string): Promise<Role[]> {
98
+ return this.db
99
+ .select({
100
+ id: schema.role.id,
101
+ name: schema.role.name,
102
+ description: schema.role.description,
103
+ createdAt: schema.role.createdAt,
104
+ updatedAt: schema.role.updatedAt,
105
+ })
106
+ .from(schema.userRole)
107
+ .innerJoin(schema.role, eq(schema.userRole.roleId, schema.role.id))
108
+ .where(eq(schema.userRole.userId, userId));
109
+ }
110
+
111
+ async getUserPermissions(userId: string): Promise<Permission[]> {
112
+ return this.db
113
+ .selectDistinct({
114
+ id: schema.permission.id,
115
+ name: schema.permission.name,
116
+ description: schema.permission.description,
117
+ createdAt: schema.permission.createdAt,
118
+ updatedAt: schema.permission.updatedAt,
119
+ })
120
+ .from(schema.userRole)
121
+ .innerJoin(schema.role, eq(schema.userRole.roleId, schema.role.id))
122
+ .innerJoin(schema.rolePermission, eq(schema.role.id, schema.rolePermission.roleId))
123
+ .innerJoin(schema.permission, eq(schema.rolePermission.permissionId, schema.permission.id))
124
+ .where(eq(schema.userRole.userId, userId));
125
+ }
126
+
127
+ async assignRoleToUser(userId: string, roleId: string): Promise<void> {
128
+ await this.db
129
+ .insert(schema.userRole)
130
+ .values({
131
+ id: `ur_${userId}_${roleId}`,
132
+ userId,
133
+ roleId,
134
+ createdAt: new Date(),
135
+ })
136
+ .onConflictDoNothing();
137
+ }
138
+
139
+ async removeRoleFromUser(userId: string, roleId: string): Promise<void> {
140
+ await this.db
141
+ .delete(schema.userRole)
142
+ .where(and(eq(schema.userRole.userId, userId), eq(schema.userRole.roleId, roleId)));
143
+ }
144
+ }
@@ -0,0 +1,34 @@
1
+ import { eq } from "drizzle-orm";
2
+ import type { DbClient } from "../index";
3
+ import * as schema from "../schema";
4
+ import type { User, Role } from "../types";
5
+
6
+ export class UserService {
7
+ constructor(private db: DbClient) {}
8
+
9
+ async getUsersWithRoles(): Promise<(User & { roles: Role[] })[]> {
10
+ const users = await this.db.select().from(schema.user);
11
+ const result: (User & { roles: Role[] })[] = [];
12
+
13
+ for (const u of users) {
14
+ const roles = await this.db
15
+ .select({
16
+ id: schema.role.id,
17
+ name: schema.role.name,
18
+ description: schema.role.description,
19
+ createdAt: schema.role.createdAt,
20
+ updatedAt: schema.role.updatedAt,
21
+ })
22
+ .from(schema.userRole)
23
+ .innerJoin(schema.role, eq(schema.userRole.roleId, schema.role.id))
24
+ .where(eq(schema.userRole.userId, u.id));
25
+
26
+ result.push({
27
+ ...u,
28
+ roles,
29
+ });
30
+ }
31
+
32
+ return result;
33
+ }
34
+ }
@@ -0,0 +1,34 @@
1
+ import { InferSelectModel, InferInsertModel } from "drizzle-orm";
2
+ import * as schema from "../schema";
3
+
4
+ export type User = InferSelectModel<typeof schema.user>;
5
+ export type NewUser = InferInsertModel<typeof schema.user>;
6
+
7
+ export type Session = InferSelectModel<typeof schema.session>;
8
+ export type NewSession = InferInsertModel<typeof schema.session>;
9
+
10
+ export type Account = InferSelectModel<typeof schema.account>;
11
+ export type NewAccount = InferInsertModel<typeof schema.account>;
12
+
13
+ export type Verification = InferSelectModel<typeof schema.verification>;
14
+ export type NewVerification = InferInsertModel<typeof schema.verification>;
15
+
16
+ export type Role = InferSelectModel<typeof schema.role>;
17
+ export type NewRole = InferInsertModel<typeof schema.role>;
18
+
19
+ export type Permission = InferSelectModel<typeof schema.permission>;
20
+ export type NewPermission = InferInsertModel<typeof schema.permission>;
21
+
22
+ export type RolePermission = InferSelectModel<typeof schema.rolePermission>;
23
+ export type NewRolePermission = InferInsertModel<typeof schema.rolePermission>;
24
+
25
+ export type UserRole = InferSelectModel<typeof schema.userRole>;
26
+ export type NewUserRole = InferInsertModel<typeof schema.userRole>;
27
+
28
+ export interface UserWithRoles extends User {
29
+ roles: Role[];
30
+ }
31
+
32
+ export interface RoleWithPermissions extends Role {
33
+ permissions: Permission[];
34
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "@thunder/tsconfig/base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@thunder/api",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "main": "src/index.ts",
6
+ "scripts": {
7
+ "typecheck": "tsc --noEmit",
8
+ "dev": "wrangler dev src/index.ts --port 8787",
9
+ "deploy": "wrangler deploy src/index.ts"
10
+ },
11
+ "dependencies": {
12
+ "@better-auth/drizzle-adapter": "^1.6.23",
13
+ "@thunder/db": "workspace:*",
14
+ "better-auth": "^1.6.23",
15
+ "hono": "^4.12.28"
16
+ },
17
+ "devDependencies": {
18
+ "@thunder/tsconfig": "workspace:*",
19
+ "typescript": "^5.9.3",
20
+ "wrangler": "^4.107.0"
21
+ }
22
+ }
@@ -0,0 +1,29 @@
1
+ import { betterAuth } from "better-auth";
2
+ import { drizzleAdapter } from "@better-auth/drizzle-adapter";
3
+ import type { DbClient } from "@thunder/db";
4
+
5
+ export const getAuthInstance = (db: DbClient, env: {
6
+ BETTER_AUTH_SECRET: string;
7
+ GOOGLE_CLIENT_ID: string;
8
+ GOOGLE_CLIENT_SECRET: string;
9
+ BETTER_AUTH_URL: string;
10
+ }) => {
11
+ return betterAuth({
12
+ database: drizzleAdapter(db, {
13
+ provider: "pg",
14
+ }),
15
+ secret: env.BETTER_AUTH_SECRET,
16
+ baseURL: env.BETTER_AUTH_URL,
17
+ socialProviders: {
18
+ google: {
19
+ clientId: env.GOOGLE_CLIENT_ID,
20
+ clientSecret: env.GOOGLE_CLIENT_SECRET,
21
+ },
22
+ },
23
+ emailAndPassword: {
24
+ enabled: true,
25
+ },
26
+ });
27
+ };
28
+
29
+ export type AuthType = ReturnType<typeof getAuthInstance>;