@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,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,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,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>;
|