create-entity-server 0.0.9

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.
Files changed (63) hide show
  1. package/bin/create.js +280 -0
  2. package/package.json +42 -0
  3. package/template/.env.example +14 -0
  4. package/template/configs/cache.json +22 -0
  5. package/template/configs/cors.json +7 -0
  6. package/template/configs/database.json +23 -0
  7. package/template/configs/jwt.json +7 -0
  8. package/template/configs/logging.json +45 -0
  9. package/template/configs/security.json +21 -0
  10. package/template/configs/server.json +10 -0
  11. package/template/entities/Account/account_audit.json +17 -0
  12. package/template/entities/Auth/account.json +60 -0
  13. package/template/entities/Auth/api_keys.json +26 -0
  14. package/template/entities/Auth/license.json +36 -0
  15. package/template/entities/Auth/rbac_roles.json +76 -0
  16. package/template/entities/README.md +380 -0
  17. package/template/entities/System/system_audit_log.json +65 -0
  18. package/template/entities/company.json +22 -0
  19. package/template/entities/product.json +36 -0
  20. package/template/entities/todo.json +16 -0
  21. package/template/samples/README.md +65 -0
  22. package/template/samples/flutter/lib/entity_server_client.dart +218 -0
  23. package/template/samples/flutter/pubspec.yaml +14 -0
  24. package/template/samples/java/EntityServerClient.java +304 -0
  25. package/template/samples/java/EntityServerExample.java +49 -0
  26. package/template/samples/kotlin/EntityServerClient.kt +194 -0
  27. package/template/samples/node/package.json +16 -0
  28. package/template/samples/node/src/EntityServerClient.js +246 -0
  29. package/template/samples/node/src/example.js +39 -0
  30. package/template/samples/php/ci4/Controllers/ProductController.php +141 -0
  31. package/template/samples/php/ci4/Libraries/EntityServer.php +260 -0
  32. package/template/samples/php/laravel/Http/Controllers/ProductController.php +62 -0
  33. package/template/samples/php/laravel/Services/EntityServerService.php +210 -0
  34. package/template/samples/python/entity_server.py +225 -0
  35. package/template/samples/python/example.py +50 -0
  36. package/template/samples/react/src/api/entityServerClient.ts +290 -0
  37. package/template/samples/react/src/example.tsx +127 -0
  38. package/template/samples/react/src/hooks/useEntity.ts +105 -0
  39. package/template/samples/swift/EntityServerClient.swift +221 -0
  40. package/template/scripts/api-key.ps1 +123 -0
  41. package/template/scripts/api-key.sh +130 -0
  42. package/template/scripts/cleanup-history.ps1 +69 -0
  43. package/template/scripts/cleanup-history.sh +54 -0
  44. package/template/scripts/cli.ps1 +24 -0
  45. package/template/scripts/cli.sh +27 -0
  46. package/template/scripts/entity.ps1 +70 -0
  47. package/template/scripts/entity.sh +72 -0
  48. package/template/scripts/generate-env-keys.ps1 +125 -0
  49. package/template/scripts/generate-env-keys.sh +148 -0
  50. package/template/scripts/install-systemd.sh +222 -0
  51. package/template/scripts/normalize-entities.ps1 +87 -0
  52. package/template/scripts/normalize-entities.sh +132 -0
  53. package/template/scripts/rbac-role.ps1 +124 -0
  54. package/template/scripts/rbac-role.sh +127 -0
  55. package/template/scripts/remove-systemd.sh +158 -0
  56. package/template/scripts/reset-all.ps1 +83 -0
  57. package/template/scripts/reset-all.sh +95 -0
  58. package/template/scripts/run.ps1 +239 -0
  59. package/template/scripts/run.sh +315 -0
  60. package/template/scripts/sync.ps1 +145 -0
  61. package/template/scripts/sync.sh +178 -0
  62. package/template/scripts/update-server.ps1 +117 -0
  63. package/template/scripts/update-server.sh +165 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Entity Server React 훅
