Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

This is a Go-based CLI tool for interacting with JuliaHub, a platform for Julia computing. The CLI provides commands for authentication, dataset management, registry management, project management, user information, Git integration, and Julia integration.
This is a Go-based CLI tool for interacting with JuliaHub, a platform for Julia computing. The CLI provides commands for authentication, dataset management, registry management, project management, user information, token management, Git integration, and Julia integration.

## Architecture

Expand All @@ -16,6 +16,7 @@ The application follows a command-line interface pattern using the Cobra library
- **registries.go**: Registry operations (list) with REST API integration
- **projects.go**: Project management using GraphQL API with user filtering
- **user.go**: User information retrieval using GraphQL API and REST API for listing users
- **tokens.go**: Token management operations (list) with REST API integration
- **git.go**: Git integration (clone, push, fetch, pull) with JuliaHub authentication
- **julia.go**: Julia installation and management
- **run.go**: Julia execution with JuliaHub configuration
Expand All @@ -30,7 +31,7 @@ The application follows a command-line interface pattern using the Cobra library
- Stores tokens securely in `~/.juliahub` with 0600 permissions

2. **API Integration**:
- **REST API**: Used for dataset operations (`/api/v1/datasets`, `/datasets/{uuid}/url/{version}`), registry operations (`/api/v1/ui/registries/descriptions`) and user management (`/app/config/features/manage`)
- **REST API**: Used for dataset operations (`/api/v1/datasets`, `/datasets/{uuid}/url/{version}`), registry operations (`/api/v1/ui/registries/descriptions`), token management (`/app/token/activelist`) and user management (`/app/config/features/manage`)
- **GraphQL API**: Used for projects and user info (`/v1/graphql`)
- **Headers**: All GraphQL requests require `X-Hasura-Role: jhuser` header
- **Authentication**: Uses ID tokens (`token.IDToken`) for API calls
Expand All @@ -41,8 +42,9 @@ The application follows a command-line interface pattern using the Cobra library
- `jh registry`: Registry operations (list with REST API, supports verbose mode)
- `jh project`: Project management (list with GraphQL, supports user filtering)
- `jh user`: User information (info with GraphQL)
- `jh admin`: Administrative commands (user management)
- `jh admin`: Administrative commands (user management, token management)
- `jh admin user`: User management (list all users with REST API, supports verbose mode)
- `jh admin token`: Token management (list all tokens with REST API, supports verbose mode)
- `jh clone`: Git clone with JuliaHub authentication and project name resolution
- `jh push/fetch/pull`: Git operations with JuliaHub authentication
- `jh git-credential`: Git credential helper for seamless authentication
Expand Down Expand Up @@ -104,6 +106,13 @@ go run . admin user list
go run . admin user list --verbose
```

### Test token operations
```bash
go run . admin token list
go run . admin token list --verbose
TZ=America/New_York go run . admin token list --verbose # With specific timezone
```

### Test Git operations
```bash
go run . clone john/my-project # Clone from another user
Expand Down Expand Up @@ -177,6 +186,7 @@ The application uses OAuth2 device flow:
### REST API Integration
- **Dataset operations**: Use presigned URLs for upload/download
- **User management**: `/app/config/features/manage` endpoint for listing all users
- **Token management**: `/app/token/activelist` endpoint for listing all API tokens
- **Authentication**: Bearer token with ID token
- **Upload workflow**: 3-step process (request presigned URL, upload to URL, close upload)

Expand Down Expand Up @@ -294,6 +304,10 @@ jh run setup
- Admin user list command (`jh admin user list`) uses REST API endpoint `/app/config/features/manage` which requires appropriate permissions
- User list output is concise by default (Name and Email only); use `--verbose` flag for detailed information (UUID, groups, features)
- Registry list output is concise by default (UUID and Name only); use `--verbose` flag for detailed information (owner, creation date, package count, description)
- Admin token list command (`jh admin token list`) uses REST API endpoint `/app/token/activelist` which requires appropriate permissions
- Token list output is concise by default (Subject, Created By, and Expired status only); use `--verbose` flag for detailed information (signature, creation date, expiration date with estimate indicator)
- Token dates are formatted in human-readable format and converted to local timezone (respects system timezone or TZ environment variable)
- Token expiration estimate indicator only shown when `expires_at_is_estimate` is true in API response

