create-bcp-app 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -3
- package/bin/create-bcp-app.mjs +148 -3
- package/package.json +1 -1
- package/src/index.d.mts +6 -0
- package/src/index.mjs +8 -0
- package/src/project-options.mjs +366 -14
package/README.md
CHANGED
|
@@ -18,6 +18,9 @@ Select a database:
|
|
|
18
18
|
3) PostgreSQL
|
|
19
19
|
4) SQLite
|
|
20
20
|
5) MongoDB
|
|
21
|
+
Select authentication:
|
|
22
|
+
1) None
|
|
23
|
+
2) JWT Cookie
|
|
21
24
|
```
|
|
22
25
|
|
|
23
26
|
## Generated application defaults
|
|
@@ -75,8 +78,71 @@ import "bcp/server-only";
|
|
|
75
78
|
|
|
76
79
|
This prevents database code from being used in a page/client module graph. Query the database from an API route and call that API from client components.
|
|
77
80
|
|
|
81
|
+
For MySQL, the generated helper exports a reusable `db` pool and caches that pool on `globalThis` during development so hot reloads do not create a new connection pool every time:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import {
|
|
85
|
+
db,
|
|
86
|
+
} from "@/lib/database";
|
|
87
|
+
|
|
88
|
+
const [rows] =
|
|
89
|
+
await db.query(
|
|
90
|
+
"SELECT 1"
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The generated MySQL `.env.example` uses separate connection settings:
|
|
95
|
+
|
|
96
|
+
```dotenv
|
|
97
|
+
DB_HOST=localhost
|
|
98
|
+
DB_PORT=3306
|
|
99
|
+
DB_USER=
|
|
100
|
+
DB_PASSWORD=
|
|
101
|
+
DB_NAME=bcp_app
|
|
102
|
+
```
|
|
103
|
+
|
|
78
104
|
The generator intentionally installs database drivers directly and does not force an ORM.
|
|
79
105
|
|
|
106
|
+
### JWT Cookie authentication
|
|
107
|
+
|
|
108
|
+
Selecting `JWT Cookie` creates a secure-by-default authentication foundation:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
lib/
|
|
112
|
+
└─ auth.ts
|
|
113
|
+
|
|
114
|
+
app/api/auth/
|
|
115
|
+
├─ login/route.ts
|
|
116
|
+
├─ logout/route.ts
|
|
117
|
+
└─ me/route.ts
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The preset uses the session primitives from `bcp/server` and adds this environment variable:
|
|
121
|
+
|
|
122
|
+
```dotenv
|
|
123
|
+
BCP_SESSION_SECRET=
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Set `BCP_SESSION_SECRET` to a cryptographically random secret of at least 32 bytes before using authentication.
|
|
127
|
+
|
|
128
|
+
The generated `authenticateCredentials(email, password)` intentionally returns `null` until the application connects it to its own user table and password-hash verification. This prevents a newly generated application from accepting unverified credentials or trusting identity data sent directly by the browser.
|
|
129
|
+
|
|
130
|
+
After implementing credential verification, the generated flow is:
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
POST /api/auth/login
|
|
134
|
+
-> authenticateCredentials()
|
|
135
|
+
-> createSession()
|
|
136
|
+
-> HttpOnly JWT cookie
|
|
137
|
+
|
|
138
|
+
GET /api/auth/me
|
|
139
|
+
-> getSession()
|
|
140
|
+
-> current user
|
|
141
|
+
|
|
142
|
+
POST /api/auth/logout
|
|
143
|
+
-> destroySession()
|
|
144
|
+
```
|
|
145
|
+
|
|
80
146
|
## Options
|
|
81
147
|
|
|
82
148
|
```text
|
|
@@ -84,7 +150,8 @@ The generator intentionally installs database drivers directly and does not forc
|
|
|
84
150
|
--tailwind Enable Tailwind CSS without prompting
|
|
85
151
|
--no-tailwind Disable Tailwind CSS without prompting
|
|
86
152
|
--database <database> none | mysql | postgresql | sqlite | mongodb
|
|
87
|
-
|
|
153
|
+
--auth <preset> none | jwt-cookie
|
|
154
|
+
-y, --yes Accept defaults (Tailwind enabled, no database, no auth)
|
|
88
155
|
--bcp <specifier> Override dependencies.bcp
|
|
89
156
|
-h, --help Show help
|
|
90
157
|
```
|
|
@@ -92,13 +159,14 @@ The generator intentionally installs database drivers directly and does not forc
|
|
|
92
159
|
Examples:
|
|
93
160
|
|
|
94
161
|
```bash
|
|
95
|
-
npx create-bcp-app my-app --tailwind --database mysql
|
|
162
|
+
npx create-bcp-app my-app --tailwind --database mysql --auth jwt-cookie
|
|
96
163
|
npx create-bcp-app my-app --no-tailwind --database mongodb
|
|
164
|
+
npx create-bcp-app my-app --auth jwt-cookie
|
|
97
165
|
npx create-bcp-app my-app --yes
|
|
98
166
|
```
|
|
99
167
|
|
|
100
168
|
The `--bcp` option is primarily useful for prerelease and local package testing, for example:
|
|
101
169
|
|
|
102
170
|
```bash
|
|
103
|
-
npx create-bcp-app my-app --bcp file:../bcp-0.1.
|
|
171
|
+
npx create-bcp-app my-app --bcp file:../bcp-0.1.8.tgz
|
|
104
172
|
```
|
package/bin/create-bcp-app.mjs
CHANGED
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
createBcpApp,
|
|
15
15
|
} from "../src/index.mjs";
|
|
16
16
|
import {
|
|
17
|
+
AUTH_CHOICES,
|
|
17
18
|
DATABASE_CHOICES,
|
|
19
|
+
normalizeAuth,
|
|
18
20
|
normalizeDatabase,
|
|
19
21
|
} from "../src/project-options.mjs";
|
|
20
22
|
|
|
@@ -39,6 +41,8 @@ let tailwind =
|
|
|
39
41
|
undefined;
|
|
40
42
|
let database =
|
|
41
43
|
undefined;
|
|
44
|
+
let auth =
|
|
45
|
+
undefined;
|
|
42
46
|
let acceptDefaults =
|
|
43
47
|
false;
|
|
44
48
|
|
|
@@ -105,6 +109,28 @@ for (
|
|
|
105
109
|
continue;
|
|
106
110
|
}
|
|
107
111
|
|
|
112
|
+
if (
|
|
113
|
+
value === "--auth"
|
|
114
|
+
) {
|
|
115
|
+
const nextValue =
|
|
116
|
+
args[
|
|
117
|
+
index + 1
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
if (!nextValue) {
|
|
121
|
+
fail(
|
|
122
|
+
"--auth requires a value."
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
auth =
|
|
127
|
+
normalizeAuth(
|
|
128
|
+
nextValue
|
|
129
|
+
);
|
|
130
|
+
index++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
108
134
|
if (
|
|
109
135
|
value === "--bcp"
|
|
110
136
|
) {
|
|
@@ -156,6 +182,7 @@ if (!projectDirectory) {
|
|
|
156
182
|
await resolveInteractiveOptions({
|
|
157
183
|
tailwind,
|
|
158
184
|
database,
|
|
185
|
+
auth,
|
|
159
186
|
acceptDefaults,
|
|
160
187
|
});
|
|
161
188
|
|
|
@@ -168,6 +195,8 @@ if (!projectDirectory) {
|
|
|
168
195
|
selected.tailwind,
|
|
169
196
|
database:
|
|
170
197
|
selected.database,
|
|
198
|
+
auth:
|
|
199
|
+
selected.auth,
|
|
171
200
|
packageManager:
|
|
172
201
|
"npm",
|
|
173
202
|
});
|
|
@@ -191,6 +220,9 @@ if (!projectDirectory) {
|
|
|
191
220
|
console.log(
|
|
192
221
|
`Database: ${formatDatabaseName(result.database)}`
|
|
193
222
|
);
|
|
223
|
+
console.log(
|
|
224
|
+
`Authentication: ${formatAuthName(result.auth)}`
|
|
225
|
+
);
|
|
194
226
|
console.log("");
|
|
195
227
|
console.log(
|
|
196
228
|
"Next steps:"
|
|
@@ -221,12 +253,15 @@ if (!projectDirectory) {
|
|
|
221
253
|
async function resolveInteractiveOptions({
|
|
222
254
|
tailwind: selectedTailwind,
|
|
223
255
|
database: selectedDatabase,
|
|
256
|
+
auth: selectedAuth,
|
|
224
257
|
acceptDefaults: useDefaults,
|
|
225
258
|
}) {
|
|
226
259
|
let resolvedTailwind =
|
|
227
260
|
selectedTailwind;
|
|
228
261
|
let resolvedDatabase =
|
|
229
262
|
selectedDatabase;
|
|
263
|
+
let resolvedAuth =
|
|
264
|
+
selectedAuth;
|
|
230
265
|
|
|
231
266
|
const interactive =
|
|
232
267
|
Boolean(
|
|
@@ -244,6 +279,11 @@ async function resolveInteractiveOptions({
|
|
|
244
279
|
resolvedDatabase ??
|
|
245
280
|
"none"
|
|
246
281
|
),
|
|
282
|
+
auth:
|
|
283
|
+
normalizeAuth(
|
|
284
|
+
resolvedAuth ??
|
|
285
|
+
"none"
|
|
286
|
+
),
|
|
247
287
|
};
|
|
248
288
|
}
|
|
249
289
|
|
|
@@ -257,6 +297,11 @@ async function resolveInteractiveOptions({
|
|
|
257
297
|
resolvedDatabase ??
|
|
258
298
|
"none"
|
|
259
299
|
),
|
|
300
|
+
auth:
|
|
301
|
+
normalizeAuth(
|
|
302
|
+
resolvedAuth ??
|
|
303
|
+
"none"
|
|
304
|
+
),
|
|
260
305
|
};
|
|
261
306
|
}
|
|
262
307
|
|
|
@@ -294,6 +339,16 @@ async function resolveInteractiveOptions({
|
|
|
294
339
|
prompt
|
|
295
340
|
);
|
|
296
341
|
}
|
|
342
|
+
|
|
343
|
+
if (
|
|
344
|
+
resolvedAuth ===
|
|
345
|
+
undefined
|
|
346
|
+
) {
|
|
347
|
+
resolvedAuth =
|
|
348
|
+
await askAuth(
|
|
349
|
+
prompt
|
|
350
|
+
);
|
|
351
|
+
}
|
|
297
352
|
} finally {
|
|
298
353
|
prompt.close();
|
|
299
354
|
}
|
|
@@ -307,6 +362,10 @@ async function resolveInteractiveOptions({
|
|
|
307
362
|
normalizeDatabase(
|
|
308
363
|
resolvedDatabase
|
|
309
364
|
),
|
|
365
|
+
auth:
|
|
366
|
+
normalizeAuth(
|
|
367
|
+
resolvedAuth
|
|
368
|
+
),
|
|
310
369
|
};
|
|
311
370
|
}
|
|
312
371
|
|
|
@@ -427,6 +486,77 @@ async function askDatabase(
|
|
|
427
486
|
}
|
|
428
487
|
}
|
|
429
488
|
|
|
489
|
+
async function askAuth(
|
|
490
|
+
prompt
|
|
491
|
+
) {
|
|
492
|
+
const labels = {
|
|
493
|
+
none: "None",
|
|
494
|
+
"jwt-cookie":
|
|
495
|
+
"JWT Cookie",
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
console.log(
|
|
499
|
+
"Select authentication:"
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
AUTH_CHOICES.forEach(
|
|
503
|
+
(
|
|
504
|
+
value,
|
|
505
|
+
index
|
|
506
|
+
) => {
|
|
507
|
+
console.log(
|
|
508
|
+
` ${index + 1}) ${labels[value]}`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
while (true) {
|
|
514
|
+
const answer =
|
|
515
|
+
(
|
|
516
|
+
await prompt.question(
|
|
517
|
+
"Authentication (1): "
|
|
518
|
+
)
|
|
519
|
+
).trim();
|
|
520
|
+
|
|
521
|
+
if (!answer) {
|
|
522
|
+
return "none";
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const index =
|
|
526
|
+
Number(
|
|
527
|
+
answer
|
|
528
|
+
) - 1;
|
|
529
|
+
|
|
530
|
+
if (
|
|
531
|
+
Number.isInteger(
|
|
532
|
+
index
|
|
533
|
+
) &&
|
|
534
|
+
index >= 0 &&
|
|
535
|
+
index <
|
|
536
|
+
AUTH_CHOICES.length
|
|
537
|
+
) {
|
|
538
|
+
return AUTH_CHOICES[
|
|
539
|
+
index
|
|
540
|
+
];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const normalized =
|
|
544
|
+
answer.toLowerCase();
|
|
545
|
+
|
|
546
|
+
if (
|
|
547
|
+
AUTH_CHOICES.includes(
|
|
548
|
+
normalized
|
|
549
|
+
)
|
|
550
|
+
) {
|
|
551
|
+
return normalized;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
console.log(
|
|
555
|
+
"Choose a number from the list or enter an authentication preset name."
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
430
560
|
function formatDatabaseName(
|
|
431
561
|
value
|
|
432
562
|
) {
|
|
@@ -442,6 +572,19 @@ function formatDatabaseName(
|
|
|
442
572
|
value;
|
|
443
573
|
}
|
|
444
574
|
|
|
575
|
+
function formatAuthName(
|
|
576
|
+
value
|
|
577
|
+
) {
|
|
578
|
+
const names = {
|
|
579
|
+
none: "None",
|
|
580
|
+
"jwt-cookie":
|
|
581
|
+
"JWT Cookie",
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
return names[value] ??
|
|
585
|
+
value;
|
|
586
|
+
}
|
|
587
|
+
|
|
445
588
|
function printHelp() {
|
|
446
589
|
console.log(`
|
|
447
590
|
create-bcp-app
|
|
@@ -452,23 +595,25 @@ Usage:
|
|
|
452
595
|
Interactive setup:
|
|
453
596
|
- Choose whether to use Tailwind CSS
|
|
454
597
|
- Choose a database: None, MySQL, PostgreSQL, SQLite or MongoDB
|
|
598
|
+
- Choose authentication: None or JWT Cookie
|
|
455
599
|
|
|
456
600
|
Options:
|
|
457
601
|
--no-install Create files without running npm install
|
|
458
602
|
--tailwind Enable Tailwind CSS without prompting
|
|
459
603
|
--no-tailwind Disable Tailwind CSS without prompting
|
|
460
604
|
--database <database> none | mysql | postgresql | sqlite | mongodb
|
|
461
|
-
|
|
605
|
+
--auth <preset> none | jwt-cookie
|
|
606
|
+
-y, --yes Accept defaults (Tailwind enabled, no database, no auth)
|
|
462
607
|
--bcp <specifier> Override the package specifier stored under dependencies.bcp
|
|
463
608
|
-h, --help Show this help message
|
|
464
609
|
|
|
465
610
|
Examples:
|
|
466
611
|
npx create-bcp-app my-app
|
|
467
|
-
npx create-bcp-app my-app --tailwind --database mysql
|
|
612
|
+
npx create-bcp-app my-app --tailwind --database mysql --auth jwt-cookie
|
|
468
613
|
npx create-bcp-app my-app --no-tailwind --database mongodb
|
|
469
614
|
npx create-bcp-app my-app --yes
|
|
470
615
|
npx create-bcp-app my-app --no-install
|
|
471
|
-
npx create-bcp-app my-app --bcp file:../bcp-0.1.
|
|
616
|
+
npx create-bcp-app my-app --bcp file:../bcp-0.1.8.tgz
|
|
472
617
|
`);
|
|
473
618
|
}
|
|
474
619
|
|
package/package.json
CHANGED
package/src/index.d.mts
CHANGED
|
@@ -5,12 +5,17 @@ export type BcpDatabase =
|
|
|
5
5
|
| "sqlite"
|
|
6
6
|
| "mongodb";
|
|
7
7
|
|
|
8
|
+
export type BcpAuthPreset =
|
|
9
|
+
| "none"
|
|
10
|
+
| "jwt-cookie";
|
|
11
|
+
|
|
8
12
|
export interface CreateBcpAppOptions {
|
|
9
13
|
projectDirectory: string;
|
|
10
14
|
install?: boolean;
|
|
11
15
|
bcpPackage?: string;
|
|
12
16
|
tailwind?: boolean;
|
|
13
17
|
database?: BcpDatabase;
|
|
18
|
+
auth?: BcpAuthPreset;
|
|
14
19
|
packageManager?: "npm";
|
|
15
20
|
}
|
|
16
21
|
|
|
@@ -20,6 +25,7 @@ export interface CreateBcpAppResult {
|
|
|
20
25
|
packageSpecifier: string;
|
|
21
26
|
tailwind: boolean;
|
|
22
27
|
database: BcpDatabase;
|
|
28
|
+
auth: BcpAuthPreset;
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
export function createBcpApp(
|
package/src/index.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
applyProjectOptions,
|
|
12
|
+
normalizeAuth,
|
|
12
13
|
normalizeDatabase,
|
|
13
14
|
} from "./project-options.mjs";
|
|
14
15
|
|
|
@@ -42,6 +43,10 @@ export async function createBcpApp(
|
|
|
42
43
|
normalizeDatabase(
|
|
43
44
|
options.database
|
|
44
45
|
);
|
|
46
|
+
const auth =
|
|
47
|
+
normalizeAuth(
|
|
48
|
+
options.auth
|
|
49
|
+
);
|
|
45
50
|
|
|
46
51
|
ensureTargetDirectory(
|
|
47
52
|
targetDirectory
|
|
@@ -119,6 +124,7 @@ export async function createBcpApp(
|
|
|
119
124
|
packageJson,
|
|
120
125
|
tailwind,
|
|
121
126
|
database,
|
|
127
|
+
auth,
|
|
122
128
|
});
|
|
123
129
|
|
|
124
130
|
fs.writeFileSync(
|
|
@@ -152,6 +158,8 @@ export async function createBcpApp(
|
|
|
152
158
|
selectedOptions.tailwind,
|
|
153
159
|
database:
|
|
154
160
|
selectedOptions.database,
|
|
161
|
+
auth:
|
|
162
|
+
selectedOptions.auth,
|
|
155
163
|
};
|
|
156
164
|
}
|
|
157
165
|
|
package/src/project-options.mjs
CHANGED
|
@@ -9,6 +9,11 @@ export const DATABASE_CHOICES = [
|
|
|
9
9
|
"mongodb",
|
|
10
10
|
];
|
|
11
11
|
|
|
12
|
+
export const AUTH_CHOICES = [
|
|
13
|
+
"none",
|
|
14
|
+
"jwt-cookie",
|
|
15
|
+
];
|
|
16
|
+
|
|
12
17
|
const DATABASE_PRESETS = {
|
|
13
18
|
mysql: {
|
|
14
19
|
dependencies: {
|
|
@@ -16,25 +21,68 @@ const DATABASE_PRESETS = {
|
|
|
16
21
|
},
|
|
17
22
|
devDependencies: {},
|
|
18
23
|
env: [
|
|
19
|
-
"
|
|
24
|
+
"DB_HOST=localhost",
|
|
25
|
+
"DB_PORT=3306",
|
|
26
|
+
"DB_USER=",
|
|
27
|
+
"DB_PASSWORD=",
|
|
28
|
+
"DB_NAME=bcp_app",
|
|
20
29
|
],
|
|
21
30
|
source: `import "bcp/server-only";
|
|
22
31
|
|
|
23
32
|
import mysql from "mysql2/promise";
|
|
24
33
|
|
|
25
|
-
const
|
|
26
|
-
|
|
34
|
+
const globalForDatabase =
|
|
35
|
+
globalThis as typeof globalThis & {
|
|
36
|
+
__bcpDb?: mysql.Pool;
|
|
37
|
+
};
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
export const db =
|
|
40
|
+
globalForDatabase.__bcpDb ??
|
|
41
|
+
mysql.createPool({
|
|
42
|
+
host:
|
|
43
|
+
process.env.DB_HOST ??
|
|
44
|
+
"localhost",
|
|
45
|
+
|
|
46
|
+
port:
|
|
47
|
+
Number(
|
|
48
|
+
process.env.DB_PORT ??
|
|
49
|
+
3306
|
|
50
|
+
),
|
|
51
|
+
|
|
52
|
+
user:
|
|
53
|
+
process.env.DB_USER ??
|
|
54
|
+
"root",
|
|
55
|
+
|
|
56
|
+
password:
|
|
57
|
+
process.env.DB_PASSWORD ??
|
|
58
|
+
"",
|
|
59
|
+
|
|
60
|
+
database:
|
|
61
|
+
process.env.DB_NAME ??
|
|
62
|
+
"bcp_app",
|
|
63
|
+
|
|
64
|
+
waitForConnections:
|
|
65
|
+
true,
|
|
66
|
+
|
|
67
|
+
connectionLimit:
|
|
68
|
+
10,
|
|
69
|
+
|
|
70
|
+
queueLimit:
|
|
71
|
+
0,
|
|
72
|
+
|
|
73
|
+
charset:
|
|
74
|
+
"utf8mb4",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
process.env.NODE_ENV !==
|
|
79
|
+
"production"
|
|
80
|
+
) {
|
|
81
|
+
globalForDatabase.__bcpDb =
|
|
82
|
+
db;
|
|
32
83
|
}
|
|
33
84
|
|
|
34
|
-
export
|
|
35
|
-
mysql.createPool(
|
|
36
|
-
connectionString
|
|
37
|
-
);
|
|
85
|
+
export default db;
|
|
38
86
|
`,
|
|
39
87
|
},
|
|
40
88
|
postgresql: {
|
|
@@ -149,6 +197,241 @@ export async function getDatabase() {
|
|
|
149
197
|
},
|
|
150
198
|
};
|
|
151
199
|
|
|
200
|
+
const JWT_COOKIE_AUTH_FILES = {
|
|
201
|
+
"lib/auth.ts": `import "bcp/server-only";
|
|
202
|
+
|
|
203
|
+
import {
|
|
204
|
+
createSession,
|
|
205
|
+
destroySession,
|
|
206
|
+
getSession,
|
|
207
|
+
} from "bcp/server";
|
|
208
|
+
|
|
209
|
+
export interface AuthenticatedUser {
|
|
210
|
+
id: string | number;
|
|
211
|
+
email: string;
|
|
212
|
+
role?: string;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface UserSession {
|
|
216
|
+
userId: string | number;
|
|
217
|
+
email: string;
|
|
218
|
+
role?: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function authenticateCredentials(
|
|
222
|
+
email: string,
|
|
223
|
+
password: string
|
|
224
|
+
): Promise<AuthenticatedUser | null> {
|
|
225
|
+
/*
|
|
226
|
+
* Secure starter behavior: authentication is disabled until the
|
|
227
|
+
* application verifies the supplied credentials against its own user
|
|
228
|
+
* store and password-hash strategy.
|
|
229
|
+
*/
|
|
230
|
+
void email;
|
|
231
|
+
void password;
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function createUserSession(
|
|
237
|
+
user: AuthenticatedUser
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
await createSession({
|
|
240
|
+
userId:
|
|
241
|
+
user.id,
|
|
242
|
+
email:
|
|
243
|
+
user.email,
|
|
244
|
+
...(user.role === undefined
|
|
245
|
+
? {}
|
|
246
|
+
: {
|
|
247
|
+
role:
|
|
248
|
+
user.role,
|
|
249
|
+
}),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function getCurrentUser() {
|
|
254
|
+
return getSession<UserSession>();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function clearUserSession() {
|
|
258
|
+
return destroySession();
|
|
259
|
+
}
|
|
260
|
+
`,
|
|
261
|
+
"app/api/auth/login/route.ts": `import {
|
|
262
|
+
json,
|
|
263
|
+
} from "bcp/server";
|
|
264
|
+
|
|
265
|
+
import {
|
|
266
|
+
authenticateCredentials,
|
|
267
|
+
createUserSession,
|
|
268
|
+
} from "@/lib/auth";
|
|
269
|
+
|
|
270
|
+
export async function POST(
|
|
271
|
+
request: Request
|
|
272
|
+
) {
|
|
273
|
+
let body: unknown;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
body =
|
|
277
|
+
await request.json();
|
|
278
|
+
} catch {
|
|
279
|
+
return json(
|
|
280
|
+
{
|
|
281
|
+
error:
|
|
282
|
+
"Invalid JSON body.",
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
status:
|
|
286
|
+
400,
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (
|
|
292
|
+
!body ||
|
|
293
|
+
typeof body !== "object" ||
|
|
294
|
+
Array.isArray(body)
|
|
295
|
+
) {
|
|
296
|
+
return json(
|
|
297
|
+
{
|
|
298
|
+
error:
|
|
299
|
+
"Email and password are required.",
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
status:
|
|
303
|
+
400,
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const input =
|
|
309
|
+
body as Record<
|
|
310
|
+
string,
|
|
311
|
+
unknown
|
|
312
|
+
>;
|
|
313
|
+
const email =
|
|
314
|
+
typeof input.email ===
|
|
315
|
+
"string"
|
|
316
|
+
? input.email.trim()
|
|
317
|
+
: "";
|
|
318
|
+
const password =
|
|
319
|
+
typeof input.password ===
|
|
320
|
+
"string"
|
|
321
|
+
? input.password
|
|
322
|
+
: "";
|
|
323
|
+
|
|
324
|
+
if (
|
|
325
|
+
!email ||
|
|
326
|
+
!password
|
|
327
|
+
) {
|
|
328
|
+
return json(
|
|
329
|
+
{
|
|
330
|
+
error:
|
|
331
|
+
"Email and password are required.",
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
status:
|
|
335
|
+
400,
|
|
336
|
+
}
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const user =
|
|
341
|
+
await authenticateCredentials(
|
|
342
|
+
email,
|
|
343
|
+
password
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
if (!user) {
|
|
347
|
+
return json(
|
|
348
|
+
{
|
|
349
|
+
error:
|
|
350
|
+
"Invalid credentials.",
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
status:
|
|
354
|
+
401,
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
await createUserSession(
|
|
360
|
+
user
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
return json({
|
|
364
|
+
success:
|
|
365
|
+
true,
|
|
366
|
+
user: {
|
|
367
|
+
id:
|
|
368
|
+
user.id,
|
|
369
|
+
email:
|
|
370
|
+
user.email,
|
|
371
|
+
role:
|
|
372
|
+
user.role ??
|
|
373
|
+
null,
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
`,
|
|
378
|
+
"app/api/auth/logout/route.ts": `import {
|
|
379
|
+
json,
|
|
380
|
+
} from "bcp/server";
|
|
381
|
+
|
|
382
|
+
import {
|
|
383
|
+
clearUserSession,
|
|
384
|
+
} from "@/lib/auth";
|
|
385
|
+
|
|
386
|
+
export async function POST() {
|
|
387
|
+
await clearUserSession();
|
|
388
|
+
|
|
389
|
+
return json({
|
|
390
|
+
success:
|
|
391
|
+
true,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
`,
|
|
395
|
+
"app/api/auth/me/route.ts": `import {
|
|
396
|
+
json,
|
|
397
|
+
} from "bcp/server";
|
|
398
|
+
|
|
399
|
+
import {
|
|
400
|
+
getCurrentUser,
|
|
401
|
+
} from "@/lib/auth";
|
|
402
|
+
|
|
403
|
+
export async function GET() {
|
|
404
|
+
const session =
|
|
405
|
+
await getCurrentUser();
|
|
406
|
+
|
|
407
|
+
if (!session) {
|
|
408
|
+
return json(
|
|
409
|
+
{
|
|
410
|
+
error:
|
|
411
|
+
"Unauthorized.",
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
status:
|
|
415
|
+
401,
|
|
416
|
+
}
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return json({
|
|
421
|
+
user: {
|
|
422
|
+
id:
|
|
423
|
+
session.userId,
|
|
424
|
+
email:
|
|
425
|
+
session.email,
|
|
426
|
+
role:
|
|
427
|
+
session.role ??
|
|
428
|
+
null,
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
`,
|
|
433
|
+
};
|
|
434
|
+
|
|
152
435
|
export function normalizeDatabase(
|
|
153
436
|
value
|
|
154
437
|
) {
|
|
@@ -172,16 +455,44 @@ export function normalizeDatabase(
|
|
|
172
455
|
return normalized;
|
|
173
456
|
}
|
|
174
457
|
|
|
458
|
+
export function normalizeAuth(
|
|
459
|
+
value
|
|
460
|
+
) {
|
|
461
|
+
const normalized =
|
|
462
|
+
String(
|
|
463
|
+
value ?? "none"
|
|
464
|
+
)
|
|
465
|
+
.trim()
|
|
466
|
+
.toLowerCase();
|
|
467
|
+
|
|
468
|
+
if (
|
|
469
|
+
!AUTH_CHOICES.includes(
|
|
470
|
+
normalized
|
|
471
|
+
)
|
|
472
|
+
) {
|
|
473
|
+
throw new Error(
|
|
474
|
+
`Unsupported authentication preset: ${normalized}. Choose one of: ${AUTH_CHOICES.join(", ")}.`
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return normalized;
|
|
479
|
+
}
|
|
480
|
+
|
|
175
481
|
export function applyProjectOptions({
|
|
176
482
|
targetDirectory,
|
|
177
483
|
packageJson,
|
|
178
484
|
tailwind = false,
|
|
179
485
|
database = "none",
|
|
486
|
+
auth = "none",
|
|
180
487
|
}) {
|
|
181
488
|
const selectedDatabase =
|
|
182
489
|
normalizeDatabase(
|
|
183
490
|
database
|
|
184
491
|
);
|
|
492
|
+
const selectedAuth =
|
|
493
|
+
normalizeAuth(
|
|
494
|
+
auth
|
|
495
|
+
);
|
|
185
496
|
|
|
186
497
|
if (tailwind) {
|
|
187
498
|
configureTailwind(
|
|
@@ -214,7 +525,17 @@ export function applyProjectOptions({
|
|
|
214
525
|
);
|
|
215
526
|
appendEnvExample(
|
|
216
527
|
targetDirectory,
|
|
217
|
-
preset.env
|
|
528
|
+
preset.env,
|
|
529
|
+
"Database"
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (
|
|
534
|
+
selectedAuth ===
|
|
535
|
+
"jwt-cookie"
|
|
536
|
+
) {
|
|
537
|
+
configureJwtCookieAuth(
|
|
538
|
+
targetDirectory
|
|
218
539
|
);
|
|
219
540
|
}
|
|
220
541
|
|
|
@@ -225,9 +546,39 @@ export function applyProjectOptions({
|
|
|
225
546
|
),
|
|
226
547
|
database:
|
|
227
548
|
selectedDatabase,
|
|
549
|
+
auth:
|
|
550
|
+
selectedAuth,
|
|
228
551
|
};
|
|
229
552
|
}
|
|
230
553
|
|
|
554
|
+
function configureJwtCookieAuth(
|
|
555
|
+
targetDirectory
|
|
556
|
+
) {
|
|
557
|
+
for (
|
|
558
|
+
const [
|
|
559
|
+
relativePath,
|
|
560
|
+
source,
|
|
561
|
+
]
|
|
562
|
+
of Object.entries(
|
|
563
|
+
JWT_COOKIE_AUTH_FILES
|
|
564
|
+
)
|
|
565
|
+
) {
|
|
566
|
+
writeFile(
|
|
567
|
+
targetDirectory,
|
|
568
|
+
relativePath,
|
|
569
|
+
source
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
appendEnvExample(
|
|
574
|
+
targetDirectory,
|
|
575
|
+
[
|
|
576
|
+
"BCP_SESSION_SECRET=",
|
|
577
|
+
],
|
|
578
|
+
"Authentication"
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
231
582
|
function configureTailwind(
|
|
232
583
|
targetDirectory,
|
|
233
584
|
packageJson
|
|
@@ -415,7 +766,8 @@ function appendGitIgnore(
|
|
|
415
766
|
|
|
416
767
|
function appendEnvExample(
|
|
417
768
|
targetDirectory,
|
|
418
|
-
lines
|
|
769
|
+
lines,
|
|
770
|
+
section
|
|
419
771
|
) {
|
|
420
772
|
const filePath =
|
|
421
773
|
path.join(
|
|
@@ -434,7 +786,7 @@ function appendEnvExample(
|
|
|
434
786
|
|
|
435
787
|
fs.writeFileSync(
|
|
436
788
|
filePath,
|
|
437
|
-
`${current}${current ? "\n\n" : ""}#
|
|
789
|
+
`${current}${current ? "\n\n" : ""}# ${section}\n${lines.join("\n")}\n`,
|
|
438
790
|
"utf8"
|
|
439
791
|
);
|
|
440
792
|
}
|