feat: add admin dashboard
This commit is contained in:
@@ -14,13 +14,22 @@ type Repository struct {
|
||||
}
|
||||
|
||||
type DocumentRecord struct {
|
||||
ID string
|
||||
Path string
|
||||
CurrentHash string
|
||||
Title string
|
||||
Tags []string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ID string `json:"id"`
|
||||
Path string `json:"path"`
|
||||
CurrentHash string `json:"currentHash"`
|
||||
Title string `json:"title"`
|
||||
Tags []string `json:"tags"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type UserRecord struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Role string `json:"role"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
LastSeenAt time.Time `json:"lastSeenAt,omitempty"`
|
||||
}
|
||||
|
||||
type AttachmentRecord struct {
|
||||
@@ -151,6 +160,82 @@ func (r *Repository) ListDocuments(ctx context.Context) ([]DocumentRecord, error
|
||||
return records, rows.Err()
|
||||
}
|
||||
|
||||
func (r *Repository) ListUsers(ctx context.Context) ([]UserRecord, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT id, email, COALESCE(display_name, ''), COALESCE(role, 'viewer'), created_at, COALESCE(last_seen_at, '')
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list users: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var users []UserRecord
|
||||
for rows.Next() {
|
||||
var (
|
||||
user UserRecord
|
||||
created string
|
||||
lastSeen string
|
||||
)
|
||||
if err := rows.Scan(&user.ID, &user.Email, &user.DisplayName, &user.Role, &created, &lastSeen); err != nil {
|
||||
return nil, fmt.Errorf("scan user: %w", err)
|
||||
}
|
||||
createdAt, err := time.Parse(time.RFC3339, created)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse user created_at: %w", err)
|
||||
}
|
||||
user.CreatedAt = createdAt
|
||||
if lastSeen != "" {
|
||||
parsed, err := time.Parse(time.RFC3339, lastSeen)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse user last_seen_at: %w", err)
|
||||
}
|
||||
user.LastSeenAt = parsed
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
return users, rows.Err()
|
||||
}
|
||||
|
||||
func (r *Repository) UpsertUser(ctx context.Context, user UserRecord) (UserRecord, error) {
|
||||
now := time.Now().UTC()
|
||||
if user.ID == "" {
|
||||
user.ID = "user:" + user.Email
|
||||
}
|
||||
if user.Role == "" {
|
||||
user.Role = "viewer"
|
||||
}
|
||||
if user.DisplayName == "" {
|
||||
user.DisplayName = user.Email
|
||||
}
|
||||
user.CreatedAt = now
|
||||
user.LastSeenAt = now
|
||||
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO users (id, email, display_name, role, created_at, last_seen_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(email) DO UPDATE SET
|
||||
display_name = excluded.display_name,
|
||||
role = excluded.role,
|
||||
last_seen_at = excluded.last_seen_at
|
||||
`, user.ID, user.Email, user.DisplayName, user.Role, now.Format(time.RFC3339), now.Format(time.RFC3339)); err != nil {
|
||||
return UserRecord{}, fmt.Errorf("upsert user %s: %w", user.Email, err)
|
||||
}
|
||||
|
||||
users, err := r.ListUsers(ctx)
|
||||
if err != nil {
|
||||
return UserRecord{}, err
|
||||
}
|
||||
for _, candidate := range users {
|
||||
if candidate.Email == user.Email {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
return UserRecord{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (r *Repository) SaveAttachment(ctx context.Context, record AttachmentRecord) error {
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO attachments (hash, original_name, content_type, size_bytes, created_at)
|
||||
|
||||
Reference in New Issue
Block a user