3
+ *
4
+ * @tanstack/react-query 기반
5
+ * 설치: npm install @tanstack/react-query
6
+ */
7
+
8
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
9
+ import type {
10
+ EntityListParams,
11
+ EntityQueryFilter,
12
+ } from "../api/entityServerClient";
13
+ import { entityServer } from "../api/entityServerClient";
14
+
15
+ // ─── 조회 훅 ─────────────────────────────────────────────────────────────────
16
+
17
+ /** 단건 조회 */
18
+ export function useEntityGet<T = unknown>(entity: string, seq: number | null) {
19
+ return useQuery({
20
+ queryKey: ["entity", entity, seq],
21
+ queryFn: () => entityServer.get<T>(entity, seq!),
22
+ enabled: seq != null,
23
+ });
24
+ }
25
+
26
+ /** 목록 조회 */
27
+ export function useEntityList<T = unknown>(
28
+ entity: string,
29
+ params: EntityListParams = {},
30
+ ) {
31
+ return useQuery({
32
+ queryKey: ["entity", entity, "list", params],
33
+ queryFn: () => entityServer.list<T>(entity, params),
34
+ });
35
+ }
36
+
37
+ /** 건수 조회 */
38
+ export function useEntityCount(entity: string) {
39
+ return useQuery({
40
+ queryKey: ["entity", entity, "count"],
41
+ queryFn: () => entityServer.count(entity),
42
+ });
43
+ }
44
+
45
+ /** 필터 검색 */
46
+ export function useEntityQuery<T = unknown>(
47
+ entity: string,
48
+ filter: EntityQueryFilter[],
49
+ params: EntityListParams = {},
50
+ ) {
51
+ return useQuery({
52
+ queryKey: ["entity", entity, "query", filter, params],
53
+ queryFn: () => entityServer.query<T>(entity, filter, params),
54
+ enabled: filter.length > 0,
55
+ });
56
+ }
57
+
58
+ /** 변경 이력 조회 */
59
+ export function useEntityHistory<T = unknown>(
60
+ entity: string,
61
+ seq: number | null,
62
+ ) {
63
+ return useQuery({
64
+ queryKey: ["entity", entity, seq, "history"],
65
+ queryFn: () => entityServer.history<T>(entity, seq!),
66
+ enabled: seq != null,
67
+ });
68
+ }
69
+
70
+ // ─── 뮤테이션 훅 ─────────────────────────────────────────────────────────────
71
+
72
+ /** 생성 또는 수정 */
73
+ export function useEntitySubmit(entity: string) {
74
+ const qc = useQueryClient();
75
+ return useMutation({
76
+ mutationFn: (data: Record<string, unknown>) =>
77
+ entityServer.submit(entity, data),
78
+ onSuccess: () => {
79
+ qc.invalidateQueries({ queryKey: ["entity", entity] });
80
+ },
81
+ });
82
+ }
83
+
84
+ /** 삭제 */
85
+ export function useEntityDelete(entity: string) {
86
+ const qc = useQueryClient();
87
+ return useMutation({
88
+ mutationFn: (seq: number) => entityServer.delete(entity, seq),
89
+ onSuccess: () => {
90
+ qc.invalidateQueries({ queryKey: ["entity", entity] });
91
+ },
92
+ });
93
+ }
94
+
95
+ /** 롤백 */
96
+ export function useEntityRollback(entity: string) {
97
+ const qc = useQueryClient();
98
+ return useMutation({
99
+ mutationFn: (historySeq: number) =>
100
+ entityServer.rollback(entity, historySeq),
101
+ onSuccess: () => {
102
+ qc.invalidateQueries({ queryKey: ["entity", entity] });
103
+ },
104
+ });
105
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Entity Server 클라이언트 (Swift / iOS)
3
+ *
4
+ * 의존성 (Package.swift / SPM):
5
+ * .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.8.0")
6
+ *
7
+ * HMAC API Key 인증 방식 사용 예:
8
+ * let client = EntityServerClient(
9
+ * baseUrl: "http://your-server:47200",
10
+ * apiKey: "your-api-key",
11
+ * hmacSecret: "your-hmac-secret",
12
+ * magicLen: 4 // 서버 packet_magic_len 과 동일
13
+ * )
14
+ * let result = try await client.list("product")
15
+ *
16
+ * 트랜잭션 사용 예:
17
+ * try await es.transStart()
18
+ * do {
19
+ * let orderRef = try await es.submit(entity: "order", data: ["user_seq": 1, "total": 9900]) // seq: "$tx.0"
20
+ * try await es.submit(entity: "order_item",
21
+ * data: ["order_seq": orderRef["seq"] as Any, "item_seq": 5]) // "$tx.0" 자동 치환
22
+ * let result = try await es.transCommit()
23
+ * let orderSeq = (result["results"] as? [[String: Any]])?[0]["seq"] // 실제 seq
24
+ * } catch {
25
+ * try? await es.transRollback()
26
+ * }
27
+ */
28
+
29
+ import CryptoSwift
30
+ import Foundation
31
+
32
+ public final class EntityServerClient {
33
+ private let baseURL: URL
34
+ private let apiKey: String
35
+ private let hmacSecret: String
36
+ private let magicLen: Int
37
+ private let session: URLSession
38
+ private var activeTxId: String? = nil
39
+
40
+ public init(
41
+ baseUrl: String = "http://localhost:47200",
42
+ apiKey: String = "",
43
+ hmacSecret: String = "",
44
+ magicLen: Int = 4,
45
+ session: URLSession = .shared
46
+ ) {
47
+ self.baseURL = URL(string: baseUrl.removingSuffix("/"))!
48
+ self.apiKey = apiKey
49
+ self.hmacSecret = hmacSecret
50
+ self.magicLen = magicLen
51
+ self.session = session
52
+ }
53
+
54
+ // ─── CRUD ─────────────────────────────────────────────────────────
55
+
56
+ public func get(entity: String, seq: Int64) async throws -> [String: Any] {
57
+ try await request(method: "GET", path: "/v1/entity/\(entity)/\(seq)")
58
+ }
59
+
60
+ public func list(entity: String, page: Int = 1, limit: Int = 20) async throws -> [String: Any] {
61
+ try await request(method: "GET", path: "/v1/entity/\(entity)/list?page=\(page)&limit=\(limit)")
62
+ }
63
+
64
+ public func count(entity: String) async throws -> [String: Any] {
65
+ try await request(method: "GET", path: "/v1/entity/\(entity)/count")
66
+ }
67
+
68
+ public func query(entity: String, filter: [[String: Any]], page: Int = 1, limit: Int = 20) async throws -> [String: Any] {
69
+ let body = try JSONSerialization.data(withJSONObject: filter)
70
+ return try await request(method: "POST", path: "/v1/entity/\(entity)/query?page=\(page)&limit=\(limit)", body: body)
71
+ }
72
+
73
+ /// 트랜잭션 시작 — 서버에 트랜잭션 큐를 등록하고 transaction_id 를 반환합니다.
74
+ /// 이후 submit / delete 가 서버 큐에 쌓이고 transCommit() 시 일괄 처리됩니다.
75
+ public func transStart() async throws -> String {
76
+ let res = try await request(method: "POST", path: "/v1/transaction/start")
77
+ guard let txId = res["transaction_id"] as? String else {
78
+ throw NSError(domain: "EntityServerClient", code: -1,
79
+ userInfo: [NSLocalizedDescriptionKey: "transStart: server did not return transaction_id"])
80
+ }
81
+ activeTxId = txId
82
+ return txId
83
+ }
84
+
85
+ /// 트랜잭션 전체 롤백
86
+ /// transactionId 생략 시 transStart() 로 시작한 활성 트랜잭션을 롤백합니다.
87
+ public func transRollback(transactionId: String? = nil) async throws -> [String: Any] {
88
+ guard let txId = transactionId ?? activeTxId else {
89
+ throw NSError(domain: "EntityServerClient", code: -1,
90
+ userInfo: [NSLocalizedDescriptionKey: "No active transaction. Call transStart() first."])
91
+ }
92
+ activeTxId = nil
93
+ return try await request(method: "POST", path: "/v1/transaction/rollback/\(txId)")
94
+ }
95
+
96
+ /// 트랜잭션 커밋 — 서버 큐에 쌓인 작업을 단일 DB 트랜잭션으로 일괄 처리합니다.
97
+ /// transactionId 생략 시 transStart() 로 시작한 활성 트랜잭션을 사용합니다.
98
+ public func transCommit(transactionId: String? = nil) async throws -> [String: Any] {
99
+ guard let txId = transactionId ?? activeTxId else {
100
+ throw NSError(domain: "EntityServerClient", code: -1,
101
+ userInfo: [NSLocalizedDescriptionKey: "No active transaction. Call transStart() first."])
102
+ }
103
+ activeTxId = nil
104
+ return try await request(method: "POST", path: "/v1/transaction/commit/\(txId)")
105
+ }
106
+
107
+ public func submit(entity: String, data: [String: Any], transactionId: String? = nil) async throws -> [String: Any] {
108
+ let body = try JSONSerialization.data(withJSONObject: data)
109
+ var extra: [String: String] = [:]
110
+ if let txId = transactionId ?? activeTxId { extra["X-Transaction-ID"] = txId }
111
+ return try await request(method: "POST", path: "/v1/entity/\(entity)/submit", body: body, extraHeaders: extra)
112
+ }
113
+
114
+ public func delete(entity: String, seq: Int64, transactionId: String? = nil, hard: Bool = false) async throws -> [String: Any] {
115
+ let q = hard ? "?hard=true" : ""
116
+ var extra: [String: String] = [:]
117
+ if let txId = transactionId ?? activeTxId { extra["X-Transaction-ID"] = txId }
118
+ return try await request(method: "DELETE", path: "/v1/entity/\(entity)/delete/\(seq)\(q)", extraHeaders: extra)
119
+ }
120
+
121
+ public func history(entity: String, seq: Int64, page: Int = 1, limit: Int = 50) async throws -> [String: Any] {
122
+ try await request(method: "GET", path: "/v1/entity/\(entity)/history/\(seq)?page=\(page)&limit=\(limit)")
123
+ }
124
+
125
+ public func rollback(entity: String, historySeq: Int64) async throws -> [String: Any] {
126
+ try await request(method: "POST", path: "/v1/entity/\(entity)/rollback/\(historySeq)")
127
+ }
128
+
129
+ // ─── 내부 ─────────────────────────────────────────────────────────
130
+
131
+ private func request(method: String, path: String, body: Data? = nil, extraHeaders: [String: String] = [:]) async throws -> [String: Any] {
132
+ let url = baseURL.appendingPathComponent(path, isDirectory: false)
133
+ var req = URLRequest(url: url)
134
+ req.httpMethod = method
135
+
136
+ let bodyStr = body.map { String(data: $0, encoding: .utf8) ?? "" } ?? ""
137
+ let timestamp = String(Int(Date().timeIntervalSince1970))
138
+ let nonce = UUID().uuidString
139
+ let signature = try sign(method: method, path: path, timestamp: timestamp, nonce: nonce, body: bodyStr)
140
+
141
+ req.setValue("application/json", forHTTPHeaderField: "Content-Type")
142
+ req.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
143
+ req.setValue(timestamp, forHTTPHeaderField: "X-Timestamp")
144
+ req.setValue(nonce, forHTTPHeaderField: "X-Nonce")
145
+ req.setValue(signature, forHTTPHeaderField: "X-Signature")
146
+ extraHeaders.forEach { req.setValue($0.value, forHTTPHeaderField: $0.key) }
147
+ req.httpBody = body
148
+
149
+ let (data, response) = try await session.data(for: req)
150
+ let http = response as! HTTPURLResponse
151
+ let contentType = http.value(forHTTPHeaderField: "Content-Type") ?? ""
152
+
153
+ // 패킷 암호화 응답: application/octet-stream → 복호화
154
+ if contentType.contains("application/octet-stream") {
155
+ return try decryptPacket(data)
156
+ }
157
+
158
+ guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
159
+ throw EntityServerError.invalidResponse
160
+ }
161
+ return json
162
+ }
163
+
164
+ /**
165
+ * XChaCha20-Poly1305 패킷 복호화
166
+ * 포맷: [magic:magicLen][nonce:24][ciphertext+tag]
167
+ * 키: sha256(hmac_secret)
168
+ */
169
+ private func decryptPacket(_ data: Data) throws -> [String: Any] {
170
+ let key = Array(SHA2(variant: .sha256).calculate(for: Array(hmacSecret.utf8)))
171
+ let bytes = Array(data)
172
+ let nonce = Array(bytes[magicLen..<(magicLen + 24)])
173
+ let ciphertext = Array(bytes[(magicLen + 24)...])
174
+
175
+ // XChaCha20-Poly1305 복호화 (tag는 ciphertext 마지막 16바이트)
176
+ let xchacha = XChaCha20Poly1305(key: key, iv: nonce, aad: [])
177
+ let plaintext = try xchacha.decrypt(ciphertext)
178
+
179
+ guard let json = try JSONSerialization.jsonObject(with: Data(plaintext)) as? [String: Any] else {
180
+ throw EntityServerError.decryptionFailed
181
+ }
182
+ return json
183
+ }
184
+
185
+ /** HMAC-SHA256 서명 */
186
+ private func sign(method: String, path: String, timestamp: String, nonce: String, body: String) throws -> String {
187
+ let payload = [method, path, timestamp, nonce, body].joined(separator: "|")
188
+ let mac = try HMAC(key: Array(hmacSecret.utf8), variant: .sha256).authenticate(Array(payload.utf8))
189
+ return mac.map { String(format: "%02x", $0) }.joined()
190
+ }
191
+ }
192
+
193
+ // MARK: - XChaCha20Poly1305 helper (CryptoSwift wrapper)
194
+ private struct XChaCha20Poly1305 {
195
+ let key: [UInt8]
196
+ let iv: [UInt8]
197
+ let aad: [UInt8]
198
+
199
+ func decrypt(_ ciphertext: [UInt8]) throws -> [UInt8] {
200
+ // CryptoSwift ChaCha20.Poly1305 AEAD - tag는 마지막 16바이트
201
+ let tag = Array(ciphertext.suffix(16))
202
+ let ct = Array(ciphertext.dropLast(16))
203
+ let chacha = try ChaCha20(key: key, iv: iv)
204
+ let decrypted = try chacha.decrypt(ct)
205
+ // Poly1305 태그 검증
206
+ let poly = try Poly1305(key: chacha.keystream().prefix(32)).authenticate(ct + aad)
207
+ guard poly == tag else { throw EntityServerError.decryptionFailed }
208
+ return decrypted
209
+ }
210
+ }
211
+
212
+ public enum EntityServerError: Error {
213
+ case invalidResponse
214
+ case decryptionFailed
215
+ }
216
+
217
+ private extension String {
218
+ func removingSuffix(_ suffix: String) -> String {
219
+ hasSuffix(suffix) ? String(dropLast(suffix.count)) : self
220
+ }
221
+ }
@@ -0,0 +1,123 @@
1
+ # API Key Management Script (CLI mode) - Windows PowerShell
2
+ # Can be used even when the server is stopped.
3
+ param(
4
+ [Parameter(Position=0)]
5
+ [string]$SubCommand = ""
6
+ )
7
+
8
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
9
+ $ProjectRoot = Split-Path -Parent $ScriptDir
10
+ $BinPath = Join-Path $ProjectRoot "bin\entity-cli.exe"
11
+
12
+ Set-Location $ProjectRoot
13
+
14
+ # Load language from .env
15
+ $Language = "ko"
16
+ $EnvFile = Join-Path $ProjectRoot ".env"
17
+ if (Test-Path $EnvFile) {
18
+ $LangLine = Get-Content $EnvFile | Where-Object { $_ -match '^LANGUAGE=' } | Select-Object -First 1
19
+ if ($LangLine) { $Language = $LangLine -replace '^LANGUAGE=', '' }
20
+ }
21
+
22
+ function Show-Help {
23
+ if ($Language -eq "en") {
24
+ Write-Host "API Key Management (CLI mode)"
25
+ Write-Host "============================="
26
+ Write-Host ""
27
+ Write-Host "Manage api_keys entity directly via CLI binary."
28
+ Write-Host "Server does NOT need to be running."
29
+ Write-Host ""
30
+ Write-Host "Usage: .\api-key.ps1 <subcommand> [options]"
31
+ Write-Host ""
32
+ Write-Host "Subcommands:"
33
+ Write-Host " list List API keys"
34
+ Write-Host " add Create a new API key (key/secret auto-generated)"
35
+ Write-Host " delete Delete an API key by seq"
36
+ Write-Host " help Show this help"
37
+ Write-Host ""
38
+ Write-Host "list options:"
39
+ Write-Host " --limit=<n> Max rows to show (default: 20)"
40
+ Write-Host ""
41
+ Write-Host "add options:"
42
+ Write-Host " --role=<name> Role name (default: admin)"
43
+ Write-Host " --entities=<json> Allowed entities JSON (default: [`"*`"])"
44
+ Write-Host " --description=<t> Description"
45
+ Write-Host " --user-seq=<n> Associated user seq"
46
+ Write-Host " --apply Execute (default is dry-run)"
47
+ Write-Host ""
48
+ Write-Host "delete options:"
49
+ Write-Host " --seq=<n> API key seq to delete (required)"
50
+ Write-Host " --apply Execute (default is dry-run)"
51
+ Write-Host ""
52
+ Write-Host "Examples:"
53
+ Write-Host " .\api-key.ps1 list"
54
+ Write-Host " .\api-key.ps1 list --limit=50"
55
+ Write-Host " .\api-key.ps1 add --role=admin --apply"
56
+ Write-Host " .\api-key.ps1 delete --seq=3 --apply"
57
+ } else {
58
+ Write-Host "API 키 관리 (CLI 모드)"
59
+ Write-Host "===================="
60
+ Write-Host ""
61
+ Write-Host "CLI 바이너리로 api_keys 엔티티를 직접 조작합니다."
62
+ Write-Host "서버가 실행 중이지 않아도 사용 가능합니다."
63
+ Write-Host ""
64
+ Write-Host "사용법: .\api-key.ps1 <하위명령> [옵션]"
65
+ Write-Host ""
66
+ Write-Host "하위 명령:"
67
+ Write-Host " list API 키 목록 조회"
68
+ Write-Host " add 새 API 키 생성 (키/시크릿 자동 생성)"
69
+ Write-Host " delete API 키 삭제 (seq 지정)"
70
+ Write-Host " help 도움말 출력"
71
+ Write-Host ""
72
+ Write-Host "list 옵션:"
73
+ Write-Host " --limit=<n> 최대 출력 행 수 (기본: 20)"
74
+ Write-Host ""
75
+ Write-Host "add 옵션:"
76
+ Write-Host " --role=<이름> 역할명 (기본: admin)"
77
+ Write-Host " --entities=<json> 허용 엔티티 JSON (기본: [`"*`"])"
78
+ Write-Host " --description=<t> 설명"
79
+ Write-Host " --user-seq=<n> 연결 사용자 seq"
80
+ Write-Host " --apply 실제 실행 (기본: dry-run)"
81
+ Write-Host ""
82
+ Write-Host "delete 옵션:"
83
+ Write-Host " --seq=<n> 삭제할 API 키 seq (필수)"
84
+ Write-Host " --apply 실제 실행 (기본: dry-run)"
85
+ Write-Host ""
86
+ Write-Host "예제:"
87
+ Write-Host " .\api-key.ps1 list"
88
+ Write-Host " .\api-key.ps1 list --limit=50"
89
+ Write-Host " .\api-key.ps1 add --role=admin --apply"
90
+ Write-Host " .\api-key.ps1 delete --seq=3 --apply"
91
+ }
92
+ }
93
+
94
+ if (-not $SubCommand) {
95
+ Show-Help
96
+ exit 0
97
+ }
98
+
99
+ if (-not (Test-Path $BinPath)) {
100
+ if ($Language -eq "en") { Write-Host "X bin/entity-cli.exe not found. Run: .\scripts\build.ps1" }
101
+ else { Write-Host "X bin/entity-cli.exe 파일이 없습니다. 먼저 .\scripts\build.ps1 를 실행하세요." }
102
+ exit 1
103
+ }
104
+
105
+ # Collect remaining args (all args after SubCommand)
106
+ $remainingArgs = $args
107
+
108
+ switch ($SubCommand) {
109
+ { $_ -in @("list", "show", "add", "delete") } {
110
+ $env:ENTITY_CLI_NAME = "api-key"
111
+ & $BinPath api-key $SubCommand @remainingArgs
112
+ }
113
+ { $_ -in @("help", "-h", "--help") } {
114
+ Show-Help
115
+ }
116
+ default {
117
+ if ($Language -eq "en") { Write-Host "X Unknown subcommand: $SubCommand" }
118
+ else { Write-Host "X 알 수 없는 하위 명령: $SubCommand" }
119
+ Write-Host ""
120
+ Show-Help
121
+ exit 1
122
+ }
123
+ }
@@ -0,0 +1,130 @@
1
+ #!/bin/bash
2
+ # API 키 관리 스크립트 (CLI 바이너리 직접 호출)
3
+ # 서버가 중단된 상태에서도 사용 가능합니다.
4
+ # HTTP API 방식은 api-keys.sh 를 사용하세요.
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
8
+ BIN_PATH="$PROJECT_ROOT/bin/entity-cli"
9
+
10
+ cd "$PROJECT_ROOT"
11
+
12
+ # Load language from .env
13
+ if [ -f .env ]; then
14
+ LANGUAGE=$(grep '^LANGUAGE=' .env | cut -d '=' -f2)
15
+ fi
16
+ LANGUAGE=${LANGUAGE:-ko}
17
+
18
+ show_help() {
19
+ if [ "$LANGUAGE" = "en" ]; then
20
+ echo "API Key Management (CLI mode)"
21
+ echo "============================="
22
+ echo ""
23
+ echo "Manage api_keys entity directly via CLI binary."
24
+ echo "Server does NOT need to be running."
25
+ echo ""
26
+ echo "Usage: $0 <subcommand> [options]"
27
+ echo ""
28
+ echo "Subcommands:"
29
+ echo " list List API keys"
30
+ echo " add Create a new API key (key/secret auto-generated)"
31
+ echo " delete Delete an API key by seq"
32
+ echo " help Show this help"
33
+ echo ""
34
+ echo "list options:"
35
+ echo " --limit=<n> Max rows to show (default: 20)"
36
+ echo ""
37
+ echo "add options:"
38
+ echo " --role=<name> Role name (default: admin)"
39
+ echo " --entities=<json> Allowed entities JSON (default: [\"*\"])"
40
+ echo " --description=<t> Description"
41
+ echo " --user-seq=<n> Associated user seq"
42
+ echo " --apply Execute (default is dry-run)"
43
+ echo ""
44
+ echo "delete options:"
45
+ echo " --seq=<n> API key seq to delete (required)"
46
+ echo " --apply Execute (default is dry-run)"
47
+ echo ""
48
+ echo "Examples:"
49
+ echo " $0 list"
50
+ echo " $0 list --limit=50"
51
+ echo " $0 add --role=admin --apply"
52
+ echo " $0 add --role=viewer --entities='[\"user\",\"product\"]' --description=\"Viewer key\" --apply"
53
+ echo " $0 add --role=admin --user-seq=1 --apply"
54
+ echo " $0 delete --seq=3 --apply"
55
+ else
56
+ echo "API 키 관리 (CLI 모드)"
57
+ echo "===================="
58
+ echo ""
59
+ echo "CLI 바이너리로 api_keys 엔티티를 직접 조작합니다."
60
+ echo "서버가 실행 중이지 않아도 사용 가능합니다."
61
+ echo ""
62
+ echo "사용법: $0 <하위명령> [옵션]"
63
+ echo ""
64
+ echo "하위 명령:"
65
+ echo " list API 키 목록 조회"
66
+ echo " add 새 API 키 생성 (키/시크릿 자동 생성)"
67
+ echo " delete API 키 삭제 (seq 지정)"
68
+ echo " help 도움말 출력"
69
+ echo ""
70
+ echo "list 옵션:"
71
+ echo " --limit=<n> 최대 출력 행 수 (기본: 20)"
72
+ echo ""
73
+ echo "add 옵션:"
74
+ echo " --role=<이름> 역할명 (기본: admin)"
75
+ echo " --entities=<json> 허용 엔티티 JSON (기본: [\"*\"])"
76
+ echo " --description=<t> 설명"
77
+ echo " --user-seq=<n> 연결 사용자 seq"
78
+ echo " --apply 실제 실행 (기본: dry-run)"
79
+ echo ""
80
+ echo "delete 옵션:"
81
+ echo " --seq=<n> 삭제할 API 키 seq (필수)"
82
+ echo " --apply 실제 실행 (기본: dry-run)"
83
+ echo ""
84
+ echo "예제:"
85
+ echo " $0 list"
86
+ echo " $0 list --limit=50"
87
+ echo " $0 add --role=admin --apply"
88
+ echo " $0 add --role=viewer --entities='[\"user\",\"product\"]' --description=\"뷰어 키\" --apply"
89
+ echo " $0 add --role=admin --user-seq=1 --apply"
90
+ echo " $0 delete --seq=3 --apply"
91
+ fi
92
+ }
93
+
94
+ # 인자 없으면 도움말
95
+ if [ $# -eq 0 ]; then
96
+ show_help
97
+ exit 0
98
+ fi
99
+
100
+ # CLI 바이너리 존재 확인
101
+ if [ ! -f "$BIN_PATH" ]; then
102
+ if [ "$LANGUAGE" = "en" ]; then
103
+ echo "❌ bin/entity-cli not found. Run: ./scripts/build.sh"
104
+ else
105
+ echo "❌ bin/entity-cli 파일이 없습니다. 먼저 ./scripts/build.sh 를 실행하세요."
106
+ fi
107
+ exit 1
108
+ fi
109
+
110
+ SUBCOMMAND="$1"
111
+ shift
112
+
113
+ case "$SUBCOMMAND" in
114
+ list|show|add|delete)
115
+ ENTITY_CLI_NAME="api-key" "$BIN_PATH" api-key "$SUBCOMMAND" "$@"
116
+ ;;
117
+ help|-h|--help)
118
+ show_help
119
+ ;;
120
+ *)
121
+ if [ "$LANGUAGE" = "en" ]; then
122
+ echo "❌ Unknown subcommand: $SUBCOMMAND"
123
+ else
124
+ echo "❌ 알 수 없는 하위 명령: $SUBCOMMAND"
125
+ fi
126
+ echo ""
127
+ show_help
128
+ exit 1
129
+ ;;
130
+ esac
@@ -0,0 +1,69 @@
1
+ # cleanup-history.ps1 — history_ttl 기준 이력 정리
2
+ #
3
+ # 사용법:
4
+ # .\scripts\cleanup-history.ps1 # 도움말
5
+ # .\scripts\cleanup-history.ps1 --apply # 전체 이력 정리 실행
6
+ # .\scripts\cleanup-history.ps1 --entity=account # dry-run (특정 엔티티)
7
+ # .\scripts\cleanup-history.ps1 --entity=account --apply
8
+
9
+ param(
10
+ [string]$Entity = "",
11
+ [switch]$Apply
12
+ )
13
+
14
+ $ErrorActionPreference = "Stop"
15
+
16
+ $ProjectRoot = Split-Path -Parent $PSScriptRoot
17
+ Set-Location $ProjectRoot
18
+
19
+ # LANGUAGE 로드
20
+ $Lang = $env:LANGUAGE
21
+ if (-not $Lang) {
22
+ $EnvFile = Join-Path $ProjectRoot ".env"
23
+ if (Test-Path $EnvFile) {
24
+ $line = Get-Content $EnvFile | Where-Object { $_ -match '^LANGUAGE=' } | Select-Object -Last 1
25
+ if ($line) { $Lang = $line -replace '^LANGUAGE=', '' }
26
+ }
27
+ }
28
+ if (-not $Lang) { $Lang = "ko" }
29
+
30
+ function Show-Help {
31
+ if ($Lang -eq "en") {
32
+ Write-Host "History TTL Cleanup"
33
+ Write-Host "==================="
34
+ Write-Host ""
35
+ Write-Host "Usage: .\scripts\cleanup-history.ps1 [-Entity <name>] [-Apply]"
36
+ Write-Host ""
37
+ Write-Host "Options:"
38
+ Write-Host " -Entity <name> Cleanup only one entity history"
39
+ Write-Host " -Apply Execute delete (default: dry-run)"
40
+ } else {
41
+ Write-Host "히스토리 TTL 정리"
42
+ Write-Host "================"
43
+ Write-Host ""
44
+ Write-Host "사용법: .\scripts\cleanup-history.ps1 [-Entity <이름>] [-Apply]"
45
+ Write-Host ""
46
+ Write-Host "옵션:"
47
+ Write-Host " -Entity <이름> 특정 엔티티 히스토리만 정리"
48
+ Write-Host " -Apply 실제 삭제 실행 (기본: dry-run)"
49
+ }
50
+ }
51
+
52
+ # 인자 없으면 도움말
53
+ if (-not $Apply -and -not $Entity) {
54
+ Show-Help
55
+ exit 0
56
+ }
57
+
58
+ $CliBin = Join-Path $ProjectRoot "entity-cli.exe"
59
+ if (-not (Test-Path $CliBin)) {
60
+ if ($Lang -eq "en") { Write-Error "❌ entity-cli.exe not found" }
61
+ else { Write-Error "❌ entity-cli.exe 파일이 없습니다" }
62
+ exit 1
63
+ }
64
+
65
+ $Args = @("cleanup-history")
66
+ if ($Entity) { $Args += "--entity=$Entity" }
67
+ if ($Apply) { $Args += "--apply" }
68
+
69
+ & $CliBin @Args