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_CLIENT_TOKEN_DEFAULT_TTL_DAYS=365
PORTFOLIOHUB_BOT_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 / PostgreSQL connection settings
TIMESCALEDB_HOST=localhost TIMESCALEDB_HOST=localhost
TIMESCALEDB_PORT=5432 TIMESCALEDB_PORT=5432
+8 -3
View File
@@ -81,10 +81,15 @@ def issue_service_token(
runtime_id=issued.auth_token.runtime_id, runtime_id=issued.auth_token.runtime_id,
token=issued.token, token=issued.token,
) )
render_service_token_result(ctx.obj.console, ctx.obj.output_format, response)
if ctx.obj.output_format != "json": # 발급 시 1회에 한해 원문 token 출력 (이후 마스킹된 값만 표시됨)
ctx.obj.console.print(f"masked_token: {mask_token_text(issued.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") @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: def print_service_token_list(console: Console, tokens: list[AuthTokenResponse]) -> None:
table = Table(title="Service Tokens", expand=True) for i, token in enumerate(tokens, 1):
table.add_column("Name", style="cyan", no_wrap=True) status = "[red]revoked[/red]" if token.revoked_at else "[green]active[/green]"
table.add_column("Permission", no_wrap=True) expires = token.expires_at.isoformat(timespec="seconds") if token.expires_at else "never"
table.add_column("User ID", no_wrap=True) last_used = token.last_used_at.isoformat(timespec="seconds") if token.last_used_at else "-"
table.add_column("Prefix", no_wrap=True) revoked_at = token.revoked_at.isoformat(timespec="seconds") if token.revoked_at else "-"
table.add_column("Expires At", no_wrap=True) created_at = token.created_at.isoformat(timespec="seconds") if token.created_at else "N/A"
table.add_column("Revoked At", no_wrap=True) lines = [
table.add_column("Created At", no_wrap=True) f"[bold cyan]{token.name}[/bold cyan] ({status})",
for token in tokens: f" token : {token.token_prefix}",
table.add_row( f" id : {token.id}",
token.name, f" user_id : {token.user_id}",
token.permission, f" perm : {token.permission}",
token.user_id, f" expires : {expires}",
token.token_prefix, f" last_used: {last_used}",
token.expires_at.isoformat(timespec="seconds") if token.expires_at else "never", f" created : {created_at}",
token.revoked_at.isoformat(timespec="seconds") if token.revoked_at else "-", ]
token.created_at.isoformat(timespec="seconds") if token.created_at else "N/A", if token.revoked_at:
) lines.append(f" revoked : {revoked_at}")
console.print(table) 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: def print_comment_table(console: Console, comments: list[CommentResponse]) -> None: