xenfra 0.2.1__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xenfra-0.2.3/PKG-INFO +115 -0
- xenfra-0.2.3/README.md +83 -0
- {xenfra-0.2.1 → xenfra-0.2.3}/pyproject.toml +19 -20
- xenfra-0.2.3/src/xenfra/__init__.py +0 -0
- xenfra-0.2.3/src/xenfra/commands/__init__.py +3 -0
- xenfra-0.2.3/src/xenfra/commands/auth.py +186 -0
- xenfra-0.2.3/src/xenfra/commands/deployments.py +443 -0
- xenfra-0.2.3/src/xenfra/commands/intelligence.py +312 -0
- xenfra-0.2.3/src/xenfra/commands/projects.py +163 -0
- xenfra-0.2.3/src/xenfra/commands/security_cmd.py +235 -0
- xenfra-0.2.3/src/xenfra/main.py +70 -0
- xenfra-0.2.3/src/xenfra/utils/__init__.py +3 -0
- xenfra-0.2.3/src/xenfra/utils/auth.py +148 -0
- xenfra-0.2.3/src/xenfra/utils/codebase.py +86 -0
- xenfra-0.2.3/src/xenfra/utils/config.py +278 -0
- xenfra-0.2.3/src/xenfra/utils/security.py +350 -0
- xenfra-0.2.1/PKG-INFO +0 -95
- xenfra-0.2.1/README.md +0 -57
- xenfra-0.2.1/src/xenfra/__init__.py +0 -1
- xenfra-0.2.1/src/xenfra/api/auth.py +0 -51
- xenfra-0.2.1/src/xenfra/api/billing.py +0 -80
- xenfra-0.2.1/src/xenfra/api/connections.py +0 -163
- xenfra-0.2.1/src/xenfra/api/main.py +0 -175
- xenfra-0.2.1/src/xenfra/api/webhooks.py +0 -146
- xenfra-0.2.1/src/xenfra/cli/main.py +0 -211
- xenfra-0.2.1/src/xenfra/config.py +0 -24
- xenfra-0.2.1/src/xenfra/db/models.py +0 -51
- xenfra-0.2.1/src/xenfra/db/session.py +0 -17
- xenfra-0.2.1/src/xenfra/dependencies.py +0 -35
- xenfra-0.2.1/src/xenfra/dockerizer.py +0 -89
- xenfra-0.2.1/src/xenfra/engine.py +0 -292
- xenfra-0.2.1/src/xenfra/mcp_client.py +0 -149
- xenfra-0.2.1/src/xenfra/models.py +0 -54
- xenfra-0.2.1/src/xenfra/recipes.py +0 -23
- xenfra-0.2.1/src/xenfra/security.py +0 -58
- xenfra-0.2.1/src/xenfra/templates/Dockerfile.j2 +0 -25
- xenfra-0.2.1/src/xenfra/templates/cloud-init.sh.j2 +0 -68
- xenfra-0.2.1/src/xenfra/templates/docker-compose.yml.j2 +0 -33
- xenfra-0.2.1/src/xenfra/utils.py +0 -69
xenfra-0.2.3/PKG-INFO
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: xenfra
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: A 'Zen Mode' infrastructure engine for Python developers.
|
|
5
|
+
Author: xenfra-cloud
|
|
6
|
+
Author-email: xenfra-cloud <xenfracloud@gmail.com>
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
13
|
+
Classifier: Topic :: System :: Systems Administration
|
|
14
|
+
Requires-Dist: click>=8.1.7
|
|
15
|
+
Requires-Dist: rich>=14.2.0
|
|
16
|
+
Requires-Dist: sqlmodel>=0.0.16
|
|
17
|
+
Requires-Dist: python-digitalocean>=1.17.0
|
|
18
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
19
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
20
|
+
Requires-Dist: fabric>=3.2.2
|
|
21
|
+
Requires-Dist: xenfra-sdk
|
|
22
|
+
Requires-Dist: httpx>=0.27.0
|
|
23
|
+
Requires-Dist: keyring>=25.7.0
|
|
24
|
+
Requires-Dist: keyrings-alt>=5.0.2
|
|
25
|
+
Requires-Dist: pytest>=8.0.0 ; extra == 'test'
|
|
26
|
+
Requires-Dist: pytest-mock>=3.12.0 ; extra == 'test'
|
|
27
|
+
Requires-Python: >=3.13
|
|
28
|
+
Project-URL: Homepage, https://github.com/xenfra-cloud/xenfra
|
|
29
|
+
Project-URL: Issues, https://github.com/xenfra-cloud/xenfra/issues
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Xenfra CLI
|
|
34
|
+
|
|
35
|
+
## Xenfra CLI: Deploy Python Apps with Zen Mode
|
|
36
|
+
|
|
37
|
+
The Xenfra CLI is a powerful and intuitive command-line interface designed to streamline the deployment of Python applications to DigitalOcean. Built with a "Zen Mode" philosophy, it automates complex infrastructure tasks, allowing developers to focus on writing code.
|
|
38
|
+
|
|
39
|
+
### ✨ Key Features
|
|
40
|
+
|
|
41
|
+
* **Zero-Configuration Deployment:** Automatically detects your project's framework and dependencies.
|
|
42
|
+
* **AI-Powered Auto-Healing:** Diagnoses common deployment failures and suggests, or even applies, fixes automatically.
|
|
43
|
+
* **Real-time Monitoring:** View deployment status and stream live application logs directly from your terminal.
|
|
44
|
+
* **Integrated Project Management:** Easily list, view, and destroy your deployed projects.
|
|
45
|
+
* **Secure Authentication:** Uses OAuth2 PKCE flow for secure, token-based authentication.
|
|
46
|
+
|
|
47
|
+
### 🚀 Quickstart
|
|
48
|
+
|
|
49
|
+
#### 1. Installation
|
|
50
|
+
|
|
51
|
+
Install the Xenfra CLI using `uv` (recommended) or `pip`:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv pip install xenfra-cli
|
|
55
|
+
# or
|
|
56
|
+
pip install xenfra-cli
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### 2. Authentication
|
|
60
|
+
|
|
61
|
+
Log in to your Xenfra account. This will open your web browser to complete the OAuth2 flow.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
xenfra auth login
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
#### 3. Initialize Your Project
|
|
68
|
+
|
|
69
|
+
Navigate to your Python project's root directory and run `init`. The CLI will scan your codebase, detect its characteristics, and generate a `xenfra.yaml` configuration file.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
cd your-python-project/
|
|
73
|
+
xenfra init
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### 4. Deploy Your Application
|
|
77
|
+
|
|
78
|
+
Once `xenfra.yaml` is configured, deploy your application. The CLI will handle provisioning a DigitalOcean Droplet, setting up Docker, and deploying your code.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
xenfra deploy
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 📋 Usage Examples
|
|
85
|
+
|
|
86
|
+
* **Monitor Deployment Status:**
|
|
87
|
+
```bash
|
|
88
|
+
xenfra status <deployment-id>
|
|
89
|
+
```
|
|
90
|
+
* **Stream Application Logs:**
|
|
91
|
+
```bash
|
|
92
|
+
xenfra logs <deployment-id>
|
|
93
|
+
```
|
|
94
|
+
* **List Deployed Projects:**
|
|
95
|
+
```bash
|
|
96
|
+
xenfra projects list
|
|
97
|
+
```
|
|
98
|
+
* **Diagnose a Failed Deployment (AI-Powered):**
|
|
99
|
+
```bash
|
|
100
|
+
xenfra diagnose <deployment-id>
|
|
101
|
+
# Or to diagnose from a log file:
|
|
102
|
+
xenfra diagnose --logs error.log
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 📚 Documentation
|
|
106
|
+
|
|
107
|
+
For more detailed information, advanced configurations, and API references, please refer to the [official Xenfra Documentation](https://docs.xenfra.com/cli) (Link will be updated upon final deployment).
|
|
108
|
+
|
|
109
|
+
### 🤝 Contributing
|
|
110
|
+
|
|
111
|
+
We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for more details.
|
|
112
|
+
|
|
113
|
+
### 📄 License
|
|
114
|
+
|
|
115
|
+
This project is licensed under the [MIT License](LICENSE).
|
xenfra-0.2.3/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Xenfra CLI
|
|
2
|
+
|
|
3
|
+
## Xenfra CLI: Deploy Python Apps with Zen Mode
|
|
4
|
+
|
|
5
|
+
The Xenfra CLI is a powerful and intuitive command-line interface designed to streamline the deployment of Python applications to DigitalOcean. Built with a "Zen Mode" philosophy, it automates complex infrastructure tasks, allowing developers to focus on writing code.
|
|
6
|
+
|
|
7
|
+
### ✨ Key Features
|
|
8
|
+
|
|
9
|
+
* **Zero-Configuration Deployment:** Automatically detects your project's framework and dependencies.
|
|
10
|
+
* **AI-Powered Auto-Healing:** Diagnoses common deployment failures and suggests, or even applies, fixes automatically.
|
|
11
|
+
* **Real-time Monitoring:** View deployment status and stream live application logs directly from your terminal.
|
|
12
|
+
* **Integrated Project Management:** Easily list, view, and destroy your deployed projects.
|
|
13
|
+
* **Secure Authentication:** Uses OAuth2 PKCE flow for secure, token-based authentication.
|
|
14
|
+
|
|
15
|
+
### 🚀 Quickstart
|
|
16
|
+
|
|
17
|
+
#### 1. Installation
|
|
18
|
+
|
|
19
|
+
Install the Xenfra CLI using `uv` (recommended) or `pip`:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv pip install xenfra-cli
|
|
23
|
+
# or
|
|
24
|
+
pip install xenfra-cli
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
#### 2. Authentication
|
|
28
|
+
|
|
29
|
+
Log in to your Xenfra account. This will open your web browser to complete the OAuth2 flow.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
xenfra auth login
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
#### 3. Initialize Your Project
|
|
36
|
+
|
|
37
|
+
Navigate to your Python project's root directory and run `init`. The CLI will scan your codebase, detect its characteristics, and generate a `xenfra.yaml` configuration file.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd your-python-project/
|
|
41
|
+
xenfra init
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
#### 4. Deploy Your Application
|
|
45
|
+
|
|
46
|
+
Once `xenfra.yaml` is configured, deploy your application. The CLI will handle provisioning a DigitalOcean Droplet, setting up Docker, and deploying your code.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
xenfra deploy
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 📋 Usage Examples
|
|
53
|
+
|
|
54
|
+
* **Monitor Deployment Status:**
|
|
55
|
+
```bash
|
|
56
|
+
xenfra status <deployment-id>
|
|
57
|
+
```
|
|
58
|
+
* **Stream Application Logs:**
|
|
59
|
+
```bash
|
|
60
|
+
xenfra logs <deployment-id>
|
|
61
|
+
```
|
|
62
|
+
* **List Deployed Projects:**
|
|
63
|
+
```bash
|
|
64
|
+
xenfra projects list
|
|
65
|
+
```
|
|
66
|
+
* **Diagnose a Failed Deployment (AI-Powered):**
|
|
67
|
+
```bash
|
|
68
|
+
xenfra diagnose <deployment-id>
|
|
69
|
+
# Or to diagnose from a log file:
|
|
70
|
+
xenfra diagnose --logs error.log
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 📚 Documentation
|
|
74
|
+
|
|
75
|
+
For more detailed information, advanced configurations, and API references, please refer to the [official Xenfra Documentation](https://docs.xenfra.com/cli) (Link will be updated upon final deployment).
|
|
76
|
+
|
|
77
|
+
### 🤝 Contributing
|
|
78
|
+
|
|
79
|
+
We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for more details.
|
|
80
|
+
|
|
81
|
+
### 📄 License
|
|
82
|
+
|
|
83
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "xenfra"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
4
4
|
description = "A 'Zen Mode' infrastructure engine for Python developers."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
7
7
|
{ name = "xenfra-cloud", email = "xenfracloud@gmail.com" }
|
|
8
8
|
]
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
classifiers = [
|
|
11
11
|
"Programming Language :: Python :: 3",
|
|
12
12
|
"License :: OSI Approved :: MIT License",
|
|
@@ -16,39 +16,38 @@ classifiers = [
|
|
|
16
16
|
"Topic :: Software Development :: Build Tools",
|
|
17
17
|
"Topic :: System :: Systems Administration",
|
|
18
18
|
]
|
|
19
|
+
|
|
19
20
|
dependencies = [
|
|
20
|
-
"fabric>=3.2.2",
|
|
21
|
-
"python-digitalocean>=1.17.0",
|
|
22
|
-
"python-dotenv>=1.2.1",
|
|
23
|
-
"rich>=14.2.0",
|
|
24
|
-
"fastapi>=0.110.0",
|
|
25
|
-
"uvicorn[standard]>=0.27.1",
|
|
26
21
|
"click>=8.1.7",
|
|
22
|
+
"rich>=14.2.0",
|
|
27
23
|
"sqlmodel>=0.0.16",
|
|
28
|
-
"
|
|
29
|
-
"python-
|
|
30
|
-
"
|
|
24
|
+
"python-digitalocean>=1.17.0",
|
|
25
|
+
"python-dotenv>=1.2.1",
|
|
26
|
+
"pyyaml>=6.0.1",
|
|
27
|
+
"fabric>=3.2.2",
|
|
28
|
+
"xenfra-sdk",
|
|
31
29
|
"httpx>=0.27.0",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"bcrypt==4.0.1",
|
|
35
|
-
"Jinja2>=3.1.3",
|
|
36
|
-
"pytest>=9.0.2",
|
|
30
|
+
"keyring>=25.7.0",
|
|
31
|
+
"keyrings.alt>=5.0.2",
|
|
37
32
|
]
|
|
33
|
+
requires-python = ">=3.13"
|
|
34
|
+
|
|
35
|
+
[tool.uv.sources]
|
|
36
|
+
xenfra-sdk = { workspace = true }
|
|
38
37
|
|
|
39
38
|
[project.urls]
|
|
40
39
|
Homepage = "https://github.com/xenfra-cloud/xenfra"
|
|
41
40
|
Issues = "https://github.com/xenfra-cloud/xenfra/issues"
|
|
42
41
|
|
|
43
|
-
[project.scripts]
|
|
44
|
-
xenfra = "xenfra.cli.main:main"
|
|
45
|
-
|
|
46
42
|
[project.optional-dependencies]
|
|
47
43
|
test = [
|
|
48
44
|
"pytest>=8.0.0",
|
|
49
45
|
"pytest-mock>=3.12.0",
|
|
50
46
|
]
|
|
51
47
|
|
|
48
|
+
[project.scripts]
|
|
49
|
+
xenfra = "xenfra_cli.main:main"
|
|
50
|
+
|
|
52
51
|
[build-system]
|
|
53
52
|
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
54
|
-
build-backend = "uv_build"
|
|
53
|
+
build-backend = "uv_build"
|
|
File without changes
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication commands for Xenfra CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import secrets
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import webbrowser
|
|
10
|
+
from http.server import HTTPServer
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
import httpx
|
|
14
|
+
import keyring
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
|
|
17
|
+
from ..utils.auth import (
|
|
18
|
+
API_BASE_URL,
|
|
19
|
+
CLI_CLIENT_ID,
|
|
20
|
+
CLI_LOCAL_SERVER_END_PORT,
|
|
21
|
+
CLI_LOCAL_SERVER_START_PORT,
|
|
22
|
+
CLI_REDIRECT_PATH,
|
|
23
|
+
SERVICE_ID,
|
|
24
|
+
AuthCallbackHandler,
|
|
25
|
+
clear_tokens,
|
|
26
|
+
get_auth_token,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
console = Console()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@click.group()
|
|
33
|
+
def auth():
|
|
34
|
+
"""Authentication commands."""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@auth.command()
|
|
39
|
+
def login():
|
|
40
|
+
"""Login to Xenfra using OAuth2 PKCE flow."""
|
|
41
|
+
global oauth_data
|
|
42
|
+
oauth_data = {"code": None, "state": None, "error": None}
|
|
43
|
+
|
|
44
|
+
# 1. Generate PKCE parameters
|
|
45
|
+
code_verifier = secrets.token_urlsafe(96)
|
|
46
|
+
code_challenge = (
|
|
47
|
+
base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest())
|
|
48
|
+
.decode()
|
|
49
|
+
.rstrip("=")
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# 2. Generate state for CSRF protection
|
|
53
|
+
state = secrets.token_urlsafe(32)
|
|
54
|
+
|
|
55
|
+
# 3. Start local HTTP server
|
|
56
|
+
server_port = None
|
|
57
|
+
httpd_instance = None
|
|
58
|
+
for port in range(CLI_LOCAL_SERVER_START_PORT, CLI_LOCAL_SERVER_END_PORT + 1):
|
|
59
|
+
try:
|
|
60
|
+
server_address = ("127.0.0.1", port)
|
|
61
|
+
httpd_instance = HTTPServer(server_address, AuthCallbackHandler)
|
|
62
|
+
server_port = port
|
|
63
|
+
break
|
|
64
|
+
except OSError:
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
if not server_port:
|
|
68
|
+
console.print(
|
|
69
|
+
f"[bold red]Error: No available ports in range {CLI_LOCAL_SERVER_START_PORT}-{CLI_LOCAL_SERVER_END_PORT}[/bold red]"
|
|
70
|
+
)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
redirect_uri = f"http://localhost:{server_port}{CLI_REDIRECT_PATH}"
|
|
74
|
+
|
|
75
|
+
# 4. Construct Authorization URL
|
|
76
|
+
auth_url = (
|
|
77
|
+
f"{API_BASE_URL}/auth/authorize?"
|
|
78
|
+
f"client_id={CLI_CLIENT_ID}&"
|
|
79
|
+
f"redirect_uri={urllib.parse.quote(redirect_uri)}&"
|
|
80
|
+
f"response_type=code&"
|
|
81
|
+
f"scope={urllib.parse.quote('openid profile')}&"
|
|
82
|
+
f"state={state}&"
|
|
83
|
+
f"code_challenge={code_challenge}&"
|
|
84
|
+
f"code_challenge_method=S256"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
console.print("[bold blue]Opening browser for login...[/bold blue]")
|
|
88
|
+
console.print(
|
|
89
|
+
f"[dim]If browser doesn't open, navigate to:[/dim]\n[link={auth_url}]{auth_url}[/link]"
|
|
90
|
+
)
|
|
91
|
+
webbrowser.open(auth_url)
|
|
92
|
+
|
|
93
|
+
# 5. Run local server to capture redirect
|
|
94
|
+
try:
|
|
95
|
+
AuthCallbackHandler.server = httpd_instance # type: ignore
|
|
96
|
+
httpd_instance.handle_request() # type: ignore
|
|
97
|
+
console.print("[dim]Local OAuth server shut down.[/dim]")
|
|
98
|
+
except Exception as e:
|
|
99
|
+
console.print(f"[bold red]Error running OAuth server: {e}[/bold red]")
|
|
100
|
+
if httpd_instance:
|
|
101
|
+
httpd_instance.server_close()
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
if oauth_data["error"]:
|
|
105
|
+
console.print(f"[bold red]Login failed: {oauth_data['error']}[/bold red]")
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
if not oauth_data["code"]:
|
|
109
|
+
console.print("[bold red]Login failed: No authorization code received.[/bold red]")
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
# 6. Verify state
|
|
113
|
+
if oauth_data["state"] != state:
|
|
114
|
+
console.print("[bold red]Login failed: State mismatch (possible CSRF attack)[/bold red]")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
# 7. Exchange code for tokens
|
|
118
|
+
console.print("[bold cyan]Exchanging authorization code for tokens...[/bold cyan]")
|
|
119
|
+
try:
|
|
120
|
+
with httpx.Client() as client:
|
|
121
|
+
response = client.post(
|
|
122
|
+
f"{API_BASE_URL}/auth/token",
|
|
123
|
+
data={
|
|
124
|
+
"grant_type": "authorization_code",
|
|
125
|
+
"client_id": CLI_CLIENT_ID,
|
|
126
|
+
"code": oauth_data["code"],
|
|
127
|
+
"code_verifier": code_verifier,
|
|
128
|
+
"redirect_uri": redirect_uri,
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
response.raise_for_status()
|
|
132
|
+
token_data = response.json()
|
|
133
|
+
access_token = token_data.get("access_token")
|
|
134
|
+
refresh_token = token_data.get("refresh_token")
|
|
135
|
+
|
|
136
|
+
if access_token and refresh_token:
|
|
137
|
+
keyring.set_password(SERVICE_ID, "access_token", access_token)
|
|
138
|
+
keyring.set_password(SERVICE_ID, "refresh_token", refresh_token)
|
|
139
|
+
console.print("[bold green]Login successful! Tokens saved securely.[/bold green]")
|
|
140
|
+
else:
|
|
141
|
+
console.print("[bold red]Login failed: No tokens received.[/bold red]")
|
|
142
|
+
except httpx.RequestError as exc:
|
|
143
|
+
console.print(f"[bold red]Token exchange failed: {exc}[/bold red]")
|
|
144
|
+
except httpx.HTTPStatusError as exc:
|
|
145
|
+
console.print(f"[bold red]Token exchange failed: {exc.response.status_code}[/bold red]")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@auth.command()
|
|
149
|
+
def logout():
|
|
150
|
+
"""Logout and clear stored tokens."""
|
|
151
|
+
clear_tokens()
|
|
152
|
+
console.print("[bold green]Logged out successfully.[/bold green]")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@auth.command()
|
|
156
|
+
@click.option("--token", is_flag=True, help="Show access token")
|
|
157
|
+
def whoami(token):
|
|
158
|
+
"""Show current authenticated user."""
|
|
159
|
+
access_token = get_auth_token()
|
|
160
|
+
|
|
161
|
+
if not access_token:
|
|
162
|
+
console.print("[bold red]Not logged in. Run 'xenfra login' first.[/bold red]")
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
from jose import jwt
|
|
167
|
+
|
|
168
|
+
# For display purposes only, in a CLI context where the token has just
|
|
169
|
+
# been retrieved from a secure source (keyring), we can disable
|
|
170
|
+
# signature verification.
|
|
171
|
+
#
|
|
172
|
+
# SECURITY BEST PRACTICE: In a real application, especially a server,
|
|
173
|
+
# you would fetch the public key from the SSO's JWKS endpoint and
|
|
174
|
+
# fully verify the token's signature to ensure its integrity.
|
|
175
|
+
claims = jwt.decode(
|
|
176
|
+
access_token, options={"verify_signature": False} # OK for local display
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
console.print("[bold green]Logged in as:[/bold green]")
|
|
180
|
+
console.print(f" User ID: {claims.get('sub')}")
|
|
181
|
+
console.print(f" Email: {claims.get('email', 'N/A')}")
|
|
182
|
+
|
|
183
|
+
if token:
|
|
184
|
+
console.print(f"\n[dim]Access Token:[/dim]\n{access_token}")
|
|
185
|
+
except Exception as e:
|
|
186
|
+
console.print(f"[bold red]Failed to decode token: {e}[/bold red]")
|