db-json-cli 1.0.4 → 2.0.0

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.
@@ -0,0 +1,34 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getDB } from "@/lib/db";
3
+ import { authMiddleware } from "@/lib/auth";
4
+
5
+ export async function GET(request, { params }) {
6
+ try {
7
+ const { resource, id } = params;
8
+ const db = await getDB();
9
+
10
+ if (!db[resource]) {
11
+ return NextResponse.json({ message: "Resource not found" }, { status: 404 });
12
+ }
13
+
14
+ // Check if private
15
+ const isPrivate = db.rules?.[resource] === "private";
16
+ if (isPrivate) {
17
+ const user = authMiddleware(request);
18
+ if (!user) {
19
+ return NextResponse.json({ message: "No token" }, { status: 401 });
20
+ }
21
+ }
22
+
23
+ const itemId = Number(id);
24
+ const item = db[resource].find((i) => i.id === itemId);
25
+
26
+ if (!item) {
27
+ return NextResponse.json({ message: "Not found" }, { status: 404 });
28
+ }
29
+
30
+ return NextResponse.json(item);
31
+ } catch (error) {
32
+ return NextResponse.json({ message: "Failed to fetch item" }, { status: 500 });
33
+ }
34
+ }
@@ -0,0 +1,74 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getDB, saveDB } from "@/lib/db";
3
+ import { authMiddleware } from "@/lib/auth";
4
+
5
+ export async function GET(request, { params }) {
6
+ try {
7
+ const { resource } = params;
8
+ const db = await getDB();
9
+
10
+ if (!db[resource]) {
11
+ return NextResponse.json({ message: "Resource not found" }, { status: 404 });
12
+ }
13
+
14
+ // Check if private
15
+ const isPrivate = db.rules?.[resource] === "private";
16
+ if (isPrivate) {
17
+ const user = authMiddleware(request);
18
+ if (!user) {
19
+ return NextResponse.json({ message: "No token" }, { status: 401 });
20
+ }
21
+ }
22
+
23
+ // Filter by range
24
+ const { searchParams } = new URL(request.url);
25
+ const from = searchParams.get("from");
26
+ const to = searchParams.get("to");
27
+
28
+ let data = db[resource];
29
+ if (from && to) {
30
+ const fromNum = Number(from);
31
+ const toNum = Number(to);
32
+ data = data.filter((item) => item.id >= fromNum && item.id <= toNum);
33
+ }
34
+
35
+ return NextResponse.json(data);
36
+ } catch (error) {
37
+ return NextResponse.json({ message: "Failed to fetch data" }, { status: 500 });
38
+ }
39
+ }
40
+
41
+ export async function POST(request, { params }) {
42
+ try {
43
+ const { resource } = params;
44
+ const db = await getDB();
45
+
46
+ if (!db[resource]) {
47
+ return NextResponse.json({ message: "Resource not found" }, { status: 404 });
48
+ }
49
+
50
+ // Check if private
51
+ const isPrivate = db.rules?.[resource] === "private";
52
+ if (isPrivate) {
53
+ const user = authMiddleware(request);
54
+ if (!user) {
55
+ return NextResponse.json({ message: "No token" }, { status: 401 });
56
+ }
57
+ }
58
+
59
+ const newItem = await request.json();
60
+ if (!newItem || typeof newItem !== "object") {
61
+ return NextResponse.json({ message: "Invalid body" }, { status: 400 });
62
+ }
63
+
64
+ const id = db[resource].length ? Math.max(...db[resource].map((i) => i.id)) + 1 : 1;
65
+ const item = { id, ...newItem };
66
+
67
+ db[resource].push(item);
68
+ await saveDB(db);
69
+
70
+ return NextResponse.json(item);
71
+ } catch (error) {
72
+ return NextResponse.json({ message: "Failed to create item" }, { status: 500 });
73
+ }
74
+ }
@@ -0,0 +1,26 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getDB } from "@/lib/db";
3
+
4
+ export async function GET() {
5
+ try {
6
+ const db = await getDB();
7
+ const routeList = [];
8
+
9
+ Object.keys(db).forEach((key) => {
10
+ if (["users", "rules"].includes(key)) return;
11
+ const isPrivate = db.rules?.[key] === "private";
12
+ routeList.push({
13
+ key,
14
+ count: db[key].length,
15
+ permission: isPrivate ? "private" : "public",
16
+ });
17
+ });
18
+
19
+ return NextResponse.json({
20
+ routeList,
21
+ port: process.env.PORT || 4000,
22
+ });
23
+ } catch (error) {
24
+ return NextResponse.json({ message: "Failed to load DB info" }, { status: 500 });
25
+ }
26
+ }
@@ -0,0 +1,27 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getDB } from "@/lib/db";
3
+ import { comparePassword, generateTokens } from "@/lib/auth";
4
+
5
+ export async function POST(request) {
6
+ try {
7
+ const { email, password } = await request.json();
8
+
9
+ const db = await getDB();
10
+ const user = db.users.find((u) => u.email === email);
11
+
12
+ if (!user) {
13
+ return NextResponse.json({ message: "Invalid credentials" }, { status: 401 });
14
+ }
15
+
16
+ const valid = await comparePassword(password, user.password);
17
+
18
+ if (!valid) {
19
+ return NextResponse.json({ message: "Invalid credentials" }, { status: 401 });
20
+ }
21
+
22
+ const tokens = generateTokens({ id: user.id, email: user.email });
23
+ return NextResponse.json(tokens);
24
+ } catch (error) {
25
+ return NextResponse.json({ message: "Login failed" }, { status: 500 });
26
+ }
27
+ }
@@ -0,0 +1,32 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getDB, saveDB } from "@/lib/db";
3
+ import { hashPassword, generateTokens } from "@/lib/auth";
4
+
5
+ export async function POST(request) {
6
+ try {
7
+ const { email, password, name } = await request.json();
8
+
9
+ if (!email || !password) {
10
+ return NextResponse.json({ message: "Email/password required" }, { status: 400 });
11
+ }
12
+
13
+ const db = await getDB();
14
+ const exists = db.users.find((u) => u.email === email);
15
+
16
+ if (exists) {
17
+ return NextResponse.json({ message: "User already exists" }, { status: 409 });
18
+ }
19
+
20
+ const hashed = await hashPassword(password);
21
+ const id = db.users.length ? Math.max(...db.users.map((u) => u.id)) + 1 : 1;
22
+ const newUser = { id, email, name: name || "", password: hashed };
23
+
24
+ db.users.push(newUser);
25
+ await saveDB(db);
26
+
27
+ const tokens = generateTokens({ id, email });
28
+ return NextResponse.json(tokens);
29
+ } catch (error) {
30
+ return NextResponse.json({ message: "Registration failed" }, { status: 500 });
31
+ }
32
+ }
package/app/layout.jsx ADDED
@@ -0,0 +1,7 @@
1
+ export default function RootLayout({ children }) {
2
+ return (
3
+ <html lang="en">
4
+ <body style={{ margin: 0, padding: 0 }}>{children}</body>
5
+ </html>
6
+ );
7
+ }
package/app/page.jsx ADDED
@@ -0,0 +1,321 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+
5
+ export default function HomePage() {
6
+ const [routeList, setRouteList] = useState([]);
7
+ const [port, setPort] = useState(4000);
8
+ const [loading, setLoading] = useState(true);
9
+ const [activeEndpoint, setActiveEndpoint] = useState(null);
10
+
11
+ useEffect(() => {
12
+ fetch("/api/info")
13
+ .then((res) => res.json())
14
+ .then((data) => {
15
+ setRouteList(data.routeList);
16
+ setPort(data.port);
17
+ setLoading(false);
18
+ })
19
+ .catch((err) => {
20
+ console.error("Failed to load API info:", err);
21
+ setLoading(false);
22
+ });
23
+ }, []);
24
+
25
+ if (loading) {
26
+ return (
27
+ <>
28
+ <div className="loading-container">Loading...</div>
29
+ <style jsx>{`
30
+ .loading-container {
31
+ display: flex;
32
+ justify-content: center;
33
+ align-items: center;
34
+ min-height: 100vh;
35
+ background: linear-gradient(to bottom, #1a1a2e 0%, #16213e 100%);
36
+ color: white;
37
+ font-size: 1.5rem;
38
+ }
39
+ `}</style>
40
+ </>
41
+ );
42
+ }
43
+
44
+ return (
45
+ <>
46
+ <div className="container">
47
+ <div className="header">
48
+ <div className="header-content">
49
+ <h1>🚀 db-json-cli API</h1>
50
+ <p>RESTful JSON API Documentation</p>
51
+ </div>
52
+ </div>
53
+
54
+ <div className="content">
55
+ <div className="info-box">
56
+ <h2>📖 API Information</h2>
57
+ <p>Welcome to the db-json-cli API. This API provides RESTful endpoints for managing your data.</p>
58
+ <div className="base-url">Base URL: http://localhost:{port}</div>
59
+ </div>
60
+
61
+ <div className="endpoints">
62
+ <h2>Available Endpoints</h2>
63
+
64
+ {routeList.map((r) => (
65
+ <div key={r.key}>
66
+ <div
67
+ className={`endpoint-item ${activeEndpoint === `${r.key}-get` ? "active" : ""}`}
68
+ onClick={() => setActiveEndpoint(activeEndpoint === `${r.key}-get` ? null : `${r.key}-get`)}
69
+ >
70
+ <div className="endpoint-header">
71
+ <span className="method-badge method-get">GET</span>
72
+ <span className="endpoint-path">/{r.key}</span>
73
+ <span className={`permission-badge permission-${r.permission}`}>
74
+ {r.permission === "public" ? "🔓" : "🔒"} {r.permission.toUpperCase()}
75
+ </span>
76
+ <span className="endpoint-count">
77
+ {r.count} {r.count === 1 ? "item" : "items"}
78
+ </span>
79
+ </div>
80
+ <div className="endpoint-description">
81
+ Retrieve all {r.key} or filter by ID range using ?from=1&to=10
82
+ </div>
83
+ {activeEndpoint === `${r.key}-get` && (
84
+ <div className="endpoint-actions">
85
+ <a
86
+ href={`/api/${r.key}`}
87
+ target="_blank"
88
+ rel="noopener noreferrer"
89
+ className="action-btn btn-primary"
90
+ onClick={(e) => e.stopPropagation()}
91
+ >
92
+ Try it out
93
+ </a>
94
+ </div>
95
+ )}
96
+ </div>
97
+
98
+ <div className="endpoint-item">
99
+ <div className="endpoint-header">
100
+ <span className="method-badge method-get">GET</span>
101
+ <span className="endpoint-path">/{r.key}/:id</span>
102
+ <span className={`permission-badge permission-${r.permission}`}>
103
+ {r.permission === "public" ? "🔓" : "🔒"} {r.permission.toUpperCase()}
104
+ </span>
105
+ </div>
106
+ <div className="endpoint-description">Retrieve a specific item by ID</div>
107
+ </div>
108
+
109
+ <div className="endpoint-item">
110
+ <div className="endpoint-header">
111
+ <span className="method-badge method-post">POST</span>
112
+ <span className="endpoint-path">/{r.key}</span>
113
+ <span className={`permission-badge permission-${r.permission}`}>
114
+ {r.permission === "public" ? "🔓" : "🔒"} {r.permission.toUpperCase()}
115
+ </span>
116
+ </div>
117
+ <div className="endpoint-description">Create a new item (ID will be auto-generated)</div>
118
+ </div>
119
+ </div>
120
+ ))}
121
+
122
+ <div className="endpoint-item">
123
+ <div className="endpoint-header">
124
+ <span className="method-badge method-post">POST</span>
125
+ <span className="endpoint-path">/register</span>
126
+ </div>
127
+ <div className="endpoint-description">Register a new user account</div>
128
+ </div>
129
+
130
+ <div className="endpoint-item">
131
+ <div className="endpoint-header">
132
+ <span className="method-badge method-post">POST</span>
133
+ <span className="endpoint-path">/login</span>
134
+ </div>
135
+ <div className="endpoint-description">Login and receive access tokens</div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ <div className="footer">
141
+ <p>Generated by db-json-cli • Made with ♥</p>
142
+ </div>
143
+ </div>
144
+
145
+ <style jsx>{`
146
+ * {
147
+ margin: 0;
148
+ padding: 0;
149
+ box-sizing: border-box;
150
+ }
151
+ .container {
152
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
153
+ background: linear-gradient(to bottom, #1a1a2e 0%, #16213e 100%);
154
+ color: #333;
155
+ min-height: 100vh;
156
+ }
157
+ .header {
158
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
159
+ padding: 3rem 2rem;
160
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
161
+ }
162
+ .header-content {
163
+ max-width: 1200px;
164
+ margin: 0 auto;
165
+ }
166
+ .header h1 {
167
+ color: white;
168
+ font-size: 2.5rem;
169
+ margin-bottom: 0.5rem;
170
+ font-weight: 700;
171
+ }
172
+ .header p {
173
+ color: rgba(255, 255, 255, 0.9);
174
+ font-size: 1.1rem;
175
+ }
176
+ .content {
177
+ max-width: 1200px;
178
+ margin: 0 auto;
179
+ padding: 2rem;
180
+ }
181
+ .info-box {
182
+ background: white;
183
+ border-radius: 12px;
184
+ padding: 1.5rem;
185
+ margin-bottom: 2rem;
186
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
187
+ }
188
+ .info-box h2 {
189
+ color: #667eea;
190
+ font-size: 1.3rem;
191
+ margin-bottom: 0.5rem;
192
+ }
193
+ .info-box p {
194
+ color: #666;
195
+ line-height: 1.6;
196
+ }
197
+ .base-url {
198
+ background: #f8f9fa;
199
+ padding: 0.5rem 1rem;
200
+ border-radius: 6px;
201
+ font-family: "Courier New", monospace;
202
+ color: #333;
203
+ display: inline-block;
204
+ margin-top: 0.5rem;
205
+ }
206
+ .endpoints {
207
+ background: white;
208
+ border-radius: 12px;
209
+ padding: 2rem;
210
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
211
+ }
212
+ .endpoints h2 {
213
+ color: #333;
214
+ font-size: 1.5rem;
215
+ margin-bottom: 1.5rem;
216
+ padding-bottom: 0.5rem;
217
+ border-bottom: 2px solid #667eea;
218
+ }
219
+ .endpoint-item {
220
+ background: #f8f9fa;
221
+ border: 1px solid #e9ecef;
222
+ border-radius: 8px;
223
+ padding: 1.5rem;
224
+ margin-bottom: 1rem;
225
+ transition: all 0.3s ease;
226
+ cursor: pointer;
227
+ }
228
+ .endpoint-item:hover,
229
+ .endpoint-item.active {
230
+ transform: translateY(-2px);
231
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
232
+ border-color: #667eea;
233
+ }
234
+ .endpoint-header {
235
+ display: flex;
236
+ align-items: center;
237
+ gap: 1rem;
238
+ margin-bottom: 1rem;
239
+ flex-wrap: wrap;
240
+ }
241
+ .method-badge {
242
+ padding: 0.3rem 0.8rem;
243
+ border-radius: 4px;
244
+ font-weight: 600;
245
+ font-size: 0.75rem;
246
+ text-transform: uppercase;
247
+ letter-spacing: 0.5px;
248
+ }
249
+ .method-get {
250
+ background: #e7f5ff;
251
+ color: #1971c2;
252
+ }
253
+ .method-post {
254
+ background: #d3f9d8;
255
+ color: #2f9e44;
256
+ }
257
+ .permission-badge {
258
+ padding: 0.3rem 0.8rem;
259
+ border-radius: 4px;
260
+ font-weight: 600;
261
+ font-size: 0.75rem;
262
+ text-transform: uppercase;
263
+ letter-spacing: 0.5px;
264
+ }
265
+ .permission-public {
266
+ background: #d3f9d8;
267
+ color: #2f9e44;
268
+ }
269
+ .permission-private {
270
+ background: #ffd43b;
271
+ color: #856404;
272
+ }
273
+ .endpoint-path {
274
+ font-family: "Courier New", monospace;
275
+ font-size: 1.1rem;
276
+ color: #333;
277
+ font-weight: 500;
278
+ }
279
+ .endpoint-count {
280
+ margin-left: auto;
281
+ background: #667eea;
282
+ color: white;
283
+ padding: 0.3rem 0.8rem;
284
+ border-radius: 20px;
285
+ font-size: 0.85rem;
286
+ font-weight: 600;
287
+ }
288
+ .endpoint-description {
289
+ color: #666;
290
+ line-height: 1.5;
291
+ }
292
+ .endpoint-actions {
293
+ margin-top: 1rem;
294
+ }
295
+ .action-btn {
296
+ padding: 0.5rem 1rem;
297
+ border-radius: 6px;
298
+ text-decoration: none;
299
+ font-size: 0.9rem;
300
+ font-weight: 500;
301
+ transition: all 0.2s;
302
+ display: inline-block;
303
+ }
304
+ .btn-primary {
305
+ background: #667eea;
306
+ color: white;
307
+ }
308
+ .btn-primary:hover {
309
+ background: #5568d3;
310
+ transform: translateY(-1px);
311
+ }
312
+ .footer {
313
+ text-align: center;
314
+ padding: 2rem;
315
+ color: rgba(255, 255, 255, 0.7);
316
+ font-size: 0.9rem;
317
+ }
318
+ `}</style>
319
+ </>
320
+ );
321
+ }
package/bin/cli.js ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from "child_process";
4
+ import yargs from "yargs";
5
+ import { hideBin } from "yargs/helpers";
6
+ import path from "path";
7
+ import { fileURLToPath } from "url";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const argv = yargs(hideBin(process.argv))
13
+ .option("port", { alias: "p", type: "number", default: 4000 })
14
+ .option("db", { alias: "d", type: "string", default: "./db.json" })
15
+ .option("watch", { alias: "w", type: "boolean", default: false })
16
+ .help().argv;
17
+
18
+ const projectRoot = path.join(__dirname, "..");
19
+
20
+ process.env.DB_PATH = path.resolve(argv.db);
21
+ process.env.PORT = argv.port.toString();
22
+
23
+ console.log(`✅ db-json-cli running on http://localhost:${argv.port}\n`);
24
+
25
+ try {
26
+ execSync(`cd "${projectRoot}" && npx next start -p ${argv.port}`, {
27
+ stdio: "inherit",
28
+ env: process.env,
29
+ shell: true,
30
+ });
31
+ } catch (err) {
32
+ console.error("Error:", err.message);
33
+ process.exit(1);
34
+ }
package/db.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "users": [
3
+ {
4
+ "id": 1,
5
+ "email": "test",
6
+ "name": "홍길동",
7
+ "password": "$2a$10$aRUDFO7qzoB38sPn4d/9lON24nVo5UIG6T85MhmDK6UtoTIDjsf3i"
8
+ }
9
+ ],
10
+ "rules": {
11
+ "test1": "public",
12
+ "test2": "private",
13
+ "test3": "public"
14
+ },
15
+ "test1": [
16
+ {
17
+ "id": 1,
18
+ "message": "good"
19
+ },
20
+ {
21
+ "id": 2,
22
+ "message": "good"
23
+ },
24
+ {
25
+ "id": 3,
26
+ "message": "good"
27
+ }
28
+ ],
29
+ "test2": [
30
+ {
31
+ "id": 1,
32
+ "message": "good"
33
+ },
34
+ {
35
+ "id": 2,
36
+ "message": "good"
37
+ },
38
+ {
39
+ "id": 3,
40
+ "message": "good"
41
+ }
42
+ ],
43
+ "test3": [
44
+ {
45
+ "id": 1,
46
+ "message": "good"
47
+ },
48
+ {
49
+ "id": 2,
50
+ "message": "good"
51
+ },
52
+ {
53
+ "id": 3,
54
+ "message": "good"
55
+ }
56
+ ]
57
+ }
package/jsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "paths": {
5
+ "@/*": ["./*"]
6
+ }
7
+ }
8
+ }
@@ -1,8 +1,8 @@
1
1
  import jwt from "jsonwebtoken";
