mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-14 13:20:05 +01:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67d50d2b15 | ||
|
|
e5a664b812 | ||
|
|
f722ba8dca | ||
|
|
20e6d8d186 | ||
|
|
85f37cd113 | ||
|
|
fd1a81ecd8 |
15
cmd/serve.go
15
cmd/serve.go
@@ -156,6 +156,21 @@ var serveCmd = &cobra.Command{
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to enqueue cluster bootstrap: %v", err)
|
log.Printf("failed to enqueue cluster bootstrap: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = jobs.Enqueue(
|
||||||
|
context.Background(),
|
||||||
|
uuid.NewString(),
|
||||||
|
"org_key_sweeper",
|
||||||
|
bg.OrgKeySweeperArgs{
|
||||||
|
IntervalS: 3600,
|
||||||
|
RetentionDays: 10,
|
||||||
|
},
|
||||||
|
archer.WithMaxRetries(1),
|
||||||
|
archer.WithScheduleTime(time.Now()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to enqueue org_key_sweeper: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = auth.Refresh(rt.DB, rt.Cfg.JWTPrivateEncKey)
|
_ = auth.Refresh(rt.DB, rt.Cfg.JWTPrivateEncKey)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -104,6 +104,8 @@ components:
|
|||||||
$ref: '#/components/schemas/dto.LoadBalancerResponse'
|
$ref: '#/components/schemas/dto.LoadBalancerResponse'
|
||||||
id:
|
id:
|
||||||
type: string
|
type: string
|
||||||
|
kubeconfig:
|
||||||
|
type: string
|
||||||
last_error:
|
last_error:
|
||||||
type: string
|
type: string
|
||||||
name:
|
name:
|
||||||
@@ -113,6 +115,10 @@ components:
|
|||||||
$ref: '#/components/schemas/dto.NodePoolResponse'
|
$ref: '#/components/schemas/dto.NodePoolResponse'
|
||||||
type: array
|
type: array
|
||||||
uniqueItems: false
|
uniqueItems: false
|
||||||
|
org_key:
|
||||||
|
type: string
|
||||||
|
org_secret:
|
||||||
|
type: string
|
||||||
random_token:
|
random_token:
|
||||||
type: string
|
type: string
|
||||||
region:
|
region:
|
||||||
@@ -1037,6 +1043,8 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
models.APIKey:
|
models.APIKey:
|
||||||
properties:
|
properties:
|
||||||
|
cluster_id:
|
||||||
|
type: string
|
||||||
created_at:
|
created_at:
|
||||||
format: date-time
|
format: date-time
|
||||||
type: string
|
type: string
|
||||||
@@ -1046,6 +1054,8 @@ components:
|
|||||||
id:
|
id:
|
||||||
format: uuid
|
format: uuid
|
||||||
type: string
|
type: string
|
||||||
|
is_ephemeral:
|
||||||
|
type: boolean
|
||||||
last_used_at:
|
last_used_at:
|
||||||
format: date-time
|
format: date-time
|
||||||
type: string
|
type: string
|
||||||
@@ -1056,6 +1066,8 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
prefix:
|
prefix:
|
||||||
type: string
|
type: string
|
||||||
|
purpose:
|
||||||
|
type: string
|
||||||
revoked:
|
revoked:
|
||||||
type: boolean
|
type: boolean
|
||||||
scope:
|
scope:
|
||||||
|
|||||||
@@ -129,6 +129,12 @@ func NewJobs(gdb *gorm.DB, dbUrl string) (*Jobs, error) {
|
|||||||
archer.WithTimeout(60*time.Minute),
|
archer.WithTimeout(60*time.Minute),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
c.Register(
|
||||||
|
"org_key_sweeper",
|
||||||
|
OrgKeySweeperWorker(gdb, jobs),
|
||||||
|
archer.WithInstances(1),
|
||||||
|
archer.WithTimeout(5*time.Minute),
|
||||||
|
)
|
||||||
return jobs, nil
|
return jobs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
95
internal/bg/org_key_sweeper.go
Normal file
95
internal/bg/org_key_sweeper.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package bg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dyaksa/archer"
|
||||||
|
"github.com/dyaksa/archer/job"
|
||||||
|
"github.com/glueops/autoglue/internal/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrgKeySweeperArgs struct {
|
||||||
|
IntervalS int `json:"interval_seconds,omitempty"`
|
||||||
|
RetentionDays int `json:"retention_days,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrgKeySweeperResult struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
MarkedRevoked int `json:"marked_revoked"`
|
||||||
|
DeletedEphemeral int `json:"deleted_ephemeral"`
|
||||||
|
ElapsedMs int `json:"elapsed_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func OrgKeySweeperWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||||
|
return func(ctx context.Context, j job.Job) (any, error) {
|
||||||
|
args := OrgKeySweeperArgs{
|
||||||
|
IntervalS: 3600,
|
||||||
|
RetentionDays: 10,
|
||||||
|
}
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
_ = j.ParseArguments(&args)
|
||||||
|
if args.IntervalS <= 0 {
|
||||||
|
args.IntervalS = 3600
|
||||||
|
}
|
||||||
|
if args.RetentionDays <= 0 {
|
||||||
|
args.RetentionDays = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// 1) Mark expired keys as revoked
|
||||||
|
res1 := db.Model(&models.APIKey{}).
|
||||||
|
Where("expires_at IS NOT NULL AND expires_at <= ? AND revoked = false", now).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"revoked": true,
|
||||||
|
"updated_at": now,
|
||||||
|
})
|
||||||
|
|
||||||
|
if res1.Error != nil {
|
||||||
|
log.Error().Err(res1.Error).Msg("[org_key_sweeper] mark expired revoked failed")
|
||||||
|
return nil, res1.Error
|
||||||
|
}
|
||||||
|
markedRevoked := int(res1.RowsAffected)
|
||||||
|
|
||||||
|
// 2) Hard-delete ephemeral keys that are revoked and older than retention
|
||||||
|
cutoff := now.Add(-time.Duration(args.RetentionDays) * 24 * time.Hour)
|
||||||
|
res2 := db.
|
||||||
|
Where("is_ephemeral = ? AND revoked = ? AND updated_at <= ?", true, true, cutoff).
|
||||||
|
Delete(&models.APIKey{})
|
||||||
|
|
||||||
|
if res2.Error != nil {
|
||||||
|
log.Error().Err(res2.Error).Msg("[org_key_sweeper] delete revoked ephemeral keys failed")
|
||||||
|
return nil, res2.Error
|
||||||
|
}
|
||||||
|
deletedEphemeral := int(res2.RowsAffected)
|
||||||
|
|
||||||
|
out := OrgKeySweeperResult{
|
||||||
|
Status: "ok",
|
||||||
|
MarkedRevoked: markedRevoked,
|
||||||
|
DeletedEphemeral: deletedEphemeral,
|
||||||
|
ElapsedMs: int(time.Since(start).Milliseconds()),
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Int("marked_revoked", markedRevoked).
|
||||||
|
Int("deleted_ephemeral", deletedEphemeral).
|
||||||
|
Msg("[org_key_sweeper] cleanup tick ok")
|
||||||
|
|
||||||
|
// Re-enqueue the sweeper
|
||||||
|
next := time.Now().Add(time.Duration(args.IntervalS) * time.Second)
|
||||||
|
_, _ = jobs.Enqueue(
|
||||||
|
ctx,
|
||||||
|
uuid.NewString(),
|
||||||
|
"org_key_sweeper",
|
||||||
|
args,
|
||||||
|
archer.WithScheduleTime(next),
|
||||||
|
archer.WithMaxRetries(1),
|
||||||
|
)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -158,10 +158,11 @@ func ClusterPrepareWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
|||||||
dtoCluster.Kubeconfig = &kubeconfig
|
dtoCluster.Kubeconfig = &kubeconfig
|
||||||
}
|
}
|
||||||
|
|
||||||
orgKey, orgSecret, err := createOrgScopedKeyForPayload(
|
orgKey, orgSecret, err := findOrCreateClusterAutomationKey(
|
||||||
db,
|
db,
|
||||||
c.OrganizationID,
|
c.OrganizationID,
|
||||||
fmt.Sprintf("cluster-%s-%s", c.Name, c.ID.String()),
|
c.ID,
|
||||||
|
24*time.Hour,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -584,12 +585,28 @@ func randomB64URL(n int) (string, error) {
|
|||||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createOrgScopedKeyForPayload(db *gorm.DB, orgID uuid.UUID, name string) (orgKey string, orgSecret string, err error) {
|
func findOrCreateClusterAutomationKey(
|
||||||
|
db *gorm.DB,
|
||||||
|
orgID uuid.UUID,
|
||||||
|
clusterID uuid.UUID,
|
||||||
|
ttl time.Duration,
|
||||||
|
) (orgKey string, orgSecret string, err error) {
|
||||||
|
now := time.Now()
|
||||||
|
name := fmt.Sprintf("cluster-%s-bastion", clusterID.String())
|
||||||
|
|
||||||
|
// 1) Delete any existing ephemeral cluster-bastion key for this org+cluster
|
||||||
|
if err := db.Where(
|
||||||
|
"org_id = ? AND scope = ? AND purpose = ? AND cluster_id = ? AND is_ephemeral = ?",
|
||||||
|
orgID, "org", "cluster_bastion", clusterID, true,
|
||||||
|
).Delete(&models.APIKey{}).Error; err != nil {
|
||||||
|
return "", "", fmt.Errorf("delete existing cluster key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Mint a fresh keypair
|
||||||
keySuffix, err := randomB64URL(16)
|
keySuffix, err := randomB64URL(16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("entropy_error: %w", err)
|
return "", "", fmt.Errorf("entropy_error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sec, err := randomB64URL(32)
|
sec, err := randomB64URL(32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("entropy_error: %w", err)
|
return "", "", fmt.Errorf("entropy_error: %w", err)
|
||||||
@@ -604,13 +621,25 @@ func createOrgScopedKeyForPayload(db *gorm.DB, orgID uuid.UUID, name string) (or
|
|||||||
return "", "", fmt.Errorf("hash_error: %w", err)
|
return "", "", fmt.Errorf("hash_error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exp := now.Add(ttl)
|
||||||
|
|
||||||
|
prefix := orgKey
|
||||||
|
if len(prefix) > 12 {
|
||||||
|
prefix = prefix[:12]
|
||||||
|
}
|
||||||
|
|
||||||
rec := models.APIKey{
|
rec := models.APIKey{
|
||||||
OrgID: &orgID,
|
OrgID: &orgID,
|
||||||
Scope: "org",
|
Scope: "org",
|
||||||
Name: name,
|
Purpose: "cluster_bastion",
|
||||||
KeyHash: keyHash,
|
ClusterID: &clusterID,
|
||||||
SecretHash: &secretHash,
|
IsEphemeral: true,
|
||||||
ExpiresAt: nil,
|
Name: name,
|
||||||
|
KeyHash: keyHash,
|
||||||
|
SecretHash: &secretHash,
|
||||||
|
ExpiresAt: &exp,
|
||||||
|
Revoked: false,
|
||||||
|
Prefix: &prefix,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.Create(&rec).Error; err != nil {
|
if err := db.Create(&rec).Error; err != nil {
|
||||||
|
|||||||
@@ -828,16 +828,16 @@ func ListNodePoolLabels(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
out := make([]dto.LabelResponse, 0, len(np.Taints))
|
out := make([]dto.LabelResponse, 0, len(np.Taints))
|
||||||
for _, taint := range np.Taints {
|
for _, label := range np.Labels {
|
||||||
out = append(out, dto.LabelResponse{
|
out = append(out, dto.LabelResponse{
|
||||||
AuditFields: common.AuditFields{
|
AuditFields: common.AuditFields{
|
||||||
ID: taint.ID,
|
ID: label.ID,
|
||||||
OrganizationID: taint.OrganizationID,
|
OrganizationID: label.OrganizationID,
|
||||||
CreatedAt: taint.CreatedAt,
|
CreatedAt: label.CreatedAt,
|
||||||
UpdatedAt: taint.UpdatedAt,
|
UpdatedAt: label.UpdatedAt,
|
||||||
},
|
},
|
||||||
Key: taint.Key,
|
Key: label.Key,
|
||||||
Value: taint.Value,
|
Value: label.Value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
utils.WriteJSON(w, http.StatusOK, out)
|
utils.WriteJSON(w, http.StatusOK, out)
|
||||||
|
|||||||
@@ -585,13 +585,22 @@ func CreateOrgKey(db *gorm.DB) http.HandlerFunc {
|
|||||||
exp = &e
|
exp = &e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prefix := orgKey
|
||||||
|
if len(prefix) > 12 {
|
||||||
|
prefix = prefix[:12]
|
||||||
|
}
|
||||||
|
|
||||||
rec := models.APIKey{
|
rec := models.APIKey{
|
||||||
OrgID: &oid,
|
OrgID: &oid,
|
||||||
Scope: "org",
|
Scope: "org",
|
||||||
Name: req.Name,
|
Purpose: "user",
|
||||||
KeyHash: keyHash,
|
IsEphemeral: false,
|
||||||
SecretHash: &secretHash,
|
Name: req.Name,
|
||||||
ExpiresAt: exp,
|
KeyHash: keyHash,
|
||||||
|
SecretHash: &secretHash,
|
||||||
|
ExpiresAt: exp,
|
||||||
|
Revoked: false,
|
||||||
|
Prefix: &prefix,
|
||||||
}
|
}
|
||||||
if err := db.Create(&rec).Error; err != nil {
|
if err := db.Create(&rec).Error; err != nil {
|
||||||
utils.WriteError(w, 500, "db_error", err.Error())
|
utils.WriteError(w, 500, "db_error", err.Error())
|
||||||
|
|||||||
@@ -7,17 +7,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type APIKey struct {
|
type APIKey struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
|
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
|
||||||
Name string `gorm:"not null;default:''" json:"name"`
|
OrgID *uuid.UUID `json:"org_id,omitempty" format:"uuid"`
|
||||||
KeyHash string `gorm:"uniqueIndex;not null" json:"-"`
|
Scope string `gorm:"not null;default:''" json:"scope"`
|
||||||
Scope string `gorm:"not null;default:''" json:"scope"`
|
Purpose string `json:"purpose"`
|
||||||
UserID *uuid.UUID `json:"user_id,omitempty" format:"uuid"`
|
ClusterID *uuid.UUID `json:"cluster_id,omitempty"`
|
||||||
OrgID *uuid.UUID `json:"org_id,omitempty" format:"uuid"`
|
IsEphemeral bool `json:"is_ephemeral"`
|
||||||
SecretHash *string `json:"-"`
|
Name string `gorm:"not null;default:''" json:"name"`
|
||||||
ExpiresAt *time.Time `json:"expires_at,omitempty" format:"date-time"`
|
KeyHash string `gorm:"uniqueIndex;not null" json:"-"`
|
||||||
Revoked bool `gorm:"not null;default:false" json:"revoked"`
|
SecretHash *string `json:"-"`
|
||||||
Prefix *string `json:"prefix,omitempty"`
|
UserID *uuid.UUID `json:"user_id,omitempty" format:"uuid"`
|
||||||
LastUsedAt *time.Time `json:"last_used_at,omitempty" format:"date-time"`
|
ExpiresAt *time.Time `json:"expires_at,omitempty" format:"date-time"`
|
||||||
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()" format:"date-time"`
|
Revoked bool `gorm:"not null;default:false" json:"revoked"`
|
||||||
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()" format:"date-time"`
|
Prefix *string `json:"prefix,omitempty"`
|
||||||
|
LastUsedAt *time.Time `json:"last_used_at,omitempty" format:"date-time"`
|
||||||
|
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()" format:"date-time"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -46,13 +46,13 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.557.0",
|
"lucide-react": "^0.561.0",
|
||||||
"motion": "^12.23.26",
|
"motion": "^12.23.26",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"rapidoc": "^9.3.8",
|
"rapidoc": "^9.3.8",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.3",
|
||||||
"react-day-picker": "^9.12.0",
|
"react-day-picker": "^9.12.0",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.3",
|
||||||
"react-hook-form": "^7.68.0",
|
"react-hook-form": "^7.68.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^3.0.6",
|
||||||
@@ -60,14 +60,14 @@
|
|||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.18",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.39.1",
|
"@eslint/js": "9.39.1",
|
||||||
"@ianvs/prettier-plugin-sort-imports": "4.7.0",
|
"@ianvs/prettier-plugin-sort-imports": "4.7.0",
|
||||||
"@types/node": "24.10.2",
|
"@types/node": "25.0.1",
|
||||||
"@types/react": "19.2.7",
|
"@types/react": "19.2.7",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"@vitejs/plugin-react": "5.1.2",
|
"@vitejs/plugin-react": "5.1.2",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useForm } from "react-hook-form"
|
|||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge.tsx"
|
||||||
import { Button } from "@/components/ui/button.tsx"
|
import { Button } from "@/components/ui/button.tsx"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx"
|
||||||
import {
|
import {
|
||||||
@@ -35,10 +36,12 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table.tsx"
|
} from "@/components/ui/table.tsx"
|
||||||
|
|
||||||
|
// 1) No coerce; we’ll do the conversion in onChange
|
||||||
const createSchema = z.object({
|
const createSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
expires_in_hours: z.number().min(1).max(43800),
|
expires_in_hours: z.number().int().min(1).max(43800),
|
||||||
})
|
})
|
||||||
|
|
||||||
type CreateValues = z.infer<typeof createSchema>
|
type CreateValues = z.infer<typeof createSchema>
|
||||||
|
|
||||||
export const OrgApiKeys = () => {
|
export const OrgApiKeys = () => {
|
||||||
@@ -52,6 +55,7 @@ export const OrgApiKeys = () => {
|
|||||||
queryFn: () => withRefresh(() => api.listOrgKeys({ id: orgId! })),
|
queryFn: () => withRefresh(() => api.listOrgKeys({ id: orgId! })),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 2) Form holds numbers directly
|
||||||
const form = useForm<CreateValues>({
|
const form = useForm<CreateValues>({
|
||||||
resolver: zodResolver(createSchema),
|
resolver: zodResolver(createSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -71,7 +75,7 @@ export const OrgApiKeys = () => {
|
|||||||
void qc.invalidateQueries({ queryKey: ["org:keys", orgId] })
|
void qc.invalidateQueries({ queryKey: ["org:keys", orgId] })
|
||||||
setShowSecret({ key: resp.org_key, secret: resp.org_secret })
|
setShowSecret({ key: resp.org_key, secret: resp.org_secret })
|
||||||
toast.success("Key created")
|
toast.success("Key created")
|
||||||
form.reset({ name: "", expires_in_hours: undefined })
|
form.reset({ name: "", expires_in_hours: 720 })
|
||||||
},
|
},
|
||||||
onError: (e: any) => toast.error(e?.message ?? "Failed to create key"),
|
onError: (e: any) => toast.error(e?.message ?? "Failed to create key"),
|
||||||
})
|
})
|
||||||
@@ -124,7 +128,17 @@ export const OrgApiKeys = () => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Expires In (hours)</FormLabel>
|
<FormLabel>Expires In (hours)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="e.g. 720" {...field} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="e.g. 720"
|
||||||
|
{...field}
|
||||||
|
// 3) Convert string → number (or undefined if empty)
|
||||||
|
value={field.value ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = e.target.value
|
||||||
|
field.onChange(v === "" ? undefined : Number(v))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -148,6 +162,7 @@ export const OrgApiKeys = () => {
|
|||||||
<TableHead>Scope</TableHead>
|
<TableHead>Scope</TableHead>
|
||||||
<TableHead>Created</TableHead>
|
<TableHead>Created</TableHead>
|
||||||
<TableHead>Expires</TableHead>
|
<TableHead>Expires</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
<TableHead className="w-28" />
|
<TableHead className="w-28" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -160,6 +175,33 @@ export const OrgApiKeys = () => {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{k.expires_at ? new Date(k.expires_at).toLocaleString() : "-"}
|
{k.expires_at ? new Date(k.expires_at).toLocaleString() : "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{(() => {
|
||||||
|
const isExpired = k.expires_at ? new Date(k.expires_at) <= new Date() : false
|
||||||
|
|
||||||
|
if (k.revoked) {
|
||||||
|
return (
|
||||||
|
<Badge variant="destructive" className="font-mono">
|
||||||
|
Revoked
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExpired) {
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" className="font-mono">
|
||||||
|
Expired
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge variant="secondary" className="font-mono">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<Button variant="destructive" size="sm" onClick={() => deleteMut.mutate(k.id!)}>
|
<Button variant="destructive" size="sm" onClick={() => deleteMut.mutate(k.id!)}>
|
||||||
Delete
|
Delete
|
||||||
|
|||||||
30
ui/yarn.lock
30
ui/yarn.lock
@@ -2101,7 +2101,7 @@
|
|||||||
"@tailwindcss/oxide-win32-arm64-msvc" "4.1.18"
|
"@tailwindcss/oxide-win32-arm64-msvc" "4.1.18"
|
||||||
"@tailwindcss/oxide-win32-x64-msvc" "4.1.18"
|
"@tailwindcss/oxide-win32-x64-msvc" "4.1.18"
|
||||||
|
|
||||||
"@tailwindcss/vite@^4.1.17":
|
"@tailwindcss/vite@^4.1.18":
|
||||||
version "4.1.18"
|
version "4.1.18"
|
||||||
resolved "https://registry.yarnpkg.com/@tailwindcss/vite/-/vite-4.1.18.tgz#614b9d5483559518c72d31bca05d686f8df28e9a"
|
resolved "https://registry.yarnpkg.com/@tailwindcss/vite/-/vite-4.1.18.tgz#614b9d5483559518c72d31bca05d686f8df28e9a"
|
||||||
integrity sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==
|
integrity sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==
|
||||||
@@ -2240,10 +2240,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
||||||
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
||||||
|
|
||||||
"@types/node@24.10.2":
|
"@types/node@25.0.1":
|
||||||
version "24.10.2"
|
version "25.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.2.tgz#82a57476a19647d8f2c7750d0924788245e39b26"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.1.tgz#9c41c277a1b16491174497cd075f8de7c27a1ac4"
|
||||||
integrity sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==
|
integrity sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~7.16.0"
|
undici-types "~7.16.0"
|
||||||
|
|
||||||
@@ -3292,9 +3292,9 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
|||||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||||
|
|
||||||
fast-equals@^5.0.1:
|
fast-equals@^5.0.1:
|
||||||
version "5.3.3"
|
version "5.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.3.3.tgz#e55f96198269278533348c22f1ab1a0fb957e22a"
|
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.3.4.tgz#41f99f413c2d3a39a1ca9a2e1652abd9c4ee9feb"
|
||||||
integrity sha512-/boTcHZeIAQ2r/tL11voclBHDeP9WPxLt+tyAbVSyyXuUFyh0Tne7gJZTqGbxnvj79TjLdCXLOY7UIPhyG5MTw==
|
integrity sha512-d+yU9iNQbbC098NOuMlAIth/g+owbpX/uuOkH/DQcC2fMMyjOlX292Op29DrUKq388m4UUyOdWakUH/msGypOg==
|
||||||
|
|
||||||
fast-glob@^3.3.3:
|
fast-glob@^3.3.3:
|
||||||
version "3.3.3"
|
version "3.3.3"
|
||||||
@@ -4019,10 +4019,10 @@ lru-cache@^5.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist "^3.0.2"
|
yallist "^3.0.2"
|
||||||
|
|
||||||
lucide-react@^0.557.0:
|
lucide-react@^0.561.0:
|
||||||
version "0.557.0"
|
version "0.561.0"
|
||||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.557.0.tgz#372f6069b227fcca816962d5bbbd9c8dee5e97bd"
|
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.561.0.tgz#8eb440395cf01b27da9c65cb014eb2c71f77e656"
|
||||||
integrity sha512-x6LrJAuYGywdjH4rBgI8ygWnCsn8GTvS9/BhSORNWsuv3LNLV39ZOUg6UTJa9nFUl0fHY8bytDSThH12pNHyLQ==
|
integrity sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A==
|
||||||
|
|
||||||
magic-string@^0.30.21:
|
magic-string@^0.30.21:
|
||||||
version "0.30.21"
|
version "0.30.21"
|
||||||
@@ -4608,7 +4608,7 @@ react-day-picker@^9.12.0:
|
|||||||
date-fns "^4.1.0"
|
date-fns "^4.1.0"
|
||||||
date-fns-jalali "^4.1.0-0"
|
date-fns-jalali "^4.1.0-0"
|
||||||
|
|
||||||
react-dom@^19.2.1:
|
react-dom@^19.2.3:
|
||||||
version "19.2.3"
|
version "19.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17"
|
||||||
integrity sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==
|
integrity sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==
|
||||||
@@ -4706,7 +4706,7 @@ react-transition-group@^4.4.5:
|
|||||||
loose-envify "^1.4.0"
|
loose-envify "^1.4.0"
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
|
|
||||||
react@^19.2.1:
|
react@^19.2.3:
|
||||||
version "19.2.3"
|
version "19.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-19.2.3.tgz#d83e5e8e7a258cf6b4fe28640515f99b87cd19b8"
|
resolved "https://registry.yarnpkg.com/react/-/react-19.2.3.tgz#d83e5e8e7a258cf6b4fe28640515f99b87cd19b8"
|
||||||
integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==
|
integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==
|
||||||
@@ -5132,7 +5132,7 @@ tailwind-merge@^3.4.0:
|
|||||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.4.0.tgz#5a264e131a096879965f1175d11f8c36e6b64eca"
|
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.4.0.tgz#5a264e131a096879965f1175d11f8c36e6b64eca"
|
||||||
integrity sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==
|
integrity sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==
|
||||||
|
|
||||||
tailwindcss@4.1.18, tailwindcss@^4.1.17:
|
tailwindcss@4.1.18, tailwindcss@^4.1.18:
|
||||||
version "4.1.18"
|
version "4.1.18"
|
||||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.1.18.tgz#f488ba47853abdb5354daf9679d3e7791fc4f4e3"
|
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.1.18.tgz#f488ba47853abdb5354daf9679d3e7791fc4f4e3"
|
||||||
integrity sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==
|
integrity sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==
|
||||||
|
|||||||
Reference in New Issue
Block a user