create-sprint 0.0.38 ā 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 +55 -34
- package/dist/index.js +76 -85
- package/package.json +2 -2
- package/src/generators.ts +60 -34
- package/src/index.ts +94 -92
package/dist/generators.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export function
|
|
3
|
-
|
|
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 };
|
|
4
9
|
}
|
|
5
10
|
export function getTypeScriptPackageJson(name, telemetry) {
|
|
6
11
|
const deps = {
|
|
7
|
-
"sprint-es": "^0.0.
|
|
12
|
+
"sprint-es": "^0.0.38"
|
|
8
13
|
};
|
|
9
14
|
const devDeps = {
|
|
10
15
|
"@types/node": "^22.0.0",
|
|
@@ -34,7 +39,7 @@ export function getTypeScriptPackageJson(name, telemetry) {
|
|
|
34
39
|
}
|
|
35
40
|
export function getJavaScriptPackageJson(name, telemetry) {
|
|
36
41
|
const deps = {
|
|
37
|
-
"sprint-es": "^0.0.
|
|
42
|
+
"sprint-es": "^0.0.38"
|
|
38
43
|
};
|
|
39
44
|
if (telemetry === "sentry" || telemetry === "glitchtip") {
|
|
40
45
|
deps["@sentry/node"] = "^8.0.0";
|
|
@@ -54,7 +59,7 @@ export function getJavaScriptPackageJson(name, telemetry) {
|
|
|
54
59
|
dev: "sprint-es dev",
|
|
55
60
|
"generate:keys": "sprint-es generate-keys"
|
|
56
61
|
},
|
|
57
|
-
dependencies: deps
|
|
62
|
+
dependencies: deps
|
|
58
63
|
};
|
|
59
64
|
}
|
|
60
65
|
export function getTsConfig() {
|
|
@@ -200,9 +205,7 @@ export const homeController = (req, res) => {
|
|
|
200
205
|
export function getAdminController(language) {
|
|
201
206
|
if (language === "typescript") {
|
|
202
207
|
return `import { Handler } from "sprint-es";
|
|
203
|
-
import { signEncrypted, verifyEncrypted } from "sprint-es/jwt";
|
|
204
|
-
|
|
205
|
-
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key-change-in-production";
|
|
208
|
+
import { signEncrypted, verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
|
|
206
209
|
|
|
207
210
|
export const adminController: Handler = (req, res) => {
|
|
208
211
|
res.json({
|
|
@@ -227,10 +230,14 @@ export const jwtGenerateController: Handler = (req, res) => {
|
|
|
227
230
|
return res.status(400).json({ error: "userId is required" });
|
|
228
231
|
}
|
|
229
232
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
+
}
|
|
234
241
|
};
|
|
235
242
|
|
|
236
243
|
export const jwtValidateController: Handler = (req, res) => {
|
|
@@ -240,20 +247,23 @@ export const jwtValidateController: Handler = (req, res) => {
|
|
|
240
247
|
return res.status(401).json({ error: "No token provided" });
|
|
241
248
|
}
|
|
242
249
|
|
|
243
|
-
|
|
250
|
+
try {
|
|
251
|
+
const { publicKey } = getJwtFromEnv();
|
|
252
|
+
const decoded = verifyEncrypted(token, publicKey);
|
|
244
253
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
254
|
+
if (!decoded) {
|
|
255
|
+
return res.status(401).json({ error: "Invalid token" });
|
|
256
|
+
}
|
|
248
257
|
|
|
249
|
-
|
|
258
|
+
res.json({ valid: true, payload: decoded });
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return res.status(500).json({ error: "JWT not configured" });
|
|
261
|
+
}
|
|
250
262
|
};
|
|
251
263
|
`;
|
|
252
264
|
}
|
|
253
265
|
return `import { Handler } from "sprint-es";
|
|
254
|
-
import { signEncrypted, verifyEncrypted } from "sprint-es/jwt";
|
|
255
|
-
|
|
256
|
-
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key-change-in-production";
|
|
266
|
+
import { signEncrypted, verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
|
|
257
267
|
|
|
258
268
|
export const adminController = (req, res) => {
|
|
259
269
|
res.json({
|
|
@@ -278,10 +288,14 @@ export const jwtGenerateController = (req, res) => {
|
|
|
278
288
|
return res.status(400).json({ error: "userId is required" });
|
|
279
289
|
}
|
|
280
290
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
+
}
|
|
285
299
|
};
|
|
286
300
|
|
|
287
301
|
export const jwtValidateController = (req, res) => {
|
|
@@ -291,13 +305,18 @@ export const jwtValidateController = (req, res) => {
|
|
|
291
305
|
return res.status(401).json({ error: "No token provided" });
|
|
292
306
|
}
|
|
293
307
|
|
|
294
|
-
|
|
308
|
+
try {
|
|
309
|
+
const { publicKey } = getJwtFromEnv();
|
|
310
|
+
const decoded = verifyEncrypted(token, publicKey);
|
|
295
311
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
312
|
+
if (!decoded) {
|
|
313
|
+
return res.status(401).json({ error: "Invalid token" });
|
|
314
|
+
}
|
|
299
315
|
|
|
300
|
-
|
|
316
|
+
res.json({ valid: true, payload: decoded });
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return res.status(500).json({ error: "JWT not configured" });
|
|
319
|
+
}
|
|
301
320
|
};
|
|
302
321
|
`;
|
|
303
322
|
}
|
|
@@ -581,10 +600,11 @@ DISCORD_WEBHOOK_URL=
|
|
|
581
600
|
return env;
|
|
582
601
|
}
|
|
583
602
|
export function getEnvDevelopment(telemetry) {
|
|
584
|
-
const
|
|
603
|
+
const { publicKey, privateKey } = generateJWTKeys();
|
|
585
604
|
let env = `NODE_ENV=development
|
|
586
605
|
PORT=3000
|
|
587
|
-
|
|
606
|
+
JWT_PUBLIC_KEY=${publicKey.replace(/\n/g, "\\n")}
|
|
607
|
+
JWT_PRIVATE_KEY=${privateKey.replace(/\n/g, "\\n")}
|
|
588
608
|
`;
|
|
589
609
|
if (telemetry === "sentry" || telemetry === "glitchtip") {
|
|
590
610
|
env += `
|
|
@@ -601,10 +621,11 @@ DISCORD_WEBHOOK_URL=
|
|
|
601
621
|
return env;
|
|
602
622
|
}
|
|
603
623
|
export function getEnvProduction(telemetry) {
|
|
604
|
-
const
|
|
624
|
+
const { publicKey, privateKey } = generateJWTKeys();
|
|
605
625
|
let env = `NODE_ENV=production
|
|
606
626
|
PORT=3000
|
|
607
|
-
|
|
627
|
+
JWT_PUBLIC_KEY=${publicKey.replace(/\n/g, "\\n")}
|
|
628
|
+
JWT_PRIVATE_KEY=${privateKey.replace(/\n/g, "\\n")}
|
|
608
629
|
`;
|
|
609
630
|
if (telemetry === "sentry" || telemetry === "glitchtip") {
|
|
610
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));
|
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,12 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
|
|
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 };
|
|
5
15
|
}
|
|
6
16
|
|
|
7
17
|
export function getTypeScriptPackageJson(name: string, telemetry: string) {
|
|
8
18
|
const deps: Record<string, string> = {
|
|
9
|
-
"sprint-es": "^0.0.
|
|
19
|
+
"sprint-es": "^0.0.38"
|
|
10
20
|
};
|
|
11
21
|
|
|
12
22
|
const devDeps: Record<string, string> = {
|
|
@@ -39,7 +49,7 @@ export function getTypeScriptPackageJson(name: string, telemetry: string) {
|
|
|
39
49
|
|
|
40
50
|
export function getJavaScriptPackageJson(name: string, telemetry: string) {
|
|
41
51
|
const deps: Record<string, string> = {
|
|
42
|
-
"sprint-es": "^0.0.
|
|
52
|
+
"sprint-es": "^0.0.38"
|
|
43
53
|
};
|
|
44
54
|
|
|
45
55
|
if (telemetry === "sentry" || telemetry === "glitchtip") {
|
|
@@ -60,7 +70,7 @@ export function getJavaScriptPackageJson(name: string, telemetry: string) {
|
|
|
60
70
|
dev: "sprint-es dev",
|
|
61
71
|
"generate:keys": "sprint-es generate-keys"
|
|
62
72
|
},
|
|
63
|
-
dependencies: deps
|
|
73
|
+
dependencies: deps
|
|
64
74
|
};
|
|
65
75
|
}
|
|
66
76
|
|
|
@@ -214,9 +224,7 @@ export const homeController = (req, res) => {
|
|
|
214
224
|
export function getAdminController(language: string) {
|
|
215
225
|
if (language === "typescript") {
|
|
216
226
|
return `import { Handler } from "sprint-es";
|
|
217
|
-
import { signEncrypted, verifyEncrypted } from "sprint-es/jwt";
|
|
218
|
-
|
|
219
|
-
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key-change-in-production";
|
|
227
|
+
import { signEncrypted, verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
|
|
220
228
|
|
|
221
229
|
export const adminController: Handler = (req, res) => {
|
|
222
230
|
res.json({
|
|
@@ -241,10 +249,14 @@ export const jwtGenerateController: Handler = (req, res) => {
|
|
|
241
249
|
return res.status(400).json({ error: "userId is required" });
|
|
242
250
|
}
|
|
243
251
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
+
}
|
|
248
260
|
};
|
|
249
261
|
|
|
250
262
|
export const jwtValidateController: Handler = (req, res) => {
|
|
@@ -254,20 +266,23 @@ export const jwtValidateController: Handler = (req, res) => {
|
|
|
254
266
|
return res.status(401).json({ error: "No token provided" });
|
|
255
267
|
}
|
|
256
268
|
|
|
257
|
-
|
|
269
|
+
try {
|
|
270
|
+
const { publicKey } = getJwtFromEnv();
|
|
271
|
+
const decoded = verifyEncrypted(token, publicKey);
|
|
258
272
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
273
|
+
if (!decoded) {
|
|
274
|
+
return res.status(401).json({ error: "Invalid token" });
|
|
275
|
+
}
|
|
262
276
|
|
|
263
|
-
|
|
277
|
+
res.json({ valid: true, payload: decoded });
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return res.status(500).json({ error: "JWT not configured" });
|
|
280
|
+
}
|
|
264
281
|
};
|
|
265
282
|
`;
|
|
266
283
|
}
|
|
267
284
|
return `import { Handler } from "sprint-es";
|
|
268
|
-
import { signEncrypted, verifyEncrypted } from "sprint-es/jwt";
|
|
269
|
-
|
|
270
|
-
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key-change-in-production";
|
|
285
|
+
import { signEncrypted, verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
|
|
271
286
|
|
|
272
287
|
export const adminController = (req, res) => {
|
|
273
288
|
res.json({
|
|
@@ -292,10 +307,14 @@ export const jwtGenerateController = (req, res) => {
|
|
|
292
307
|
return res.status(400).json({ error: "userId is required" });
|
|
293
308
|
}
|
|
294
309
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
+
}
|
|
299
318
|
};
|
|
300
319
|
|
|
301
320
|
export const jwtValidateController = (req, res) => {
|
|
@@ -305,13 +324,18 @@ export const jwtValidateController = (req, res) => {
|
|
|
305
324
|
return res.status(401).json({ error: "No token provided" });
|
|
306
325
|
}
|
|
307
326
|
|
|
308
|
-
|
|
327
|
+
try {
|
|
328
|
+
const { publicKey } = getJwtFromEnv();
|
|
329
|
+
const decoded = verifyEncrypted(token, publicKey);
|
|
309
330
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
331
|
+
if (!decoded) {
|
|
332
|
+
return res.status(401).json({ error: "Invalid token" });
|
|
333
|
+
}
|
|
313
334
|
|
|
314
|
-
|
|
335
|
+
res.json({ valid: true, payload: decoded });
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return res.status(500).json({ error: "JWT not configured" });
|
|
338
|
+
}
|
|
315
339
|
};
|
|
316
340
|
`;
|
|
317
341
|
}
|
|
@@ -609,10 +633,11 @@ DISCORD_WEBHOOK_URL=
|
|
|
609
633
|
}
|
|
610
634
|
|
|
611
635
|
export function getEnvDevelopment(telemetry: string) {
|
|
612
|
-
const
|
|
636
|
+
const { publicKey, privateKey } = generateJWTKeys();
|
|
613
637
|
let env = `NODE_ENV=development
|
|
614
638
|
PORT=3000
|
|
615
|
-
|
|
639
|
+
JWT_PUBLIC_KEY=${publicKey.replace(/\n/g, "\\n")}
|
|
640
|
+
JWT_PRIVATE_KEY=${privateKey.replace(/\n/g, "\\n")}
|
|
616
641
|
`;
|
|
617
642
|
|
|
618
643
|
if (telemetry === "sentry" || telemetry === "glitchtip") {
|
|
@@ -631,10 +656,11 @@ DISCORD_WEBHOOK_URL=
|
|
|
631
656
|
}
|
|
632
657
|
|
|
633
658
|
export function getEnvProduction(telemetry: string) {
|
|
634
|
-
const
|
|
659
|
+
const { publicKey, privateKey } = generateJWTKeys();
|
|
635
660
|
let env = `NODE_ENV=production
|
|
636
661
|
PORT=3000
|
|
637
|
-
|
|
662
|
+
JWT_PUBLIC_KEY=${publicKey.replace(/\n/g, "\\n")}
|
|
663
|
+
JWT_PRIVATE_KEY=${privateKey.replace(/\n/g, "\\n")}
|
|
638
664
|
`;
|
|
639
665
|
|
|
640
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
|
|