2
2
  import bcrypt from "bcryptjs";
3
3
 
4
- const ACCESS_SECRET = "access-secret";
5
- const REFRESH_SECRET = "refresh-secret";
4
+ const ACCESS_SECRET = process.env.ACCESS_SECRET || "access-secret";
5
+ const REFRESH_SECRET = process.env.REFRESH_SECRET || "refresh-secret";
6
6
 
7
7
  export const hashPassword = async (password) => {
8
8
  const salt = await bcrypt.genSalt(10);
@@ -13,22 +13,24 @@ export const comparePassword = async (password, hash) => {
13
13
  return bcrypt.compare(password, hash);
14
14
  };
15
15
 
16
- export const generateTokens = async (payload) => {
16
+ export const generateTokens = (payload) => {
17
17
  const accessToken = jwt.sign(payload, ACCESS_SECRET, { expiresIn: "1h" });
18
18
  const refreshToken = jwt.sign(payload, REFRESH_SECRET, { expiresIn: "7d" });
19
19
  return { accessToken, refreshToken };
20
20
  };
21
21
 
22
- export const authMiddleware = async (req, res, next) => {
23
- const auth = req.headers["authorization"];
24
- if (!auth) return res.status(401).json({ message: "No token" });
25
-
26
- const token = auth.split(" ")[1];
22
+ export const verifyToken = (token) => {
27
23
  try {
28
- const decoded = jwt.verify(token, ACCESS_SECRET);
29
- req.user = decoded;
30
- next();
24
+ return jwt.verify(token, ACCESS_SECRET);
31
25
  } catch (e) {
32
- return res.status(401).json({ message: "Invalid token" });
26
+ return null;
33
27
  }
34
28
  };
29
+
30
+ export const authMiddleware = (req) => {
31
+ const auth = req.headers.get("authorization");
32
+ if (!auth) return null;
33
+
34
+ const token = auth.split(" ")[1];
35
+ return verifyToken(token);
36
+ };
package/lib/db.js ADDED
@@ -0,0 +1,47 @@
1
+ import fs from "fs-extra";
2
+
3
+ let dbCache = null;
4
+ let dbPath = process.env.DB_PATH || "./db.json";
5
+
6
+ export const getDB = async () => {
7
+ if (dbCache) return dbCache;
8
+
9
+ const defaultDB = {
10
+ users: [],
11
+ rules: { test1: "public", test2: "private", test3: "public" },
12
+ test1: [
13
+ { id: 1, message: "good" },
14
+ { id: 2, message: "good" },
15
+ { id: 3, message: "good" },
16
+ ],
17
+ test2: [
18
+ { id: 1, message: "good" },
19
+ { id: 2, message: "good" },
20
+ { id: 3, message: "good" },
21
+ ],
22
+ test3: [
23
+ { id: 1, message: "good" },
24
+ { id: 2, message: "good" },
25
+ { id: 3, message: "good" },
26
+ ],
27
+ };
28
+
29
+ if (fs.existsSync(dbPath)) {
30
+ dbCache = await fs.readJson(dbPath);
31
+ } else {
32
+ dbCache = defaultDB;
33
+ await fs.writeJson(dbPath, defaultDB, { spaces: 2 });
34
+ }
35
+
36
+ return dbCache;
37
+ };
38
+
39
+ export const saveDB = async (db) => {
40
+ dbCache = db;
41
+ await fs.writeJson(dbPath, db, { spaces: 2 });
42
+ };
43
+
44
+ export const refreshDB = async () => {
45
+ dbCache = null;
46
+ return await getDB();
47
+ };
package/next.config.js ADDED
@@ -0,0 +1,21 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ reactStrictMode: true,
4
+ async headers() {
5
+ return [
6
+ {
7
+ source: "/(.*)",
8
+ headers: [
9
+ {
10
+ key: "Content-Security-Policy",
11
+ // 아예 모든 연결을 허용해서 테스트해보세요 (개발용)
12
+ value:
13
+ "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' http://localhost:4000;",
14
+ },
15
+ ],
16
+ },
17
+ ];
18
+ },
19
+ };
20
+
21
+ export default nextConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "db-json-cli",
3
- "version": "1.0.4",
3
+ "version": "2.0.0",
4
4
  "license": "MIT",
5
5
  "description": "Lightweight JSON-based backend server with JWT authentication and CLI support",
6
6
  "repository": {
@@ -19,22 +19,23 @@
19
19
  "express",
20
20
  "api"
21
21
  ],
22
+ "type": "module",
22
23
  "bin": {
23
- "db-json-cli": "./bin/db-json-cli.js"
24
+ "db-json-cli": "./bin/cli.js"
24
25
  },
25
- "type": "module",
26
26
  "scripts": {
27
- "start": "node bin/db-json-cli.js"
27
+ "dev": "next dev",
28
+ "build": "next build",
29
+ "start": "next start",
30
+ "prepublishOnly": "npm run build"
28
31
  },
29
32
  "dependencies": {
33
+ "next": "^14.0.0",
34
+ "react": "^18.2.0",
35
+ "react-dom": "^18.2.0",
36
+ "fs-extra": "^11.2.0",
37
+ "jsonwebtoken": "^9.0.2",
30
38
  "bcryptjs": "^2.4.3",
31
- "body-parser": "^1.20.2",
32
- "chokidar": "^4.0.3",
33
- "cors": "^2.8.5",
34
- "ejs": "^3.1.10",
35
- "express": "^4.21.2",
36
- "fs-extra": "^11.1.1",
37
- "jsonwebtoken": "^9.0.0",
38
- "yargs": "^18.0.0"
39
+ "yargs": "^17.7.2"
39
40
  }
40
41
  }
@@ -1,30 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import yargs from "yargs";
4
- import { hideBin } from "yargs/helpers";
5
- import { startServer } from "../src/server.js";
6
-
7
- const argv = yargs(hideBin(process.argv))
8
- .option("port", {
9
- alias: "p",
10
- type: "number",
11
- default: 4000,
12
- description: "Port to run the server on",
13
- })
14
- .option("db", {
15
- alias: "d",
16
- type: "string",
17
- default: "./db.json",
18
- description: "Path to JSON database file",
19
- })
20
- .option("watch", {
21
- alias: "w",
22
- type: "boolean",
23
- default: false,
24
- description: "Watch DB file for changes",
25
- })
26
- .help().argv;
27
-
28
- if (argv.watch) process.env.WATCH = "true";
29
-
30
- startServer(argv.db, argv.port);
package/src/index.ejs DELETED
@@ -1,261 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>db-json-cli API Documentation</title>
7
- <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
- body {
14
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
15
- background: linear-gradient(to bottom, #1a1a2e 0%, #16213e 100%);
16
- color: #333;
17
- min-height: 100vh;
18
- }
19
- .header {
20
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
21
- padding: 3rem 2rem;
22
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
23
- }
24
- .container {
25
- max-width: 1200px;
26
- margin: 0 auto;
27
- }
28
- .header h1 {
29
- color: white;
30
- font-size: 2.5rem;
31
- margin-bottom: 0.5rem;
32
- font-weight: 700;
33
- }
34
- .header p {
35
- color: rgba(255, 255, 255, 0.9);
36
- font-size: 1.1rem;
37
- }
38
- .content {
39
- padding: 2rem;
40
- }
41
- .info-box {
42
- background: white;
43
- border-radius: 12px;
44
- padding: 1.5rem;
45
- margin-bottom: 2rem;
46
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
47
- }
48
- .info-box h2 {
49
- color: #667eea;
50
- font-size: 1.3rem;
51
- margin-bottom: 0.5rem;
52
- }
53
- .info-box p {
54
- color: #666;
55
- line-height: 1.6;
56
- }
57
- .base-url {
58
- background: #f8f9fa;
59
- padding: 0.5rem 1rem;
60
- border-radius: 6px;
61
- font-family: "Courier New", monospace;
62
- color: #333;
63
- display: inline-block;
64
- margin-top: 0.5rem;
65
- }
66
- .endpoints {
67
- background: white;
68
- border-radius: 12px;
69
- padding: 2rem;
70
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
71
- }
72
- .endpoints h2 {
73
- color: #333;
74
- font-size: 1.5rem;
75
- margin-bottom: 1.5rem;
76
- padding-bottom: 0.5rem;
77
- border-bottom: 2px solid #667eea;
78
- }
79
- .endpoint-item {
80
- background: #f8f9fa;
81
- border: 1px solid #e9ecef;
82
- border-radius: 8px;
83
- padding: 1.5rem;
84
- margin-bottom: 1rem;
85
- transition: all 0.3s ease;
86
- }
87
- .endpoint-item:hover {
88
- transform: translateY(-2px);
89
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
90
- border-color: #667eea;
91
- }
92
- .endpoint-header {
93
- display: flex;
94
- align-items: center;
95
- gap: 1rem;
96
- margin-bottom: 1rem;
97
- flex-wrap: wrap;
98
- }
99
- .method-badge {
100
- padding: 0.3rem 0.8rem;
101
- border-radius: 4px;
102
- font-weight: 600;
103
- font-size: 0.75rem;
104
- text-transform: uppercase;
105
- letter-spacing: 0.5px;
106
- }
107
- .method-get {
108
- background: #e7f5ff;
109
- color: #1971c2;
110
- }
111
- .method-post {
112
- background: #d3f9d8;
113
- color: #2f9e44;
114
- }
115
- .permission-badge {
116
- padding: 0.3rem 0.8rem;
117
- border-radius: 4px;
118
- font-weight: 600;
119
- font-size: 0.75rem;
120
- text-transform: uppercase;
121
- letter-spacing: 0.5px;
122
- }
123
- .permission-public {
124
- background: #d3f9d8;
125
- color: #2f9e44;
126
- }
127
- .permission-private {
128
- background: #ffd43b;
129
- color: #856404;
130
- }
131
- .endpoint-path {
132
- font-family: "Courier New", monospace;
133
- font-size: 1.1rem;
134
- color: #333;
135
- font-weight: 500;
136
- }
137
- .endpoint-count {
138
- margin-left: auto;
139
- background: #667eea;
140
- color: white;
141
- padding: 0.3rem 0.8rem;
142
- border-radius: 20px;
143
- font-size: 0.85rem;
144
- font-weight: 600;
145
- }
146
- .endpoint-description {
147
- color: #666;
148
- margin-bottom: 1rem;
149
- line-height: 1.5;
150
- }
151
- .endpoint-actions {
152
- display: flex;
153
- gap: 0.5rem;
154
- }
155
- .action-btn {
156
- padding: 0.5rem 1rem;
157
- border-radius: 6px;
158
- text-decoration: none;
159
- font-size: 0.9rem;
160
- font-weight: 500;
161
- transition: all 0.2s;
162
- display: inline-block;
163
- }
164
- .btn-primary {
165
- background: #667eea;
166
- color: white;
167
- }
168
- .btn-primary:hover {
169
- background: #5568d3;
170
- transform: translateY(-1px);
171
- }
172
- .btn-secondary {
173
- background: #e9ecef;
174
- color: #333;
175
- }
176
- .btn-secondary:hover {
177
- background: #dee2e6;
178
- }
179
- .footer {
180
- text-align: center;
181
- padding: 2rem;
182
- color: rgba(255, 255, 255, 0.7);
183
- font-size: 0.9rem;
184
- }
185
- </style>
186
- </head>
187
- <body>
188
- <div class="header">
189
- <div class="container">
190
- <h1>🚀 db-json-cli API</h1>
191
- <p>RESTful JSON API Documentation</p>
192
- </div>
193
- </div>
194
-
195
- <div class="container content">
196
- <div class="info-box">
197
- <h2>📖 API Information</h2>
198
- <p>Welcome to the db-json-cli API. This API provides RESTful endpoints for managing your data.</p>
199
- <div class="base-url">Base URL: http://localhost:<%= port %></div>
200
- </div>
201
-
202
- <div class="endpoints">
203
- <h2>Available Endpoints</h2>
204
- <% routeList.forEach(r => { %>
205
- <div class="endpoint-item">
206
- <div class="endpoint-header">
207
- <span class="method-badge method-get">GET</span>
208
- <span class="endpoint-path">/<%= r.key %></span>
209
- <span class="permission-badge permission-<%= r.permission %>"><%= r.permission.toUpperCase() %></span>
210
- <span class="endpoint-count"><%= r.count %> <%= r.count === 1 ? 'item' : 'items' %></span>
211
- </div>
212
- <div class="endpoint-description">Retrieve all <%= r.key %> or filter by ID range using ?from=1&to=10</div>
213
- <div class="endpoint-actions">
214
- <a href="http://localhost:<%= port %>/<%= r.key %>" target="_blank" class="action-btn btn-primary">
215
- Try it out
216
- </a>
217
- </div>
218
- </div>
219
-
220
- <div class="endpoint-item">
221
- <div class="endpoint-header">
222
- <span class="method-badge method-get">GET</span>
223
- <span class="endpoint-path">/<%= r.key %>/:id</span>
224
- <span class="permission-badge permission-<%= r.permission %>"><%= r.permission.toUpperCase() %></span>
225
- </div>
226
- <div class="endpoint-description">Retrieve a specific item by ID</div>
227
- </div>
228
-
229
- <div class="endpoint-item">
230
- <div class="endpoint-header">
231
- <span class="method-badge method-post">POST</span>
232
- <span class="endpoint-path">/<%= r.key %></span>
233
- <span class="permission-badge permission-<%= r.permission %>"><%= r.permission.toUpperCase() %></span>
234
- </div>
235
- <div class="endpoint-description">Create a new item (ID will be auto-generated)</div>
236
- </div>
237
- <% }) %>
238
-
239
- <div class="endpoint-item">
240
- <div class="endpoint-header">
241
- <span class="method-badge method-post">POST</span>
242
- <span class="endpoint-path">/register</span>
243
- </div>
244
- <div class="endpoint-description">Register a new user account</div>
245
- </div>
246
-
247
- <div class="endpoint-item">
248
- <div class="endpoint-header">
249
- <span class="method-badge method-post">POST</span>
250
- <span class="endpoint-path">/login</span>
251
- </div>
252
- <div class="endpoint-description">Login and receive access tokens</div>
253
- </div>
254
- </div>
255
- </div>
256
-
257
- <div class="footer">
258
- <p>Generated by db-json-cli • Made with ♥</p>
259
- </div>
260
- </body>
261
- </html>
package/src/server.js DELETED
@@ -1,146 +0,0 @@
1
- import express from "express";
2
- import fs from "fs-extra";
3
- import bodyParser from "body-parser";
4
- import cors from "cors";
5
- import { hashPassword, comparePassword, generateTokens, authMiddleware } from "./auth.js";
6
-
7
- import path from "path";
8
- import { fileURLToPath } from "url";
9
-
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = path.dirname(__filename);
12
-
13
- export const startServer = async (dbPath, port = 4000) => {
14
- const app = express();
15
- app.use(cors());
16
- app.use(bodyParser.json());
17
-
18
- // EJS 설정
19
- app.set("view engine", "ejs");
20
- app.set("views", __dirname);
21
-
22
- app.use(express.static("public"));
23
-
24
- let db = {
25
- users: [],
26
- rules: { test1: "public", test2: "private", test3: "public" },
27
- test1: [
28
- { id: 1, message: "good" },
29
- { id: 2, message: "good" },
30
- { id: 3, message: "good" },
31
- ],
32
- test2: [
33
- { id: 1, message: "good" },
34
- { id: 2, message: "good" },
35
- { id: 3, message: "good" },
36
- ],
37
- test3: [
38
- { id: 1, message: "good" },
39
- { id: 2, message: "good" },
40
- { id: 3, message: "good" },
41
- ],
42
- };
43
- if (fs.existsSync(dbPath)) db = await fs.readJson(dbPath);
44
-
45
- const saveDB = async () => fs.writeJson(dbPath, db, { spaces: 2 });
46
-
47
- // REGISTER
48
- app.post("/register", async (req, res) => {
49
- const { email, password, name } = req.body;
50
- if (!email || !password) return res.status(400).json({ message: "Email/password required" });
51
-
52
- const exists = db.users.find((u) => u.email === email);
53
- if (exists) return res.status(409).json({ message: "User already exists" });
54
-
55
- const hashed = await hashPassword(password);
56
- const id = db.users.length ? Math.max(...db.users.map((u) => u.id)) + 1 : 1;
57
- const newUser = { id, email, name: name || "", password: hashed };
58
- db.users.push(newUser);
59
- await saveDB();
60
-
61
- const tokens = generateTokens({ id, email });
62
- res.json(tokens);
63
- });
64
-
65
- // LOGIN
66
- app.post("/login", async (req, res) => {
67
- const { email, password } = req.body;
68
- const user = db.users.find((u) => u.email === email);
69
- if (!user) return res.status(401).json({ message: "Invalid credentials" });
70
-
71
- const valid = await comparePassword(password, user.password);
72
- if (!valid) return res.status(401).json({ message: "Invalid credentials" });
73
-
74
- const tokens = generateTokens({ id: user.id, email: user.email });
75
- res.json(tokens);
76
- });
77
-
78
- // AUTO ROUTES
79
- const routeList = [];
80
- Object.keys(db).forEach((key) => {
81
- if (["users", "rules"].includes(key)) return;
82
- const isPrivate = db.rules?.[key] === "private";
83
- const route = express.Router();
84
-
85
- // GET /key
86
- route.get("/", async (req, res) => {
87
- const { from, to } = req.query;
88
- let data = db[key];
89
- if (from && to) {
90
- const fromNum = Number(from);
91
- const toNum = Number(to);
92
- data = data.filter((item) => item.id >= fromNum && item.id <= toNum);
93
- }
94
- res.json(data);
95
- });
96
-
97
- // GET /key/:id
98
- route.get("/:id", async (req, res) => {
99
- const id = Number(req.params.id);
100
- const item = db[key].find((i) => i.id === id);
101
- if (!item) return res.status(404).json({ message: "Not found" });
102
- res.json(item);
103
- });
104
-
105
- // POST /key
106
- route.post("/", async (req, res) => {
107
- const newItem = req.body;
108
- if (!newItem || typeof newItem !== "object") return res.status(400).json({ message: "Invalid body" });
109
-
110
- const id = db[key].length ? Math.max(...db[key].map((i) => i.id)) + 1 : 1;
111
- const item = { id, ...newItem };
112
- db[key].push(item);
113
- await saveDB();
114
- res.json(item);
115
- });
116
-
117
- if (isPrivate) {
118
- app.use(`/${key}`, authMiddleware, route);
119
- } else {
120
- app.use(`/${key}`, route);
121
- }
122
-
123
- // routeList에 추가 (권한 정보 포함)
124
- routeList.push({
125
- key,
126
- count: db[key].length,
127
- permission: isPrivate ? "private" : "public"
128
- });
129
- });
130
-
131
- // Index HTML
132
- app.get("/", (req, res) => {
133
- res.render("index", { routeList, port });
134
- });
135
-
136
- app.listen(port, () => {
137
- console.log(`✅ db-json-cli Success! ✧*。٩(ˊᗜˋ*)و✧*。\n`);
138
- console.log(`Index:\nhttp://localhost:${port}/\n\n`);
139
-
140
- console.log("Endpoints:");
141
- routeList.forEach((r) => {
142
- const badge = r.permission === "private" ? "🔒 PRIVATE" : "🔓 PUBLIC";
143
- console.log(`${badge} - http://localhost:${port}/${r.key} (${r.count} items)`);
144
- });
145
- });
146
- };