You've already forked agentic-coding-workflow
feat(config,workspace,user): jobs/notify config + workspace fetch repo + user ListAdmins
This commit is contained in:
@@ -56,4 +56,6 @@ type Service interface {
|
||||
Get(ctx context.Context, id uuid.UUID) (*User, error)
|
||||
// Bootstrap 在系统首次启动时创建初始管理员账号,已存在同邮箱用户时为幂等。
|
||||
Bootstrap(ctx context.Context, email, password string) (*User, error)
|
||||
// ListAdmins 返回所有管理员用户,供内部模块(如 jobs runner)使用。
|
||||
ListAdmins(ctx context.Context) ([]*User, error)
|
||||
}
|
||||
|
||||
@@ -36,3 +36,9 @@ DELETE FROM user_sessions WHERE token_hash = $1;
|
||||
|
||||
-- name: DeleteExpiredSessions :exec
|
||||
DELETE FROM user_sessions WHERE expires_at <= now();
|
||||
|
||||
-- name: ListAdmins :many
|
||||
SELECT id, email, password_hash, display_name, is_admin, created_at, updated_at
|
||||
FROM users
|
||||
WHERE is_admin = true
|
||||
ORDER BY id;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package user 内的 repository.go 是 sqlc 生成的查询包(usersqlc)之上的薄
|
||||
// 包装层。它的唯一职责是把 “驱动层类型”(pgtype.UUID / pgtype.Timestamptz /
|
||||
// int32 等)和 “领域类型”(uuid.UUID / time.Time / int / *time.Time)做双向
|
||||
// 包装层。它的唯一职责是把 "驱动层类型"(pgtype.UUID / pgtype.Timestamptz /
|
||||
// int32 等)和 "领域类型"(uuid.UUID / time.Time / int / *time.Time)做双向
|
||||
// 转换,并把底层的 pgx.ErrNoRows 翻译为 errs.AppError,使 service 层可以面
|
||||
// 向干净的领域接口编程,无需直接依赖驱动细节。
|
||||
//
|
||||
@@ -35,6 +35,7 @@ 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)
|
||||
ListAdmins(ctx context.Context) ([]*User, error)
|
||||
|
||||
CreateSession(ctx context.Context, s *Session) error
|
||||
GetSessionByTokenHash(ctx context.Context, hash []byte) (*Session, error)
|
||||
@@ -61,7 +62,7 @@ func toPgUUID(u uuid.UUID) pgtype.UUID {
|
||||
}
|
||||
|
||||
// fromPgUUID 把 pgtype.UUID 转回领域 uuid.UUID。Valid=false 时返回 uuid.Nil,
|
||||
// 让上层用零值判定 “无值”,无需关心驱动层的可空封装。
|
||||
// 让上层用零值判定 "无值",无需关心驱动层的可空封装。
|
||||
func fromPgUUID(p pgtype.UUID) uuid.UUID {
|
||||
if !p.Valid {
|
||||
return uuid.Nil
|
||||
@@ -168,8 +169,8 @@ func (r *pgRepo) CreateSession(ctx context.Context, s *Session) error {
|
||||
}
|
||||
|
||||
// GetSessionByTokenHash 按 sha256 摘要反查会话。该方法主要给鉴权中间件
|
||||
// 使用,因此 ErrNoRows 翻译为 Unauthorized 而非 NotFound:从语义看 “找不到
|
||||
// 会话” 等价于 “未认证”,不应该把内部存在性细节透给客户端。
|
||||
// 使用,因此 ErrNoRows 翻译为 Unauthorized 而非 NotFound:从语义看 "找不到
|
||||
// 会话" 等价于 "未认证",不应该把内部存在性细节透给客户端。
|
||||
func (r *pgRepo) GetSessionByTokenHash(ctx context.Context, hash []byte) (*Session, error) {
|
||||
row, err := r.q.GetSessionByTokenHash(ctx, hash)
|
||||
if err != nil {
|
||||
@@ -191,10 +192,23 @@ func (r *pgRepo) TouchSession(ctx context.Context, hash []byte) error {
|
||||
}
|
||||
|
||||
// DeleteSessionByTokenHash 删除指定哈希的会话。删除不存在的行不算错误,
|
||||
// 调用方可以放心当作 “登出幂等” 使用。
|
||||
// 调用方可以放心当作 "登出幂等" 使用。
|
||||
func (r *pgRepo) DeleteSessionByTokenHash(ctx context.Context, hash []byte) error {
|
||||
if err := r.q.DeleteSessionByTokenHash(ctx, hash); err != nil {
|
||||
return errs.Wrap(err, errs.CodeInternal, "delete session by token hash")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListAdmins 返回所有 is_admin=true 的用户,供 jobs runner 等内部模块使用。
|
||||
func (r *pgRepo) ListAdmins(ctx context.Context) ([]*User, error) {
|
||||
rows, err := r.q.ListAdmins(ctx)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "list admins")
|
||||
}
|
||||
out := make([]*User, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, rowToUser(row))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -172,3 +172,8 @@ func (s *service) Bootstrap(ctx context.Context, email, password string) (*User,
|
||||
}
|
||||
return s.repo.CreateUser(ctx, u)
|
||||
}
|
||||
|
||||
// ListAdmins 返回所有管理员用户,供内部模块(如 jobs runner)使用。
|
||||
func (s *service) ListAdmins(ctx context.Context) ([]*User, error) {
|
||||
return s.repo.ListAdmins(ctx)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ func (r *fakeRepo) GetUserByID(_ context.Context, id uuid.UUID) (*User, error) {
|
||||
}
|
||||
|
||||
func (r *fakeRepo) CountUsers(_ context.Context) (int, error) { return len(r.users), nil }
|
||||
func (r *fakeRepo) ListAdmins(_ context.Context) ([]*User, error) { return nil, nil }
|
||||
|
||||
func (r *fakeRepo) CreateSession(_ context.Context, s *Session) error {
|
||||
r.sessions[string(s.TokenHash)] = s
|
||||
|
||||
@@ -160,6 +160,41 @@ func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (User, error)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listAdmins = `-- name: ListAdmins :many
|
||||
SELECT id, email, password_hash, display_name, is_admin, created_at, updated_at
|
||||
FROM users
|
||||
WHERE is_admin = true
|
||||
ORDER BY id
|
||||
`
|
||||
|
||||
func (q *Queries) ListAdmins(ctx context.Context) ([]User, error) {
|
||||
rows, err := q.db.Query(ctx, listAdmins)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []User
|
||||
for rows.Next() {
|
||||
var i User
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.DisplayName,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const touchSession = `-- name: TouchSession :exec
|
||||
UPDATE user_sessions
|
||||
SET last_seen_at = now()
|
||||
|
||||
Reference in New Issue
Block a user