Skip to content

Commit 30935a8

Browse files
authored
Merge pull request #602 from chaitin/team-reset-random-password
团队成员重置密码返回随机密码
2 parents 0c9ae4b + 8a48d51 commit 30935a8

12 files changed

Lines changed: 370 additions & 56 deletions

File tree

.github/workflows/offline-package.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ jobs:
6262
ARCH: ${{ matrix.arch }}
6363
DOCKER_VERSION: ${{ inputs.docker_version || '29.0.4' }}
6464
PACKAGE_TGZ: "true"
65+
AWS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
66+
AWS_SECRET_ACCESS_KEY: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
67+
AWS_DEFAULT_REGION: ${{ secrets.OSS_REGION || 'us-east-1' }}
68+
AWS_EC2_METADATA_DISABLED: "true"
69+
AWS_REQUEST_CHECKSUM_CALCULATION: when_required
70+
AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
71+
OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT }}
72+
OSS_ADDRESSING_STYLE: ${{ secrets.OSS_ADDRESSING_STYLE || 'virtual' }}
6573
run: ./scripts/build-offline-package.sh
6674

6775
- name: Upload package to private OSS

backend/biz/team/handler/http/v1/user.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ func NewTeamGroupUserHandler(i *do.Injector) (*TeamGroupUserHandler, error) {
6363
u.POST("/with-password", web.BindHandler(h.AddUserWithPassword), auth.TeamAuth(), adminAuth, audit.Audit("add_team_user_with_password"))
6464
u.POST("", web.BindHandler(h.AddUser), auth.TeamAuth(), adminAuth, audit.Audit("add_team_user"))
6565
u.GET("", web.BindHandler(h.MemberList), auth.TeamAuth(), adminAuth)
66+
u.PUT("/:user_id/passwords/reset", web.BindHandler(h.ResetPassword), auth.TeamAuth(), adminAuth, audit.Audit("reset_team_user_password"))
6667
u.PUT("/:user_id", web.BindHandler(h.UpdateUser), auth.TeamAuth(), adminAuth, audit.Audit("update_team_user"))
6768

6869
g := w.Group("/api/v1/teams/groups")
@@ -255,6 +256,28 @@ func (h *TeamGroupUserHandler) AddAdmin(c *web.Context, req domain.AddTeamAdminR
255256
return c.Success(resp)
256257
}
257258

259+
// ResetPassword 重置团队成员密码
260+
//
261+
// @Summary 重置团队成员密码
262+
// @Description 管理员为团队成员生成新密码,密码只在响应中返回一次
263+
// @Tags 【Team 管理员】分组成员管理
264+
// @Accept json
265+
// @Produce json
266+
// @Security MonkeyCodeAITeamAuth
267+
// @Param user_id path string true "用户ID"
268+
// @Success 200 {object} web.Resp{data=domain.TeamUserPassword} "成功"
269+
// @Failure 401 {object} web.Resp "未授权"
270+
// @Failure 500 {object} web.Resp "服务器内部错误"
271+
// @Router /api/v1/teams/users/{user_id}/passwords/reset [put]
272+
func (h *TeamGroupUserHandler) ResetPassword(c *web.Context, req domain.ResetPasswordReq) error {
273+
teamUser := middleware.GetTeamUser(c)
274+
resp, err := h.usecase.ResetPassword(c.Request().Context(), teamUser, &req)
275+
if err != nil {
276+
return err
277+
}
278+
return c.Success(resp)
279+
}
280+
258281
// MemberList 获取团队成员列表
259282
//
260283
// @Summary 获取团队成员列表

backend/biz/team/repo/user.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,14 @@ func (r *TeamGroupUserRepo) CreateUsersWithPassword(ctx context.Context, teamID
203203
return users, nil
204204
}
205205

206+
func (r *TeamGroupUserRepo) ResetPassword(ctx context.Context, userID uuid.UUID, newPassword string) error {
207+
hashedPassword, err := crypto.HashPassword(newPassword)
208+
if err != nil {
209+
return errcode.ErrPasswordHashFailed
210+
}
211+
return r.db.User.UpdateOneID(userID).SetPassword(hashedPassword).Exec(ctx)
212+
}
213+
206214
// CreateAdmin 创建团队管理员
207215
func (r *TeamGroupUserRepo) CreateAdmin(ctx context.Context, teamID uuid.UUID, req *domain.AddTeamAdminReq) (*db.User, error) {
208216
// 检查邮箱是否已注册
@@ -439,7 +447,7 @@ func (r *TeamGroupUserRepo) InitTeam(ctx context.Context, email string, name str
439447
initTeam, err = tx.Team.Create().
440448
SetID(uuid.New()).
441449
SetName(name).
442-
SetMemberLimit(1000).
450+
SetMemberLimit(5).
443451
Save(ctx)
444452
if err != nil {
445453
return err

backend/biz/team/repo/user_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,45 @@ func TestCreateUsersWithPasswordStoresHashedPassword(t *testing.T) {
196196
t.Fatalf("verify password failed: %v", err)
197197
}
198198
}
199+
200+
func TestResetPasswordStoresHashedPassword(t *testing.T) {
201+
ctx := context.Background()
202+
client := newTeamRepoTestDB(t)
203+
repo := &TeamGroupUserRepo{
204+
db: client,
205+
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
206+
}
207+
userID := uuid.New()
208+
oldPassword, err := crypto.HashPassword("old-password")
209+
if err != nil {
210+
t.Fatal(err)
211+
}
212+
if _, err := client.User.Create().
213+
SetID(userID).
214+
SetName("member").
215+
SetEmail("member@example.com").
216+
SetPassword(oldPassword).
217+
SetRole(consts.UserRoleSubAccount).
218+
SetStatus(consts.UserStatusActive).
219+
Save(ctx); err != nil {
220+
t.Fatal(err)
221+
}
222+
223+
if err := repo.ResetPassword(ctx, userID, "NewPassword123456"); err != nil {
224+
t.Fatal(err)
225+
}
226+
227+
updated, err := client.User.Get(ctx, userID)
228+
if err != nil {
229+
t.Fatal(err)
230+
}
231+
if updated.Password == "" || updated.Password == "NewPassword123456" {
232+
t.Fatalf("password should be hashed, got %q", updated.Password)
233+
}
234+
if err := crypto.VerifyPassword(updated.Password, "NewPassword123456"); err != nil {
235+
t.Fatalf("verify new password failed: %v", err)
236+
}
237+
if err := crypto.VerifyPassword(updated.Password, "old-password"); err == nil {
238+
t.Fatal("old password should not remain valid")
239+
}
240+
}

backend/biz/team/usecase/user.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,22 @@ func (u *TeamGroupUserUsecase) AddUserWithPassword(ctx context.Context, teamUser
167167
}, nil
168168
}
169169

170+
func (u *TeamGroupUserUsecase) ResetPassword(ctx context.Context, teamUser *domain.TeamUser, req *domain.ResetPasswordReq) (*domain.TeamUserPassword, error) {
171+
member, err := u.repo.GetMember(ctx, teamUser.GetTeamID(), req.UserID)
172+
if err != nil {
173+
return nil, err
174+
}
175+
password := random.String(16)
176+
if err := u.repo.ResetPassword(ctx, req.UserID, password); err != nil {
177+
return nil, err
178+
}
179+
resp := &domain.TeamUserPassword{Password: password}
180+
if member.Edges.User != nil {
181+
resp.Email = member.Edges.User.Email
182+
}
183+
return resp, nil
184+
}
185+
170186
// AddAdmin 创建团队管理员
171187
func (u *TeamGroupUserUsecase) AddAdmin(ctx context.Context, teamUser *domain.TeamUser, req *domain.AddTeamAdminReq) (*domain.AddTeamAdminResp, error) {
172188
user, err := u.repo.CreateAdmin(ctx, teamUser.GetTeamID(), req)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package usecase
2+
3+
import (
4+
"context"
5+
"io"
6+
"log/slog"
7+
"testing"
8+
9+
"github.com/google/uuid"
10+
11+
"github.com/chaitin/MonkeyCode/backend/db"
12+
"github.com/chaitin/MonkeyCode/backend/domain"
13+
"github.com/chaitin/MonkeyCode/backend/errcode"
14+
)
15+
16+
func TestResetPasswordReturnsGeneratedPasswordAndUpdatesTeamMember(t *testing.T) {
17+
ctx := context.Background()
18+
teamID := uuid.New()
19+
userID := uuid.New()
20+
repo := &resetPasswordRepoStub{
21+
member: &db.TeamMember{
22+
TeamID: teamID,
23+
UserID: userID,
24+
Edges: db.TeamMemberEdges{
25+
User: &db.User{
26+
ID: userID,
27+
Email: "member@example.com",
28+
},
29+
},
30+
},
31+
}
32+
u := &TeamGroupUserUsecase{
33+
repo: repo,
34+
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
35+
}
36+
37+
resp, err := u.ResetPassword(ctx, &domain.TeamUser{Team: &domain.Team{ID: teamID}}, &domain.ResetPasswordReq{UserID: userID})
38+
if err != nil {
39+
t.Fatal(err)
40+
}
41+
if resp.Email != "member@example.com" {
42+
t.Fatalf("email = %q, want member@example.com", resp.Email)
43+
}
44+
if len(resp.Password) != 16 {
45+
t.Fatalf("password length = %d, want 16", len(resp.Password))
46+
}
47+
if repo.resetUserID != userID {
48+
t.Fatalf("reset user id = %s, want %s", repo.resetUserID, userID)
49+
}
50+
if repo.resetPassword != resp.Password {
51+
t.Fatal("response password should match stored password input")
52+
}
53+
}
54+
55+
func TestResetPasswordRejectsUserOutsideTeam(t *testing.T) {
56+
ctx := context.Background()
57+
teamID := uuid.New()
58+
userID := uuid.New()
59+
repo := &resetPasswordRepoStub{getMemberErr: errcode.ErrNotFound}
60+
u := &TeamGroupUserUsecase{
61+
repo: repo,
62+
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
63+
}
64+
65+
_, err := u.ResetPassword(ctx, &domain.TeamUser{Team: &domain.Team{ID: teamID}}, &domain.ResetPasswordReq{UserID: userID})
66+
if err == nil {
67+
t.Fatal("expected error")
68+
}
69+
if repo.resetCalled {
70+
t.Fatal("password should not be reset when user is outside team")
71+
}
72+
}
73+
74+
type resetPasswordRepoStub struct {
75+
domain.TeamGroupUserRepo
76+
member *db.TeamMember
77+
getMemberErr error
78+
resetCalled bool
79+
resetUserID uuid.UUID
80+
resetPassword string
81+
}
82+
83+
func (s *resetPasswordRepoStub) GetMember(ctx context.Context, teamID, userID uuid.UUID) (*db.TeamMember, error) {
84+
if s.getMemberErr != nil {
85+
return nil, s.getMemberErr
86+
}
87+
return s.member, nil
88+
}
89+
90+
func (s *resetPasswordRepoStub) ResetPassword(ctx context.Context, userID uuid.UUID, newPassword string) error {
91+
s.resetCalled = true
92+
s.resetUserID = userID
93+
s.resetPassword = newPassword
94+
return nil
95+
}

backend/domain/team.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type TeamGroupUserUsecase interface {
1919
Add(ctx context.Context, teamUser *TeamUser, req *AddTeamGroupReq) (*TeamGroup, error)
2020
AddUser(ctx context.Context, teamUser *TeamUser, req *AddTeamUserReq) (*AddTeamUserResp, error)
2121
AddUserWithPassword(ctx context.Context, teamUser *TeamUser, req *AddTeamUserReq) (*AddTeamUserWithPasswordResp, error)
22+
ResetPassword(ctx context.Context, teamUser *TeamUser, req *ResetPasswordReq) (*TeamUserPassword, error)
2223
AddAdmin(ctx context.Context, teamUser *TeamUser, req *AddTeamAdminReq) (*AddTeamAdminResp, error)
2324
Update(ctx context.Context, req *UpdateTeamGroupReq) (*TeamGroup, error)
2425
Delete(ctx context.Context, teamUser *TeamUser, req *DeleteTeamGroupReq) error
@@ -38,6 +39,7 @@ type TeamGroupUserRepo interface {
3839
Create(ctx context.Context, teamID uuid.UUID, req *AddTeamGroupReq) (*db.TeamGroup, error)
3940
CreateUsers(ctx context.Context, teamID uuid.UUID, req *AddTeamUserReq) ([]*db.User, error)
4041
CreateUsersWithPassword(ctx context.Context, teamID uuid.UUID, req *AddTeamUserWithPasswordReq) ([]*db.User, error)
42+
ResetPassword(ctx context.Context, userID uuid.UUID, newPassword string) error
4143
CreateAdmin(ctx context.Context, teamID uuid.UUID, req *AddTeamAdminReq) (*db.User, error)
4244
Update(ctx context.Context, req *UpdateTeamGroupReq) (*db.TeamGroup, error)
4345
Delete(ctx context.Context, teamID, groupID uuid.UUID) error
@@ -277,6 +279,10 @@ type AddTeamUserWithPasswordResp struct {
277279
Passwords []*TeamUserPassword `json:"passwords"`
278280
}
279281

282+
type ResetPasswordReq struct {
283+
UserID uuid.UUID `param:"user_id" validate:"required" json:"-" swaggerignore:"true"`
284+
}
285+
280286
// AddTeamAdminReq 创建团队管理员请求
281287
type AddTeamAdminReq struct {
282288
Email string `json:"email" validate:"required,email"` // 邮箱
@@ -337,11 +343,6 @@ type UpdateTeamUserResp struct {
337343
User *User `json:"user"`
338344
}
339345

340-
// ResetPasswordReq 重置密码请求
341-
type ResetPasswordReq struct {
342-
UserIDs []uuid.UUID `json:"user_ids" validate:"required"` // 用户ID列表
343-
}
344-
345346
// InviteLinkToken 邀请链接令牌
346347
type InviteLinkToken struct {
347348
TeamID uuid.UUID `json:"team_id"`

backend/scripts/build-offline-package.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ ARCH="${ARCH:-amd64}"
55
DOCKER_VERSION="${DOCKER_VERSION:-29.4.3}"
66
DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v5.1.3}"
77
PROJECT_TPL_URL="${PROJECT_TPL_URL:-https://baizhiyun.oss-cn-hangzhou.aliyuncs.com/codingmatrix/project-tpl/codingmatrix-project-tpl.master.zip}"
8+
FIREFACTORY_OFFLINE_OSS_URI="${FIREFACTORY_OFFLINE_OSS_URI:-s3://monkeycode-release/firefactory/firefactory-offline_1.0.0+g741818d_amd64.tgz}"
9+
FIREFACTORY_OFFLINE_FILE="${FIREFACTORY_OFFLINE_FILE:-firefactory-offline_1.0.0+g741818d_amd64.tgz}"
10+
FIREFACTORY_OSS_ENDPOINT="${FIREFACTORY_OSS_ENDPOINT:-${OSS_ENDPOINT:-}}"
11+
OSS_ADDRESSING_STYLE="${OSS_ADDRESSING_STYLE:-virtual}"
812
OUT_DIR="${OUT_DIR:-dist/offline}"
913
PACKAGE_NAME="monkeycode-offline-linux-$ARCH"
1014
PACKAGE_DIR="$OUT_DIR/$PACKAGE_NAME"
@@ -109,6 +113,15 @@ if [ -d static ]; then
109113
cp -R static/. "$PACKAGE_DIR/static/"
110114
fi
111115
curl -fL "$PROJECT_TPL_URL" -o "$PACKAGE_DIR/static/project-tpl.zip"
116+
for required in AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY FIREFACTORY_OSS_ENDPOINT; do
117+
eval "value=\${$required:-}"
118+
if [ -z "$value" ]; then
119+
echo "$required is required"
120+
exit 1
121+
fi
122+
done
123+
aws configure set default.s3.addressing_style "$OSS_ADDRESSING_STYLE"
124+
aws s3 cp "$FIREFACTORY_OFFLINE_OSS_URI" "$PACKAGE_DIR/static/$FIREFACTORY_OFFLINE_FILE" --endpoint-url "$FIREFACTORY_OSS_ENDPOINT" --only-show-errors
112125

113126
docker build \
114127
-f build/Dockerfile \

backend/scripts/check-offline-package.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ images/redis.tar.gz
1919
images/clickhouse.tar.gz
2020
images/rustfs.tar.gz
2121
static/project-tpl.zip
22+
static/firefactory-offline_1.0.0+g741818d_amd64.tgz
2223
static/installer/x86_64/installer
2324
static/installer/x86_64/docker.tgz
2425
static/installer/x86_64/host.tgz

frontend/src/api/Api.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3452,6 +3452,30 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
34523452
...params,
34533453
}),
34543454

3455+
/**
3456+
* @description 管理员为团队成员生成新密码,密码只在响应中返回一次
3457+
*
3458+
* @tags 【Team 管理员】分组成员管理
3459+
* @name V1TeamsUsersPasswordsResetUpdate
3460+
* @summary 重置团队成员密码
3461+
* @request PUT:/api/v1/teams/users/{user_id}/passwords/reset
3462+
* @secure
3463+
*/
3464+
v1TeamsUsersPasswordsResetUpdate: (userId: string, params: RequestParams = {}) =>
3465+
this.request<
3466+
GithubComGoYokoWebResp & {
3467+
data?: DomainTeamUserPassword;
3468+
},
3469+
GithubComGoYokoWebResp
3470+
>({
3471+
path: `/api/v1/teams/users/${userId}/passwords/reset`,
3472+
method: "PUT",
3473+
secure: true,
3474+
type: ContentType.Json,
3475+
format: "json",
3476+
...params,
3477+
}),
3478+
34553479
/**
34563480
* @description 获取团队用户登录状态
34573481
*

0 commit comments

Comments
 (0)