## Implementation Details

Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A command-line interface for interacting with JuliaHub, a platform for Julia com
- **Git Integration**: Clone, push, fetch, and pull with automatic JuliaHub authentication
- **Julia Integration**: Install Julia and run with JuliaHub package server configuration
- **User Management**: Display user information and view profile details
- **Administrative Commands**: Manage users and system resources (requires admin permissions)
- **Administrative Commands**: Manage users, tokens, and system resources (requires admin permissions)

## Installation

Expand Down Expand Up @@ -186,10 +186,16 @@ go build -o jh .

### Administrative Commands (`jh admin`)

#### User Management
- `jh admin user list` - List all users (requires appropriate permissions)
- Default: Shows only Name and Email
- `jh admin user list --verbose` - Show detailed user information including UUID, groups, and features

#### Token Management
- `jh admin token list` - List all tokens (requires appropriate permissions)
- Default: Shows only Subject, Created By, and Expired status
- `jh admin token list --verbose` - Show detailed token information including signature, creation date, expiration date (with estimate indicator)

### Update (`jh update`)

- `jh update` - Check for updates and automatically install the latest version
Expand Down Expand Up @@ -254,6 +260,28 @@ jh project list --user
jh project list --user alice
```

### Administrative Operations

```bash
# List all users (requires admin permissions)
jh admin user list

# List users with detailed information
jh admin user list --verbose

# List all tokens (requires admin permissions)
jh admin token list

# List tokens with detailed information including signatures and dates
jh admin token list --verbose

# List tokens on custom server
jh admin token list -s yourinstall

# Use specific timezone for date display
TZ=America/New_York jh admin token list --verbose
```

### Git Workflow

```bash
Expand Down
50 changes: 47 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ Available command categories:
registry - Registry management (list registries)
project - Project management (list, filter by user)
user - User information and profile
admin - Administrative commands (user management)
admin - Administrative commands (user management, token management)
clone - Clone projects with automatic authentication
push - Push changes with authentication
fetch - Fetch updates with authentication
Expand Down Expand Up @@ -1036,7 +1036,7 @@ var adminCmd = &cobra.Command{
Long: `Administrative commands for JuliaHub.

These commands provide administrative functionality for managing JuliaHub
resources such as users, groups, and system configuration.
resources such as users, tokens, groups, and system configuration.

Note: Some commands may require administrative permissions.`,
}
Expand All @@ -1051,6 +1051,47 @@ Provides commands to list and manage users across the JuliaHub instance.
Note: These commands require appropriate administrative permissions.`,
}

var adminTokenCmd = &cobra.Command{
Use: "token",
Short: "Token management commands",
Long: `Administrative commands for managing API tokens on JuliaHub.

Provides commands to list and manage API tokens across the JuliaHub instance.

Note: These commands require appropriate administrative permissions.`,
}

var tokenListCmd = &cobra.Command{
Use: "list",
Short: "List all tokens",
Long: `List all API tokens from JuliaHub.

By default, displays only Subject, Created By, and Expired status for each token.
Use --verbose flag to display comprehensive information including:
- Subject and signature
- Created by and creation date
- Expiration date (with estimate indicator)
- Expiration status

