@lobehub/lobehub 2.0.0-next.157 → 2.0.0-next.159
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/.nvmrc +1 -1
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/docs/development/database-schema.dbml +7 -0
- package/locales/ar/auth.json +11 -2
- package/locales/ar/models.json +25 -13
- package/locales/bg-BG/auth.json +11 -2
- package/locales/bg-BG/models.json +25 -13
- package/locales/de-DE/auth.json +11 -2
- package/locales/de-DE/models.json +25 -13
- package/locales/en-US/auth.json +18 -9
- package/locales/en-US/models.json +25 -13
- package/locales/es-ES/auth.json +11 -2
- package/locales/es-ES/models.json +25 -13
- package/locales/fa-IR/auth.json +11 -2
- package/locales/fa-IR/models.json +25 -13
- package/locales/fr-FR/auth.json +11 -2
- package/locales/fr-FR/models.json +25 -13
- package/locales/it-IT/auth.json +11 -2
- package/locales/it-IT/models.json +25 -13
- package/locales/ja-JP/auth.json +11 -2
- package/locales/ja-JP/models.json +25 -13
- package/locales/ko-KR/auth.json +11 -2
- package/locales/ko-KR/models.json +25 -13
- package/locales/nl-NL/auth.json +11 -2
- package/locales/nl-NL/models.json +25 -13
- package/locales/pl-PL/auth.json +11 -2
- package/locales/pl-PL/models.json +25 -13
- package/locales/pt-BR/auth.json +11 -2
- package/locales/pt-BR/models.json +25 -13
- package/locales/ru-RU/auth.json +11 -2
- package/locales/ru-RU/models.json +25 -13
- package/locales/tr-TR/auth.json +11 -2
- package/locales/tr-TR/models.json +25 -13
- package/locales/vi-VN/auth.json +11 -2
- package/locales/vi-VN/models.json +25 -13
- package/locales/zh-CN/auth.json +18 -9
- package/locales/zh-CN/models.json +25 -13
- package/locales/zh-TW/auth.json +11 -2
- package/locales/zh-TW/models.json +25 -13
- package/next.config.ts +1 -1
- package/package.json +2 -1
- package/packages/database/migrations/0058_add_source_into_user_plugins.sql +1 -0
- package/packages/database/migrations/0059_add_normalized_email_indexes.sql +4 -0
- package/packages/database/migrations/meta/0058_snapshot.json +8432 -0
- package/packages/database/migrations/meta/0059_snapshot.json +8474 -0
- package/packages/database/migrations/meta/_journal.json +14 -0
- package/packages/database/src/core/migrations.json +31 -32
- package/packages/database/src/models/user.ts +13 -1
- package/packages/database/src/schemas/user.ts +39 -31
- package/src/app/(backend)/api/auth/check-user/route.ts +0 -6
- package/src/app/(backend)/api/auth/resolve-username/route.ts +52 -0
- package/src/app/[variants]/(auth)/signin/page.tsx +102 -14
- package/src/app/[variants]/(auth)/signup/[[...signup]]/BetterAuthSignUpForm.tsx +15 -0
- package/src/app/[variants]/(main)/profile/(home)/Client.tsx +152 -12
- package/src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/index.tsx +4 -9
- package/src/app/[variants]/desktopRouter.config.tsx +7 -1
- package/src/app/[variants]/mobileRouter.config.tsx +7 -1
- package/src/auth.ts +2 -0
- package/src/locales/default/auth.ts +17 -9
- package/src/server/routers/lambda/user.ts +18 -0
- package/src/services/user/index.ts +4 -0
- package/src/store/user/slices/auth/action.test.ts +2 -2
- package/src/store/user/slices/auth/action.ts +8 -8
- package/src/store/user/slices/auth/initialState.ts +1 -1
- package/src/store/user/slices/auth/selectors.ts +1 -1
- package/src/store/user/slices/common/action.ts +6 -0
|
@@ -406,6 +406,20 @@
|
|
|
406
406
|
"when": 1764734167674,
|
|
407
407
|
"tag": "0057_add_topic_user_memory_extract_status",
|
|
408
408
|
"breakpoints": true
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
"idx": 58,
|
|
412
|
+
"version": "7",
|
|
413
|
+
"when": 1764842015809,
|
|
414
|
+
"tag": "0058_add_source_into_user_plugins",
|
|
415
|
+
"breakpoints": true
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
"idx": 59,
|
|
419
|
+
"version": "7",
|
|
420
|
+
"when": 1764858574403,
|
|
421
|
+
"tag": "0059_add_normalized_email_indexes",
|
|
422
|
+
"breakpoints": true
|
|
409
423
|
}
|
|
410
424
|
],
|
|
411
425
|
"version": "6"
|
|
@@ -223,10 +223,7 @@
|
|
|
223
223
|
"hash": "9646161fa041354714f823d726af27247bcd6e60fa3be5698c0d69f337a5700b"
|
|
224
224
|
},
|
|
225
225
|
{
|
|
226
|
-
"sql": [
|
|
227
|
-
"DROP TABLE \"user_budgets\";",
|
|
228
|
-
"\nDROP TABLE \"user_subscriptions\";"
|
|
229
|
-
],
|
|
226
|
+
"sql": ["DROP TABLE \"user_budgets\";", "\nDROP TABLE \"user_subscriptions\";"],
|
|
230
227
|
"bps": true,
|
|
231
228
|
"folderMillis": 1729699958471,
|
|
232
229
|
"hash": "7dad43a2a25d1aec82124a4e53f8d82f8505c3073f23606c1dc5d2a4598eacf9"
|
|
@@ -298,9 +295,7 @@
|
|
|
298
295
|
"hash": "845a692ceabbfc3caf252a97d3e19a213bc0c433df2689900135f9cfded2cf49"
|
|
299
296
|
},
|
|
300
297
|
{
|
|
301
|
-
"sql": [
|
|
302
|
-
"ALTER TABLE \"messages\" ADD COLUMN \"reasoning\" jsonb;"
|
|
303
|
-
],
|
|
298
|
+
"sql": ["ALTER TABLE \"messages\" ADD COLUMN \"reasoning\" jsonb;"],
|
|
304
299
|
"bps": true,
|
|
305
300
|
"folderMillis": 1737609172353,
|
|
306
301
|
"hash": "2cb36ae4fcdd7b7064767e04bfbb36ae34518ff4bb1b39006f2dd394d1893868"
|
|
@@ -515,9 +510,7 @@
|
|
|
515
510
|
"hash": "a7ccf007fd185ff922823148d1eae6fafe652fc98d2fd2793f84a84f29e93cd1"
|
|
516
511
|
},
|
|
517
512
|
{
|
|
518
|
-
"sql": [
|
|
519
|
-
"ALTER TABLE \"ai_providers\" ADD COLUMN \"config\" jsonb;"
|
|
520
|
-
],
|
|
513
|
+
"sql": ["ALTER TABLE \"ai_providers\" ADD COLUMN \"config\" jsonb;"],
|
|
521
514
|
"bps": true,
|
|
522
515
|
"folderMillis": 1749309388370,
|
|
523
516
|
"hash": "39cea379f08ee4cb944875c0b67f7791387b508c2d47958bb4cd501ed1ef33eb"
|
|
@@ -635,9 +628,7 @@
|
|
|
635
628
|
"hash": "1ba9b1f74ea13348da98d6fcdad7867ab4316ed565bf75d84d160c526cdac14b"
|
|
636
629
|
},
|
|
637
630
|
{
|
|
638
|
-
"sql": [
|
|
639
|
-
"ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"virtual\" boolean DEFAULT false;"
|
|
640
|
-
],
|
|
631
|
+
"sql": ["ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"virtual\" boolean DEFAULT false;"],
|
|
641
632
|
"bps": true,
|
|
642
633
|
"folderMillis": 1759116400580,
|
|
643
634
|
"hash": "433ddae88e785f2db734e49a4c115eee93e60afe389f7919d66e5ba9aa159a37"
|
|
@@ -687,17 +678,13 @@
|
|
|
687
678
|
"hash": "4bdc6505797d7a33b622498c138cfd47f637239f6905e1c484cd01d9d5f21d6b"
|
|
688
679
|
},
|
|
689
680
|
{
|
|
690
|
-
"sql": [
|
|
691
|
-
"ALTER TABLE \"user_settings\" ADD COLUMN IF NOT EXISTS \"image\" jsonb;"
|
|
692
|
-
],
|
|
681
|
+
"sql": ["ALTER TABLE \"user_settings\" ADD COLUMN IF NOT EXISTS \"image\" jsonb;"],
|
|
693
682
|
"bps": true,
|
|
694
683
|
"folderMillis": 1760108430562,
|
|
695
684
|
"hash": "ce09b301abb80f6563abc2f526bdd20b4f69bae430f09ba2179b9e3bfec43067"
|
|
696
685
|
},
|
|
697
686
|
{
|
|
698
|
-
"sql": [
|
|
699
|
-
"ALTER TABLE \"documents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"
|
|
700
|
-
],
|
|
687
|
+
"sql": ["ALTER TABLE \"documents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"],
|
|
701
688
|
"bps": true,
|
|
702
689
|
"folderMillis": 1761554153406,
|
|
703
690
|
"hash": "bf2f21293e90e11cf60a784cf3ec219eafa95f7545d7d2f9d1449c0b0949599a"
|
|
@@ -777,17 +764,13 @@
|
|
|
777
764
|
"hash": "923ccbdf46c32be9a981dabd348e6923b4a365444241e9b8cc174bf5b914cbc5"
|
|
778
765
|
},
|
|
779
766
|
{
|
|
780
|
-
"sql": [
|
|
781
|
-
"ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"market_identifier\" text;\n"
|
|
782
|
-
],
|
|
767
|
+
"sql": ["ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"market_identifier\" text;\n"],
|
|
783
768
|
"bps": true,
|
|
784
769
|
"folderMillis": 1762870034882,
|
|
785
770
|
"hash": "4178aacb4b8892b7fd15d29209bbf9b1d1f9d7c406ba796f27542c0bcd919680"
|
|
786
771
|
},
|
|
787
772
|
{
|
|
788
|
-
"sql": [
|
|
789
|
-
"ALTER TABLE \"message_plugins\" ADD COLUMN IF NOT EXISTS \"intervention\" jsonb;\n"
|
|
790
|
-
],
|
|
773
|
+
"sql": ["ALTER TABLE \"message_plugins\" ADD COLUMN IF NOT EXISTS \"intervention\" jsonb;\n"],
|
|
791
774
|
"bps": true,
|
|
792
775
|
"folderMillis": 1762911968658,
|
|
793
776
|
"hash": "552a032cc0e595277232e70b5f9338658585bafe9481ae8346a5f322b673a68b"
|
|
@@ -816,9 +799,7 @@
|
|
|
816
799
|
"hash": "f823b521f4d25e5dc5ab238b372727d2d2d7f0aed27b5eabc8a9608ce4e50568"
|
|
817
800
|
},
|
|
818
801
|
{
|
|
819
|
-
"sql": [
|
|
820
|
-
"ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"
|
|
821
|
-
],
|
|
802
|
+
"sql": ["ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"],
|
|
822
803
|
"bps": true,
|
|
823
804
|
"folderMillis": 1764215503726,
|
|
824
805
|
"hash": "4188893a9083b3c7baebdbad0dd3f9d9400ede7584ca2394f5c64305dc9ec7b0"
|
|
@@ -859,9 +840,7 @@
|
|
|
859
840
|
"hash": "2c103eee82bdf329944fb622dd9c2b9f20df80eb54f23eb9254d2285de413099"
|
|
860
841
|
},
|
|
861
842
|
{
|
|
862
|
-
"sql": [
|
|
863
|
-
"ALTER TABLE \"user_settings\" ADD COLUMN IF NOT EXISTS \"market\" jsonb;"
|
|
864
|
-
],
|
|
843
|
+
"sql": ["ALTER TABLE \"user_settings\" ADD COLUMN IF NOT EXISTS \"market\" jsonb;"],
|
|
865
844
|
"bps": true,
|
|
866
845
|
"folderMillis": 1764335703306,
|
|
867
846
|
"hash": "28c0d738c0b1fdf5fd871363be1a1477b4accbabdc140fe8dc6e9b339aae2c89"
|
|
@@ -939,5 +918,25 @@
|
|
|
939
918
|
"bps": true,
|
|
940
919
|
"folderMillis": 1764734167674,
|
|
941
920
|
"hash": "89c134be2948d3afc360d6bac11dea0c6fd5c902bf6093ed077033adb920fd02"
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
"sql": [
|
|
924
|
+
"ALTER TABLE \"user_installed_plugins\" ADD COLUMN IF NOT EXISTS \"source\" varchar(255);"
|
|
925
|
+
],
|
|
926
|
+
"bps": true,
|
|
927
|
+
"folderMillis": 1764842015809,
|
|
928
|
+
"hash": "276514dc101fffc66794156f4ba644599a4ae5997a68b9e7f54452ee374bfa61"
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
"sql": [
|
|
932
|
+
"ALTER TABLE \"users\" ADD COLUMN IF NOT EXISTS \"normalized_email\" text;",
|
|
933
|
+
"\nCREATE INDEX IF NOT EXISTS \"users_email_idx\" ON \"users\" USING btree (\"email\");",
|
|
934
|
+
"\nCREATE INDEX IF NOT EXISTS \"users_username_idx\" ON \"users\" USING btree (\"username\");",
|
|
935
|
+
"\nCREATE UNIQUE INDEX IF NOT EXISTS \"users_normalized_email_unique_idx\" ON \"users\" USING btree (\"normalized_email\");",
|
|
936
|
+
"\n"
|
|
937
|
+
],
|
|
938
|
+
"bps": true,
|
|
939
|
+
"folderMillis": 1764858574403,
|
|
940
|
+
"hash": "7838f9938b370867470e5e11807855253d23b11c2ac6aa9e90687844a356c949"
|
|
942
941
|
}
|
|
943
|
-
]
|
|
942
|
+
]
|
|
@@ -199,7 +199,7 @@ export class UserModel {
|
|
|
199
199
|
* Normalize unique user fields so empty strings become null, keeping unique constraints safe.
|
|
200
200
|
*/
|
|
201
201
|
private static normalizeUniqueUserFields = <
|
|
202
|
-
T extends { email?: string | null; phone?: string | null },
|
|
202
|
+
T extends { email?: string | null; phone?: string | null; username?: string | null },
|
|
203
203
|
>(
|
|
204
204
|
value: T,
|
|
205
205
|
) => {
|
|
@@ -207,11 +207,16 @@ export class UserModel {
|
|
|
207
207
|
typeof value.email === 'string' && value.email.trim() === '' ? null : value.email;
|
|
208
208
|
const normalizedPhone =
|
|
209
209
|
typeof value.phone === 'string' && value.phone.trim() === '' ? null : value.phone;
|
|
210
|
+
const normalizedUsername =
|
|
211
|
+
typeof value.username === 'string' && value.username.trim() === ''
|
|
212
|
+
? null
|
|
213
|
+
: value.username?.trim();
|
|
210
214
|
|
|
211
215
|
return {
|
|
212
216
|
...value,
|
|
213
217
|
...(value.email !== undefined ? { email: normalizedEmail } : {}),
|
|
214
218
|
...(value.phone !== undefined ? { phone: normalizedPhone } : {}),
|
|
219
|
+
...(value.username !== undefined ? { username: normalizedUsername } : {}),
|
|
215
220
|
};
|
|
216
221
|
};
|
|
217
222
|
|
|
@@ -241,6 +246,13 @@ export class UserModel {
|
|
|
241
246
|
return db.query.users.findFirst({ where: eq(users.id, id) });
|
|
242
247
|
};
|
|
243
248
|
|
|
249
|
+
static findByUsername = async (db: LobeChatDatabase, username: string) => {
|
|
250
|
+
const normalizedUsername = username.trim();
|
|
251
|
+
if (!normalizedUsername) return null;
|
|
252
|
+
|
|
253
|
+
return db.query.users.findFirst({ where: eq(users.username, normalizedUsername) });
|
|
254
|
+
};
|
|
255
|
+
|
|
244
256
|
static findByEmail = async (db: LobeChatDatabase, email: string) => {
|
|
245
257
|
return db.query.users.findFirst({ where: eq(users.email, email) });
|
|
246
258
|
};
|
|
@@ -2,46 +2,54 @@
|
|
|
2
2
|
import { DEFAULT_PREFERENCE } from '@lobechat/const';
|
|
3
3
|
import type { CustomPluginParams } from '@lobechat/types';
|
|
4
4
|
import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
|
5
|
-
import { boolean, jsonb, pgTable, primaryKey, text } from 'drizzle-orm/pg-core';
|
|
5
|
+
import { boolean, index, jsonb, pgTable, primaryKey, text } from 'drizzle-orm/pg-core';
|
|
6
6
|
|
|
7
|
-
import { timestamps, timestamptz } from './_helpers';
|
|
7
|
+
import { timestamps, timestamptz, varchar255 } from './_helpers';
|
|
8
8
|
|
|
9
|
-
export const users = pgTable(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
export const users = pgTable(
|
|
10
|
+
'users',
|
|
11
|
+
{
|
|
12
|
+
id: text('id').primaryKey().notNull(),
|
|
13
|
+
username: text('username').unique(),
|
|
14
|
+
email: text('email').unique(),
|
|
15
|
+
normalizedEmail: text('normalized_email').unique(),
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
avatar: text('avatar'),
|
|
18
|
+
phone: text('phone').unique(),
|
|
19
|
+
firstName: text('first_name'),
|
|
20
|
+
lastName: text('last_name'),
|
|
21
|
+
fullName: text('full_name'),
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
isOnboarded: boolean('is_onboarded').default(false),
|
|
24
|
+
// Time user was created in Clerk
|
|
25
|
+
clerkCreatedAt: timestamptz('clerk_created_at'),
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
// Required by better-auth
|
|
28
|
+
emailVerified: boolean('email_verified').default(false).notNull(),
|
|
29
|
+
// Required by nextauth, all null allowed
|
|
30
|
+
emailVerifiedAt: timestamptz('email_verified_at'),
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
preference: jsonb('preference').$defaultFn(() => DEFAULT_PREFERENCE),
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// better-auth admin
|
|
35
|
+
role: text('role'),
|
|
36
|
+
banned: boolean('banned').default(false),
|
|
37
|
+
banReason: text('ban_reason'),
|
|
38
|
+
banExpires: timestamptz('ban_expires'),
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
// better-auth two-factor
|
|
41
|
+
twoFactorEnabled: boolean('two_factor_enabled').default(false),
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
// better-auth phone number
|
|
44
|
+
phoneNumberVerified: boolean('phone_number_verified'),
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
}
|
|
46
|
+
...timestamps,
|
|
47
|
+
},
|
|
48
|
+
(table) => ({
|
|
49
|
+
emailIdx: index('users_email_idx').on(table.email),
|
|
50
|
+
usernameIdx: index('users_username_idx').on(table.username),
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
45
53
|
|
|
46
54
|
export type NewUser = typeof users.$inferInsert;
|
|
47
55
|
export type UserItem = typeof users.$inferSelect;
|
|
@@ -76,7 +84,7 @@ export const userInstalledPlugins = pgTable(
|
|
|
76
84
|
manifest: jsonb('manifest').$type<LobeChatPluginManifest>(),
|
|
77
85
|
settings: jsonb('settings'),
|
|
78
86
|
customParams: jsonb('custom_params').$type<CustomPluginParams>(),
|
|
79
|
-
|
|
87
|
+
source: varchar255('source'),
|
|
80
88
|
...timestamps,
|
|
81
89
|
},
|
|
82
90
|
(self) => ({
|
|
@@ -6,10 +6,8 @@ import { users } from '@/database/schemas/user';
|
|
|
6
6
|
import { serverDB } from '@/database/server';
|
|
7
7
|
|
|
8
8
|
export interface CheckUserResponseData {
|
|
9
|
-
emailVerified?: boolean;
|
|
10
9
|
exists: boolean;
|
|
11
10
|
hasPassword?: boolean;
|
|
12
|
-
providers?: string[];
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
/**
|
|
@@ -47,18 +45,14 @@ export async function POST(req: NextRequest) {
|
|
|
47
45
|
})
|
|
48
46
|
.from(account)
|
|
49
47
|
.where(and(eq(account.userId, user.id)));
|
|
50
|
-
|
|
51
|
-
const providers = Array.from(new Set(accounts.map((a) => a.providerId).filter(Boolean)));
|
|
52
48
|
const hasPassword = accounts.some(
|
|
53
49
|
(a) =>
|
|
54
50
|
a.providerId === 'credential' && typeof a.password === 'string' && a.password.length > 0,
|
|
55
51
|
);
|
|
56
52
|
|
|
57
53
|
return NextResponse.json({
|
|
58
|
-
emailVerified: user.emailVerified,
|
|
59
54
|
exists: true,
|
|
60
55
|
hasPassword,
|
|
61
|
-
providers,
|
|
62
56
|
} satisfies CheckUserResponseData);
|
|
63
57
|
} catch (error) {
|
|
64
58
|
console.error('Error checking user existence:', error);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
|
|
4
|
+
import { users } from '@/database/schemas/user';
|
|
5
|
+
import { serverDB } from '@/database/server';
|
|
6
|
+
|
|
7
|
+
export interface ResolveUsernameResponseData {
|
|
8
|
+
email?: string | null;
|
|
9
|
+
exists: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a username to the associated email address.
|
|
14
|
+
* @param req - POST request with { username: string }
|
|
15
|
+
* @returns { exists: boolean, email?: string | null }
|
|
16
|
+
*/
|
|
17
|
+
export async function POST(req: NextRequest) {
|
|
18
|
+
try {
|
|
19
|
+
const body = await req.json();
|
|
20
|
+
const { username } = body;
|
|
21
|
+
|
|
22
|
+
if (!username || typeof username !== 'string') {
|
|
23
|
+
return NextResponse.json({ error: 'Username is required', exists: false }, { status: 400 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const normalizedUsername = username.trim();
|
|
27
|
+
|
|
28
|
+
if (!normalizedUsername) {
|
|
29
|
+
return NextResponse.json({ error: 'Username is required', exists: false }, { status: 400 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const [user] = await serverDB
|
|
33
|
+
.select({ email: users.email })
|
|
34
|
+
.from(users)
|
|
35
|
+
.where(eq(users.username, normalizedUsername))
|
|
36
|
+
.limit(1);
|
|
37
|
+
|
|
38
|
+
if (!user || !user.email) {
|
|
39
|
+
return NextResponse.json({ exists: false } satisfies ResolveUsernameResponseData);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return NextResponse.json({
|
|
43
|
+
email: user.email,
|
|
44
|
+
exists: true,
|
|
45
|
+
} satisfies ResolveUsernameResponseData);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Error resolving username to email:', error);
|
|
48
|
+
return NextResponse.json({ error: 'Internal server error', exists: false }, { status: 500 });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const runtime = 'nodejs';
|
|
@@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
11
11
|
import { Flexbox } from 'react-layout-kit';
|
|
12
12
|
|
|
13
13
|
import type { CheckUserResponseData } from '@/app/(backend)/api/auth/check-user/route';
|
|
14
|
+
import type { ResolveUsernameResponseData } from '@/app/(backend)/api/auth/resolve-username/route';
|
|
14
15
|
import { message } from '@/components/AntdStaticMethods';
|
|
15
16
|
import AuthIcons from '@/components/NextAuth/AuthIcons';
|
|
16
17
|
import { getAuthConfig } from '@/envs/auth';
|
|
@@ -18,7 +19,7 @@ import { requestPasswordReset, signIn } from '@/libs/better-auth/auth-client';
|
|
|
18
19
|
import { isBuiltinProvider, normalizeProviderId } from '@/libs/better-auth/utils/client';
|
|
19
20
|
import { useServerConfigStore } from '@/store/serverConfig';
|
|
20
21
|
|
|
21
|
-
const useStyles = createStyles(({ css, token }) => ({
|
|
22
|
+
const useStyles = createStyles(({ css, token, responsive }) => ({
|
|
22
23
|
backButton: css`
|
|
23
24
|
cursor: pointer;
|
|
24
25
|
font-size: 14px;
|
|
@@ -29,10 +30,13 @@ const useStyles = createStyles(({ css, token }) => ({
|
|
|
29
30
|
}
|
|
30
31
|
`,
|
|
31
32
|
card: css`
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
32
35
|
padding-block: 2.5rem;
|
|
33
36
|
padding-inline: 2rem;
|
|
34
37
|
`,
|
|
35
38
|
container: css`
|
|
39
|
+
overflow: hidden;
|
|
36
40
|
width: 360px;
|
|
37
41
|
border: 1px solid ${token.colorBorder};
|
|
38
42
|
border-radius: ${token.borderRadiusLG}px;
|
|
@@ -42,6 +46,11 @@ const useStyles = createStyles(({ css, token }) => ({
|
|
|
42
46
|
height: 1px;
|
|
43
47
|
background: ${token.colorBorder};
|
|
44
48
|
`,
|
|
49
|
+
dividerRow: css`
|
|
50
|
+
${responsive.mobile} {
|
|
51
|
+
order: -1;
|
|
52
|
+
}
|
|
53
|
+
`,
|
|
45
54
|
dividerText: css`
|
|
46
55
|
font-size: 14px;
|
|
47
56
|
color: ${token.colorTextSecondary};
|
|
@@ -51,6 +60,13 @@ const useStyles = createStyles(({ css, token }) => ({
|
|
|
51
60
|
color: ${token.colorTextSecondary};
|
|
52
61
|
text-align: center;
|
|
53
62
|
`,
|
|
63
|
+
emailForm: css`
|
|
64
|
+
margin-block-start: 0.5rem;
|
|
65
|
+
|
|
66
|
+
${responsive.mobile} {
|
|
67
|
+
margin-block: 0;
|
|
68
|
+
}
|
|
69
|
+
`,
|
|
54
70
|
footer: css`
|
|
55
71
|
padding: 1rem;
|
|
56
72
|
border-block-start: 1px solid ${token.colorBorder};
|
|
@@ -61,6 +77,12 @@ const useStyles = createStyles(({ css, token }) => ({
|
|
|
61
77
|
|
|
62
78
|
background: ${token.colorBgElevated};
|
|
63
79
|
`,
|
|
80
|
+
socialSection: css`
|
|
81
|
+
${responsive.mobile} {
|
|
82
|
+
order: 1;
|
|
83
|
+
margin-block-start: 1rem;
|
|
84
|
+
}
|
|
85
|
+
`,
|
|
64
86
|
subtitle: css`
|
|
65
87
|
margin-block-start: 0.5rem;
|
|
66
88
|
font-size: 14px;
|
|
@@ -77,13 +99,22 @@ const useStyles = createStyles(({ css, token }) => ({
|
|
|
77
99
|
`,
|
|
78
100
|
}));
|
|
79
101
|
|
|
102
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
103
|
+
const USERNAME_REGEX = /^\w+$/;
|
|
104
|
+
|
|
80
105
|
type Step = 'email' | 'password';
|
|
106
|
+
type IdentifierType = 'email' | 'username';
|
|
81
107
|
|
|
82
108
|
interface SignInFormValues {
|
|
83
109
|
email: string;
|
|
84
110
|
password: string;
|
|
85
111
|
}
|
|
86
112
|
|
|
113
|
+
interface ResolvedEmailResult {
|
|
114
|
+
email: string;
|
|
115
|
+
identifierType: IdentifierType;
|
|
116
|
+
}
|
|
117
|
+
|
|
87
118
|
export default function SignInPage() {
|
|
88
119
|
const { styles } = useStyles();
|
|
89
120
|
const theme = useTheme();
|
|
@@ -154,12 +185,56 @@ export default function SignInPage() {
|
|
|
154
185
|
}
|
|
155
186
|
};
|
|
156
187
|
|
|
188
|
+
const resolveEmailFromIdentifier = async (
|
|
189
|
+
identifier: string,
|
|
190
|
+
): Promise<ResolvedEmailResult | null> => {
|
|
191
|
+
const trimmedIdentifier = identifier.trim();
|
|
192
|
+
|
|
193
|
+
if (!trimmedIdentifier) return null;
|
|
194
|
+
|
|
195
|
+
const isEmailIdentifier = EMAIL_REGEX.test(trimmedIdentifier);
|
|
196
|
+
|
|
197
|
+
if (isEmailIdentifier) {
|
|
198
|
+
return { email: trimmedIdentifier.toLowerCase(), identifierType: 'email' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!USERNAME_REGEX.test(trimmedIdentifier)) {
|
|
202
|
+
message.error(t('betterAuth.errors.emailInvalid'));
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const response = await fetch('/api/auth/resolve-username', {
|
|
208
|
+
body: JSON.stringify({ username: trimmedIdentifier }),
|
|
209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
210
|
+
method: 'POST',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const data: ResolveUsernameResponseData = await response.json();
|
|
214
|
+
|
|
215
|
+
if (!response.ok || !data.exists || !data.email) {
|
|
216
|
+
message.error(t('betterAuth.errors.usernameNotRegistered'));
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { email: data.email, identifierType: 'username' };
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error('Error resolving username:', error);
|
|
223
|
+
message.error(t('betterAuth.signin.error'));
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
157
228
|
// Check if user exists
|
|
158
229
|
const handleCheckUser = async (values: Pick<SignInFormValues, 'email'>) => {
|
|
159
230
|
setLoading(true);
|
|
160
231
|
try {
|
|
232
|
+
const resolvedEmail = await resolveEmailFromIdentifier(values.email);
|
|
233
|
+
if (!resolvedEmail) return;
|
|
234
|
+
const { email: targetEmail, identifierType } = resolvedEmail;
|
|
235
|
+
|
|
161
236
|
const response = await fetch('/api/auth/check-user', {
|
|
162
|
-
body: JSON.stringify({ email:
|
|
237
|
+
body: JSON.stringify({ email: targetEmail }),
|
|
163
238
|
headers: { 'Content-Type': 'application/json' },
|
|
164
239
|
method: 'POST',
|
|
165
240
|
});
|
|
@@ -167,14 +242,18 @@ export default function SignInPage() {
|
|
|
167
242
|
const data: CheckUserResponseData = await response.json();
|
|
168
243
|
|
|
169
244
|
if (!data.exists) {
|
|
170
|
-
|
|
245
|
+
if (identifierType === 'username') {
|
|
246
|
+
message.error(t('betterAuth.errors.usernameNotRegistered'));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
171
250
|
const callbackUrl = searchParams.get('callbackUrl') || '/';
|
|
172
251
|
router.push(
|
|
173
|
-
`/signup?email=${encodeURIComponent(
|
|
252
|
+
`/signup?email=${encodeURIComponent(targetEmail)}&callbackUrl=${encodeURIComponent(callbackUrl)}`,
|
|
174
253
|
);
|
|
175
254
|
return;
|
|
176
255
|
}
|
|
177
|
-
setEmail(
|
|
256
|
+
setEmail(targetEmail);
|
|
178
257
|
|
|
179
258
|
if (data.hasPassword) {
|
|
180
259
|
setStep('password');
|
|
@@ -182,7 +261,7 @@ export default function SignInPage() {
|
|
|
182
261
|
}
|
|
183
262
|
|
|
184
263
|
if (enableMagicLink) {
|
|
185
|
-
await handleSendMagicLink(
|
|
264
|
+
await handleSendMagicLink(targetEmail);
|
|
186
265
|
return;
|
|
187
266
|
}
|
|
188
267
|
|
|
@@ -302,13 +381,11 @@ export default function SignInPage() {
|
|
|
302
381
|
|
|
303
382
|
{step === 'email' && (
|
|
304
383
|
<>
|
|
305
|
-
<p className={styles.subtitle}>{t('betterAuth.signin.emailStep.subtitle')}</p>
|
|
306
|
-
|
|
307
384
|
{/* Social Login Section Skeleton */}
|
|
308
385
|
{!serverConfigInit && (
|
|
309
|
-
<Flexbox
|
|
386
|
+
<Flexbox className={styles.socialSection} gap={12}>
|
|
310
387
|
<Skeleton.Button active block size="large" />
|
|
311
|
-
<Flexbox align="center" gap={12} horizontal>
|
|
388
|
+
<Flexbox align="center" className={styles.dividerRow} gap={12} horizontal>
|
|
312
389
|
<div className={styles.divider} />
|
|
313
390
|
<Skeleton.Input active size="small" style={{ minWidth: 80, width: 80 }} />
|
|
314
391
|
<div className={styles.divider} />
|
|
@@ -318,7 +395,7 @@ export default function SignInPage() {
|
|
|
318
395
|
|
|
319
396
|
{/* Social Login Section */}
|
|
320
397
|
{serverConfigInit && oAuthSSOProviders.length > 0 && (
|
|
321
|
-
<Flexbox
|
|
398
|
+
<Flexbox className={styles.socialSection} gap={12}>
|
|
322
399
|
{oAuthSSOProviders.map((provider) => (
|
|
323
400
|
<Button
|
|
324
401
|
block
|
|
@@ -333,7 +410,7 @@ export default function SignInPage() {
|
|
|
333
410
|
))}
|
|
334
411
|
|
|
335
412
|
{/* Divider */}
|
|
336
|
-
<Flexbox align="center" gap={12} horizontal>
|
|
413
|
+
<Flexbox align="center" className={styles.dividerRow} gap={12} horizontal>
|
|
337
414
|
<div className={styles.divider} />
|
|
338
415
|
<span className={styles.dividerText}>
|
|
339
416
|
{t('betterAuth.signin.orContinueWith')}
|
|
@@ -344,16 +421,27 @@ export default function SignInPage() {
|
|
|
344
421
|
)}
|
|
345
422
|
|
|
346
423
|
<Form
|
|
424
|
+
className={styles.emailForm}
|
|
347
425
|
form={form}
|
|
348
426
|
layout="vertical"
|
|
349
427
|
onFinish={handleCheckUser}
|
|
350
|
-
style={{ marginTop: '0.5rem' }}
|
|
351
428
|
>
|
|
352
429
|
<Form.Item
|
|
353
430
|
name="email"
|
|
354
431
|
rules={[
|
|
355
432
|
{ message: t('betterAuth.errors.emailRequired'), required: true },
|
|
356
|
-
{
|
|
433
|
+
{
|
|
434
|
+
validator: (_, value) => {
|
|
435
|
+
if (!value) return Promise.resolve();
|
|
436
|
+
|
|
437
|
+
const trimmedValue = (value as string).trim();
|
|
438
|
+
if (EMAIL_REGEX.test(trimmedValue) || USERNAME_REGEX.test(trimmedValue)) {
|
|
439
|
+
return Promise.resolve();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return Promise.reject(new Error(t('betterAuth.errors.emailInvalid')));
|
|
443
|
+
},
|
|
444
|
+
},
|
|
357
445
|
]}
|
|
358
446
|
style={{ marginBottom: 0 }}
|
|
359
447
|
>
|
|
@@ -88,6 +88,21 @@ export default function BetterAuthSignUpForm() {
|
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
if (error) {
|
|
91
|
+
const isEmailDuplicate =
|
|
92
|
+
error.code === 'FAILED_TO_CREATE_USER' &&
|
|
93
|
+
// Postgres unique constraint violation
|
|
94
|
+
(error as any)?.details?.cause?.code === '23505';
|
|
95
|
+
|
|
96
|
+
if (isEmailDuplicate) {
|
|
97
|
+
message.error(t('betterAuth.errors.emailExists'));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (error.code === 'INVALID_EMAIL') {
|
|
102
|
+
message.error(t('betterAuth.errors.emailInvalid'));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
91
106
|
message.error(error.message || t('betterAuth.signup.error'));
|
|
92
107
|
return;
|
|
93
108
|
}
|