You've already forked agentic-coding-workflow
修改
This commit is contained in:
@@ -20,7 +20,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
@@ -35,12 +37,20 @@ type Repository interface {
|
||||
GetUserByEmail(ctx context.Context, email string) (*User, error)
|
||||
GetUserByID(ctx context.Context, id uuid.UUID) (*User, error)
|
||||
CountUsers(ctx context.Context) (int, error)
|
||||
CountAdmins(ctx context.Context) (int, error)
|
||||
ListUsers(ctx context.Context) ([]*User, error)
|
||||
ListAdmins(ctx context.Context) ([]*User, error)
|
||||
UpdateProfile(ctx context.Context, id uuid.UUID, displayName string) (*User, error)
|
||||
SetAdmin(ctx context.Context, id uuid.UUID, isAdmin bool) (*User, error)
|
||||
SetEnabled(ctx context.Context, id uuid.UUID, enabled bool) (*User, error)
|
||||
UpdatePassword(ctx context.Context, id uuid.UUID, passwordHash string) error
|
||||
DeleteUser(ctx context.Context, id uuid.UUID) error
|
||||
|
||||
CreateSession(ctx context.Context, s *Session) error
|
||||
GetSessionByTokenHash(ctx context.Context, hash []byte) (*Session, error)
|
||||
TouchSession(ctx context.Context, hash []byte) error
|
||||
DeleteSessionByTokenHash(ctx context.Context, hash []byte) error
|
||||
DeleteSessionsByUserID(ctx context.Context, userID uuid.UUID) error
|
||||
}
|
||||
|
||||
// pgRepo 是 Repository 的 Postgres 实现,内部持有一个 sqlc 生成的 Queries。
|
||||
@@ -80,6 +90,7 @@ func rowToUser(row usersqlc.User) *User {
|
||||
PasswordHash: row.PasswordHash,
|
||||
DisplayName: row.DisplayName,
|
||||
IsAdmin: row.IsAdmin,
|
||||
Enabled: row.Enabled,
|
||||
CreatedAt: row.CreatedAt.Time,
|
||||
UpdatedAt: row.UpdatedAt.Time,
|
||||
}
|
||||
@@ -114,11 +125,20 @@ func (r *pgRepo) CreateUser(ctx context.Context, u *User) (*User, error) {
|
||||
IsAdmin: u.IsAdmin,
|
||||
})
|
||||
if err != nil {
|
||||
if isUniqueViolation(err) {
|
||||
return nil, errs.New(errs.CodeConflict, "邮箱已被占用")
|
||||
}
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "create user")
|
||||
}
|
||||
return rowToUser(row), nil
|
||||
}
|
||||
|
||||
// isUniqueViolation 判断底层错误是否为 Postgres 唯一约束冲突(如 email 重复)。
|
||||
func isUniqueViolation(err error) bool {
|
||||
var pgErr *pgconn.PgError
|
||||
return errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UniqueViolation
|
||||
}
|
||||
|
||||
// GetUserByEmail 按邮箱精确查询用户。pgx.ErrNoRows 翻译为 NotFound,方便
|
||||
// 上层(例如登录流程)分类响应;其它错误一律 Internal。
|
||||
func (r *pgRepo) GetUserByEmail(ctx context.Context, email string) (*User, error) {
|
||||
@@ -212,3 +232,97 @@ func (r *pgRepo) ListAdmins(ctx context.Context) ([]*User, error) {
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CountAdmins 返回启用状态的管理员数量,用于 service 层的“最后一个管理员”保护。
|
||||
func (r *pgRepo) CountAdmins(ctx context.Context) (int, error) {
|
||||
n, err := r.q.CountAdmins(ctx)
|
||||
if err != nil {
|
||||
return 0, errs.Wrap(err, errs.CodeInternal, "count admins")
|
||||
}
|
||||
return int(n), nil
|
||||
}
|
||||
|
||||
// ListUsers 返回全部用户,按创建时间升序。
|
||||
func (r *pgRepo) ListUsers(ctx context.Context) ([]*User, error) {
|
||||
rows, err := r.q.ListUsers(ctx)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "list users")
|
||||
}
|
||||
out := make([]*User, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, rowToUser(row))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// UpdateProfile 更新 display_name 并返回更新后的行。目标不存在时返回 NotFound。
|
||||
func (r *pgRepo) UpdateProfile(ctx context.Context, id uuid.UUID, displayName string) (*User, error) {
|
||||
row, err := r.q.UpdateUserProfile(ctx, usersqlc.UpdateUserProfileParams{
|
||||
ID: toPgUUID(id),
|
||||
DisplayName: displayName,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, errs.New(errs.CodeNotFound, "用户不存在")
|
||||
}
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "update user profile")
|
||||
}
|
||||
return rowToUser(row), nil
|
||||
}
|
||||
|
||||
// SetAdmin 设置 is_admin 并返回更新后的行。目标不存在时返回 NotFound。
|
||||
func (r *pgRepo) SetAdmin(ctx context.Context, id uuid.UUID, isAdmin bool) (*User, error) {
|
||||
row, err := r.q.SetUserAdmin(ctx, usersqlc.SetUserAdminParams{
|
||||
ID: toPgUUID(id),
|
||||
IsAdmin: isAdmin,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, errs.New(errs.CodeNotFound, "用户不存在")
|
||||
}
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "set user admin")
|
||||
}
|
||||
return rowToUser(row), nil
|
||||
}
|
||||
|
||||
// SetEnabled 设置 enabled 并返回更新后的行。目标不存在时返回 NotFound。
|
||||
func (r *pgRepo) SetEnabled(ctx context.Context, id uuid.UUID, enabled bool) (*User, error) {
|
||||
row, err := r.q.SetUserEnabled(ctx, usersqlc.SetUserEnabledParams{
|
||||
ID: toPgUUID(id),
|
||||
Enabled: enabled,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, errs.New(errs.CodeNotFound, "用户不存在")
|
||||
}
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "set user enabled")
|
||||
}
|
||||
return rowToUser(row), nil
|
||||
}
|
||||
|
||||
// UpdatePassword 覆写密码哈希。:exec 不读结果集,目标不存在不报错(幂等)。
|
||||
func (r *pgRepo) UpdatePassword(ctx context.Context, id uuid.UUID, passwordHash string) error {
|
||||
if err := r.q.UpdateUserPassword(ctx, usersqlc.UpdateUserPasswordParams{
|
||||
ID: toPgUUID(id),
|
||||
PasswordHash: passwordHash,
|
||||
}); err != nil {
|
||||
return errs.Wrap(err, errs.CodeInternal, "update user password")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户。其会话经 ON DELETE CASCADE 一并清除。
|
||||
func (r *pgRepo) DeleteUser(ctx context.Context, id uuid.UUID) error {
|
||||
if err := r.q.DeleteUser(ctx, toPgUUID(id)); err != nil {
|
||||
return errs.Wrap(err, errs.CodeInternal, "delete user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSessionsByUserID 撤销某用户的全部会话(改密/禁用时使用)。
|
||||
func (r *pgRepo) DeleteSessionsByUserID(ctx context.Context, userID uuid.UUID) error {
|
||||
if err := r.q.DeleteSessionsByUserID(ctx, toPgUUID(userID)); err != nil {
|
||||
return errs.Wrap(err, errs.CodeInternal, "delete sessions by user id")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user