create-sprint 0.0.38 ā 0.0.44
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 +62 -112
- package/package.json +2 -2
- package/src/generators.ts +60 -34
- package/src/index.ts +83 -122
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");
|
|
@@ -84,83 +93,15 @@ function parseArgs(args) {
|
|
|
84
93
|
return options;
|
|
85
94
|
}
|
|
86
95
|
;
|
|
87
|
-
async function
|
|
88
|
-
const name = await input({
|
|
89
|
-
message: "Enter project name:",
|
|
90
|
-
validate: (value) => {
|
|
91
|
-
return validateProjectName(value) || true;
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
return name;
|
|
95
|
-
}
|
|
96
|
-
;
|
|
97
|
-
async function selectLanguage() {
|
|
98
|
-
const language = await select({
|
|
99
|
-
message: "Select your preferred language:",
|
|
100
|
-
choices: [
|
|
101
|
-
{
|
|
102
|
-
name: "TypeScript",
|
|
103
|
-
value: "typescript",
|
|
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
|
-
]
|
|
112
|
-
});
|
|
113
|
-
return language;
|
|
114
|
-
}
|
|
115
|
-
;
|
|
116
|
-
async function selectTelemetry() {
|
|
117
|
-
const telemetry = await select({
|
|
118
|
-
message: "Select error tracking/telemetry solution:",
|
|
119
|
-
choices: [
|
|
120
|
-
{
|
|
121
|
-
name: "None",
|
|
122
|
-
value: "none",
|
|
123
|
-
description: "No error tracking integration",
|
|
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
|
-
]
|
|
141
|
-
});
|
|
142
|
-
return telemetry;
|
|
143
|
-
}
|
|
144
|
-
;
|
|
145
|
-
async function createProject(projectName, language, telemetryArg, useDockerArg) {
|
|
96
|
+
async function createProject(projectName, language, telemetry, useDocker) {
|
|
146
97
|
const isCurrentDir = projectName === ".";
|
|
147
98
|
const targetDir = isCurrentDir ? process.cwd() : join(process.cwd(), projectName);
|
|
148
99
|
if (!isCurrentDir && existsSync(targetDir)) {
|
|
149
|
-
|
|
100
|
+
p.cancel(`Directory "${projectName}" already exists.`);
|
|
150
101
|
process.exit(1);
|
|
151
102
|
}
|
|
152
103
|
if (!isCurrentDir)
|
|
153
104
|
await mkdir(targetDir, { recursive: true });
|
|
154
|
-
let telemetry = telemetryArg || "none";
|
|
155
|
-
if (!telemetryArg)
|
|
156
|
-
telemetry = await selectTelemetry();
|
|
157
|
-
let useDocker = useDockerArg || false;
|
|
158
|
-
if (!useDockerArg) {
|
|
159
|
-
useDocker = await confirm({
|
|
160
|
-
message: "Do you want to add Docker support?",
|
|
161
|
-
default: false,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
105
|
let pkgJson;
|
|
165
106
|
if (language === "typescript")
|
|
166
107
|
pkgJson = getTypeScriptPackageJson(projectName, telemetry);
|
|
@@ -181,6 +122,15 @@ async function createProject(projectName, language, telemetryArg, useDockerArg)
|
|
|
181
122
|
await mkdir(join(srcDir, "controllers"), { recursive: true });
|
|
182
123
|
await mkdir(join(srcDir, "schemas"), { recursive: true });
|
|
183
124
|
await mkdir(join(srcDir, "cronjobs"), { recursive: true });
|
|
125
|
+
await mkdir(join(srcDir, "config"), { recursive: true });
|
|
126
|
+
if (language === "typescript") {
|
|
127
|
+
await writeFile(join(srcDir, "config", "index.ts"), "");
|
|
128
|
+
await writeFile(join(srcDir, "config", "clients.ts"), "");
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
await writeFile(join(srcDir, "config", "index.js"), "");
|
|
132
|
+
await writeFile(join(srcDir, "config", "clients.js"), "");
|
|
133
|
+
}
|
|
184
134
|
await writeFile(join(srcDir, "middlewares", ".gitkeep"), "");
|
|
185
135
|
await writeFile(join(srcDir, "app." + (language === "typescript" ? "ts" : "js")), getMainFile(language));
|
|
186
136
|
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.44",
|
|
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,86 @@ 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
|
-
|
|
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
|
+
);
|
|
41
62
|
|
|
42
|
-
|
|
63
|
+
const targetDir = config.projectName === "." ? process.cwd() : join(process.cwd(), config.projectName);
|
|
43
64
|
|
|
44
|
-
|
|
65
|
+
const s = p.spinner();
|
|
66
|
+
s.start("Creating project");
|
|
67
|
+
await createProject(
|
|
68
|
+
config.projectName as string,
|
|
69
|
+
config.language as "typescript" | "javascript",
|
|
70
|
+
config.telemetry as string,
|
|
71
|
+
config.docker as boolean,
|
|
72
|
+
);
|
|
73
|
+
s.stop("Project created");
|
|
45
74
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
let installDeps = true;
|
|
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
|
-
}
|
|
75
|
+
const installDeps = await p.confirm({ message: "Install dependencies now?", initialValue: true });
|
|
57
76
|
|
|
58
77
|
if (installDeps) {
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
const s2 = p.spinner();
|
|
79
|
+
s2.start("Installing dependencies");
|
|
61
80
|
try {
|
|
62
|
-
execSync("npm install", { cwd: targetDir, stdio: "
|
|
63
|
-
|
|
81
|
+
execSync("npm install", { cwd: targetDir, stdio: "pipe" });
|
|
82
|
+
s2.stop("Dependencies installed");
|
|
64
83
|
} catch {
|
|
65
|
-
|
|
84
|
+
s2.stop("Install failed ā run npm install manually");
|
|
66
85
|
}
|
|
67
86
|
}
|
|
68
87
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
88
|
+
const cdCmd = config.projectName === "." ? "" : `cd ${config.projectName} && `;
|
|
89
|
+
p.note(
|
|
90
|
+
[
|
|
91
|
+
!installDeps ? `${cdCmd}npm install` : "",
|
|
92
|
+
`${cdCmd}npm run dev`,
|
|
93
|
+
]
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.join("\n"),
|
|
96
|
+
"Next steps"
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
p.outro("Ready. Happy shipping.");
|
|
100
|
+
};
|
|
77
101
|
|
|
78
102
|
function parseArgs(args: string[]): CLIOptions {
|
|
79
103
|
const options: CLIOptions = {};
|
|
@@ -103,94 +127,22 @@ function parseArgs(args: string[]): CLIOptions {
|
|
|
103
127
|
return options;
|
|
104
128
|
};
|
|
105
129
|
|
|
106
|
-
async function getProjectName(): Promise<string> {
|
|
107
|
-
const name = await input({
|
|
108
|
-
message: "Enter project name:",
|
|
109
|
-
validate: (value) => {
|
|
110
|
-
return validateProjectName(value) || true;
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
return name;
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
async function selectLanguage(): Promise<"typescript" | "javascript"> {
|
|
118
|
-
const language = await select({
|
|
119
|
-
message: "Select your preferred language:",
|
|
120
|
-
choices: [
|
|
121
|
-
{
|
|
122
|
-
name: "TypeScript",
|
|
123
|
-
value: "typescript",
|
|
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
|
-
]
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
return language as "typescript" | "javascript";
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
async function selectTelemetry(): Promise<"none" | "sentry" | "glitchtip" | "discord"> {
|
|
138
|
-
const telemetry = await select({
|
|
139
|
-
message: "Select error tracking/telemetry solution:",
|
|
140
|
-
choices: [
|
|
141
|
-
{
|
|
142
|
-
name: "None",
|
|
143
|
-
value: "none",
|
|
144
|
-
description: "No error tracking integration",
|
|
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
|
-
]
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
return telemetry as "none" | "sentry" | "glitchtip" | "discord";
|
|
165
|
-
};
|
|
166
|
-
|
|
167
130
|
async function createProject(
|
|
168
131
|
projectName: string,
|
|
169
132
|
language: "typescript" | "javascript",
|
|
170
|
-
|
|
171
|
-
|
|
133
|
+
telemetry: string,
|
|
134
|
+
useDocker: boolean
|
|
172
135
|
) {
|
|
173
136
|
const isCurrentDir = projectName === ".";
|
|
174
137
|
const targetDir = isCurrentDir ? process.cwd() : join(process.cwd(), projectName);
|
|
175
138
|
|
|
176
139
|
if (!isCurrentDir && existsSync(targetDir)) {
|
|
177
|
-
|
|
140
|
+
p.cancel(`Directory "${projectName}" already exists.`);
|
|
178
141
|
process.exit(1);
|
|
179
142
|
}
|
|
180
143
|
|
|
181
144
|
if (!isCurrentDir) await mkdir(targetDir, { recursive: true });
|
|
182
145
|
|
|
183
|
-
let telemetry = telemetryArg || "none";
|
|
184
|
-
if (!telemetryArg) telemetry = await selectTelemetry();
|
|
185
|
-
|
|
186
|
-
let useDocker = useDockerArg || false;
|
|
187
|
-
if (!useDockerArg) {
|
|
188
|
-
useDocker = await confirm({
|
|
189
|
-
message: "Do you want to add Docker support?",
|
|
190
|
-
default: false,
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
146
|
let pkgJson;
|
|
195
147
|
if (language === "typescript") pkgJson = getTypeScriptPackageJson(projectName, telemetry);
|
|
196
148
|
else pkgJson = getJavaScriptPackageJson(projectName, telemetry);
|
|
@@ -211,6 +163,15 @@ async function createProject(
|
|
|
211
163
|
await mkdir(join(srcDir, "controllers"), { recursive: true });
|
|
212
164
|
await mkdir(join(srcDir, "schemas"), { recursive: true });
|
|
213
165
|
await mkdir(join(srcDir, "cronjobs"), { recursive: true });
|
|
166
|
+
await mkdir(join(srcDir, "config"), { recursive: true });
|
|
167
|
+
|
|
168
|
+
if (language === "typescript") {
|
|
169
|
+
await writeFile(join(srcDir, "config", "index.ts"), "");
|
|
170
|
+
await writeFile(join(srcDir, "config", "clients.ts"), "");
|
|
171
|
+
} else {
|
|
172
|
+
await writeFile(join(srcDir, "config", "index.js"), "");
|
|
173
|
+
await writeFile(join(srcDir, "config", "clients.js"), "");
|
|
174
|
+
}
|
|
214
175
|
|
|
215
176
|
await writeFile(join(srcDir, "middlewares", ".gitkeep"), "");
|
|
216
177
|
|