This command uses the /app/token/activelist endpoint which requires
appropriate permissions to view all tokens.`,
Example: " jh admin token list\n jh admin token list --verbose",
Run: func(cmd *cobra.Command, args []string) {
server, err := getServerFromFlagOrConfig(cmd)
if err != nil {
fmt.Printf("Failed to get server config: %v\n", err)
os.Exit(1)
}

verbose, _ := cmd.Flags().GetBool("verbose")

if err := listTokens(server, verbose); err != nil {
fmt.Printf("Failed to list tokens: %v\n", err)
os.Exit(1)
}
},
}

var updateCmd = &cobra.Command{
Use: "update",
Short: "Update jh to the latest version",
Expand Down Expand Up @@ -1087,6 +1128,8 @@ func init() {
userInfoCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
userListCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
userListCmd.Flags().Bool("verbose", false, "Show detailed user information")
tokenListCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
tokenListCmd.Flags().Bool("verbose", false, "Show detailed token information")
cloneCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
pushCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
fetchCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
Expand All @@ -1100,7 +1143,8 @@ func init() {
projectCmd.AddCommand(projectListCmd)
userCmd.AddCommand(userInfoCmd)
adminUserCmd.AddCommand(userListCmd)
adminCmd.AddCommand(adminUserCmd)
adminTokenCmd.AddCommand(tokenListCmd)
adminCmd.AddCommand(adminUserCmd, adminTokenCmd)
juliaCmd.AddCommand(juliaInstallCmd)
runCmd.AddCommand(runSetupCmd)
gitCredentialCmd.AddCommand(gitCredentialHelperCmd, gitCredentialGetCmd, gitCredentialStoreCmd, gitCredentialEraseCmd, gitCredentialSetupCmd)
Expand Down
109 changes: 109 additions & 0 deletions tokens.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package main

import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)

type Token struct {
CreatedBy string `json:"created_by"`
IsExpired bool `json:"is_expired"`
CreatedAt string `json:"created_at"`
ExpiresAt string `json:"expires_at"`
ExpiresAtIsEstimate bool `json:"expires_at_is_estimate,omitempty"`
Subject string `json:"subject"`
Signature string `json:"signature"`
}

type TokensResponse struct {
Tokens []Token `json:"tokens"`
Message string `json:"message"`
Success bool `json:"success"`
}

func formatTokenDate(dateStr string) string {
// Try parsing as RFC3339 with fractional seconds
t, err := time.Parse(time.RFC3339, dateStr)
if err != nil {
return dateStr
}

// Convert to local timezone
localTime := t.Local()
return localTime.Format("Jan 02, 2006 15:04:05 -0700")
}

func listTokens(server string, verbose bool) error {
token, err := ensureValidToken()
if err != nil {
return fmt.Errorf("authentication required: %w", err)
}

url := fmt.Sprintf("https://%s/app/token/activelist", server)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}

req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.IDToken))
req.Header.Set("Accept", "application/json")

client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body))
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}

var response TokensResponse
if err := json.Unmarshal(body, &response); err != nil {
return fmt.Errorf("failed to parse response: %w", err)
}

if !response.Success {
return fmt.Errorf("API request failed: %s", response.Message)
}

// Display tokens
fmt.Printf("Tokens (%d total):\n\n", len(response.Tokens))

if verbose {
// Verbose mode: show all details
for _, tok := range response.Tokens {
fmt.Printf("Subject: %s\n", tok.Subject)
fmt.Printf("Signature: %s\n", tok.Signature)
fmt.Printf("Created By: %s\n", tok.CreatedBy)
fmt.Printf("Created At: %s\n", formatTokenDate(tok.CreatedAt))
fmt.Printf("Expires At: %s", formatTokenDate(tok.ExpiresAt))
if tok.ExpiresAtIsEstimate {
fmt.Printf(" (estimate)")
}
fmt.Printf("\n")
fmt.Printf("Expired: %t\n", tok.IsExpired)
fmt.Println()
}
} else {
// Default mode: show only Subject, Created By, and Expired status
for _, tok := range response.Tokens {
fmt.Printf("Subject: %s\n", tok.Subject)
fmt.Printf("Created By: %s\n", tok.CreatedBy)
fmt.Printf("Expired: %t\n", tok.IsExpired)
fmt.Println()
}
}

return nil
}