Skip to content

Commit 6e5947b

Browse files
committed
feat: add email binding and update local dev config
1 parent d80931c commit 6e5947b

File tree

6 files changed

+148
-27
lines changed

6 files changed

+148
-27
lines changed

desktop/electron/main.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function createWindow() {
7878
})
7979

8080
if (isDev) {
81-
const devBase = process.env.VITE_DEV_SERVER_URL || "http://localhost:5173"
81+
const devBase = process.env.VITE_DEV_SERVER_URL || "http://localhost:11180"
8282
win.loadURL(desktopEntryUrl(devBase))
8383
win.webContents.openDevTools({ mode: "detach" })
8484
} else if (process.env.MONKEYCODE_LOAD_LOCAL_DIST === "1") {

desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"postinstall": "node scripts/postinstall.cjs",
1111
"electron:build:dist": "cross-env ELECTRON=true pnpm --dir ../frontend run build",
1212
"electron:sync-web": "node scripts/sync-web-dist.mjs",
13-
"electron:dev": "concurrently -k \"pnpm --dir ../frontend dev\" \"wait-on http://localhost:5173 && electron .\"",
13+
"electron:dev": "concurrently -k \"pnpm --dir ../frontend dev\" \"wait-on http://localhost:11180 && electron .\"",
1414
"electron:pack": "electron-builder --mac --publish never",
1515
"electron:pack:dir": "electron-builder --mac dir --publish never",
1616
"electron:pack:with-dist": "pnpm electron:build:dist && pnpm electron:sync-web && electron-builder --mac -c electron-builder.full.json --publish never",

frontend/doc.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ MonkeyCode 是面向研发团队的企业级 AI 开发平台,覆盖 **需求
4040
- **默认**:点击进入新任务页(`/console/tasks`);旁的「+」为 **创建任务**(与进入同一页后创建任务等价)。展开后列出未归属项目的任务;每条任务悬停可出现 **更多** 菜单,支持 **删除任务**
4141
- **具体项目**:点击进入该项目概览(`/console/project/:projectId`);项目行右侧「+」为 **启动任务**(基于该项目仓库创建 AI 任务,未绑定仓库时该按钮不可用)。展开后列出该项目下任务,同样支持 **删除任务**
4242
- **配置**:侧栏底部点击「配置」打开设置弹窗,可管理 Git 身份、AI 大模型、系统镜像、宿主机、开发环境、通知等。
43-
- **用户区**:侧栏最底部为当前账号信息。点击展开菜单可 **修改昵称****修改密码****登出**;点击头像区域可 **修改头像**(图片 ≤5MB)。
43+
- **用户区**:侧栏最底部为当前账号信息。点击展开菜单可 **修改昵称****绑定邮箱**(已绑定邮箱时按钮禁用)、**修改密码/设置密码****登出**;点击头像区域可 **修改头像**(图片 ≤5MB)。若账号已有密码,修改密码时需要输入原密码;若账号此前未设置密码,则可直接设置新密码
4444

4545
**顶栏(个人控制台)**
4646

frontend/src/api/Api.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ export interface DomainCheckModelResp {
335335
export interface DomainCollaborator {
336336
avatar_url?: string;
337337
email?: string;
338+
has_password?: boolean;
338339
id?: string;
339340
/** 用户绑定的身份列表,例如 github, gitlab */
340341
identities?: DomainUserIdentity[];
@@ -1044,6 +1045,11 @@ export interface DomainResource {
10441045
memory?: number;
10451046
}
10461047

1048+
export interface DomainSendBindEmailVerificationReq {
1049+
/** 要绑定的邮箱地址 */
1050+
email: string;
1051+
}
1052+
10471053
export interface DomainShareGitBotReq {
10481054
/** git bot ID */
10491055
id?: string;
@@ -1161,10 +1167,6 @@ export interface DomainSpeechRecognitionEvent {
11611167
export interface DomainStats {
11621168
/** @example 5672 */
11631169
repo_stars?: number;
1164-
/** @example 2847392 */
1165-
tasks_count?: number;
1166-
/** @example 18429 */
1167-
users_count?: number;
11681170
}
11691171

11701172
export interface DomainTask {
@@ -1477,6 +1479,7 @@ export interface DomainUpdateVMReq {
14771479
export interface DomainUser {
14781480
avatar_url?: string;
14791481
email?: string;
1482+
has_password?: boolean;
14801483
id?: string;
14811484
/** 用户绑定的身份列表,例如 github, gitlab */
14821485
identities?: DomainUserIdentity[];
@@ -3304,6 +3307,26 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
33043307
...params,
33053308
}),
33063309

3310+
/**
3311+
* @description 用户已登录状态下请求绑定邮箱,系统发送验证邮件
3312+
*
3313+
* @tags 【用户】邮箱绑定
3314+
* @name V1UsersEmailBindRequestUpdate
3315+
* @summary 发送邮箱绑定验证邮件
3316+
* @request PUT:/api/v1/users/email/bind-request
3317+
* @secure
3318+
*/
3319+
v1UsersEmailBindRequestUpdate: (req: DomainSendBindEmailVerificationReq, params: RequestParams = {}) =>
3320+
this.request<WebResp, WebResp>({
3321+
path: `/api/v1/users/email/bind-request`,
3322+
method: "PUT",
3323+
body: req,
3324+
secure: true,
3325+
type: ContentType.Json,
3326+
format: "json",
3327+
...params,
3328+
}),
3329+
33073330
/**
33083331
* @description 删除文件/目录
33093332
*

frontend/src/components/console/nav/nav-user.tsx

Lines changed: 103 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ import { Label } from "@/components/ui/label"
4646
import { Spinner } from "@/components/ui/spinner"
4747
import { Button } from "@/components/ui/button"
4848
import { toast } from "sonner"
49-
import { IconLockCode, IconLogout, IconUserHexagon, IconUpload } from "@tabler/icons-react"
49+
import { IconLockCode, IconLogout, IconMail, IconUserHexagon, IconUpload } from "@tabler/icons-react"
5050
import { useCommonData } from "@/components/console/data-provider"
51+
import { isValidEmail } from "@/utils/common"
5152

5253
export default function NavUser() {
5354
const { isMobile } = useSidebar()
@@ -57,6 +58,9 @@ export default function NavUser() {
5758
const [newPassword, setNewPassword] = React.useState<string>('');
5859
const [confirmPassword, setConfirmPassword] = React.useState<string>('');
5960
const [changingPassword, setChangingPassword] = React.useState<boolean>(false);
61+
const [showBindEmailDialog, setShowBindEmailDialog] = React.useState(false);
62+
const [bindEmail, setBindEmail] = React.useState<string>('');
63+
const [bindingEmail, setBindingEmail] = React.useState<boolean>(false);
6064
const [showChangeNameDialog, setShowChangeNameDialog] = React.useState(false);
6165
const [newName, setNewName] = React.useState<string>('');
6266
const [changingName, setChangingName] = React.useState<boolean>(false);
@@ -66,6 +70,8 @@ export default function NavUser() {
6670
const [changingAvatar, setChangingAvatar] = React.useState<boolean>(false);
6771
const navigate = useNavigate()
6872
const { user, reloadUser } = useCommonData()
73+
const requiresCurrentPassword = !!user?.has_password
74+
const passwordActionLabel = requiresCurrentPassword ? '修改密码' : '设置密码'
6975

7076
const handleLogout = () => {
7177
apiRequest('v1UsersLogoutCreate', {}, [], (resp) => {
@@ -78,23 +84,27 @@ export default function NavUser() {
7884
};
7985

8086
const handleChangePassword = async () => {
87+
if (requiresCurrentPassword && !currentPassword) {
88+
toast.error('请输入当前密码');
89+
return;
90+
}
91+
8192
// 验证新密码和确认密码是否一致
8293
if (newPassword !== confirmPassword) {
8394
toast.error('新密码和确认密码不一致');
8495
return;
8596
}
8697

8798
// 验证密码长度
88-
if (newPassword.length < 6) {
89-
toast.error('新密码长度至少为6位');
99+
if (newPassword.length < 8) {
100+
toast.error('新密码长度至少为8位');
90101
return;
91102
}
92103

93104
setChangingPassword(true);
94105
await apiRequest('v1UsersPasswordsChangeUpdate', {
95-
current_password: currentPassword,
106+
current_password: requiresCurrentPassword ? currentPassword : undefined,
96107
new_password: newPassword,
97-
confirm_password: confirmPassword,
98108
}, [], (resp) => {
99109
if (resp?.code === 0) {
100110
toast.success('密码修改成功');
@@ -109,6 +119,28 @@ export default function NavUser() {
109119
setChangingPassword(false);
110120
};
111121

122+
const handleBindEmail = async () => {
123+
const email = bindEmail.trim();
124+
if (!isValidEmail(email)) {
125+
toast.error('请输入正确的邮箱地址');
126+
return;
127+
}
128+
129+
setBindingEmail(true);
130+
await apiRequest('v1UsersEmailBindRequestUpdate', {
131+
email,
132+
}, [], (resp) => {
133+
if (resp?.code === 0) {
134+
toast.success('绑定邮件已发送,请前往邮箱完成验证');
135+
setShowBindEmailDialog(false);
136+
setBindEmail('');
137+
} else {
138+
toast.error(`绑定邮箱失败:${resp?.message || '未知错误'}`);
139+
}
140+
});
141+
setBindingEmail(false);
142+
};
143+
112144
const handleChangeName = async () => {
113145
// 验证昵称不能为空
114146
if (!newName.trim()) {
@@ -233,9 +265,21 @@ export default function NavUser() {
233265
<IconUserHexagon />
234266
修改昵称
235267
</DropdownMenuItem>
268+
<DropdownMenuItem
269+
disabled={!!user?.email}
270+
onClick={() => {
271+
if (!user?.email) {
272+
setBindEmail('');
273+
setShowBindEmailDialog(true);
274+
}
275+
}}
276+
>
277+
<IconMail />
278+
绑定邮箱
279+
</DropdownMenuItem>
236280
<DropdownMenuItem onClick={() => setShowChangePasswordDialog(true)}>
237281
<IconLockCode />
238-
修改密码
282+
{passwordActionLabel}
239283
</DropdownMenuItem>
240284
<DropdownMenuSeparator />
241285
<DropdownMenuItem onClick={() => setShowLogoutDialog(true)}>
@@ -303,20 +347,22 @@ export default function NavUser() {
303347
<Dialog open={showChangePasswordDialog} onOpenChange={setShowChangePasswordDialog}>
304348
<DialogContent>
305349
<DialogHeader>
306-
<DialogTitle>修改密码</DialogTitle>
350+
<DialogTitle>{passwordActionLabel}</DialogTitle>
307351
</DialogHeader>
308352
<div className="space-y-4">
309-
<div className="space-y-2">
310-
<Label htmlFor="current-password">当前密码</Label>
311-
<Input
312-
id="current-password"
313-
type="password"
314-
placeholder="请输入当前密码"
315-
value={currentPassword}
316-
onChange={(e) => setCurrentPassword(e.target.value)}
317-
autoComplete="current-password"
318-
/>
319-
</div>
353+
{requiresCurrentPassword && (
354+
<div className="space-y-2">
355+
<Label htmlFor="current-password">当前密码</Label>
356+
<Input
357+
id="current-password"
358+
type="password"
359+
placeholder="请输入当前密码"
360+
value={currentPassword}
361+
onChange={(e) => setCurrentPassword(e.target.value)}
362+
autoComplete="current-password"
363+
/>
364+
</div>
365+
)}
320366
<div className="space-y-2">
321367
<Label htmlFor="new-password">新密码</Label>
322368
<Input
@@ -354,14 +400,52 @@ export default function NavUser() {
354400
</Button>
355401
<Button
356402
onClick={handleChangePassword}
357-
disabled={changingPassword || !currentPassword || !newPassword || !confirmPassword}
403+
disabled={changingPassword || (requiresCurrentPassword && !currentPassword) || !newPassword || !confirmPassword}
358404
>
359405
{changingPassword && <Spinner className="size-4 mr-2" />}
360406
确认修改
361407
</Button>
362408
</DialogFooter>
363409
</DialogContent>
364410
</Dialog>
411+
<Dialog open={showBindEmailDialog} onOpenChange={setShowBindEmailDialog}>
412+
<DialogContent>
413+
<DialogHeader>
414+
<DialogTitle>绑定邮箱</DialogTitle>
415+
</DialogHeader>
416+
<div className="space-y-4">
417+
<div className="space-y-2">
418+
<Label htmlFor="bind-email">邮箱</Label>
419+
<Input
420+
id="bind-email"
421+
type="email"
422+
placeholder="请输入要绑定的邮箱"
423+
value={bindEmail}
424+
onChange={(e) => setBindEmail(e.target.value)}
425+
autoComplete="email"
426+
/>
427+
</div>
428+
</div>
429+
<DialogFooter>
430+
<Button
431+
variant="outline"
432+
onClick={() => {
433+
setShowBindEmailDialog(false);
434+
setBindEmail('');
435+
}}
436+
>
437+
取消
438+
</Button>
439+
<Button
440+
onClick={handleBindEmail}
441+
disabled={bindingEmail || !bindEmail.trim()}
442+
>
443+
{bindingEmail && <Spinner className="size-4 mr-2" />}
444+
发送验证邮件
445+
</Button>
446+
</DialogFooter>
447+
</DialogContent>
448+
</Dialog>
365449
<Dialog open={showChangeAvatarDialog} onOpenChange={setShowChangeAvatarDialog}>
366450
<DialogContent className="sm:max-w-md">
367451
<DialogHeader>

frontend/vite.config.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ import { defineConfig, loadEnv } from "vite"
77
export default defineConfig(({ mode }) => {
88
const env = loadEnv(mode, process.cwd(), '')
99
const electronBuild = process.env.ELECTRON === 'true'
10+
const devPort = 11180
11+
const proxyBasicAuthUsername = env.PROXY_BASIC_AUTH_USERNAME?.trim()
12+
const proxyBasicAuthPassword = env.PROXY_BASIC_AUTH_PASSWORD?.trim()
13+
const proxyHeaders: Record<string, string> = {}
14+
15+
if (proxyBasicAuthUsername && proxyBasicAuthPassword) {
16+
proxyHeaders.Authorization = `Basic ${Buffer.from(`${proxyBasicAuthUsername}:${proxyBasicAuthPassword}`).toString('base64')}`
17+
}
1018

1119
return {
1220
base: electronBuild ? './' : '/',
@@ -28,12 +36,18 @@ export default defineConfig(({ mode }) => {
2836
},
2937
},
3038
server: {
39+
port: devPort,
3140
allowedHosts: ['.monkeycode-ai.online'],
3241
proxy: {
3342
'/api': {
3443
target: env.TARGET,
3544
changeOrigin: true,
36-
ws: true
45+
ws: true,
46+
...(Object.keys(proxyHeaders).length > 0
47+
? {
48+
headers: proxyHeaders,
49+
}
50+
: {}),
3751
}
3852
}
3953
}

0 commit comments

Comments
 (0)