Skip to content

Commit cc10e03

Browse files
committed
Created init command to configure the script
Signed-off-by: Kevin Stanley <stanleyk@objectcomputing.com>
1 parent 17d4f6f commit cc10e03

4 files changed

Lines changed: 509 additions & 3 deletions

File tree

unityauth-cli/docs/user-guide.md

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Complete command reference for the UnityAuth command-line interface.
55
## Table of Contents
66

77
- [Global Options](#global-options)
8+
- [Setup Commands](#setup-commands)
9+
- [init](#init)
810
- [Authentication Commands](#authentication-commands)
911
- [login](#login)
1012
- [logout](#logout)
@@ -60,6 +62,79 @@ unityauth --verbose login
6062

6163
---
6264

65+
## Setup Commands
66+
67+
### init
68+
69+
Initialize UnityAuth CLI with a guided setup wizard. This is the recommended first command for new users.
70+
71+
```
72+
unityauth init [OPTIONS]
73+
```
74+
75+
**Options:**
76+
77+
| Option | Description |
78+
|--------|-------------|
79+
| `--api-url TEXT` | UnityAuth API endpoint URL (skips prompt if provided) |
80+
| `--skip-login` | Skip the login step after configuration |
81+
| `--allow-http` | Allow insecure HTTP URLs (not recommended for production) |
82+
83+
**Behavior:**
84+
85+
1. Prompts for API URL (or accepts via `--api-url`)
86+
2. Tests connection to the UnityAuth server
87+
3. Saves configuration to `~/.config/unityauth-cli/config.yml`
88+
4. Optionally prompts to log in (in interactive mode)
89+
5. Displays helpful next steps
90+
91+
**Examples:**
92+
93+
```bash
94+
# Interactive setup (recommended for first-time users)
95+
unityauth init
96+
97+
# Non-interactive setup with API URL
98+
unityauth init --api-url https://auth.example.com --skip-login
99+
100+
# Setup for local development (allows HTTP)
101+
unityauth init --api-url http://localhost:8081 --allow-http --skip-login
102+
```
103+
104+
**Sample Output:**
105+
106+
```
107+
Welcome to UnityAuth CLI!
108+
Let's get you set up.
109+
110+
API URL: https://auth.example.com
111+
Testing connection to https://auth.example.com...
112+
✓ Connection successful
113+
✓ Configuration saved to ~/.config/unityauth-cli/config.yml
114+
115+
Would you like to log in now? [Y/n]: y
116+
Email: admin@example.com
117+
Password: ********
118+
✓ Logged in as admin@example.com
119+
120+
Setup complete!
121+
122+
Next steps:
123+
$ unityauth tenant list # List your tenants
124+
$ unityauth user list --tenant-id 1 # List users in tenant 1
125+
$ unityauth role list # List available roles
126+
$ unityauth --help # See all commands
127+
```
128+
129+
**Exit Codes:**
130+
131+
| Code | Meaning |
132+
|------|---------|
133+
| 0 | Setup successful |
134+
| 4 | Configuration error (invalid URL, connection failed) |
135+
136+
---
137+
63138
## Authentication Commands
64139

65140
### login
@@ -728,11 +803,14 @@ unityauth logout
728803
### "API URL not configured"
729804

730805
```bash
731-
# Check current configuration
732-
unityauth config show
806+
# First-time setup (recommended)
807+
unityauth init
733808

734-
# Set API URL
809+
# Or manually set API URL
735810
unityauth config set api_url https://auth.example.com
811+
812+
# Check current configuration
813+
unityauth config show
736814
```
737815

738816
### "Not authenticated"

unityauth-cli/src/unityauth_cli/cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,15 @@ def register_commands() -> None:
235235
"""Register all CLI commands with the main group."""
236236
from unityauth_cli.commands.login import login, logout, token_info
237237
from unityauth_cli.commands.config import config
238+
from unityauth_cli.commands.init import init
238239
from unityauth_cli.commands.users import create, update, update_profile, list_users
239240
from unityauth_cli.commands.tenants import list_tenants, tenant_users
240241
from unityauth_cli.commands.roles import list_roles
241242
from unityauth_cli.commands.permissions import list_permissions
242243

244+
# Register setup command
245+
cli.add_command(init)
246+
243247
# Register authentication commands
244248
cli.add_command(login)
245249
cli.add_command(logout)
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
"""Init command: First-time setup wizard for UnityAuth CLI.
2+
3+
Guides users through initial configuration and authentication.
4+
"""
5+
6+
import sys
7+
8+
import click
9+
import requests
10+
11+
from unityauth_cli.cli import CLIContext, console, error, info, pass_context, success, warning
12+
from unityauth_cli.config import Configuration
13+
from unityauth_cli.utils.validation import validate_url, validate_email
14+
15+
16+
@click.command()
17+
@click.option(
18+
'--api-url',
19+
help='UnityAuth API endpoint URL (skip prompt)',
20+
)
21+
@click.option(
22+
'--skip-login',
23+
is_flag=True,
24+
help='Skip the login step after configuration',
25+
)
26+
@click.option(
27+
'--allow-http',
28+
is_flag=True,
29+
help='Allow insecure HTTP URLs (not recommended)',
30+
)
31+
@pass_context
32+
def init(ctx: CLIContext, api_url: str | None, skip_login: bool, allow_http: bool) -> None:
33+
"""Initialize UnityAuth CLI with first-time setup wizard.
34+
35+
Guides you through configuring the API endpoint and optionally
36+
authenticating with your credentials.
37+
38+
\b
39+
Examples:
40+
unityauth init # Interactive setup
41+
unityauth init --api-url https://auth.example.com # Skip URL prompt
42+
unityauth init --skip-login # Configure only, no login
43+
"""
44+
console.print()
45+
console.print("[bold cyan]Welcome to UnityAuth CLI![/bold cyan]")
46+
console.print("Let's get you set up.\n")
47+
48+
# Step 1: Get API URL
49+
if not api_url:
50+
if not sys.stdin.isatty():
51+
error(
52+
"API URL required in non-interactive mode",
53+
"Use --api-url option to specify the UnityAuth API endpoint"
54+
)
55+
sys.exit(4)
56+
57+
# Show current value if configured
58+
current_url = ctx.config.get('api_url') if ctx.config else None
59+
if current_url:
60+
info(f"Current API URL: {current_url}")
61+
62+
api_url = click.prompt(
63+
'API URL',
64+
default=current_url or 'https://auth.example.com',
65+
type=str
66+
)
67+
68+
# Validate URL format
69+
api_url = api_url.strip().rstrip('/')
70+
71+
if not validate_url(api_url, require_https=not allow_http):
72+
if not allow_http and api_url.startswith('http://'):
73+
error(
74+
"HTTP URLs are not allowed by default (insecure)",
75+
"Use HTTPS or add --allow-http flag if this is intentional"
76+
)
77+
else:
78+
error(
79+
"Invalid URL format",
80+
"URL must start with https:// (e.g., https://auth.example.com)"
81+
)
82+
sys.exit(4)
83+
84+
# Step 2: Test connection
85+
info(f"Testing connection to {api_url}...")
86+
87+
try:
88+
# Try to reach the /keys endpoint (public, no auth required)
89+
response = requests.get(f"{api_url}/keys", timeout=10)
90+
91+
if response.ok:
92+
success("Connection successful")
93+
if ctx.verbose:
94+
info(f"Server responded with status {response.status_code}")
95+
else:
96+
# Server responded but with error
97+
warning(f"Server responded with status {response.status_code}")
98+
if not click.confirm("Continue anyway?", default=False):
99+
info("Setup cancelled")
100+
sys.exit(0)
101+
102+
except requests.ConnectionError:
103+
error(
104+
f"Could not connect to {api_url}",
105+
"Check that the URL is correct and the server is running"
106+
)
107+
if not click.confirm("Save configuration anyway?", default=False):
108+
info("Setup cancelled")
109+
sys.exit(0)
110+
except requests.Timeout:
111+
warning("Connection timed out")
112+
if not click.confirm("Save configuration anyway?", default=False):
113+
info("Setup cancelled")
114+
sys.exit(0)
115+
except requests.RequestException as e:
116+
warning(f"Connection test failed: {e}")
117+
if not click.confirm("Save configuration anyway?", default=False):
118+
info("Setup cancelled")
119+
sys.exit(0)
120+
121+
# Step 3: Save configuration
122+
try:
123+
config = ctx.config or Configuration()
124+
config.set('api_url', api_url)
125+
config.save()
126+
success(f"Configuration saved to {config.config_path}")
127+
except Exception as e:
128+
error(f"Failed to save configuration: {e}")
129+
sys.exit(4)
130+
131+
# Step 4: Optionally login
132+
if not skip_login:
133+
console.print()
134+
if sys.stdin.isatty() and click.confirm("Would you like to log in now?", default=True):
135+
console.print()
136+
_do_login(api_url, config, ctx.verbose)
137+
else:
138+
info("Skipping login. Run 'unityauth login' when ready.")
139+
140+
# Step 5: Show next steps
141+
console.print()
142+
console.print("[bold green]Setup complete![/bold green]")
143+
console.print()
144+
console.print("Next steps:")
145+
console.print(" [dim]$[/dim] unityauth tenant list [dim]# List your tenants[/dim]")
146+
console.print(" [dim]$[/dim] unityauth user list --tenant-id 1 [dim]# List users in tenant 1[/dim]")
147+
console.print(" [dim]$[/dim] unityauth role list [dim]# List available roles[/dim]")
148+
console.print(" [dim]$[/dim] unityauth --help [dim]# See all commands[/dim]")
149+
console.print()
150+
151+
152+
def _do_login(api_url: str, config: Configuration, verbose: bool) -> None:
153+
"""Perform login as part of init wizard.
154+
155+
Args:
156+
api_url: API endpoint URL
157+
config: Configuration instance
158+
verbose: Whether to show verbose output
159+
"""
160+
from unityauth_cli import auth
161+
from unityauth_cli.client import UnityAuthAPIClient
162+
163+
# Get email
164+
email = click.prompt('Email', type=str)
165+
166+
if not validate_email(email):
167+
error("Invalid email format")
168+
return
169+
170+
# Get password
171+
password = click.prompt('Password', hide_input=True, type=str)
172+
173+
# Attempt login
174+
if verbose:
175+
info(f"Authenticating with {api_url}...")
176+
177+
try:
178+
timeout = config.get('timeout', 30)
179+
client = UnityAuthAPIClient(api_url, timeout=timeout)
180+
response = client.post('/api/login', data={
181+
'username': email,
182+
'password': password
183+
})
184+
185+
# Extract token from response
186+
token = response.get('access_token') or response.get('accessToken')
187+
if not token:
188+
error("Login failed: No token in response")
189+
return
190+
191+
# Store token in keyring
192+
auth.store_token(api_url, token)
193+
success(f"Logged in as {email}")
194+
195+
except Exception as e:
196+
error(f"Login failed: {e}")
197+
info("You can try again later with 'unityauth login'")

0 commit comments

Comments
 (0)