create-sprint 0.0.36 → 0.0.42
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.
- package/dist/generators.js +104 -9
- package/dist/index.js +78 -87
- package/package.json +2 -2
- package/src/generators.ts +111 -9
- package/src/index.ts +96 -94
package/dist/generators.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
2
|
+
export function generateJWTKeys() {
|
|
3
|
+
const { publicKey, privateKey } = generateKeyPairSync("ec", {
|
|
4
|
+
namedCurve: "prime256v1",
|
|
5
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
6
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
|
7
|
+
});
|
|
8
|
+
return { publicKey, privateKey };
|
|
9
|
+
}
|
|
1
10
|
export function getTypeScriptPackageJson(name, telemetry) {
|
|
2
11
|
const deps = {
|
|
3
|
-
"sprint-es": "^0.0.
|
|
4
|
-
"node-cron": "^3.0.3",
|
|
5
|
-
dotenv: "^17.0.0",
|
|
12
|
+
"sprint-es": "^0.0.38"
|
|
6
13
|
};
|
|
7
14
|
const devDeps = {
|
|
8
15
|
"@types/node": "^22.0.0",
|
|
@@ -24,6 +31,7 @@ export function getTypeScriptPackageJson(name, telemetry) {
|
|
|
24
31
|
build: "sprint-es build",
|
|
25
32
|
start: "sprint-es start",
|
|
26
33
|
dev: "sprint-es dev",
|
|
34
|
+
"generate:keys": "sprint-es generate-keys"
|
|
27
35
|
},
|
|
28
36
|
dependencies: deps,
|
|
29
37
|
devDependencies: devDeps,
|
|
@@ -31,9 +39,7 @@ export function getTypeScriptPackageJson(name, telemetry) {
|
|
|
31
39
|
}
|
|
32
40
|
export function getJavaScriptPackageJson(name, telemetry) {
|
|
33
41
|
const deps = {
|
|
34
|
-
"sprint-es": "^0.0.
|
|
35
|
-
"node-cron": "^3.0.3",
|
|
36
|
-
dotenv: "^17.0.0",
|
|
42
|
+
"sprint-es": "^0.0.38"
|
|
37
43
|
};
|
|
38
44
|
if (telemetry === "sentry" || telemetry === "glitchtip") {
|
|
39
45
|
deps["@sentry/node"] = "^8.0.0";
|
|
@@ -51,8 +57,9 @@ export function getJavaScriptPackageJson(name, telemetry) {
|
|
|
51
57
|
build: "sprint-es build",
|
|
52
58
|
start: "sprint-es start",
|
|
53
59
|
dev: "sprint-es dev",
|
|
60
|
+
"generate:keys": "sprint-es generate-keys"
|
|
54
61
|
},
|
|
55
|
-
dependencies: deps
|
|
62
|
+
dependencies: deps
|
|
56
63
|
};
|
|
57
64
|
}
|
|
58
65
|
export function getTsConfig() {
|
|
@@ -147,24 +154,28 @@ export function getAdminRoute(language) {
|
|
|
147
154
|
if (language === "typescript") {
|
|
148
155
|
return `import { Router } from "sprint-es";
|
|
149
156
|
import { adminSchema } from "@/schemas/admin";
|
|
150
|
-
import { adminController, adminUsersController } from "@/controllers/admin";
|
|
157
|
+
import { adminController, adminUsersController, jwtGenerateController, jwtValidateController } from "@/controllers/admin";
|
|
151
158
|
|
|
152
159
|
const router = Router();
|
|
153
160
|
|
|
154
161
|
router.get("/", adminSchema, adminController);
|
|
155
162
|
router.get("/users", adminSchema, adminUsersController);
|
|
163
|
+
router.post("/jwt/generate", jwtGenerateController);
|
|
164
|
+
router.post("/jwt/validate", jwtValidateController);
|
|
156
165
|
|
|
157
166
|
export default router;
|
|
158
167
|
`;
|
|
159
168
|
}
|
|
160
169
|
return `import { Router } from "sprint-es";
|
|
161
170
|
import { adminSchema } from "../schemas/admin.js";
|
|
162
|
-
import { adminController, adminUsersController } from "../controllers/admin.js";
|
|
171
|
+
import { adminController, adminUsersController, jwtGenerateController, jwtValidateController } from "../controllers/admin.js";
|
|
163
172
|
|
|
164
173
|
const router = Router();
|
|
165
174
|
|
|
166
175
|
router.get("/", adminSchema, adminController);
|
|
167
176
|
router.get("/users", adminSchema, adminUsersController);
|
|
177
|
+
router.post("/jwt/generate", jwtGenerateController);
|
|
178
|
+
router.post("/jwt/validate", jwtValidateController);
|
|
168
179
|
|
|
169
180
|
export default router;
|
|
170
181
|
`;
|
|
@@ -194,6 +205,7 @@ export const homeController = (req, res) => {
|
|
|
194
205
|
export function getAdminController(language) {
|
|
195
206
|
if (language === "typescript") {
|
|
196
207
|
return `import { Handler } from "sprint-es";
|
|
208
|
+
import { signEncrypted, verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
|
|
197
209
|
|
|
198
210
|
export const adminController: Handler = (req, res) => {
|
|
199
211
|
res.json({
|
|
@@ -210,9 +222,48 @@ export const adminUsersController: Handler = (req, res) => {
|
|
|
210
222
|
]
|
|
211
223
|
});
|
|
212
224
|
};
|
|
225
|
+
|
|
226
|
+
export const jwtGenerateController: Handler = (req, res) => {
|
|
227
|
+
const { userId, role } = req.body || {};
|
|
228
|
+
|
|
229
|
+
if (!userId) {
|
|
230
|
+
return res.status(400).json({ error: "userId is required" });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const { privateKey } = getJwtFromEnv();
|
|
235
|
+
const payload = { userId, role: role || "user" };
|
|
236
|
+
const token = signEncrypted(payload, privateKey, { expiresIn: "1h" });
|
|
237
|
+
res.json({ token });
|
|
238
|
+
} catch (error) {
|
|
239
|
+
return res.status(500).json({ error: "JWT not configured" });
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export const jwtValidateController: Handler = (req, res) => {
|
|
244
|
+
const token = req.sprint?.getAuthorization() || req.headers.authorization;
|
|
245
|
+
|
|
246
|
+
if (!token) {
|
|
247
|
+
return res.status(401).json({ error: "No token provided" });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const { publicKey } = getJwtFromEnv();
|
|
252
|
+
const decoded = verifyEncrypted(token, publicKey);
|
|
253
|
+
|
|
254
|
+
if (!decoded) {
|
|
255
|
+
return res.status(401).json({ error: "Invalid token" });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
res.json({ valid: true, payload: decoded });
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return res.status(500).json({ error: "JWT not configured" });
|
|
261
|
+
}
|
|
262
|
+
};
|
|
213
263
|
`;
|
|
214
264
|
}
|
|
215
265
|
return `import { Handler } from "sprint-es";
|
|
266
|
+
import { signEncrypted, verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
|
|
216
267
|
|
|
217
268
|
export const adminController = (req, res) => {
|
|
218
269
|
res.json({
|
|
@@ -229,6 +280,44 @@ export const adminUsersController = (req, res) => {
|
|
|
229
280
|
]
|
|
230
281
|
});
|
|
231
282
|
};
|
|
283
|
+
|
|
284
|
+
export const jwtGenerateController = (req, res) => {
|
|
285
|
+
const { userId, role } = req.body || {};
|
|
286
|
+
|
|
287
|
+
if (!userId) {
|
|
288
|
+
return res.status(400).json({ error: "userId is required" });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const { privateKey } = getJwtFromEnv();
|
|
293
|
+
const payload = { userId, role: role || "user" };
|
|
294
|
+
const token = signEncrypted(payload, privateKey, { expiresIn: "1h" });
|
|
295
|
+
res.json({ token });
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return res.status(500).json({ error: "JWT not configured" });
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export const jwtValidateController = (req, res) => {
|
|
302
|
+
const token = req.sprint?.getAuthorization() || req.headers.authorization;
|
|
303
|
+
|
|
304
|
+
if (!token) {
|
|
305
|
+
return res.status(401).json({ error: "No token provided" });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const { publicKey } = getJwtFromEnv();
|
|
310
|
+
const decoded = verifyEncrypted(token, publicKey);
|
|
311
|
+
|
|
312
|
+
if (!decoded) {
|
|
313
|
+
return res.status(401).json({ error: "Invalid token" });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
res.json({ valid: true, payload: decoded });
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return res.status(500).json({ error: "JWT not configured" });
|
|
319
|
+
}
|
|
320
|
+
};
|
|
232
321
|
`;
|
|
233
322
|
}
|
|
234
323
|
export function getHomeSchema(language) {
|
|
@@ -511,8 +600,11 @@ DISCORD_WEBHOOK_URL=
|
|
|
511
600
|
return env;
|
|
512
601
|
}
|
|
513
602
|
export function getEnvDevelopment(telemetry) {
|
|
603
|
+
const { publicKey, privateKey } = generateJWTKeys();
|
|
514
604
|
let env = `NODE_ENV=development
|
|
515
605
|
PORT=3000
|
|
606
|
+
JWT_PUBLIC_KEY=${publicKey.replace(/\n/g, "\\n")}
|
|
607
|
+
JWT_PRIVATE_KEY=${privateKey.replace(/\n/g, "\\n")}
|
|
516
608
|
`;
|
|
517
609
|
if (telemetry === "sentry" || telemetry === "glitchtip") {
|
|
518
610
|
env += `
|
|
@@ -529,8 +621,11 @@ DISCORD_WEBHOOK_URL=
|
|
|
529
621
|
return env;
|
|
530
622
|
}
|
|
531
623
|
export function getEnvProduction(telemetry) {
|
|
624
|
+
const { publicKey, privateKey } = generateJWTKeys();
|
|
532
625
|
let env = `NODE_ENV=production
|
|
533
626
|
PORT=3000
|
|
627
|
+
JWT_PUBLIC_KEY=${publicKey.replace(/\n/g, "\\n")}
|
|
628
|
+
JWT_PRIVATE_KEY=${privateKey.replace(/\n/g, "\\n")}
|
|
534
629
|
`;
|
|
535
630
|
if (telemetry === "sentry" || telemetry === "glitchtip") {
|
|
536
631
|
env += `
|
package/dist/index.js
CHANGED
|
@@ -2,59 +2,68 @@ import { execSync } from "child_process";
|
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import { mkdir, writeFile } from "fs/promises";
|
|
4
4
|
import { join } from "path";
|
|
5
|
-
import
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
6
|
import { validateProjectName } from "./validators.js";
|
|
7
7
|
import { getTypeScriptPackageJson, getJavaScriptPackageJson, getTsConfig, getViteConfig, getMainFile, getHomeRoute, getAdminRoute, getHomeController, getAdminController, getAuthMiddleware, getHomeSchema, getAdminSchema, getDockerfile, getDockerCompose, getGitignore, getDockerIgnore, getSprintConfigFile, getEnvDevelopment, getEnvProduction, getExampleCronJob } from "./generators.js";
|
|
8
8
|
export async function runCLI(args) {
|
|
9
9
|
const options = parseArgs(args);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
10
|
+
p.intro("Sprint — Quickly API Framework");
|
|
11
|
+
const config = await p.group({
|
|
12
|
+
projectName: () => p.text({
|
|
13
|
+
message: "Project name:",
|
|
14
|
+
placeholder: "my-api",
|
|
15
|
+
validate: (v) => validateProjectName(v) || undefined,
|
|
16
|
+
}),
|
|
17
|
+
language: () => p.select({
|
|
18
|
+
message: "Language:",
|
|
19
|
+
options: [
|
|
20
|
+
{ value: "typescript", label: "TypeScript", hint: "recommended" },
|
|
21
|
+
{ value: "javascript", label: "JavaScript" },
|
|
22
|
+
],
|
|
23
|
+
}),
|
|
24
|
+
telemetry: () => p.select({
|
|
25
|
+
message: "Error tracking:",
|
|
26
|
+
options: [
|
|
27
|
+
{ value: "none", label: "None" },
|
|
28
|
+
{ value: "sentry", label: "Sentry", hint: "free tier available" },
|
|
29
|
+
{ value: "glitchtip", label: "GlitchTip", hint: "self-hostable" },
|
|
30
|
+
{ value: "discord", label: "Discord Webhook", hint: "sends to a channel" },
|
|
31
|
+
],
|
|
32
|
+
}),
|
|
33
|
+
docker: () => p.confirm({ message: "Add Docker support?", initialValue: false }),
|
|
34
|
+
}, {
|
|
35
|
+
onCancel: () => {
|
|
36
|
+
p.cancel("Cancelled.");
|
|
37
|
+
process.exit(0);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const targetDir = config.projectName === "." ? process.cwd() : join(process.cwd(), config.projectName);
|
|
41
|
+
const s = p.spinner();
|
|
42
|
+
s.start("Creating project");
|
|
43
|
+
await createProject(config.projectName, config.language, config.telemetry, config.docker);
|
|
44
|
+
s.stop("Project created");
|
|
45
|
+
const installDeps = await p.confirm({ message: "Install dependencies now?", initialValue: true });
|
|
39
46
|
if (installDeps) {
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
const s2 = p.spinner();
|
|
48
|
+
s2.start("Installing dependencies");
|
|
42
49
|
try {
|
|
43
|
-
execSync("npm install", { cwd: targetDir, stdio: "
|
|
44
|
-
|
|
50
|
+
execSync("npm install", { cwd: targetDir, stdio: "pipe" });
|
|
51
|
+
s2.stop("Dependencies installed");
|
|
45
52
|
}
|
|
46
53
|
catch {
|
|
47
|
-
|
|
54
|
+
s2.stop("Install failed — run npm install manually");
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
const cdCmd = config.projectName === "." ? "" : `cd ${config.projectName} && `;
|
|
58
|
+
p.note([
|
|
59
|
+
!installDeps ? `${cdCmd}npm install` : "",
|
|
60
|
+
`${cdCmd}npm run dev`,
|
|
61
|
+
]
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join("\n"), "Next steps");
|
|
64
|
+
p.outro("Ready. Happy shipping.");
|
|
57
65
|
}
|
|
66
|
+
;
|
|
58
67
|
function parseArgs(args) {
|
|
59
68
|
const options = {};
|
|
60
69
|
const hasTs = args.includes("--ts") || args.includes("--typescript");
|
|
@@ -85,63 +94,36 @@ function parseArgs(args) {
|
|
|
85
94
|
}
|
|
86
95
|
;
|
|
87
96
|
async function getProjectName() {
|
|
88
|
-
const name = await
|
|
97
|
+
const name = await p.text({
|
|
89
98
|
message: "Enter project name:",
|
|
90
|
-
validate: (value) =>
|
|
91
|
-
return validateProjectName(value) || true;
|
|
92
|
-
}
|
|
99
|
+
validate: (value) => validateProjectName(value) || undefined,
|
|
93
100
|
});
|
|
94
101
|
return name;
|
|
95
102
|
}
|
|
96
103
|
;
|
|
97
104
|
async function selectLanguage() {
|
|
98
|
-
const language = await select({
|
|
105
|
+
const language = await p.select({
|
|
99
106
|
message: "Select your preferred language:",
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
description: "Recommended - Type safety and better developer experience",
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
name: "JavaScript",
|
|
108
|
-
value: "javascript",
|
|
109
|
-
description: "Vanilla JavaScript for simpler projects",
|
|
110
|
-
}
|
|
111
|
-
]
|
|
107
|
+
options: [
|
|
108
|
+
{ value: "typescript", label: "TypeScript", hint: "recommended" },
|
|
109
|
+
{ value: "javascript", label: "JavaScript" },
|
|
110
|
+
],
|
|
112
111
|
});
|
|
113
112
|
return language;
|
|
114
113
|
}
|
|
115
114
|
;
|
|
116
115
|
async function selectTelemetry() {
|
|
117
|
-
const telemetry = await select({
|
|
116
|
+
const telemetry = await p.select({
|
|
118
117
|
message: "Select error tracking/telemetry solution:",
|
|
119
|
-
|
|
120
|
-
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
{
|
|
126
|
-
name: "Sentry",
|
|
127
|
-
value: "sentry",
|
|
128
|
-
description: "Full-featured error tracking (free tier available)",
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
name: "GlitchTip",
|
|
132
|
-
value: "glitchtip",
|
|
133
|
-
description: "Simple error tracking, can be self-hosted",
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
name: "Discord Webhook",
|
|
137
|
-
value: "discord",
|
|
138
|
-
description: "Send error notifications to Discord channel",
|
|
139
|
-
}
|
|
140
|
-
]
|
|
118
|
+
options: [
|
|
119
|
+
{ value: "none", label: "None" },
|
|
120
|
+
{ value: "sentry", label: "Sentry", hint: "free tier available" },
|
|
121
|
+
{ value: "glitchtip", label: "GlitchTip", hint: "self-hostable" },
|
|
122
|
+
{ value: "discord", label: "Discord Webhook", hint: "sends to a channel" },
|
|
123
|
+
],
|
|
141
124
|
});
|
|
142
125
|
return telemetry;
|
|
143
126
|
}
|
|
144
|
-
;
|
|
145
127
|
async function createProject(projectName, language, telemetryArg, useDockerArg) {
|
|
146
128
|
const isCurrentDir = projectName === ".";
|
|
147
129
|
const targetDir = isCurrentDir ? process.cwd() : join(process.cwd(), projectName);
|
|
@@ -156,9 +138,9 @@ async function createProject(projectName, language, telemetryArg, useDockerArg)
|
|
|
156
138
|
telemetry = await selectTelemetry();
|
|
157
139
|
let useDocker = useDockerArg || false;
|
|
158
140
|
if (!useDockerArg) {
|
|
159
|
-
useDocker = await confirm({
|
|
141
|
+
useDocker = await p.confirm({
|
|
160
142
|
message: "Do you want to add Docker support?",
|
|
161
|
-
|
|
143
|
+
initialValue: false,
|
|
162
144
|
});
|
|
163
145
|
}
|
|
164
146
|
let pkgJson;
|
|
@@ -181,6 +163,15 @@ async function createProject(projectName, language, telemetryArg, useDockerArg)
|
|
|
181
163
|
await mkdir(join(srcDir, "controllers"), { recursive: true });
|
|
182
164
|
await mkdir(join(srcDir, "schemas"), { recursive: true });
|
|
183
165
|
await mkdir(join(srcDir, "cronjobs"), { recursive: true });
|
|
166
|
+
await mkdir(join(srcDir, "config"), { recursive: true });
|
|
167
|
+
if (language === "typescript") {
|
|
168
|
+
await writeFile(join(srcDir, "config", "index.ts"), "");
|
|
169
|
+
await writeFile(join(srcDir, "config", "clients.ts"), "");
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
await writeFile(join(srcDir, "config", "index.js"), "");
|
|
173
|
+
await writeFile(join(srcDir, "config", "clients.js"), "");
|
|
174
|
+
}
|
|
184
175
|
await writeFile(join(srcDir, "middlewares", ".gitkeep"), "");
|
|
185
176
|
await writeFile(join(srcDir, "app." + (language === "typescript" ? "ts" : "js")), getMainFile(language));
|
|
186
177
|
await writeFile(join(srcDir, "routes", "home." + (language === "typescript" ? "ts" : "js")), getHomeRoute(language));
|
|
@@ -193,8 +184,8 @@ async function createProject(projectName, language, telemetryArg, useDockerArg)
|
|
|
193
184
|
await writeFile(join(srcDir, "cronjobs", "example." + (language === "typescript" ? "ts" : "js")), getExampleCronJob(language));
|
|
194
185
|
await writeFile(join(targetDir, ".env.development.example"), getEnvDevelopment(telemetry));
|
|
195
186
|
await writeFile(join(targetDir, ".env.production.example"), getEnvProduction(telemetry));
|
|
196
|
-
await writeFile(join(targetDir, ".env.development"),
|
|
197
|
-
await writeFile(join(targetDir, ".env.production"),
|
|
187
|
+
await writeFile(join(targetDir, ".env.development"), getEnvDevelopment(telemetry));
|
|
188
|
+
await writeFile(join(targetDir, ".env.production"), getEnvProduction(telemetry));
|
|
198
189
|
await writeFile(join(targetDir, ".gitignore"), getGitignore());
|
|
199
190
|
if (useDocker) {
|
|
200
191
|
await writeFile(join(targetDir, "Dockerfile"), getDockerfile(language));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-sprint",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.42",
|
|
4
4
|
"description": "Create a new Sprint API project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"author": "TPEOficial LLC",
|
|
27
27
|
"license": "Apache-2.0",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@
|
|
29
|
+
"@clack/prompts": "^1.0.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^25.3.3",
|
package/src/generators.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export interface JWTKeys {
|
|
4
|
+
publicKey: string;
|
|
5
|
+
privateKey: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function generateJWTKeys(): JWTKeys {
|
|
9
|
+
const { publicKey, privateKey } = generateKeyPairSync("ec", {
|
|
10
|
+
namedCurve: "prime256v1",
|
|
11
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
12
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
|
13
|
+
});
|
|
14
|
+
return { publicKey, privateKey };
|
|
15
|
+
}
|
|
16
|
+
|
|
1
17
|
export function getTypeScriptPackageJson(name: string, telemetry: string) {
|
|
2
18
|
const deps: Record<string, string> = {
|
|
3
|
-
"sprint-es": "^0.0.
|
|
4
|
-
"node-cron": "^3.0.3",
|
|
5
|
-
dotenv: "^17.0.0",
|
|
19
|
+
"sprint-es": "^0.0.38"
|
|
6
20
|
};
|
|
7
21
|
|
|
8
22
|
const devDeps: Record<string, string> = {
|
|
@@ -26,6 +40,7 @@ export function getTypeScriptPackageJson(name: string, telemetry: string) {
|
|
|
26
40
|
build: "sprint-es build",
|
|
27
41
|
start: "sprint-es start",
|
|
28
42
|
dev: "sprint-es dev",
|
|
43
|
+
"generate:keys": "sprint-es generate-keys"
|
|
29
44
|
},
|
|
30
45
|
dependencies: deps,
|
|
31
46
|
devDependencies: devDeps,
|
|
@@ -34,9 +49,7 @@ export function getTypeScriptPackageJson(name: string, telemetry: string) {
|
|
|
34
49
|
|
|
35
50
|
export function getJavaScriptPackageJson(name: string, telemetry: string) {
|
|
36
51
|
const deps: Record<string, string> = {
|
|
37
|
-
"sprint-es": "^0.0.
|
|
38
|
-
"node-cron": "^3.0.3",
|
|
39
|
-
dotenv: "^17.0.0",
|
|
52
|
+
"sprint-es": "^0.0.38"
|
|
40
53
|
};
|
|
41
54
|
|
|
42
55
|
if (telemetry === "sentry" || telemetry === "glitchtip") {
|
|
@@ -55,8 +68,9 @@ export function getJavaScriptPackageJson(name: string, telemetry: string) {
|
|
|
55
68
|
build: "sprint-es build",
|
|
56
69
|
start: "sprint-es start",
|
|
57
70
|
dev: "sprint-es dev",
|
|
71
|
+
"generate:keys": "sprint-es generate-keys"
|
|
58
72
|
},
|
|
59
|
-
dependencies: deps
|
|
73
|
+
dependencies: deps
|
|
60
74
|
};
|
|
61
75
|
}
|
|
62
76
|
|
|
@@ -157,24 +171,28 @@ export function getAdminRoute(language: string) {
|
|
|
157
171
|
if (language === "typescript") {
|
|
158
172
|
return `import { Router } from "sprint-es";
|
|
159
173
|
import { adminSchema } from "@/schemas/admin";
|
|
160
|
-
import { adminController, adminUsersController } from "@/controllers/admin";
|
|
174
|
+
import { adminController, adminUsersController, jwtGenerateController, jwtValidateController } from "@/controllers/admin";
|
|
161
175
|
|
|
162
176
|
const router = Router();
|
|
163
177
|
|
|
164
178
|
router.get("/", adminSchema, adminController);
|
|
165
179
|
router.get("/users", adminSchema, adminUsersController);
|
|
180
|
+
router.post("/jwt/generate", jwtGenerateController);
|
|
181
|
+
router.post("/jwt/validate", jwtValidateController);
|
|
166
182
|
|
|
167
183
|
export default router;
|
|
168
184
|
`;
|
|
169
185
|
}
|
|
170
186
|
return `import { Router } from "sprint-es";
|
|
171
187
|
import { adminSchema } from "../schemas/admin.js";
|
|
172
|
-
import { adminController, adminUsersController } from "../controllers/admin.js";
|
|
188
|
+
import { adminController, adminUsersController, jwtGenerateController, jwtValidateController } from "../controllers/admin.js";
|
|
173
189
|
|
|
174
190
|
const router = Router();
|
|
175
191
|
|
|
176
192
|
router.get("/", adminSchema, adminController);
|
|
177
193
|
router.get("/users", adminSchema, adminUsersController);
|
|
194
|
+
router.post("/jwt/generate", jwtGenerateController);
|
|
195
|
+
router.post("/jwt/validate", jwtValidateController);
|
|
178
196
|
|
|
179
197
|
export default router;
|
|
180
198
|
`;
|
|
@@ -206,6 +224,7 @@ export const homeController = (req, res) => {
|
|
|
206
224
|
export function getAdminController(language: string) {
|
|
207
225
|
if (language === "typescript") {
|
|
208
226
|
return `import { Handler } from "sprint-es";
|
|
227
|
+
import { signEncrypted, verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
|
|
209
228
|
|
|
210
229
|
export const adminController: Handler = (req, res) => {
|
|
211
230
|
res.json({
|
|
@@ -222,9 +241,48 @@ export const adminUsersController: Handler = (req, res) => {
|
|
|
222
241
|
]
|
|
223
242
|
});
|
|
224
243
|
};
|
|
244
|
+
|
|
245
|
+
export const jwtGenerateController: Handler = (req, res) => {
|
|
246
|
+
const { userId, role } = req.body || {};
|
|
247
|
+
|
|
248
|
+
if (!userId) {
|
|
249
|
+
return res.status(400).json({ error: "userId is required" });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const { privateKey } = getJwtFromEnv();
|
|
254
|
+
const payload = { userId, role: role || "user" };
|
|
255
|
+
const token = signEncrypted(payload, privateKey, { expiresIn: "1h" });
|
|
256
|
+
res.json({ token });
|
|
257
|
+
} catch (error) {
|
|
258
|
+
return res.status(500).json({ error: "JWT not configured" });
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export const jwtValidateController: Handler = (req, res) => {
|
|
263
|
+
const token = req.sprint?.getAuthorization() || req.headers.authorization;
|
|
264
|
+
|
|
265
|
+
if (!token) {
|
|
266
|
+
return res.status(401).json({ error: "No token provided" });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const { publicKey } = getJwtFromEnv();
|
|
271
|
+
const decoded = verifyEncrypted(token, publicKey);
|
|
272
|
+
|
|
273
|
+
if (!decoded) {
|
|
274
|
+
return res.status(401).json({ error: "Invalid token" });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
res.json({ valid: true, payload: decoded });
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return res.status(500).json({ error: "JWT not configured" });
|
|
280
|
+
}
|
|
281
|
+
};
|
|
225
282
|
`;
|
|
226
283
|
}
|
|
227
284
|
return `import { Handler } from "sprint-es";
|
|
285
|
+
import { signEncrypted, verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
|
|
228
286
|
|
|
229
287
|
export const adminController = (req, res) => {
|
|
230
288
|
res.json({
|
|
@@ -241,6 +299,44 @@ export const adminUsersController = (req, res) => {
|
|
|
241
299
|
]
|
|
242
300
|
});
|
|
243
301
|
};
|
|
302
|
+
|
|
303
|
+
export const jwtGenerateController = (req, res) => {
|
|
304
|
+
const { userId, role } = req.body || {};
|
|
305
|
+
|
|
306
|
+
if (!userId) {
|
|
307
|
+
return res.status(400).json({ error: "userId is required" });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const { privateKey } = getJwtFromEnv();
|
|
312
|
+
const payload = { userId, role: role || "user" };
|
|
313
|
+
const token = signEncrypted(payload, privateKey, { expiresIn: "1h" });
|
|
314
|
+
res.json({ token });
|
|
315
|
+
} catch (error) {
|
|
316
|
+
return res.status(500).json({ error: "JWT not configured" });
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export const jwtValidateController = (req, res) => {
|
|
321
|
+
const token = req.sprint?.getAuthorization() || req.headers.authorization;
|
|
322
|
+
|
|
323
|
+
if (!token) {
|
|
324
|
+
return res.status(401).json({ error: "No token provided" });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const { publicKey } = getJwtFromEnv();
|
|
329
|
+
const decoded = verifyEncrypted(token, publicKey);
|
|
330
|
+
|
|
331
|
+
if (!decoded) {
|
|
332
|
+
return res.status(401).json({ error: "Invalid token" });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
res.json({ valid: true, payload: decoded });
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return res.status(500).json({ error: "JWT not configured" });
|
|
338
|
+
}
|
|
339
|
+
};
|
|
244
340
|
`;
|
|
245
341
|
}
|
|
246
342
|
|
|
@@ -537,8 +633,11 @@ DISCORD_WEBHOOK_URL=
|
|
|
537
633
|
}
|
|
538
634
|
|
|
539
635
|
export function getEnvDevelopment(telemetry: string) {
|
|
636
|
+
const { publicKey, privateKey } = generateJWTKeys();
|
|
540
637
|
let env = `NODE_ENV=development
|
|
541
638
|
PORT=3000
|
|
639
|
+
JWT_PUBLIC_KEY=${publicKey.replace(/\n/g, "\\n")}
|
|
640
|
+
JWT_PRIVATE_KEY=${privateKey.replace(/\n/g, "\\n")}
|
|
542
641
|
`;
|
|
543
642
|
|
|
544
643
|
if (telemetry === "sentry" || telemetry === "glitchtip") {
|
|
@@ -557,8 +656,11 @@ DISCORD_WEBHOOK_URL=
|
|
|
557
656
|
}
|
|
558
657
|
|
|
559
658
|
export function getEnvProduction(telemetry: string) {
|
|
659
|
+
const { publicKey, privateKey } = generateJWTKeys();
|
|
560
660
|
let env = `NODE_ENV=production
|
|
561
661
|
PORT=3000
|
|
662
|
+
JWT_PUBLIC_KEY=${publicKey.replace(/\n/g, "\\n")}
|
|
663
|
+
JWT_PRIVATE_KEY=${privateKey.replace(/\n/g, "\\n")}
|
|
562
664
|
`;
|
|
563
665
|
|
|
564
666
|
if (telemetry === "sentry" || telemetry === "glitchtip") {
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { execSync } from "child_process";
|
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import { mkdir, writeFile } from "fs/promises";
|
|
4
4
|
import { join } from "path";
|
|
5
|
-
import
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
6
|
import { validateProjectName } from "./validators.js";
|
|
7
7
|
import { getTypeScriptPackageJson, getJavaScriptPackageJson, getTsConfig, getViteConfig, getMainFile, getHomeRoute, getAdminRoute, getHomeController, getAdminController, getAuthMiddleware, getHomeSchema, getAdminSchema, getDockerfile, getDockerCompose, getGitignore, getDockerIgnore, getSprintConfigFile, getEnvDevelopment, getEnvProduction, getExampleCronJob } from "./generators.js";
|
|
8
8
|
|
|
@@ -18,62 +18,81 @@ export interface CLIOptions {
|
|
|
18
18
|
export async function runCLI(args: string[]) {
|
|
19
19
|
const options = parseArgs(args);
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
21
|
+
p.intro("Sprint — Quickly API Framework");
|
|
22
|
+
|
|
23
|
+
const config = await p.group(
|
|
24
|
+
{
|
|
25
|
+
projectName: () =>
|
|
26
|
+
p.text({
|
|
27
|
+
message: "Project name:",
|
|
28
|
+
placeholder: "my-api",
|
|
29
|
+
validate: (v) => validateProjectName(v) || undefined,
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
language: () =>
|
|
33
|
+
p.select({
|
|
34
|
+
message: "Language:",
|
|
35
|
+
options: [
|
|
36
|
+
{ value: "typescript", label: "TypeScript", hint: "recommended" },
|
|
37
|
+
{ value: "javascript", label: "JavaScript" },
|
|
38
|
+
],
|
|
39
|
+
}),
|
|
40
|
+
|
|
41
|
+
telemetry: () =>
|
|
42
|
+
p.select({
|
|
43
|
+
message: "Error tracking:",
|
|
44
|
+
options: [
|
|
45
|
+
{ value: "none", label: "None" },
|
|
46
|
+
{ value: "sentry", label: "Sentry", hint: "free tier available" },
|
|
47
|
+
{ value: "glitchtip", label: "GlitchTip", hint: "self-hostable" },
|
|
48
|
+
{ value: "discord", label: "Discord Webhook", hint: "sends to a channel" },
|
|
49
|
+
],
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
docker: () =>
|
|
53
|
+
p.confirm({ message: "Add Docker support?", initialValue: false }),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
onCancel: () => {
|
|
57
|
+
p.cancel("Cancelled.");
|
|
58
|
+
process.exit(0);
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
);
|
|
43
62
|
|
|
44
|
-
|
|
63
|
+
const targetDir = config.projectName === "." ? process.cwd() : join(process.cwd(), config.projectName);
|
|
45
64
|
|
|
46
|
-
|
|
65
|
+
const s = p.spinner();
|
|
66
|
+
s.start("Creating project");
|
|
67
|
+
await createProject(config.projectName, config.language as "typescript" | "javascript", config.telemetry, config.docker);
|
|
68
|
+
s.stop("Project created");
|
|
47
69
|
|
|
48
|
-
|
|
49
|
-
if (options.skipInstall) {
|
|
50
|
-
installDeps = false;
|
|
51
|
-
} else {
|
|
52
|
-
installDeps = await confirm({
|
|
53
|
-
message: "Do you want to install dependencies now?",
|
|
54
|
-
default: true,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
70
|
+
const installDeps = await p.confirm({ message: "Install dependencies now?", initialValue: true });
|
|
57
71
|
|
|
58
72
|
if (installDeps) {
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
const s2 = p.spinner();
|
|
74
|
+
s2.start("Installing dependencies");
|
|
61
75
|
try {
|
|
62
|
-
execSync("npm install", { cwd: targetDir, stdio: "
|
|
63
|
-
|
|
76
|
+
execSync("npm install", { cwd: targetDir, stdio: "pipe" });
|
|
77
|
+
s2.stop("Dependencies installed");
|
|
64
78
|
} catch {
|
|
65
|
-
|
|
79
|
+
s2.stop("Install failed — run npm install manually");
|
|
66
80
|
}
|
|
67
81
|
}
|
|
68
82
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
const cdCmd = config.projectName === "." ? "" : `cd ${config.projectName} && `;
|
|
84
|
+
p.note(
|
|
85
|
+
[
|
|
86
|
+
!installDeps ? `${cdCmd}npm install` : "",
|
|
87
|
+
`${cdCmd}npm run dev`,
|
|
88
|
+
]
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.join("\n"),
|
|
91
|
+
"Next steps"
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
p.outro("Ready. Happy shipping.");
|
|
95
|
+
};
|
|
77
96
|
|
|
78
97
|
function parseArgs(args: string[]): CLIOptions {
|
|
79
98
|
const options: CLIOptions = {};
|
|
@@ -104,65 +123,39 @@ function parseArgs(args: string[]): CLIOptions {
|
|
|
104
123
|
};
|
|
105
124
|
|
|
106
125
|
async function getProjectName(): Promise<string> {
|
|
107
|
-
const name = await
|
|
126
|
+
const name = await p.text({
|
|
108
127
|
message: "Enter project name:",
|
|
109
|
-
validate: (value) =>
|
|
110
|
-
return validateProjectName(value) || true;
|
|
111
|
-
}
|
|
128
|
+
validate: (value) => validateProjectName(value) || undefined,
|
|
112
129
|
});
|
|
113
130
|
|
|
114
|
-
return name;
|
|
131
|
+
return name as string;
|
|
115
132
|
};
|
|
116
133
|
|
|
117
134
|
async function selectLanguage(): Promise<"typescript" | "javascript"> {
|
|
118
|
-
const language = await select({
|
|
135
|
+
const language = await p.select({
|
|
119
136
|
message: "Select your preferred language:",
|
|
120
|
-
|
|
121
|
-
{
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
description: "Recommended - Type safety and better developer experience",
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
name: "JavaScript",
|
|
128
|
-
value: "javascript",
|
|
129
|
-
description: "Vanilla JavaScript for simpler projects",
|
|
130
|
-
}
|
|
131
|
-
]
|
|
137
|
+
options: [
|
|
138
|
+
{ value: "typescript", label: "TypeScript", hint: "recommended" },
|
|
139
|
+
{ value: "javascript", label: "JavaScript" },
|
|
140
|
+
],
|
|
132
141
|
});
|
|
133
142
|
|
|
134
143
|
return language as "typescript" | "javascript";
|
|
135
144
|
};
|
|
136
145
|
|
|
137
146
|
async function selectTelemetry(): Promise<"none" | "sentry" | "glitchtip" | "discord"> {
|
|
138
|
-
const telemetry = await select({
|
|
147
|
+
const telemetry = await p.select({
|
|
139
148
|
message: "Select error tracking/telemetry solution:",
|
|
140
|
-
|
|
141
|
-
{
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
{
|
|
147
|
-
name: "Sentry",
|
|
148
|
-
value: "sentry",
|
|
149
|
-
description: "Full-featured error tracking (free tier available)",
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
name: "GlitchTip",
|
|
153
|
-
value: "glitchtip",
|
|
154
|
-
description: "Simple error tracking, can be self-hosted",
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
name: "Discord Webhook",
|
|
158
|
-
value: "discord",
|
|
159
|
-
description: "Send error notifications to Discord channel",
|
|
160
|
-
}
|
|
161
|
-
]
|
|
149
|
+
options: [
|
|
150
|
+
{ value: "none", label: "None" },
|
|
151
|
+
{ value: "sentry", label: "Sentry", hint: "free tier available" },
|
|
152
|
+
{ value: "glitchtip", label: "GlitchTip", hint: "self-hostable" },
|
|
153
|
+
{ value: "discord", label: "Discord Webhook", hint: "sends to a channel" },
|
|
154
|
+
],
|
|
162
155
|
});
|
|
163
156
|
|
|
164
157
|
return telemetry as "none" | "sentry" | "glitchtip" | "discord";
|
|
165
|
-
}
|
|
158
|
+
}
|
|
166
159
|
|
|
167
160
|
async function createProject(
|
|
168
161
|
projectName: string,
|
|
@@ -185,10 +178,10 @@ async function createProject(
|
|
|
185
178
|
|
|
186
179
|
let useDocker = useDockerArg || false;
|
|
187
180
|
if (!useDockerArg) {
|
|
188
|
-
useDocker = await confirm({
|
|
181
|
+
useDocker = await p.confirm({
|
|
189
182
|
message: "Do you want to add Docker support?",
|
|
190
|
-
|
|
191
|
-
});
|
|
183
|
+
initialValue: false,
|
|
184
|
+
}) as boolean;
|
|
192
185
|
}
|
|
193
186
|
|
|
194
187
|
let pkgJson;
|
|
@@ -211,6 +204,15 @@ async function createProject(
|
|
|
211
204
|
await mkdir(join(srcDir, "controllers"), { recursive: true });
|
|
212
205
|
await mkdir(join(srcDir, "schemas"), { recursive: true });
|
|
213
206
|
await mkdir(join(srcDir, "cronjobs"), { recursive: true });
|
|
207
|
+
await mkdir(join(srcDir, "config"), { recursive: true });
|
|
208
|
+
|
|
209
|
+
if (language === "typescript") {
|
|
210
|
+
await writeFile(join(srcDir, "config", "index.ts"), "");
|
|
211
|
+
await writeFile(join(srcDir, "config", "clients.ts"), "");
|
|
212
|
+
} else {
|
|
213
|
+
await writeFile(join(srcDir, "config", "index.js"), "");
|
|
214
|
+
await writeFile(join(srcDir, "config", "clients.js"), "");
|
|
215
|
+
}
|
|
214
216
|
|
|
215
217
|
await writeFile(join(srcDir, "middlewares", ".gitkeep"), "");
|
|
216
218
|
|
|
@@ -232,8 +234,8 @@ async function createProject(
|
|
|
232
234
|
await writeFile(join(targetDir, ".env.development.example"), getEnvDevelopment(telemetry));
|
|
233
235
|
await writeFile(join(targetDir, ".env.production.example"), getEnvProduction(telemetry));
|
|
234
236
|
|
|
235
|
-
await writeFile(join(targetDir, ".env.development"),
|
|
236
|
-
await writeFile(join(targetDir, ".env.production"),
|
|
237
|
+
await writeFile(join(targetDir, ".env.development"), getEnvDevelopment(telemetry));
|
|
238
|
+
await writeFile(join(targetDir, ".env.production"), getEnvProduction(telemetry));
|
|
237
239
|
|
|
238
240
|
await writeFile(join(targetDir, ".gitignore"), getGitignore());
|
|
239
241
|
|