feat: add credential encryption key config and improve service token CLI display

- Add CREDENTIAL_ENCRYPTION_KEY environment variable with Fernet key generation guide
- Improve service token issue command to display raw token once with warning
- Refactor token list display function for better UX (new print_service_token_list)
- Keep backward compatibility with print_service_token_table alias
This commit is contained in:
2026-05-02 12:25:25 +00:00
parent c31ca50bdd
commit 8370d998d2
3 changed files with 38 additions and 23 deletions
+4
View File
@@ -14,6 +14,10 @@ PORTFOLIOHUB_SESSION_TOKEN_TTL_HOURS=12
PORTFOLIOHUB_CLIENT_TOKEN_DEFAULT_TTL_DAYS=365
PORTFOLIOHUB_BOT_TOKEN_DEFAULT_TTL_DAYS=365
# Credential encryption key (Fernet, required when DB_ENABLED=true)
# Generate: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
CREDENTIAL_ENCRYPTION_KEY=
# TimescaleDB / PostgreSQL connection settings
TIMESCALEDB_HOST=localhost
TIMESCALEDB_PORT=5432
+8 -3
View File
@@ -81,10 +81,15 @@ def issue_service_token(
runtime_id=issued.auth_token.runtime_id,
token=issued.token,
)
render_service_token_result(ctx.obj.console, ctx.obj.output_format, response)
if ctx.obj.output_format != "json":
ctx.obj.console.print(f"masked_token: {mask_token_text(issued.token)}")
# 발급 시 1회에 한해 원문 token 출력 (이후 마스킹된 값만 표시됨)
if ctx.obj.output_format == "json":
from app.cli.core.utils import render_output
render_output(ctx.obj.console, "json", response.model_dump(mode="json"))
else:
ctx.obj.console.print("[bold yellow]⚠ 토큰 원문은 지금 1회만 표시됩니다. 안전한 곳에 저장하세요.[/bold yellow]")
ctx.obj.console.print(f"[bold green]token:[/bold green] {issued.token}")
render_service_token_result(ctx.obj.console, ctx.obj.output_format, response)
@token_app.command("revoke")
+26 -20
View File
@@ -163,26 +163,32 @@ def print_user_detail(console: Console, user: UserResponse) -> None:
)
def print_service_token_table(console: Console, tokens: list[AuthTokenResponse]) -> None:
table = Table(title="Service Tokens", expand=True)
table.add_column("Name", style="cyan", no_wrap=True)
table.add_column("Permission", no_wrap=True)
table.add_column("User ID", no_wrap=True)
table.add_column("Prefix", no_wrap=True)
table.add_column("Expires At", no_wrap=True)
table.add_column("Revoked At", no_wrap=True)
table.add_column("Created At", no_wrap=True)
for token in tokens:
table.add_row(
token.name,
token.permission,
token.user_id,
token.token_prefix,
token.expires_at.isoformat(timespec="seconds") if token.expires_at else "never",
token.revoked_at.isoformat(timespec="seconds") if token.revoked_at else "-",
token.created_at.isoformat(timespec="seconds") if token.created_at else "N/A",
)
console.print(table)
def print_service_token_list(console: Console, tokens: list[AuthTokenResponse]) -> None:
for i, token in enumerate(tokens, 1):
status = "[red]revoked[/red]" if token.revoked_at else "[green]active[/green]"
expires = token.expires_at.isoformat(timespec="seconds") if token.expires_at else "never"
last_used = token.last_used_at.isoformat(timespec="seconds") if token.last_used_at else "-"
revoked_at = token.revoked_at.isoformat(timespec="seconds") if token.revoked_at else "-"
created_at = token.created_at.isoformat(timespec="seconds") if token.created_at else "N/A"
lines = [
f"[bold cyan]{token.name}[/bold cyan] ({status})",
f" token : {token.token_prefix}",
f" id : {token.id}",
f" user_id : {token.user_id}",
f" perm : {token.permission}",
f" expires : {expires}",
f" last_used: {last_used}",
f" created : {created_at}",
]
if token.revoked_at:
lines.append(f" revoked : {revoked_at}")
console.print("\n".join(lines))
if i < len(tokens):
console.print("[dim]─[/dim]" * 40)
# Keep old name as alias for backward compatibility
print_service_token_table = print_service_token_list
def print_comment_table(console: Console, comments: list[CommentResponse]) -> None: