diff --git a/.env.example b/.env.example index b8b02f8..884f69c 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/app/cli/commands/service_tokens.py b/app/cli/commands/service_tokens.py index b659566..4cf812c 100644 --- a/app/cli/commands/service_tokens.py +++ b/app/cli/commands/service_tokens.py @@ -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") diff --git a/app/cli/core/utils.py b/app/cli/core/utils.py index a6b436c..b925e8e 100644 --- a/app/cli/core/utils.py +++ b/app/cli/core/utils.py @@ -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: