@lobehub/lobehub 2.0.0-next.139 → 2.0.0-next.140
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/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/docs/development/database-schema.dbml +39 -1
- package/package.json +1 -1
- package/packages/database/migrations/0053_better_auth_admin.sql +5 -0
- package/packages/database/migrations/0054_better_auth_two_factor.sql +47 -0
- package/packages/database/migrations/meta/0053_snapshot.json +8247 -0
- package/packages/database/migrations/meta/0054_snapshot.json +8402 -0
- package/packages/database/migrations/meta/_journal.json +14 -0
- package/packages/database/src/core/migrations.json +31 -0
- package/packages/database/src/index.ts +1 -0
- package/packages/database/src/repositories/tableViewer/index.test.ts +1 -1
- package/packages/database/src/schemas/betterAuth.ts +104 -47
- package/packages/database/src/schemas/user.ts +14 -1
- package/src/auth.ts +40 -22
- package/src/envs/auth.ts +9 -2
- package/src/libs/better-auth/auth-client.ts +2 -0
|
@@ -371,6 +371,20 @@
|
|
|
371
371
|
"when": 1764500630663,
|
|
372
372
|
"tag": "0052_topic_and_messages",
|
|
373
373
|
"breakpoints": true
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
"idx": 53,
|
|
377
|
+
"version": "7",
|
|
378
|
+
"when": 1764511006123,
|
|
379
|
+
"tag": "0053_better_auth_admin",
|
|
380
|
+
"breakpoints": true
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
"idx": 54,
|
|
384
|
+
"version": "7",
|
|
385
|
+
"when": 1764579351312,
|
|
386
|
+
"tag": "0054_better_auth_two_factor",
|
|
387
|
+
"breakpoints": true
|
|
374
388
|
}
|
|
375
389
|
],
|
|
376
390
|
"version": "6"
|
|
@@ -859,5 +859,36 @@
|
|
|
859
859
|
"bps": true,
|
|
860
860
|
"folderMillis": 1764500630663,
|
|
861
861
|
"hash": "94721bc06910a456a4756c9b0c27ef5d7ff55b7ea8c772acf58052c0155c693b"
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
"sql": [
|
|
865
|
+
"ALTER TABLE \"auth_sessions\" ADD COLUMN IF NOT EXISTS \"impersonated_by\" text;",
|
|
866
|
+
"\nALTER TABLE \"users\" ADD COLUMN IF NOT EXISTS \"role\" text;",
|
|
867
|
+
"\nALTER TABLE \"users\" ADD COLUMN IF NOT EXISTS \"banned\" boolean DEFAULT false;",
|
|
868
|
+
"\nALTER TABLE \"users\" ADD COLUMN IF NOT EXISTS \"ban_reason\" text;",
|
|
869
|
+
"\nALTER TABLE \"users\" ADD COLUMN IF NOT EXISTS \"ban_expires\" timestamp with time zone;\n"
|
|
870
|
+
],
|
|
871
|
+
"bps": true,
|
|
872
|
+
"folderMillis": 1764511006123,
|
|
873
|
+
"hash": "523a8418ceeadbe80c34affee07cdfa2e8076e297473c55c73e74648a9be1e45"
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
"sql": [
|
|
877
|
+
"CREATE TABLE IF NOT EXISTS \"two_factor\" (\n \"backup_codes\" text NOT NULL,\n \"id\" text PRIMARY KEY NOT NULL,\n \"secret\" text NOT NULL,\n \"user_id\" text NOT NULL\n);\n",
|
|
878
|
+
"\nALTER TABLE \"users\" ADD COLUMN IF NOT EXISTS \"two_factor_enabled\" boolean DEFAULT false;\n",
|
|
879
|
+
"\nALTER TABLE \"users\" ADD COLUMN IF NOT EXISTS \"phone_number\" text;\n",
|
|
880
|
+
"\nALTER TABLE \"users\" ADD COLUMN IF NOT EXISTS \"phone_number_verified\" boolean;\n",
|
|
881
|
+
"\nDO $$\nBEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_constraint WHERE conname = 'two_factor_user_id_users_id_fk'\n ) THEN\n ALTER TABLE \"two_factor\"\n ADD CONSTRAINT \"two_factor_user_id_users_id_fk\"\n FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\n END IF;\nEND $$;\n",
|
|
882
|
+
"\nCREATE INDEX IF NOT EXISTS \"two_factor_secret_idx\" ON \"two_factor\" USING btree (\"secret\");\n",
|
|
883
|
+
"\nCREATE INDEX IF NOT EXISTS \"two_factor_user_id_idx\" ON \"two_factor\" USING btree (\"user_id\");\n",
|
|
884
|
+
"\nCREATE INDEX IF NOT EXISTS \"account_userId_idx\" ON \"accounts\" USING btree (\"user_id\");\n",
|
|
885
|
+
"\nCREATE INDEX IF NOT EXISTS \"auth_session_userId_idx\" ON \"auth_sessions\" USING btree (\"user_id\");\n",
|
|
886
|
+
"\nCREATE INDEX IF NOT EXISTS \"verification_identifier_idx\" ON \"verifications\" USING btree (\"identifier\");\n",
|
|
887
|
+
"\nDO $$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'users_email_unique') THEN\n ALTER TABLE \"users\" ADD CONSTRAINT \"users_email_unique\" UNIQUE (\"email\");\n END IF;\nEND $$;\n",
|
|
888
|
+
"\nDO $$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'users_phone_number_unique') THEN\n ALTER TABLE \"users\" ADD CONSTRAINT \"users_phone_number_unique\" UNIQUE (\"phone_number\");\n END IF;\nEND $$;\n"
|
|
889
|
+
],
|
|
890
|
+
"bps": true,
|
|
891
|
+
"folderMillis": 1764579351312,
|
|
892
|
+
"hash": "22fb7a65764b1f3e3c1ae2ce95d448685e6a01d1fb2f8c3f925c655f0824c161"
|
|
862
893
|
}
|
|
863
894
|
]
|
|
@@ -23,7 +23,7 @@ describe('TableViewerRepo', () => {
|
|
|
23
23
|
it('should return all tables with counts', async () => {
|
|
24
24
|
const result = await repo.getAllTables();
|
|
25
25
|
|
|
26
|
-
expect(result.length).toEqual(
|
|
26
|
+
expect(result.length).toEqual(72);
|
|
27
27
|
expect(result[0]).toEqual({ name: 'accounts', count: 0, type: 'BASE TABLE' });
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { relations } from 'drizzle-orm';
|
|
2
|
+
import { index, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
|
|
2
3
|
|
|
3
4
|
import { users } from './user';
|
|
4
5
|
|
|
@@ -15,49 +16,105 @@ import { users } from './user';
|
|
|
15
16
|
// .notNull(),
|
|
16
17
|
// });
|
|
17
18
|
|
|
18
|
-
export const session = pgTable(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.notNull(),
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
19
|
+
export const session = pgTable(
|
|
20
|
+
'auth_sessions',
|
|
21
|
+
{
|
|
22
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
23
|
+
expiresAt: timestamp('expires_at').notNull(),
|
|
24
|
+
id: text('id').primaryKey(),
|
|
25
|
+
impersonatedBy: text('impersonated_by'),
|
|
26
|
+
ipAddress: text('ip_address'),
|
|
27
|
+
token: text('token').notNull().unique(),
|
|
28
|
+
updatedAt: timestamp('updated_at')
|
|
29
|
+
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
30
|
+
.notNull(),
|
|
31
|
+
userAgent: text('user_agent'),
|
|
32
|
+
userId: text('user_id')
|
|
33
|
+
.notNull()
|
|
34
|
+
.references(() => users.id, { onDelete: 'cascade' }),
|
|
35
|
+
},
|
|
36
|
+
(table) => [index('auth_session_userId_idx').on(table.userId)],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export const account = pgTable(
|
|
40
|
+
'accounts',
|
|
41
|
+
{
|
|
42
|
+
accessToken: text('access_token'),
|
|
43
|
+
accessTokenExpiresAt: timestamp('access_token_expires_at'),
|
|
44
|
+
accountId: text('account_id').notNull(),
|
|
45
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
46
|
+
id: text('id').primaryKey(),
|
|
47
|
+
idToken: text('id_token'),
|
|
48
|
+
password: text('password'),
|
|
49
|
+
providerId: text('provider_id').notNull(),
|
|
50
|
+
refreshToken: text('refresh_token'),
|
|
51
|
+
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
|
|
52
|
+
scope: text('scope'),
|
|
53
|
+
updatedAt: timestamp('updated_at')
|
|
54
|
+
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
55
|
+
.notNull(),
|
|
56
|
+
userId: text('user_id')
|
|
57
|
+
.notNull()
|
|
58
|
+
.references(() => users.id, { onDelete: 'cascade' }),
|
|
59
|
+
},
|
|
60
|
+
(table) => [index('account_userId_idx').on(table.userId)],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
export const verification = pgTable(
|
|
64
|
+
'verifications',
|
|
65
|
+
{
|
|
66
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
67
|
+
expiresAt: timestamp('expires_at').notNull(),
|
|
68
|
+
id: text('id').primaryKey(),
|
|
69
|
+
identifier: text('identifier').notNull(),
|
|
70
|
+
updatedAt: timestamp('updated_at')
|
|
71
|
+
.defaultNow()
|
|
72
|
+
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
73
|
+
.notNull(),
|
|
74
|
+
value: text('value').notNull(),
|
|
75
|
+
},
|
|
76
|
+
(table) => [index('verification_identifier_idx').on(table.identifier)],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
export const twoFactor = pgTable(
|
|
80
|
+
'two_factor',
|
|
81
|
+
{
|
|
82
|
+
backupCodes: text('backup_codes').notNull(),
|
|
83
|
+
id: text('id').primaryKey(),
|
|
84
|
+
secret: text('secret').notNull(),
|
|
85
|
+
userId: text('user_id')
|
|
86
|
+
.notNull()
|
|
87
|
+
.references(() => users.id, { onDelete: 'cascade' }),
|
|
88
|
+
},
|
|
89
|
+
(table) => [
|
|
90
|
+
index('two_factor_secret_idx').on(table.secret),
|
|
91
|
+
index('two_factor_user_id_idx').on(table.userId),
|
|
92
|
+
],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
export const usersRelations = relations(users, ({ many }) => ({
|
|
96
|
+
accounts: many(account),
|
|
97
|
+
sessions: many(session),
|
|
98
|
+
twoFactors: many(twoFactor),
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
export const sessionRelations = relations(session, ({ one }) => ({
|
|
102
|
+
users: one(users, {
|
|
103
|
+
fields: [session.userId],
|
|
104
|
+
references: [users.id],
|
|
105
|
+
}),
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
export const accountRelations = relations(account, ({ one }) => ({
|
|
109
|
+
users: one(users, {
|
|
110
|
+
fields: [account.userId],
|
|
111
|
+
references: [users.id],
|
|
112
|
+
}),
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
export const twoFactorRelations = relations(twoFactor, ({ one }) => ({
|
|
116
|
+
users: one(users, {
|
|
117
|
+
fields: [twoFactor.userId],
|
|
118
|
+
references: [users.id],
|
|
119
|
+
}),
|
|
120
|
+
}));
|
|
@@ -9,7 +9,7 @@ import { timestamps, timestamptz } from './_helpers';
|
|
|
9
9
|
export const users = pgTable('users', {
|
|
10
10
|
id: text('id').primaryKey().notNull(),
|
|
11
11
|
username: text('username').unique(),
|
|
12
|
-
email: text('email'),
|
|
12
|
+
email: text('email').unique(),
|
|
13
13
|
|
|
14
14
|
avatar: text('avatar'),
|
|
15
15
|
phone: text('phone'),
|
|
@@ -28,6 +28,19 @@ export const users = pgTable('users', {
|
|
|
28
28
|
|
|
29
29
|
preference: jsonb('preference').$defaultFn(() => DEFAULT_PREFERENCE),
|
|
30
30
|
|
|
31
|
+
// better-auth admin
|
|
32
|
+
role: text('role'),
|
|
33
|
+
banned: boolean('banned').default(false),
|
|
34
|
+
banReason: text('ban_reason'),
|
|
35
|
+
banExpires: timestamptz('ban_expires'),
|
|
36
|
+
|
|
37
|
+
// better-auth two-factor
|
|
38
|
+
twoFactorEnabled: boolean('two_factor_enabled').default(false),
|
|
39
|
+
|
|
40
|
+
// better-auth phone number
|
|
41
|
+
phoneNumber: text('phone_number').unique(),
|
|
42
|
+
phoneNumberVerified: boolean('phone_number_verified'),
|
|
43
|
+
|
|
31
44
|
...timestamps,
|
|
32
45
|
});
|
|
33
46
|
|
package/src/auth.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
|
2
|
-
import { serverDB } from '@lobechat/database';
|
|
2
|
+
import { createNanoId, idGenerator, serverDB } from '@lobechat/database';
|
|
3
3
|
import { betterAuth } from 'better-auth';
|
|
4
4
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
|
5
|
-
import { genericOAuth, magicLink } from 'better-auth/plugins';
|
|
5
|
+
import { admin, genericOAuth, magicLink } from 'better-auth/plugins';
|
|
6
6
|
|
|
7
7
|
import { authEnv } from '@/envs/auth';
|
|
8
8
|
import {
|
|
@@ -51,6 +51,7 @@ const getTrustedOrigins = () => {
|
|
|
51
51
|
|
|
52
52
|
const defaults = [
|
|
53
53
|
authEnv.NEXT_PUBLIC_AUTH_URL,
|
|
54
|
+
normalizeOrigin(process.env.APP_URL),
|
|
54
55
|
normalizeOrigin(process.env.VERCEL_BRANCH_URL),
|
|
55
56
|
normalizeOrigin(process.env.VERCEL_URL),
|
|
56
57
|
].filter(Boolean) as string[];
|
|
@@ -72,10 +73,6 @@ export const auth = betterAuth({
|
|
|
72
73
|
secret: authEnv.AUTH_SECRET,
|
|
73
74
|
trustedOrigins: getTrustedOrigins(),
|
|
74
75
|
|
|
75
|
-
database: drizzleAdapter(serverDB, {
|
|
76
|
-
provider: 'pg',
|
|
77
|
-
}),
|
|
78
|
-
|
|
79
76
|
emailAndPassword: {
|
|
80
77
|
autoSignIn: true,
|
|
81
78
|
enabled: true,
|
|
@@ -111,7 +108,44 @@ export const auth = betterAuth({
|
|
|
111
108
|
},
|
|
112
109
|
},
|
|
113
110
|
|
|
111
|
+
database: drizzleAdapter(serverDB, {
|
|
112
|
+
provider: 'pg',
|
|
113
|
+
}),
|
|
114
|
+
user: {
|
|
115
|
+
additionalFields: {
|
|
116
|
+
username: {
|
|
117
|
+
required: false,
|
|
118
|
+
type: 'string',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
fields: {
|
|
122
|
+
image: 'avatar',
|
|
123
|
+
// NOTE: use drizzle filed instead of db field, so use fullName instead of full_name
|
|
124
|
+
name: 'fullName',
|
|
125
|
+
},
|
|
126
|
+
modelName: 'users',
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
socialProviders,
|
|
130
|
+
advanced: {
|
|
131
|
+
database: {
|
|
132
|
+
/**
|
|
133
|
+
* Align Better Auth user IDs with our shared idGenerator for consistency.
|
|
134
|
+
* Other models use the shared nanoid generator (12 chars) to keep IDs consistent project-wide.
|
|
135
|
+
*/
|
|
136
|
+
generateId: ({ model }) => {
|
|
137
|
+
// Better Auth passes the model name; handle both singular and plural for safety.
|
|
138
|
+
if (model === 'user' || model === 'users') {
|
|
139
|
+
return idGenerator('user', 12);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Other models: use shared nanoid generator (12 chars) to keep consistency.
|
|
143
|
+
return createNanoId(12)();
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
114
147
|
plugins: [
|
|
148
|
+
admin(),
|
|
115
149
|
...(genericOAuthProviders.length > 0
|
|
116
150
|
? [
|
|
117
151
|
genericOAuth({
|
|
@@ -139,20 +173,4 @@ export const auth = betterAuth({
|
|
|
139
173
|
]
|
|
140
174
|
: []),
|
|
141
175
|
],
|
|
142
|
-
socialProviders,
|
|
143
|
-
|
|
144
|
-
user: {
|
|
145
|
-
additionalFields: {
|
|
146
|
-
username: {
|
|
147
|
-
required: false,
|
|
148
|
-
type: 'string',
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
fields: {
|
|
152
|
-
image: 'avatar',
|
|
153
|
-
// NOTE: use drizzle filed instead of db field, so use fullName instead of full_name
|
|
154
|
-
name: 'fullName',
|
|
155
|
-
},
|
|
156
|
-
modelName: 'users',
|
|
157
|
-
},
|
|
158
176
|
});
|
package/src/envs/auth.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/* eslint-disable sort-keys-fix/sort-keys-fix , typescript-sort-keys/interface */
|
|
2
|
+
import { enableBetterAuth, enableClerk, enableNextAuth } from '@lobechat/const';
|
|
2
3
|
import { createEnv } from '@t3-oss/env-nextjs';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
|
|
5
|
-
import { enableBetterAuth, enableClerk, enableNextAuth } from '@/const/auth';
|
|
6
|
-
|
|
7
6
|
/**
|
|
8
7
|
* Resolve public auth URL with compatibility fallbacks for NextAuth and Vercel deployments.
|
|
9
8
|
*/
|
|
@@ -18,6 +17,14 @@ const resolvePublicAuthUrl = () => {
|
|
|
18
17
|
}
|
|
19
18
|
}
|
|
20
19
|
|
|
20
|
+
if (process.env.APP_URL) {
|
|
21
|
+
try {
|
|
22
|
+
return new URL(process.env.APP_URL).origin;
|
|
23
|
+
} catch {
|
|
24
|
+
// ignore invalid APP_URL
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
if (process.env.VERCEL_URL) {
|
|
22
29
|
try {
|
|
23
30
|
const normalizedVercelUrl = process.env.VERCEL_URL.startsWith('http')
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
adminClient,
|
|
2
3
|
genericOAuthClient,
|
|
3
4
|
inferAdditionalFields,
|
|
4
5
|
magicLinkClient,
|
|
@@ -27,6 +28,7 @@ export const {
|
|
|
27
28
|
/** The base URL of the server (optional if you're using the same domain) */
|
|
28
29
|
baseURL: NEXT_PUBLIC_AUTH_URL,
|
|
29
30
|
plugins: [
|
|
31
|
+
adminClient(),
|
|
30
32
|
inferAdditionalFields<typeof auth>(),
|
|
31
33
|
genericOAuthClient(),
|
|
32
34
|
...(enableMagicLink ? [magicLinkClient()] : []),
|